/** zemismart * Tuya Window Shade (v.0.1.0) Hubitat v1 * Copyright 2020 iquix * * 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. This DTH is coded based on iquix's tuya-window-shade DTH. https://github.com/iquix/Smartthings/blob/master/devicetypes/iquix/tuya-window-shade.src/tuya-window-shade.groovy https://raw.githubusercontent.com/shin4299/XiaomiSJ/master/devicetypes/shinjjang/zemismart-zigbee-blind.src/zemismart-zigbee-blind.groovy */ import groovy.json.JsonOutput //mc // import physicalgraph.zigbee.zcl.DataType import hubitat.zigbee.zcl.DataType import hubitat.helper.HexUtils metadata { definition(name: "ZemiSmart Zigbee Blind", namespace: "ShinJjang", author: "ShinJjang-iquix", ocfDeviceType: "oic.d.blind", vid: "generic-shade") { capability "Actuator" capability "Configuration" capability "Window Shade" // mc not supported in HE capability "Window Shade Preset" capability "Switch Level" capability "Switch" capability "Light" //GH command "pause" command "presetPosition" attribute "Direction", "enum", ["Reverse","Forward"] fingerprint endpointId: "01", profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0019", manufacturer: "_TYST11_wmcdj3aq", model: "mcdj3aq", deviceJoinName: "Zemismart Zigbee Blind" //mc changeed endpointId from 0x01 to 01 } preferences { input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, range: "0..100", required: false, displayDuringSetup: false input name: "Direction", type: "enum", title: "Direction Set", defaultValue: "00", options:["01": "Reverse", "00": "Forward"], displayDuringSetup: true input "logEnable", "bool", title: "Enable logging", required: true, defaultValue: true } // removed tiles section as not used in Hubitat } private getCLUSTER_TUYA() { 0xEF00 } private getSETDATA() { 0x00 } // Parse incoming device messages to generate events def parse(String description) { if (description?.startsWith('catchall:') || description?.startsWith('read attr -')) { Map descMap = zigbee.parseDescriptionAsMap(description) //if(logEnable == true) log.debug "Pasred Map $descMap" if (descMap?.clusterInt==CLUSTER_TUYA) { if ( descMap?.command == "01" || descMap?.command == "02" ) { int dp = zigbee.convertHexToInt(descMap?.data[3]+descMap?.data[2]) if(logEnable == true) log.debug "dp = " + dp String data6 = descMap.data[6] switch (dp) { case 1025: // 0x04 0x01: Confirm opening/closing/stopping (triggered from Zigbee) if (data6 == "00") { if(logEnable == true) log.debug "parsed opening" //levelEventMoving(100) sendEvent([name:"windowShade", value: "opening", descriptionText: "Zigbee Opnening"]) } else if (data6 == "02") { if(logEnable == true) log.debug "parsed closing" //levelEventMoving(0) sendEvent([name:"windowShade", value: "closing" , descriptionText: "Zigbee closing" ]) } else {log.debug "parsed else case $dp open/close/stop zigbee" + descMap.data[6]} break; case 1031: // 0x04 0x07: Confirm opening/closing/stopping (triggered from remote) if (data6 == "01") { if(logEnable == true) log.trace "remote closing" // levelEventMoving(0) sendEvent([name:"windowShade", value: "closing" , descriptionText: "Remote closing" ]) } else if (data6 == "00") { if(logEnable == true) log.trace "remote opening" //levelEventMoving(100) sendEvent([name:"windowShade", value: "opening", descriptionText: "Remote Opnening"]) } else {log.debug "parsed else case $dp open/close/stop remote ${descMap.data[6]}"} break; case 514: // 0x02 0x02: Started moving to position (triggered from Zigbee) int pos = zigbee.convertHexToInt(descMap.data[9]) if(logEnable == true) log.debug "moving to position :"+pos levelEventMoving(pos) break; case 515: // 0x03: Arrived at position int pos = zigbee.convertHexToInt(descMap.data[9]) if(logEnable == true) log.debug "${device.displayName} arrived at position:$pos , desc:$description" levelEventArrived(pos) break; log.warn "UN-handled CLUSTER_TUYA case " + dp " " descMap } data6 = null } } else { if(logEnable == true) log.warn "UN-Pasred Map $descMap" } descMap = null } } private levelEventMoving(int currentLevel) { int lastLevel = device.currentValue("level") if(logEnable == true) log.debug "${device.displayName} levelEventMoving - targert level: ${currentLevel} lastLevel: ${lastLevel}" if (lastLevel == "undefined" || currentLevel == lastLevel) { //Ignore invalid reports log.debug "${device.displayName} Ignore invalid reports" + lastLevel } else { if (lastLevel < currentLevel) { sendEvent([name:"windowShade", value: "opening"]) } else if (lastLevel > currentLevel) { sendEvent([name:"windowShade", value: "closing"]) } else {} //last and current the same } } private levelEventArrived(int level) { if(logEnable == true) log.info "arrived at " + level //debunce start when in revers mode time in mill's def nowtime = now() if (nowtime-50 <= state.lastmsg){ def diff = nowtime-state.lastmsg state.lastmsg = nowtime log.warn "${device.displayName} debouced level = $level, two message recived in $diff millisecondes filter is set to 50" //nowtime = ${nowtimeform.format('HH:mm: ss: SSS')}, lastmsg = ${stateform.format('HH:mm: ss: SSS')} differance ${diffform.format('SSS')} millisecondes" return } state.lastmsg = nowtime //debunce end if (level == 0) { sendEvent(name: "windowShade", value: "closed")} else if (level == 100) { sendEvent(name: "windowShade", value: "open")} else if (level > 0 && level < 100) { sendEvent(name: "windowShade", value: "partially open")} else { sendEvent(name: "windowShade", value: "unknown")} sendEvent(name: "position", value: (level)) //To enable in GoggleHome sendEvent(name: "level", value: (level)) //dimmer if (level < 100){ sendEvent(name: "switch", value: "on")} else {sendEvent(name: "switch", value: "off")} //google home end } def close() { if(logEnable == true) log.info "close()" //int currentLevel = device.currentValue("level") if (device.currentValue("level") == 0) { sendEvent(name: "windowShade", value: "closed") return } sendTuyaCommand("0104", "00", "0102") } def open() { if(logEnable == true) log.info "open()" //int currentLevel = device.currentValue("level") if (device.currentValue("level") == 100) { sendEvent(name: "windowShade", value: "open") return } sendTuyaCommand("0104", "00", "0100") } def pause() { if(logEnable == true) log.info "pause()" sendTuyaCommand("0104", "00", "0101") } def stopPositionChange(){ pause() } def startPositionChange(pos){ if (pos == "open") open() else close() } def setLevel(data, rate = null) { if(logEnable == true) log.info "setLevel("+data+")" int currentLevel = device.currentValue("level") if (currentLevel == data) { sendEvent(name: "level", value: currentLevel) sendEvent(name: "position", value: currentLevel) //HE capability attribute } else { sendTuyaCommand("0202", "00", "04000000"+HexUtils.integerToHexString(data.intValue(), 1)) } } def setPosition(position){ //mc new for HE Commands if(logEnable == true) log.info "setPos to $position" setLevel(position) } def presetPosition() { //custom command preset setLevel(preset ?: 50) } def installed() { sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) } def updated() { def val = Direction sendEvent([name:"Direction", value: (val == "00" ? "Forward" : "Reverse")]) DirectionSet(val) } def initialize() { // Runs on reboot, or can be triggered manually. byte randomSixty = Math.abs(new Random().nextInt() % 60) log.info "Initialize running updated in $randomSixty seconds" state.lastmsg = now() runIn(randomSixty,updated) } private DirectionSet(Dval) { if(logEnable == true) log.info "Direction set ${Dval} " sendTuyaCommand("05040001", Dval, "") //not tested } def configure() { log.info "configure() running updated" updated() } private sendTuyaCommand(dp, fn, data) { if(logEnable == true) log.trace "send tuya command ${dp},${fn},${data}" zigbee.command(CLUSTER_TUYA, SETDATA, "00" + zigbee.convertToHexString(rand(256), 2) + dp + fn + data) } private rand(n) { return (new Random().nextInt(n)) } def on (){ if(logEnable == true) log.warn "$device - some thing thinks im a switch and is turning me on" close() } def off () { if(logEnable == true) log.warn "$device - some thing thinks im a switch and is turning me off" open() }