Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,49 @@
import physicalgraph.zigbee.zcl.DataType

metadata {
definition(name: "ZigBee Window Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "generic-shade") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Window Shade"
capability "Health Check"
capability "Switch Level"

command "pause"

fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0102", outClusters: "0019", model: "E2B0-KR000Z0-HA", deviceJoinName: "SOMFY Blind Controller/eZEX" // SY-IoT201-BD
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0102", outClusters: "000A", manufacturer: "Feibit Co.Ltd", model: "FTB56-ZT218AK1.6", deviceJoinName: "Wistar Curtain Motor(CMJ)"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0102", outClusters: "000A", manufacturer: "Feibit Co.Ltd", model: "FTB56-ZT218AK1.8", deviceJoinName: "Wistar Curtain Motor(CMJ)"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0102", outClusters: "0003", manufacturer: "REXENSE", model: "DY0010", deviceJoinName: "Smart Curtain Motor(DT82TV)"
}


tiles(scale: 2) {
multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4) {
tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") {
attributeState "open", label: 'Open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing"
attributeState "closed", label: 'Closed', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening"
attributeState "partially open", label: 'Partially open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#d45614", nextState: "closing"
attributeState "opening", label: 'Opening', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "partially open"
attributeState "closing", label: 'Closing', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "partially open"
}
}
standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "pause", label:"", icon:'st.sonos.pause-btn', action:'pause', backgroundColor:"#cccccc"
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("shadeLevel", "device.level", width: 4, height: 1) {
state "level", label: 'Shade is ${currentValue}% up', defaultState: true
}
controlTile("levelSliderControl", "device.level", "slider", width:2, height: 1, inactiveLabel: false) {
state "level", action:"switch level.setLevel"
}

main "windowShade"
details(["windowShade", "contPause", "shadeLevel", "levelSliderControl", "refresh"])
}
definition(name: "ZigBee Window Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "generic-shade") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Window Shade"
capability "Health Check"
capability "Switch Level"

command "pause"

fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0102", outClusters: "0019", model: "E2B0-KR000Z0-HA", deviceJoinName: "SOMFY Blind Controller/eZEX" // SY-IoT201-BD
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0102", outClusters: "000A", manufacturer: "Feibit Co.Ltd", model: "FTB56-ZT218AK1.6", deviceJoinName: "Wistar Curtain Motor(CMJ)"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0102", outClusters: "000A", manufacturer: "Feibit Co.Ltd", model: "FTB56-ZT218AK1.8", deviceJoinName: "Wistar Curtain Motor(CMJ)"
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0102", outClusters: "0003", manufacturer: "REXENSE", model: "DY0010", deviceJoinName: "Smart Curtain Motor(DT82TV)"
fingerprint manufacturer: "IKEA of Sweden", model: "KADRILJ roller blind", deviceJoinName: "IKEA Kadrilj Roller Blind" // raw description 01 0104 0202 00 09 0000 0001 0003 0004 0005 0020 0102 1000 FC7C 02 0019 1000
}

tiles(scale: 2) {
multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4) {
tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") {
attributeState "open", label: 'Open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing"
attributeState "closed", label: 'Closed', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening"
attributeState "partially open", label: 'Partially open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#d45614", nextState: "closing"
attributeState "opening", label: 'Opening', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "partially open"
attributeState "closing", label: 'Closing', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "partially open"
}
}
standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
state "pause", label:"", icon:'st.sonos.pause-btn', action:'pause', backgroundColor:"#cccccc"
}
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) {
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
}
valueTile("shadeLevel", "device.level", width: 4, height: 1) {
state "level", label: 'Shade is ${currentValue}% up', defaultState: true
}
controlTile("levelSliderControl", "device.level", "slider", width:2, height: 1, inactiveLabel: false) {
state "level", action:"switch level.setLevel"
}

main "windowShade"
details(["windowShade", "contPause", "shadeLevel", "levelSliderControl", "refresh"])
}
}

private getCLUSTER_WINDOW_COVERING() { 0x0102 }
Expand All @@ -76,119 +76,170 @@ private List<Map> collectAttributes(Map descMap) {
if (descMap.additionalAttrs) {
descMaps.addAll(descMap.additionalAttrs)
}

return descMaps
}

// Parse incoming device messages to generate events
def parse(String description) {
log.debug "description:- ${description}"
if (description?.startsWith("read attr -")) {
Map descMap = zigbee.parseDescriptionAsMap(description)
if (supportsLiftPercentage() && descMap?.clusterInt == CLUSTER_WINDOW_COVERING && descMap.value) {
log.debug "attr: ${descMap?.attrInt}, value: ${descMap?.value}, descValue: ${Integer.parseInt(descMap.value, 16)}, ${device.getDataValue("model")}"
List<Map> descMaps = collectAttributes(descMap)
def liftmap = descMaps.find { it.attrInt == ATTRIBUTE_POSITION_LIFT }
if (liftmap && liftmap.value) {
def newLevel = zigbee.convertHexToInt(liftmap.value)
levelEventHandler(newLevel)
}
} else if (!supportsLiftPercentage() && descMap?.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER && descMap.value) {
def valueInt = Math.round((zigbee.convertHexToInt(descMap.value)) / 255 * 100)

levelEventHandler(valueInt)
}
}
log.debug "description:- ${description}"
if (description?.startsWith("read attr -")) {
Map descMap = zigbee.parseDescriptionAsMap(description)
if (isBindingTableMessage(description)) {
parseBindingTableMessage(description)
} else if (supportsLiftPercentage() && descMap?.clusterInt == CLUSTER_WINDOW_COVERING && descMap.value) {
log.debug "attr: ${descMap?.attrInt}, value: ${descMap?.value}, descValue: ${Integer.parseInt(descMap.value, 16)}, ${device.getDataValue("model")}"
List<Map> descMaps = collectAttributes(descMap)
def liftmap = descMaps.find { it.attrInt == ATTRIBUTE_POSITION_LIFT }
if (liftmap && liftmap.value) {
def newLevel = zigbee.convertHexToInt(liftmap.value)
if (shouldInvertLiftPercentage()) {
// some devices report % level of being closed (instead of % level of being opened)
// inverting that logic is needed here to avoid a code duplication
newLevel = 100 - newLevel
}
levelEventHandler(newLevel)
}
} else if (!supportsLiftPercentage() && descMap?.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER && descMap.value) {
def valueInt = Math.round((zigbee.convertHexToInt(descMap.value)) / 255 * 100)

levelEventHandler(valueInt)
}
}
}

def levelEventHandler(currentLevel) {
def lastLevel = device.currentValue("level")
log.debug "levelEventHandle - currentLevel: ${currentLevel} lastLevel: ${lastLevel}"
if (lastLevel == "undefined" || currentLevel == lastLevel) { //Ignore invalid reports
log.debug "Ignore invalid reports"
} else {
sendEvent(name: "level", value: currentLevel)
if (currentLevel == 0 || currentLevel == 100) {
sendEvent(name: "windowShade", value: currentLevel == 0 ? "closed" : "open")
} else {
if (lastLevel < currentLevel) {
sendEvent([name:"windowShade", value: "opening"])
} else if (lastLevel > currentLevel) {
sendEvent([name:"windowShade", value: "closing"])
}
runIn(1, "updateFinalState", [overwrite:true])
}
}
def lastLevel = device.currentValue("level")
log.debug "levelEventHandle - currentLevel: ${currentLevel} lastLevel: ${lastLevel}"
if (lastLevel == "undefined" || currentLevel == lastLevel) { //Ignore invalid reports
log.debug "Ignore invalid reports"
} else {
sendEvent(name: "level", value: currentLevel)
if (currentLevel == 0 || currentLevel == 100) {
sendEvent(name: "windowShade", value: currentLevel == 0 ? "closed" : "open")
} else {
if (lastLevel < currentLevel) {
sendEvent([name:"windowShade", value: "opening"])
} else if (lastLevel > currentLevel) {
sendEvent([name:"windowShade", value: "closing"])
}
runIn(1, "updateFinalState", [overwrite:true])
}
}
}

def updateFinalState() {
def level = device.currentValue("level")
log.debug "updateFinalState: ${level}"
if (level > 0 && level < 100) {
sendEvent(name: "windowShade", value: "partially open")
}
def level = device.currentValue("level")
log.debug "updateFinalState: ${level}"
if (level > 0 && level < 100) {
sendEvent(name: "windowShade", value: "partially open")
}
}

def supportsLiftPercentage() {
device.getDataValue("manufacturer") != "Feibit Co.Ltd"
device.getDataValue("manufacturer") != "Feibit Co.Ltd"
}

def close() {
log.info "close()"
zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_CLOSE)
log.info "close()"
zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_CLOSE)
}

def open() {
log.info "open()"
zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_OPEN)
log.info "open()"
zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_OPEN)
}

def setLevel(data, rate = null) {
log.info "setLevel()"
def cmd
if (supportsLiftPercentage()) {
cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(data, 2))
} else {
cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(data * 255 / 100), 2))
}

return cmd
log.info "setLevel()"
def cmd
if (supportsLiftPercentage()) {
if (shouldInvertLiftPercentage()) {
// some devices keeps % level of being closed (instead of % level of being opened)
// inverting that logic is needed here
data = 100 - data
}
cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(data, 2))
} else {
cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(data * 255 / 100), 2))
}
return cmd
}

def pause() {
log.info "pause()"
zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_PAUSE)
log.info "pause()"
zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_PAUSE)
}

/**
* PING is used by Device-Watch in attempt to reach the Device
* */
def ping() {
return refresh()
return refresh()
}

def refresh() {
log.info "refresh()"
def cmds
if (supportsLiftPercentage()) {
cmds = zigbee.readAttribute(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT)
} else {
cmds = zigbee.readAttribute(zigbee.LEVEL_CONTROL_CLUSTER, ATTRIBUTE_CURRENT_LEVEL)
}
return cmds
log.info "refresh()"
def cmds
if (supportsLiftPercentage()) {
cmds = zigbee.readAttribute(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT)
} else {
cmds = zigbee.readAttribute(zigbee.LEVEL_CONTROL_CLUSTER, ATTRIBUTE_CURRENT_LEVEL)
}
return cmds
}

def configure() {
// Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time)
log.info "configure()"
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
log.debug "Configuring Reporting and Bindings."

def cmds
if (supportsLiftPercentage()) {
cmds = zigbee.configureReporting(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT, DataType.UINT8, 0, 600, null)
} else {
cmds = zigbee.levelConfig()
}
return refresh() + cmds
// Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time)
log.info "configure()"
sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
log.debug "Configuring Reporting and Bindings."

def cmds
if (supportsLiftPercentage()) {
cmds = zigbee.configureReporting(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT, DataType.UINT8, 0, 600, null)
} else {
cmds = zigbee.levelConfig()
}

if (usesLocalGroupBinding()) {
cmds += readDeviceBindingTable()
}

return refresh() + cmds
}

def usesLocalGroupBinding() {
isIkeaKadrilj()
}

private def parseBindingTableMessage(description) {
Integer groupAddr = getGroupAddrFromBindingTable(description)
if (groupAddr) {
List cmds = addHubToGroup(groupAddr)
cmds?.collect { new physicalgraph.device.HubAction(it) }
}
}

private Integer getGroupAddrFromBindingTable(description) {
log.info "Parsing binding table - '$description'"
def btr = zigbee.parseBindingTableResponse(description)
def groupEntry = btr?.table_entries?.find { it.dstAddrMode == 1 }
log.info "Found ${groupEntry}"
!groupEntry?.dstAddr ?: Integer.parseInt(groupEntry.dstAddr, 16)
}

private List addHubToGroup(Integer groupAddr) {
["st cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}", "delay 200"]
}

private List readDeviceBindingTable() {
["zdo mgmt-bind 0x${device.deviceNetworkId} 0", "delay 200"]
}

def shouldInvertLiftPercentage() {
return isIkeaKadrilj()
}

def isIkeaKadrilj() {
device.getDataValue("model") == "KADRILJ roller blind"
}