/**
 *  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"
        
        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"
}
    
    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: "infoLogging", type: "bool", title: "Enable info logging", defaultValue: true
    }
}
    

ArrayList<String> parse(String description) {
    //log.debug "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)]
                    ]
                log.warn "encoding: 42 parse 37 IF true"
            } 
            else {
                msgMap = zigbee.parseDescriptionAsMap(description) //modle name
                log.warn "encoding: 42 parse 51 ELSE true"
            }
        } 
        else {
            msgMap = zigbee.parseDescriptionAsMap(description)
        }
    
        if(msgMap.containsKey("encoding") && msgMap.containsKey("value") && msgMap["encoding"] != "41" && msgMap["encoding"] != "42") {
            log.warn "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
            }
            log.warn "line 64 section used"
        }

    switch(msgMap["cluster"] + '_' + msgMap["attrId"]) {
        case "0000_0001":
            log.trace("Application ID Received")
            if(msgMap['value']) {
                updateDataValue("application", msgMap['value'])
            }
            break
        case "0000_0004":
        log.trace("Manufacturer Name Received ${msgMap['value']}")
            if(msgMap['value']) {
                updateDataValue("manufacturer", msgMap['value'])
            }
            break
        case "0000_0005":
            log.trace("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":
                    log.trace("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]
                        switch(commandType){
//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)}
                                log.debug "${device.displayName} other EF00 cluster - ${commandType} - ${values} ${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(){
    log.warn "debug logging disabled..."
    device.updateSetting("logEnable",[value:"false",type:"bool"])
}
// from markus toolbox driver
Map unpackStructInMap(Map msgMap, String originalEncoding="4C") {
     
    msgMap['encoding'] = originalEncoding
    List<String> values = msgMap['value'].split("(?<=\\G..)")
    logging("unpackStructInMap() values=$values", 1)
    Integer numElements = Integer.parseInt(values.take(2).reverse().join(), 16)
    values = values.drop(2)
    List r = []
    Integer cType = null
    List ret = null
    while(values != []) {
        cType = Integer.parseInt(values.take(1)[0], 16)
        values = values.drop(1)
        ret = zigbee_generic_convertStructValueToList(values, cType)
        r += ret[0]
        values = ret[1]
    }
    if(r.size() != numElements) throw new Exception("The STRUCT specifies $numElements elements, found ${r.size()}!")
     
    msgMap['value'] = r
    return msgMap
}
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"]
}

// BEGIN:getLoggingFunction()
private boolean logging(message) {
    boolean didLogging = false
     
    Integer logLevelLocal = 0
    if (infoLogging == null || infoLogging == true) {
        log.info "$message"
        didLogging = true
    }
    return didLogging
}
// END:  getLoggingFunction()

//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" // ??
    
    log.debug "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 off() { // this is away setting heat point
    def dp = "0404"
    def fn = "0001"
    def data = "00" // off
    if(isMoesModel() == true)
    {
        dp = "0204"
        data = "02" // off
    }
	log.info "Off command, this is away setting heat point"
    sendTuyaCommand(dp,fn,data)
}

def heat() { //manual and hubitat control
    def dp = "0404"
    def fn = "0001"
    def data = "02" // on
    if(isMoesModel() == true)
    {
        dp = "0204"
        data = "01" // on
    }
	log.info "On/heat/manual command"
    sendTuyaCommand(dp,fn,data)
}

def auto() {
    if(isMoesModel() == true)
    {
        log.info "Auto mode is not available for Moes TRV. => Defaulting to heat mode instead."
        heat()
    }
    else{
        def dp = "0404"
        def fn = "0001"
        def data = "01" // auto mode, internal schdual
        log.info "auto mode, internal schdual command ${zigbee.convertToHexString(rand(256), 2)}${dp}${fn}${data}"
        sendTuyaCommand(dp,fn,data)
    }
}

def setHeatingSetpoint(preciseDegrees) {
	if (preciseDegrees != null) {
        def dp = "0202"
        def fn = "00"
        def SP = preciseDegrees *10
        def X = (SP / 256).intValue()
        def Y = SP.intValue() % 256

        if(isMoesModel() == true)
        {
            dp = "1002"
            SP = preciseDegrees *2
            log.info "Moes Model"
        }

        def data = "040000" + zigbee.convertToHexString(X.intValue(), 2) + zigbee.convertToHexString(Y.intValue(), 2)
	    log.info "heating setpoint to ${preciseDegrees}"
	    sendTuyaCommand(dp,fn,data)
	}
}

def setThermostatMode(String value) {
    switch (value) {
        case "heat":
        case "emergency heat":
            return heat()
        
        case "eco":
        case "cool":
            return eco()
        
        case "auto":
            return auto()
        
        default:
            return off()
    }
}

def updated() {
    sendEvent(name: "supportedThermostatFanModes", value: [""])
    sendEvent(name: "supportedThermostatModes", value: ["off", "heat", "auto"] )
    sendEvent(name: "thermostatFanMode", value: "off")
    log.info "${device.displayName} updated..."
}
def installed() {
    sendEvent(name: "supportedThermostatFanModes", value: [""])
    sendEvent(name: "supportedThermostatModes", value: ["off", "heat", "auto"] )
    sendEvent(name: "thermostatFanMode", value: "auto")
    
}
def configure(){    
    log.warn "configure..."
    runIn(1800,logsOff)    
	//binding to Thermostat cluster"
    // Set unused default values (for Google Home Integration)
    sendEvent(name: "coolingSetpoint", value: "30")
    sendEvent(name: "thermostatFanMode", value:"auto")
    updateDataValue("lastRunningMode", "heat") // heat is the only compatible mode for this device
    updated()

}
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 {
        log.warn("No VALID Simple Descriptor Data received!")
    }
    sdi = null
}


//unused commands and redirected
def eco() { //holiday
    log.info "eco mode is not available for this device. => Defaulting to off mode instead."
    off()
}

def cool() {
    log.info "cool mode is not available for this device. => Defaulting to eco mode instead."
    eco()
}

def emergencyHeat() {
    log.info "emergencyHeat mode is not available for this device. => Defaulting to heat mode instead."
	heat()
}

def setCoolingSetpoint(degrees) {
    log.info "setCoolingSetpoint is not available for this device"
}

def fanAuto() {
    log.info "fanAuto mode is not available for this device"
}

def fanCirculate(){
    log.info "fanCirculate mode is not available for this device"
}

def fanOn(){
    log.info "fanOn mode is not available for this device"
}

def setSchedule(JSON_OBJECT){
    log.info "setSchedule is not available for this device"
}

def setThermostatFanMode(fanmode){
    log.info "setThermostatFanMode is not available for this device"
}


List zigbee_generic_convertStructValueToList(List values, Integer cType) {
    Map rMap = [:]
    rMap['raw'] = [:]
    List ret = zigbee_generic_convertStructValue(rMap, values, cType, "NA", "NA")
    return [ret[0]["NA"], ret[1]]
}

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)
    }
    
}