Tuya Zigbee water/gas valve

That's too bad. Most of Markus's drivers do not have configure. With his drivers I hit "initialize" and that takes care of...something.
So even after hitting initialize, the valve will not close or report open/close? I'd ignore the debug info as well as Info Logging. Give it a few minutes to see if things work out

Nope - no sign of life ...

I disabled these options.

The strange thing is that the valve is sending a lot of messages to the hub - those messages were about incoming messages not properly handled (at least I think so ...).

I'll do.

Thanks again!

Just thought I'd try to awaken an old thread. Ever make progress with this? These are 20% the cost of the Sinope, and don't require a plumber to install. I'd like to give it a shot, but don't want to head down this road if there's nothing there.

EcoNet Controls EVC200-HCSML The Bulldog Valve Robot, Z-Wave Water Valve, Smart Home Controller Required, Easy to Install and No Plumbing Required, Valves up to 1.5" https://www.amazon.com/dp/B07DJZCFBH/ref=cm_sw_r_cp_api_glt_fabc_6X2BMR0PHE0XYE7F7PDE

Yes, those are great. I have one of those in a whole different application, and it works well. At least here in Canada, it's 4 times the price. Then again, if I value my time tinkering ...

And, as it turns out I do not value my time, I decided to take a chance on AliExpress. This valve seems to work perfectly. During pairing it was not able to figure out what it was, although it paired fine first try. I set it to the Sinope Water Valve, and it's been perfect ever since.

Probably generic valve would work as well. You should capture the fingerprint for @mike.maxwell

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