Tuya Zigbee water/gas valve

I'll try generic valve again when I can, but I'm pretty sure I tried it and it did not work. If someone could point me to docs on how to pull the fingerprint I'll do that for sure.

When ever you change driver, click save then click configure. As to the fingerprint, reset the device (don't delete it from HE) then re pair it with the log window open, cut and paste and tag mike maxwell

@maffpt did you succeed to get your valve working?

Hi,

I was able to make it work using the following driver:

Tuya gas/water valve

But I did some updates but, to be honest, I can’t remember right now which … sorry

This is what’s working:

/**
 *  Date: 2021-03-31
 */

import hubitat.helper.HexUtils

metadata {
	definition (name: "Zigbee - Tuya TRV", namespace: "Mark-C-uk", author: "MarkC") {
  {
//        capability "Configuration"
//        capability "TemperatureMeasurement"
//        capability "Thermostat"
//        capability "ThermostatHeatingSetpoint"
//        capability "ThermostatCoolingSetpoint"
//        capability "ThermostatSetpoint"
//        capability "Refresh"
//        capability "Battery"
        
    capability "Refresh"
    capability "Valve"
        
    attribute "valve", "enum", ["open", "closed"]
    attribute "status", "string"
      

//        attribute "valve", "String"
//        attribute "WindowOpenDetection","String"
//        attribute "autolock","String"
//        attribute "childLock","String"
        
    ///fingerprint endpointId: "01", profileId: "0104", inClusters: "0000,0004,0005,EF00", outClusters: "0019,000A", manufacturer: "_TZE200_ckud7u2l", model: "TS0601", deviceJoinName: "Zigbee - Tuya TRV" 
    /// // Moes TRV
    ///fingerprint endpointId: "01", profileId: "0104", inClusters: "0000,000A,0004,0005,EF00", outClusters: "0019", manufacturer: "_TZE200_zion52ef", model: "TS0601", deviceJoinName: "Zigbee - Tuya TRV"
    fingerprint endpointId: "01", profileId: "0104", inClusters: "0000,0004,0005,EF00", outClusters: "0019,000A", manufacturer: "_TZE200_vrjkcam9", model: "TS0601", application: "44", deviceJoinName: "Tuya Zigbee Water/Gas Valve"
}
    
    preferences {
        //input("lock", "enum", title: "Do you want to lock your thermostat's physical keypad?", options: ["No", "Yes"], defaultValue: "No", required: false, displayDuringSetup: false)
        input name: "debugLogging", type: "bool", title: "Enable debug logging", defaultValue: true
        input name: "infoLogging", type: "bool", title: "Enable info logging", defaultValue: true
    }
}
    

ArrayList<String> parse (String description) {
    logDebug "parse $description"
    ArrayList<String> cmd = []
    Map msgMap = null

    if(description.indexOf('encoding: 42') >= 0) {
        List values = description.split("value: ")[1].split("(?<=\\G..)")
        String fullValue = values.join()
        Integer zeroIndex = values.indexOf("01")
            if(zeroIndex > -1) {
                msgMap = zigbee.parseDescriptionAsMap(description.replace(fullValue, values.take(zeroIndex).join()))
                values = values.drop(zeroIndex + 3)
                msgMap["additionalAttrs"] = [
                    ["encoding": "41",
                    "value": parseXiaomiStruct(values.join(), isFCC0=false, hasLength=true)]
                    ]
                logWarn "encoding: 42 parse 37 IF true"
            } 
            else {
                msgMap = zigbee.parseDescriptionAsMap(description) //modle name
                logWarn "encoding: 42 parse 51 ELSE true"
            }
        } 
        else {
            msgMap = zigbee.parseDescriptionAsMap(description)
        }
    
        if(msgMap.containsKey("encoding") && msgMap.containsKey("value") && msgMap["encoding"] != "41" && msgMap["encoding"] != "42") {
            logWarn "pase lin 59 used - ${description}"
            msgMap["valueParsed"] = zigbee_generic_decodeZigbeeData(msgMap["value"], msgMap["encoding"])
        }
        
        if(msgMap == [:] && description.indexOf("zone") == 0) {
            msgMap["type"] = "zone"
            java.util.regex.Matcher zoneMatcher = description =~ /.*zone.*status.*0x(?<status>([0-9a-fA-F][0-9a-fA-F])+).*extended.*status.*0x(?<statusExtended>([0-9a-fA-F][0-9a-fA-F])+).*/
            if(zoneMatcher.matches()) {
                  msgMap["parsed"] = true
                  msgMap["status"] = zoneMatcher.group("status")
                  msgMap["statusInt"] = Integer.parseInt(msgMap["status"], 16)
                  msgMap["statusExtended"] = zoneMatcher.group("statusExtended")
                  msgMap["statusExtendedInt"] = Integer.parseInt(msgMap["statusExtended"], 16)
            } 
            else {
               msgMap["parsed"] = false
            }
            logWarn "line 64 section used"
        }

    switch(msgMap["cluster"] + '_' + msgMap["attrId"]) {
        case "0000_0001":
            logDebug("Application ID Received")
            if(msgMap['value']) {
                updateDataValue("application", msgMap['value'])
            }
            break
        case "0000_0004":
        logDebug("Manufacturer Name Received ${msgMap['value']}")
            if(msgMap['value']) {
                updateDataValue("manufacturer", msgMap['value'])
            }
            break
        case "0000_0005":
            logDebug("Model Name Received")
            if(msgMap['value']) {
                updateDataValue('model', msgMap['value'])
            }    
            break
        default:
            //log.debug " ${msgMap["cluster"]}  ${msgMap["attrId"]} $msgMap"
            switch(msgMap["clusterId"]) {
///
/*
                case "0013":
                    logging("MULTISTATE CLUSTER EVENT")
                    break
                case "8021":
                    logging("BIND RESPONSE CLUSTER EVENT")
                    break
                case "8001":
                    logging("GENERAL CLUSTER EVENT")
                    break
                case "8004":
                    logTrace("Simple Descriptor Information Received - description:${description} | parseMap:${msgMap}")
                    updateDataFromSimpleDescriptorData(msgMap["data"])
                    break
                case "8031":
                    logging("Link Quality Cluster Event - description:${description} | parseMap:${msgMap}")

                    break
                case "8032":
                    logging("Routing Table Cluster Event - description:${description} | parseMap:${msgMap}")
                    break
                case "8021":
                case "8038":
                    logging("GENERAL CATCHALL (0x${msgMap["clusterId"]}")
                    break
///////////TUYA TRV messages////////////
*/
///
                case "EF00":  
                    //log.debug "clutsInt= ${msgMap[clusterInt]} ,att ID ${msgMap["attrId"]}, cluster ${msgMap["clusterId"]} -- ${msgMap}"
                    List data = msgMap['data']
                    if (data[2] && data[3])
                    {
                        String commandType = data[2] + data[3]
                        //logDebug "commandType = ${commandType}"
                        switch(commandType)
                        {
                          case "0101": // whatever it is ...
                            logDebug "case = EF00 - command = 0101 - data = ${data}"
                            //def wrkData = "${data[1]}.${data[6]}" // put the relevant data together to make coding easier ... ok. it's a hack, but works!
                            if (data [1] == "00" && data [6] == "01") 
                            { // open
                              switch (device.currentValue("valve", true))
                              {
                                case "open":
                                  // Already processed - let's just ignore it
                                  break
                                case "closed":
                                  sendEvent (name: "valve", value: "open")
                                  sendEvent (name: "status", value: "opening finished")
                                  break
                              }
                            }
                            else if (data [1] == "00" && data [6] == "00")
                            { // close
                              switch (device.currentValue("valve", true))
                              {
                                case "open":
                                  sendEvent (name: "valve", value: "closed")
                                  sendEvent (name: "status", value: "closing finished")
                                  break
                                case "closed":
                                  // Already processed - let's just ignore it
                                  break
                              }
                            }
                            else if (data [6] == "01")
                            { // opening
                                  sendEvent (name: "status", value: "opening")
                            }
                            else if (data [6] == "00")
                            { // closing
                                  sendEvent (name: "status", value: "closing")
                            }
                            else
                            { // error - unexpected
                            }
                            break
/*
///
//set point temp
                            case "0202": //set point temp
                                String SetPoint = HexUtils.hexStringToInt("${data[-2]}${data[-1]}") / 10
                                logging("${device.displayName} Temp Set Point ${SetPoint}, data ${msgMap["data"]}")
                                sendEvent(name: "heatingSetpoint", value: SetPoint.toFloat(), unit: "C")
                                sendEvent(name: "thermostatSetpoint", value: SetPoint.toFloat(), unit: "C")
                                if (device.currentValue("thermostatMode") != "off" && SetPoint.toFloat() > device.currentValue("temperature").toFloat()) { 
                                    sendEvent(name: "thermostatOperatingState", value: "heating")}
                                else { sendEvent(name: "thermostatOperatingState", value: "idle")}
                            break
                            case "1002": // Moes setpoint
                                String SetPoint = HexUtils.hexStringToInt("${data[-1]}") / 2
                                logging("${device.displayName} Temp Set Point ${SetPoint}, data ${msgMap["data"]}")
                                sendEvent(name: "heatingSetpoint", value: SetPoint.toFloat(), unit: "C")
                                sendEvent(name: "thermostatSetpoint", value: SetPoint.toFloat(), unit: "C")
                            break
//7202 away preset temperature
                            case "0702": //0x7202 away/off preset temperature
                                String SetPoint = HexUtils.hexStringToInt(data[9]) / 10
                                logging("${device.displayName} AWAY Temp Set Point ${commandType}, data9 ${SetPoint}")
                            break
//0302 Temperature
                            case '0302': //Temperature
                                String temperature = HexUtils.hexStringToInt("${data[-2]}${data[-1]}") / 10
                                logging("${device.displayName} Temp ${temperature}, data ${msgMap["data"]}")
                                sendEvent(name: "temperature", value: temperature, unit: "C" )
                                if (device.currentValue("thermostatMode") != "off" && temperature.toFloat() < device.currentValue("thermostatSetpoint").toFloat()) {
                                    sendEvent(name: "thermostatOperatingState", value: "heating")}
                                else { sendEvent(name: "thermostatOperatingState", value: "idle")}
                            break
                            case '1802': //Moes Temperature
                                String temperature = HexUtils.hexStringToInt("${data[-2]}${data[-1]}") / 10
                                logging("${device.displayName} Temp ${temperature}, data ${msgMap["data"]}")
                                sendEvent(name: "temperature", value: temperature, unit: "C" )
                            break
// Mode                            
                            case '0404': // Mode
                                String mode = HexUtils.hexStringToInt(data[6])
                                logging("${device.displayName} mode Code=${mode}")
                                switch (mode){
                                    case '0':
                                        sendEvent(name: "thermostatMode", value: "off" )
                                    break
                                    case '1':
                                        sendEvent(name: "thermostatMode", value: "auto" , descriptionText:"internal programming of device")
                                    break
                                    case '2':
                                        sendEvent(name: "thermostatMode", value: "heat" )
                                    break
                                }
                            break
                            case "0204": //Moes Mode
                            String mode = HexUtils.hexStringToInt(data[6])
                                logging("${device.displayName} mode Code=${mode}")
                                switch (mode){
                                    case '2':
                                        sendEvent(name: "thermostatMode", value: "off" , descriptionText:"Holiday Mode")
                                    break
                                    case '0':
                                        sendEvent(name: "thermostatMode", value: "auto" , descriptionText:"Using internally programmed schedule")
                                    break
                                    case '1':
                                        sendEvent(name: "thermostatMode", value: "heat" , descriptionText:"Manual Mode")
                                    break
                                }
                            break
// battery --- DEV
                            case '1502':
                                //String values = data.collect{c -> HexUtils.hexStringToInt(c)}
                                String batt = HexUtils.hexStringToInt(data[-1])
                            state.batdev = batt //dv to see it it is ever peported
                                logging("${device.displayName} battery ${batt}")
                                sendEvent(name: "battery", value: batt, unit:"%", descriptionText: "reported from 1502")
                            break
                            case '6702':
                                //String values = data.collect{c -> HexUtils.hexStringToInt(c)}
                                String batt = (HexUtils.hexStringToInt(data[-1]).toFloat() /3) *10
                                logging("${device.displayName} battery ${batt}")
                                sendEvent(name: "battery", value: batt, unit:"%", descriptionText: "reported from 6702" )
                                // 6702 - [68, 13, 103, 2, 0, 4, 0, 0, 0, 35] ?3.5 volt?
                                // 6702 - [126, 162, 103, 2, 0, 4, 0, 0, 0, 30] ?3 volt?
                            break
                            
                            case '0D05': // battery low warning
                            String battminmax = HexUtils.hexStringToInt(data[-1])
                            logging("${device.displayName} battery $data")
                            if (battminmax == "0"){
                                    if (device.currentValue("battery") == null || device.currentValue("battery") == 0 ) {
                                        sendEvent(name: "battery", value: 100, unit:"%" )
                                    }
                            }
                            else if (battminmax == "10" ||battminmax == "16"){//10 for low 
                                sendEvent(name: "battery", value: 0, unit:"%", descriptionText: "battery value $battminmax" )
                            }
                            break
                            case "2202": //Moes Low battery warning? -- Dev
                                logging("${device.displayName} Unknown Moes ${commandType} , data ${msgMap["data"]}")
                            break
                            case '6D02': // Valve position
                                String valve = HexUtils.hexStringToInt(data[-1])
                                logging("${device.displayName} valve position ${valve}")
                                sendEvent(name: "valve", value: valve, unit: "%", descriptionText: "Valve open ${valve}%")
                            break
        //// Temperature correction reporting ---DEV   
                            case '2C02': //Temperature correction reporting
                                String temperatureCorr = HexUtils.hexStringToInt(data[9])/ 10
                                logging("${device.displayName} Temp correction reporting DEV STILL, ${temperatureCorr}, data ${msgMap["data"]}")
                            break
         // Child lock --- DEV                   
                            case '0701': // Child lock
                                String locked = HexUtils.hexStringToInt(data[6])
                                logging("${device.displayName} child lock ${commandType}, ${locked} 1 - is locked 0 is unlocked")
                                switch (locked){
                                    case '0':
                                        sendEvent(name: "childLock", value: "off" )
                                    break
                                    case '1':
                                    sendEvent(name: "childLock", value: "on")
                                    break
                                }
                            break
                            case '7401': // auto lock setting A3
                                String autolock = HexUtils.hexStringToInt(data[6])
                                switch (autolock){
                                    case '0':
                                        logging("${device.displayName} Auto lock A3 Off")
                                        sendEvent(name: "autolock", value: "off")
                                    break
                                    case '1':
                                        logging("${device.displayName} Auto lock A3 On ather 10min")
                                        sendEvent(name: "autolock", value: "on")
                                    break
                                }
                            break
                            case '6800': //window open detection
                                String WinTemp = HexUtils.hexStringToInt(data[7])
                                String WinMink = HexUtils.hexStringToInt(data[8])
                                logging("${device.displayName} window open detection ${WinTemp}deg in ${WinMin}min will trigger shutdown")
                                sendEvent(name: "WindowOpenDetection", value: "${WinTemp}deg in ${WinMin}min")
                            break
                            
                           case '6902': //boost -- Dev
                                String values = data.collect{c -> HexUtils.hexStringToInt(c)}
                                logging("${device.displayName} boost ${values}")
                            break
                            
                            case '7000': // schedule setting aka Auto mode -- Dev
                                values = data.collect{c -> HexUtils.hexStringToInt(c)}
                                logging("${device.displayName} schedual P1 ${data[6]}:${data[7]} = ${data[8]}deg , ${data[9]}:${data[10]} = ${data[11]}deg ,more ${data} ")
                                state.SchduleP1 = "${values[6]}:${values[7]} = ${values[8]}deg , ${values[9]}:${values[10]} = ${values[11]}deg ,more ${values}"
                            break
                            case '7001': // schedule setting aka Auto mode -- Dev
                                values = data.collect{c -> HexUtils.hexStringToInt(c)}
                                logging("${device.displayName} schedual P2 ${data[6]}:${data[7]} = ${data[8]}deg , ${data[9]}:${data[10]} = ${data[11]}deg ,more ${data} ")
                                state.SchduleP2 = "${values[6]}:${values[7]} = ${values[8]}deg , ${values[9]}:${values[10]} = ${values[11]}deg ,more ${values}"
                            break
                            case '7100': // schedule setting aka Auto mode -- Dev
                                values = data.collect{c -> HexUtils.hexStringToInt(c)}
                                logging("${device.displayName} schedual P3? ${data[6]}:${data[7]} = ${data[8]}deg , ${data[9]}:${data[10]} = ${data[11]}deg ,more ${data} ")
                                state.SchduleP3 = "${values[6]}:${values[7]} = ${values[8]}deg , ${values[9]}:${values[10]} = ${values[11]}deg ,more ${values}"
                            break
                            
// 0x7502 away preset number of days                            
                            case '7502':
                            logging("${device.displayName} away preset number of days ${HexUtils.hexStringToInt(data[-1])} ")
                            break
*/
///                            
                            default:
                                String values = data.collect{c -> HexUtils.hexStringToInt(c)}
                                logDebug "${device.displayName} other EF00 cluster - ${commandType} - Values: ${values} // Data: ${data}"
                                break
                        }
                    }
                    else { 
                        // found data in map of, data:[02, 19]], data:[00, 00]]
                        //logging("other cluster EF00 but map null- ${data}")
                    }
                    break
                
                /////////////////////////////////////////////////////////////////////////////////////////mc
                default:
                    //log.debug "Unhandled Event IGNORE THIS - description:${description} | msgMap:${msgMap}"
                    break
            }
            break
    }
    msgMap = null
    return cmd
}

def logsOff(){
    logWarn "debug logging disabled..."
    device.updateSetting("logEnable",[value:"false",type:"bool"])
}

// from markus toolbox driver
def zigbee_generic_decodeZigbeeData(String value, String cTypeStr, boolean reverseBytes=true) {
    List values = value.split("(?<=\\G..)")
    values = reverseBytes == true ? values.reverse() : values
    Integer cType = Integer.parseInt(cTypeStr, 16)
    Map rMap = [:]
    rMap['raw'] = [:]
    List ret = zigbee_generic_convertStructValue(rMap, values, cType, "NA", "NA")
    return ret[0]["NA"]
}


//end markus toobox ////////////////////


boolean isMoesModel(String manufacturer=null) {
    manufacturer = manufacturer != null ? manufacturer : getDeviceDataByName('manufacturer')
    switch(manufacturer) {
        case "_TZE200_zion52ef":
            return true
            break
        default:
            return false
    }
}

////////////////////////////////////////////////////////////////////////////

def refresh() {
    def dp = "0302"
    def fn = "0"
    def data = "00" // ??
    
    logDebug "refresh"
    zigbee.readAttribute(0 , 0 )
    //zigbee.readAttribute(0, 0, 770 )
    //zigbee.readAttribute(0x0000, CLUSTER_TUYA) //get setting but not temparture
    //zigbee.readAttribute(0x0302, CLUSTER_TUYA) 
    //zigbee.configureReporting(0x0000, CLUSTER_TUYA)
    //zigbee.readAttribute(CLUSTER_TUYA, 0x0402) 
    
    //configureReporting(java.lang.Integer, java.lang.Integer, java.lang.Integer, java.lang.Integer, java.lang.Integer), 
    //zigbee.readAttribute(0x0000, 0x0402)
    
    
//    zigbee.readAttribute(0x0000, 0x0005) //encoding: 42 parse 51 ELSE true Model Name Received 0000_0005
//    zigbee.readAttribute(0x0000, 0x0004)
   //zigbee.readAttribute(0x0000, 0x0000) // 0000 0000
    
    // nothing zigbee.readAttribute(0x0000, CLUSTER_TUYA, [:] ) 
    // nothing zigbee.readAttribute(0x0402,0x0000) 
    // error   zigbee.readAttribute(0x0000)
    // nothing zigbee.readAttribute(0x0000, 0x0021)
    
    //sendTuyaCommand(dp,fn,data)
    // ????????private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 }
//    return  [
//            "he rattr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0000  0x0005 {}","delay 600", 
//            "he rattr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0000, 0x0004 {}"
//    ]
}    



///////////// commands ///////////////
private sendTuyaCommand(dp, fn, data) { // everything goes through here
	//log.info "sending ${zigbee.convertToHexString(rand(256), 2)}=${dp},${fn},${data}"
	zigbee.command(CLUSTER_TUYA, SETDATA, "00" + zigbee.convertToHexString(rand(256), 2) + dp + fn + data)
}

private getCLUSTER_TUYA() { 0xEF00 }
private getSETDATA() { 0x00 }
private rand(n) { return (new Random().nextInt(n))} 

///
def close ( ) 
{
  logInfo "Close valve command received."
  
  if (device.currentValue ("valve", true) == "closed")
  {  // it is already closed - let's ignore the command
    logInfo ("Close valve command ignored - valve already closed!")
    sendStatusEvent ("Close command ignored - valve already closed")
  }
  else
  {  // it is open - let's close it
    def dp = "0404"
    def fn = "0001"
    def data = "00" // off
    
    sendTuyaCommand(dp,fn,data)
  } 
//    off ()    
}


def open ()
{
  logInfo ("Open valve command received.")
  
  if (device.currentValue ("valve", true) == "open")
  {  // it is already open - let's ignore the command
    logInfo ("Open valve command ignored - valve already open!")
    sendStatusEvent ("Open command ignored - valve already open")
  }
  else
  {
    def dp = "0404"
    def fn = "0001"
    def data = "01" // auto mode, internal schdual
    
    sendTuyaCommand(dp,fn,data)
  }
}


def sendStatusEvent (payload)
{
  sendEvent (name: "status", value: payload)
}

///

def sendEventProgress (name, value, totalTime, stepTime)
{
    def dot = ""
    def progress = value.trim() + " "
    
    for (int thisTime = 0; thisTime <= totalTime; thisTime += stepTime)
    {
        sendEvent (name: "${name}", value: "${progress += "."}" )
        //pauseExecution (stepTime)
    }
}

void updateDataFromSimpleDescriptorData(List<String> data) {
    Map<String,String> sdi = parseSimpleDescriptorData(data)
    if(sdi != [:]) {
        updateDataValue("endpointId", sdi['endpointId'])
        updateDataValue("profileId", sdi['profileId'])
        updateDataValue("inClusters", sdi['inClusters'])
        updateDataValue("outClusters", sdi['outClusters'])
        getInfo(true, sdi)
    } else {
        logWarn("No VALID Simple Descriptor Data received!")
    }
    sdi = null
}


List zigbee_generic_convertStructValue(Map r, List values, Integer cType, String cKey, String cTag) {
    String cTypeStr = cType != null ? integerToHexString(cType, 1) : null
    switch(cType) {
        case 0x10:
            r["raw"][cKey] = values.take(1)[0]
            r[cKey] = Integer.parseInt(r["raw"][cKey], 16) != 0
            values = values.drop(1)
            break
        case 0x18:
        case 0x20:
            r["raw"][cKey] = values.take(1)[0]
            r[cKey] = Integer.parseInt(r["raw"][cKey], 16)
            values = values.drop(1)
            break
        case 0x19:
        case 0x21:
            r["raw"][cKey] = values.take(2).reverse().join()
            r[cKey] = Integer.parseInt(r["raw"][cKey], 16)
            values = values.drop(2)
            break
        case 0x1A:
        case 0x22:
            r["raw"][cKey] = values.take(3).reverse().join()
            r[cKey] = Integer.parseInt(r["raw"][cKey], 16)
            values = values.drop(3)
            break
        case 0x1B:
        case 0x23:
            r["raw"][cKey] = values.take(4).reverse().join()
            r[cKey] = Long.parseLong(r["raw"][cKey], 16)
            values = values.drop(4)
            break
        case 0x1C:
        case 0x24:
            r["raw"][cKey] = values.take(5).reverse().join()
            r[cKey] = Long.parseLong(r["raw"][cKey], 16)
            values = values.drop(5)
            break
        case 0x1D:
        case 0x25:
            r["raw"][cKey] = values.take(6).reverse().join()
            r[cKey] = Long.parseLong(r["raw"][cKey], 16)
            values = values.drop(6)
            break
        case 0x1E:
        case 0x26:
            r["raw"][cKey] = values.take(7).reverse().join()
            r[cKey] = Long.parseLong(r["raw"][cKey], 16)
            values = values.drop(7)
            break
        case 0x1F:
        case 0x27:
            r["raw"][cKey] = values.take(8).reverse().join()
            r[cKey] = new BigInteger(r["raw"][cKey], 16)
            values = values.drop(8)
            break
        case 0x28:
            r["raw"][cKey] = values.take(1).reverse().join()
            r[cKey] = convertToSignedInt8(Integer.parseInt(r["raw"][cKey], 16))
            values = values.drop(1)
            break
        case 0x29:
            r["raw"][cKey] = values.take(2).reverse().join()
            r[cKey] = (Integer) (short) Integer.parseInt(r["raw"][cKey], 16)
            values = values.drop(2)
            break
        case 0x2B:
            r["raw"][cKey] = values.take(4).reverse().join()
            r[cKey] = (Integer) Long.parseLong(r["raw"][cKey], 16)
            values = values.drop(4)
            break
        case 0x30:
            r["raw"][cKey] = values.take(1)[0]
            r[cKey] = Integer.parseInt(r["raw"][cKey], 16)
            values = values.drop(1)
            break
        case 0x31:
            r["raw"][cKey] = values.take(2).reverse().join()
            r[cKey] = Integer.parseInt(r["raw"][cKey], 16)
            values = values.drop(2)
            break
        case 0x39:
            r["raw"][cKey] = values.take(4).reverse().join()
            r[cKey] = parseSingleHexToFloat(r["raw"][cKey])
            values = values.drop(4)
            break
        case 0x42:
            Integer strLength = Integer.parseInt(values.take(1)[0], 16)
            values = values.drop(1)
            r["raw"][cKey] = values.take(strLength)
            r[cKey] = r["raw"][cKey].collect { 
                (char)(int) Integer.parseInt(it, 16)
            }.join()
            values = values.drop(strLength)
            break
        default:
            throw new Exception("The Struct used an unrecognized type: $cTypeStr ($cType) for tag 0x$cTag with key $cKey (values: $values, map: $r)")
    }
    return [r, values]
}
String integerToHexString(BigDecimal value, Integer minBytes, boolean reverse=false) {
    return integerToHexString(value.intValue(), minBytes, reverse=reverse)
}

String integerToHexString(Integer value, Integer minBytes, boolean reverse=false) {
    if(reverse == true) {
        return HexUtils.integerToHexString(value, minBytes).split("(?<=\\G..)").reverse().join()
    } else {
        return HexUtils.integerToHexString(value, minBytes)
    }
    
}


//
// Logging stuff
//

def logDebug (message) { if ( debugLogging ) log.debug (message) }
def logInfo  (message) { if ( infoLogging ) log.info (message) }
def logWarn  (message) { log.warn (message) }

Hope it works with you guys!

2 Likes

should post that to HPM

There is a new one .. :slight_smile:
Can you test it with your device?

I mean when you have time only.. I am trying to consolidate several different types of Tuya valves in one driver, and can't test the TS0601 model myself.

Edit: the temporary link to the driver development version in Github was replaced with a link to the HE community thread for the new driver.

1 Like

What is your valve power-on behavior ?

After powering on, does the valve go to an open or closed state or stay in the last state?

I’m traveling right now, so I can’t test it.

I’ll be back home by May, 13th and I’ll test it.

I promise!

1 Like

As far I remember, it stays at the same status as before.

But when I get back home I’ll test it.

1 Like

Thank you @maffpt , there is no rush...

Have a pleasant and safe trip!

Dude, you're a rockstar. You managed to get all 63 versions(exaggeration but there are MANY) of that Tuya valve. I hate how Tuya does that, making many versions of the same exact thing. If I do buy any Tuya stuff it has to be right at the time someone reports it as working, and even then it's a crapshoot if it'll work, and you'll never get a refund because they're specified to work with Tuya hub only.

2 Likes

The problem with the many different versions of a product that looks the same is more complicated... These devices are actually not made by Tuya, but by many other different companies. What is in common is that these different implementations or modifications are certified by Tuya to work with their Cloud platform. So no matter how the Zigbee commands are implemented, the product will always work when used with Tuya Zigbee gateway and the gateway communicates to Tuya Cloud servers. This allows for small startup companies to come very quickly on the market - use the Tuya IOT developments platform, pass the Tuya certification, pay Tuya a tax for using their cloud platform - and your product is ready to be sold to millions of SmartLife (and many other white labels) system users.

What makes it complicated here is that we are trying to bypass the Cloud communication, thus we can not benefit from the abstraction layer that Tuya Cloud API provides. And we have to deal with the lower level Zigbee communication specifics for each manufacturer.

2 Likes

It was pointed out recently that Hubitat met with Tuya and they declined to help sort through their id clusters and only wanted cloud implementation so they could collect data. HE of course said NO rather emphatically. They weren't going to let that happen. Keep up the good work sir!

2 Likes

I'm back!

As I remembered, the valve stays at the same position when powered off/on.

Probably, analysing the Zigbee clusters could be possible to identify other parameters - but I don't know how to do it.

Hope it helps anyway.

1 Like

Thank you for the information!. Staying at the same position when powered off/on is probably the best default behavior for most use cases. I wasn't sure which is the default setting, as I have experimented with changing it from the SmartLife app.

I have all the needed technical information on how to make the 'power on behavior' configurable from the HE driver, and hopefully, will have the time to implement it in the driver in the next weeks.

1 Like

Great news!

The driver I use is adaptation of a TRV valve and a lot of things there make no sense for a water/gas valve - ao, your driver surely will be welcome!

You try using the dedicated driver from this link Tuya Zigbee water/gas valve - #17 by kkossev


I see now it needs some more code cleanup, but it works fine with my Tuya valve.

I recently received one of the old style Tuya Zigbee valves from Aliexpress. It paired quickly and the Sinope Valve driver worked fine.

I'll revive this thread since this is where I found the info i was looking for.

I have 4 of those black squarish valves from Ali Express. Using the Sinopé driver successfully since it's the only built-in ZigBee valve driver. However, I'd be keen to try the custom Tuya one from kkossev mentioned above. I assume it's available from the Package Manager?

The reason i was searching the forum is that this morning 2 of the valves were shut, and i could not figure it why. But the explanations above shed some light: We had a power outage during the night. Those valves were not all purchased at the same time, so it could be that they are different models even though they look identical, and one model closes the valve when powered on.
This behaviour is not a good idea in my opinion. It shut the gas to the house and i had to relight the pilot light.

A final thought, i like the design of the valve from the original post. The clamp seems more sturdy. I hope it works with that driver as well.

Anyway, thanks for the driver kkossev.

Hi Stephane,

The Tuya valve driver is published here : [RELEASE] Tuya Zigbee Valve driver (w/ healthStatus)

There were a lot of changes in the latest developments branch version related to the more complex water irrigation valves/timers, but the older version that is currently installed from HPM supports the power-on behavior configuration :

This driver also adds some additional monitoring of the device health, firing an healthStatus offline event if the valve did not report its presence for more than 12 hours.

3 Likes