/* groovylint-disable BuilderMethodWithSideEffects, CompileStatic, DuplicateListLiteral, DuplicateMapLiteral, DuplicateNumberLiteral, DuplicateStringLiteral, FactoryMethodName, ImplicitClosureParameter, ImplicitReturnStatement, InsecureRandom, LineLength, MethodCount, MethodParameterTypeRequired, MethodReturnTypeRequired, NestedBlockDepth, NoDef, NoJavaUtilDate, ParameterName, PublicMethodsBeforeNonPublicMethods, UnnecessaryGetter, UnnecessaryPackageReference, VariableTypeRequired */
/*
 *  revision 1.0.0 - 2021-05-25 - martinkura - latest original driver version update
 *  revision 1.0.1 - 2022-02-22 - kkossev - added Moes 4-Gang Switch / ZTS-EU4
 *  revision 1.0.2 - 2022-02-27 - kkossev - added more Tuya fingerprints for 1,2,3 and 4 gangs TS0601 wall switches
 *  revision 1.0.3 - 2022-09-26 - kkossev - added Zemismart 6 Gangs Wall Light Switch
 *  revision 1.0.4 - 2022-10-12 - kkossev - _TZE200_tz32mtza bug fix; code cleanup
 *  revision 1.0.5 - 2023-03-16 - kkossev - added OZ Smart 1-2-3-4 gang switches _TZE200_gbagoilo _TZE200_nh9m9emk _TZE200_go3tvswy _TZE200_mexisfik
 *  revision 1.0.6 - 2023-04-24 - kkossev - added importUrl; _TZE200_aqnazj70 _TZE200_wunufsil _TZE200_oisqyl4o _TZE200_atpwqgml
 *  revision 1.0.7 - 2023-07-18 - kkossev - added _TZE200_7deq70b8 (@pabutterworth)
 *  revision 1.0.8 - 2023-11-20 - kkossev - added TS0601 _TZE204_dqolcpcp (@alex1) (only the first 6 relays should be working)
 *  revision 1.0.9 - 2024-01-02 - kkossev - added TS0601 _TZE200_r731zlxk
 *  revision 1.1.0 - 2024-03-08 - kkossev - Groovy linting; addeed Zemismart 4-gang switch _TZE200_1n2kyphz; _TZE200_shkxsgis; _TZE204_shkxsgis; added testParse; Tuya cluster 0xEF00 data size check;
 *  revision 1.1.1 - 2024-04-03 - kkossev - added TS0601 _TZE200_m*******j @Joao;
 *  revision 1.1.2 - 2024-04-28 - kkossev - removed the T3E fingerprint; added _TZE204_aagrxlbd;
 *  revision 1.1.3 - 2024-05-06 - kkossev - added _TZE204_xjknlqz8
 *  revision 1.1.4 - 2024-05-17 - hhorigian - added _TZE204_4cl0dzt4
 *  revision 1.1.5 - 2024-06-28 - kkossev - added TS0006 _TYZB01_ltundz9m _TZ3000_jyupj3fw
 *  revision 1.1.6 - 2024-07-22 - hhorigian - added _TZE204_rkbxtclc, _TZE204_zpvusbtv, _TZE204_rzdkn5rxc
 *  revision 1.1.7 - 2024-08-20 - kkossev - added _TZE204_dvosyycn (limited to 6 relays only);
 *  revision 1.2.0 - 2024-08-20 - kkossev - added syncTuyaDateTime()
 *  revision 1.3.0 - 2024-08-30 - kkossev/hhorigian - TratoZemismartKeypad configuration options
 *  revision 1.3.1 - 2024-09-07 - kkossev - added Mercator Ikuü six switch _TZE200_wnp4d4va @hpgurgel
 *  revision 1.3.2 - 2024-09-29 - kkossev/hhorigian - adding _TZE204_hwyydvqm (only thhe first switch is working!)
 *  revision 1.3.3 - 2024-10-23 - kkossev - adding TS0601 _TZE204_lmgrbuwf (NOVADIGITAL)
 *
*/

static String version() { '1.3.3' }
static String timeStamp() { '2024/10/23 9:39 PM' }

import hubitat.device.HubAction
import hubitat.device.Protocol
import groovy.transform.Field

@Field static final Boolean _DEBUG = false

metadata {
    definition(name: 'Moes ZigBee Wall Switch 1/2/3-Gang', namespace: 'Moes 1.31', author: 'Martin Kura', importUrl: 'https://raw.githubusercontent.com/martinkura-svk/Hubitat/main/Moes%20ZigBee%20Wall%20Switch') {
        capability 'Initialize'
        capability 'Actuator'
        capability 'Refresh'
        capability 'Switch'

        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_amp6tsvy', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes 1-Gang Switch / ZTS-EU1'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_oisqyl4o', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'42', deviceJoinName: 'No Neutral Push Button Light Switch 1 Gang'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_g1ib5ldv', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes 2-Gang Switch / ZTS-EU2'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_wunufsil', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'42', deviceJoinName: 'No Neutral Push Button Light Switch 2 Gang'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_tz32mtza', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes 3-Gang Switch / ZTS-EU3'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_atpwqgml', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'42', deviceJoinName: 'No Neutral Push Button Light Switch 3 Gang'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_k6jhsr0q', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes 4-Gang Switch / ZTS-EU4'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_aqnazj70', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'42', deviceJoinName: 'Touch Switch 4 Gang No Neutral'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_9mahtqtg', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Zemismart 6 Gangs Wall Light Switch'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_wnp4d4va', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'42', deviceJoinName: 'Mercator Ikuü Six Switch'             // https://community.hubitat.com/t/driver-needed-for-a-6-gang-mercator-ikuu-zigbee-switch/142341/3?u=kkossev
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_r731zlxk', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Zemismart 6 Gangs Wall Light Switch'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_gbagoilo', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'46', deviceJoinName: 'OZ Smart Single Light Switch'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_nh9m9emk', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'46', deviceJoinName: 'OZ Smart Double Light Switch'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_go3tvswy', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'46', deviceJoinName: 'OZ Smart Triple Light Switch'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_mexisfik', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'46', deviceJoinName: 'OZ Smart Quad Light Switch'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_7deq70b8', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes 2-gang switch'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_dqolcpcp', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Tuya 12-way Relay Moduleh'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_1n2kyphz', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Zemismart 4-gang switch'              // https://www.aliexpress.com/item/1005003972289459.html
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_shkxsgis', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Zemismart 4-gang switch'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_shkxsgis', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Zemismart 4-gang switch'
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_aagrxlbd', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Tuya 4-gang switch'                   // @Gabriel
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_xjknlqz8', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Tuya 6-gang switch'                   // @Vartan BR - TrTRON
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_4cl0dzt4', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Zemismart 6 Gangs Wall Light Switch'  // @Vartan BR - QA
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_rkbxtclc', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Keypad 3-gang switch'                 // @Vartan BR - TR
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_zpvusbtv', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Zemismart 2-gang switch'              // @Vartan BR - QA
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_rzdkn5rx', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Zemismart 1-gang switch'              // @Vartan BR - QA
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_dvosyycn', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Tuya ZXYH 8IN/8OUT board'             // https://community.hubitat.com/t/device-labels-changing-on-reboot/141754/5?u=kkossev
        fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_lmgrbuwf', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'NOVADIGITAL 1 socket 2 switches'      // https://www.novadigitalsmart.com.br/produtos/interruptor-tecla-fisica-tomada-4x2-ntzb-01-02-w-b
    }
    
    attribute 'switchLightMode', 'enum', ['off', 'on', 'position']
    attribute 'powerOnBehavior', 'enum', ['off', 'on', 'last state']
    attribute 'lastCheckin', 'string'
    attribute 'backlightLevel', 'number'
    attribute 'childLock', 'enum', ['off', 'on']
    attribute 'backlightMode', 'enum', ['off', 'on']
    attribute 'lightIndicatorMode', 'enum', ['none', 'lit when on', 'lit when off']
    attribute 'onColor', 'enum', ['Red', 'Blue', 'Green', 'White', 'Yellow', 'Magenta', 'Cyan', 'WarmWhite', 'WarmYellow']
    attribute 'offColor', 'enum', ['Red', 'Blue', 'Green', 'White', 'Yellow', 'Magenta', 'Cyan', 'WarmWhite', 'WarmYellow']


    if (_DEBUG == true) {
        command 'testParse', [[name:'val', type: 'STRING', description: 'description', constraints: ['STRING']]]
        command 'queryAllTuyaDP'
        command 'test'
        command 'tuyaTest', [[name:'dpCommand', type: 'STRING', description: 'Tuya DP Command', constraints: ['STRING']], [name:'dpValue',   type: 'STRING', description: 'Tuya DP value', constraints: ['STRING']], [name:'dpType',    type: 'ENUM',   constraints: ['DP_TYPE_VALUE', 'DP_TYPE_BOOL', 'DP_TYPE_ENUM'], description: 'DP data type']]    
    }

    preferences {
        input(name: 'infoLogging', type: 'bool', title: ("<b>Enable info logging</b>"), description: '', defaultValue: true)
        input(name: 'debugLogging', type: 'bool', title: ("<b>Enable debug logging</b>"), description: '', defaultValue: true)
        if (device) {
            if (isTratoZemismartKeypad()) {
                input(name: 'backlightLevel', type: 'number', title: ("<b>Led: Backlight Level</b>"), description: ("Select led backlight level 0-100 (default: 50)"), defaultValue: 50, range: [0..100])
                input(name: 'backlightMode', type: 'enum', title: ("<b>Led: Backlight Mode</b>"), description: ("Select backlight mode (default: on)"), options: ['off', 'on'], defaultValue: 'on')
                input(name: 'lightIndicatorMode', type: 'enum', title: ("<b>Led: Indicator Mode</b>"), description: ("Select led indicator mode (default: lit when on)"), options: ['none', 'lit when on', 'lit when off'], defaultValue: 'lit when on')
                input(name: 'onColor', type: 'enum', title: ("<b>Led: On Color</b>"), description: ("Select led color when switch is ON(default: Blue)"), options: ['Red', 'Blue', 'Green', 'White', 'Yellow', 'Magenta', 'Cyan', 'WarmWhite', 'WarmYellow'], defaultValue: 'Blue')
                input(name: 'offColor', type: 'enum', title: ("<b>Led: Off Color</b>"), description: ("Select led color when switch is OFF(default: White)"), options: ['Red', 'Blue', 'Green', 'White', 'Yellow', 'Magenta', 'Cyan', 'WarmWhite', 'WarmYellow'], defaultValue: 'White')
                input(name: 'childLlock', type: 'enum', title: ("<b>Child Lock</b>"), description: ("Select child lock mode (default: off)"),  options: ['off', 'on'], defaultValue: 'off')

            }
            else {
                input(name: 'switchLightMode', type: 'enum', title: ("<b>Switch Backlight Mode</b>"), description: ("Select type of backlight indicator (default: Position)"), options: ['off', 'on', 'position'], defaultValue: 'position')
            }
            input(name: 'powerOnBehavior', type: 'enum', title: ("<b>Switch Power On Behavior</b>"), description: ("Select relay renew state after AC failed (default: OFF)"), options: ['off', 'on', 'last state'], defaultValue: 'off')
        }
    }
}

boolean isTratoZemismartKeypad() {
    return device.getDataValue('manufacturer') in ['_TZE204_rzdkn5rx', '_TZE204_zpvusbtv', '_TZE204_rkbxtclc'/*, '_TZ3000_excgg5kb'*/]  // _TZ3000_excgg5kb for tests only - TOBEDEL!
}

void initializeVars() {
    device.updateSetting('switchLightMode', [type:'enum', value: isTratoZemismartKeypad() ? 'on' : 'position'])
    device.updateSetting('powerOnBehavior', [type:'enum', value:'off'])
    device.updateSetting('debugLogging', [type:'bool', value:'true'])
    device.updateSetting('infoLogging', [type:'bool', value:'true'])
}

def initialize() {
    if (infoLogging) { log.info 'Initializing...' }
    log.warn 'Debug logging will be automatically disabled after 24 hours!'
    initializeVars()
    setupChildDevices()
    if (debugLogging) { runIn(86400, logsOff) }
    refresh()
}

void logsOff() {
    log.warn 'Debug logging disabled...'
    device.updateSetting('debugLogging', [value:'false' ,type:'bool'])
}

def installed() {
    log.info 'Installing...'
    log.warn 'Debug logging will be automatically disabled after 24 hours!'
    setupChildDevices()
    initializeVars()
    if (debugLogging) { runIn(86400, logsOff) }
    refresh()
}

def updated() {
    List<String> cmds = []
    String dpValHex = ''
    int key = 0
    log.warn "debug logging is: ${debugLogging == true}"
    log.warn "description logging is: ${infoLogging == true}"
    if (debugLogging) { runIn(86400, logsOff) }
    if (debugLogging) { log.debug 'Parent updated' }
    cmds = switchLightModeConfig() + powerOnBehaviorConfig() /*+ refresh()*/
    if (isTratoZemismartKeypad()) {
        cmds += getTuyaCommand('66', DP_TYPE_VALUE, zigbee.convertToHexString(settings?.backlightLevel as int, 8))
        key = BacklightSwitchOptions.find { it.value == settings?.backlightMode }?.key
        cmds += getTuyaCommand('10', DP_TYPE_BOOL, zigbee.convertToHexString(key as int, 2))
        key = ChildLockOptions.find { it.value == settings?.childLlock }?.key
        cmds += getTuyaCommand('65', DP_TYPE_BOOL, zigbee.convertToHexString(key as int, 2))
        key = LightIndicatorOptions.find { it.value == settings?.lightIndicatorMode }?.key
        cmds += getTuyaCommand('0F', DP_TYPE_ENUM, zigbee.convertToHexString(key as int, 2))
        key = ColorOptions.find { it.value == settings?.onColor }?.key
        cmds += getTuyaCommand('67', DP_TYPE_ENUM, zigbee.convertToHexString(key as int, 2))
        key = ColorOptions.find { it.value == settings?.offColor }?.key
        cmds += getTuyaCommand('68', DP_TYPE_ENUM, zigbee.convertToHexString(key as int, 2))
        key = PowerOnBehaviorOptions.find { it.value == settings?.powerOnBehavior }?.key
        cmds += getTuyaCommand('0E', DP_TYPE_ENUM, zigbee.convertToHexString(key as int, 2))
        
    }
    if (infoLogging) { log.info 'Updated...' }
    sendZigbeeCommands(cmds)
}


private static int getCLUSTER_TUYA()          { 0xEF00 }
private static int getSETDATA()               { 0x00 }
private static int getSETTIME()               { 0x24 }
private static String getDP_TYPE_RAW()        { '01' }    // [ bytes ]
private static String getDP_TYPE_BOOL()       { '01' }    // [ 0/1 ]
private static String getDP_TYPE_VALUE()      { '02' }    // [ 4 byte value ]
private static String getDP_TYPE_STRING()     { '03' }    // [ N byte string ]
private static String getDP_TYPE_ENUM()       { '04' }    // [ 0-255 ]
private static String getDP_TYPE_BITMAP()     { '05' }    // [ 1,2,4 bytes ] as bits

// Parse incoming device messages to generate events
def parse(String description) {
    if (debugLogging) { log.debug "description: ${description}" }
    if (description?.startsWith('catchall:') || description?.startsWith('read attr -')) {
        Map descMap = zigbee.parseDescriptionAsMap(description)
        if (descMap?.clusterInt == CLUSTER_TUYA) {
            if (debugLogging) { log.debug "descMap: ${descMap}" }
            if ((descMap?.command in ['00', '01', '02']) && descMap?.data?.size() >= 7) {
                def switchFunc = (descMap?.data[2])
                def switchAttr = (descMap?.data[3])
                def switchState = (descMap?.data[6]) == '01' ? 'on' : 'off'
                if (switchFunc in ['01', '02', '03', '04', '05', '06'] && switchAttr == '01') {
                    def cd = getChildDevice("${device.id}-${switchFunc}")
                    if (cd == null) {
                        return createEvent(name: 'switch', value: switchState)
                    }
                    if (descMap?.command == '00') {
                        // switch toggled
                        cd.parse([[name: 'switch', value:switchState, descriptionText: "Child switch ${switchFunc} turned $switchState"]])
                    }
                    else if (descMap?.command in ['01', '02']) {
                        // report switch status
                        cd.parse([[name: 'switch', value:switchState, descriptionText: "Child switch ${switchFunc} is $switchState"]])
                    }
                    if (switchState == 'on') {
                        if (debugLogging) { log.debug 'Parent Switch ON' }
                        return createEvent(name: 'switch', value: 'on')
                    }
                    else if (switchState == 'off') {
                        def cdsOn = 0
                        // cound number of switches on
                        getChildDevices().each { child ->
                            if (getChildId(child) != switchFunc && child.currentValue('switch') == 'on') {
                                cdsOn++
                            }
                        }
                        if (cdsOn == 0) {
                            if (debugLogging) { log.debug 'Parent Switch OFF' }
                            return createEvent(name: 'switch', value: 'off')
                        }
                    }
                }
                else if (isTratoZemismartKeypad()) {
                    parseTratoZemismartKeypad(descMap)  // andy event will be generated there
                    return [:]
                }
                else {
                    if (debugLogging) { log.debug "unprocessed Tuya command switchFunc: 0x${switchFunc} " }
                }
            }
            else if (descMap?.command == '24') {
                syncTuyaDateTime()
            }
        }
    }
}

void parseTratoZemismartKeypad(Map descMap) {
    if (descMap?.data?.size() < 7) {
        if (debugLogging) { log.warn "parseTratoZemismartKeypad: unprocessed descMap = ${descMap}" }
        return
    }
    int dataLen = descMap?.data.size()
    //log.warn "dataLen=${dataLen}"
    //def transid = zigbee.convertHexToInt(descMap?.data[1])           // "transid" is just a "counter", a response will have the same transid as the command
    if (dataLen <= 5) {
        if (debugLogging) { log.warn "unprocessed short Tuya command response: dp_id=${descMap?.data[3]} dp=${descMap?.data[2]} fncmd_len=${fncmd_len} data=${descMap?.data})" }
        return
    }
    for (int i = 0; i < (dataLen - 4); ) {
        int dp = zigbee.convertHexToInt(descMap?.data[2 + i])          // "dp" field describes the action/message of a command frame
        int dp_id = zigbee.convertHexToInt(descMap?.data[3 + i])       // "dp_identifier" is device dependant
        int fncmd_len = zigbee.convertHexToInt(descMap?.data[5 + i])
        int fncmd = getTuyaAttributeValue(descMap?.data, i)            //
        processTuyaDP(descMap, dp, dp_id, fncmd)
        i = i + fncmd_len + 4
    }
}

@Field static final Map<Integer,String> SwitchStateOptions = [0: 'off', 1: 'on']
@Field static final Map<Integer,String> ChildLockOptions = [0: 'off', 1: 'on']                          // 0x65
@Field static final Map<Integer,String> PowerOnBehaviorOptions = [0: 'off', 1: 'on', 2: 'last state']   // 0x0E   'Turn power Off', 'Turn power On', 'Restore previous state'],
@Field static final Map<Integer,String> BacklightSwitchOptions = [0: 'off', 1: 'on']      // 0x10
@Field static final Map<Integer,String> LightIndicatorOptions = [0: 'none', 1: 'lit when on', 2: 'lit when off']        // 0x0F
@Field static final Map<Integer,String> ColorOptions =  [0: 'Red', 1: 'Blue', 2: 'Green', 3: 'White', 4: 'Yellow', 5: 'Magenta', 6: 'Cyan', 7: 'WarmWhite', 8: 'WarmYellow']

void processTuyaDP(final Map descMap, final int dp, final int dp_id, final int fncmd, final int dp_len=0) {
    if (debugLogging) { log.trace "processTuyaDP: dp=${dp} dp_id=${dp_id} fncmd=${fncmd} dp_len=${dp_len}" }
    Map event = [:]
    String description = ''
    String strValue = ''
    switch (dp) {
        case 0x01 : // (1) switch   // handled in the main parse method
        case 0x02 : // (2) switch  Boolean	"{true,false}"
        case 0x03 : // (3) switch
        case 0x04 : // (4) switch
        case 0x05 : // (5) switch
        case 0x06 : // (6) switch
            if (debugLogging) { log.debug "Switch ${dp} is ${fncmd}" }
            break
        case 0x07 : // timer 1  countdown_1	Integer {  "unit": "s",  "min": 0,  "max": 43200,  "scale": 0,  "step": 1 }
        case 0x08 : // timer 2
        case 0x09 : // timer 3
            int gang = dp - 0x07 + 1
            description = "Gang #${gang} auto timer is ${fncmd}"
            break
        case 0x0D : // (13) - Master Switch switch_all	Boolean	"{true,false}"
            strValue = SwitchStateOptions[fncmd]
            description = "Master switch is ${strValue}"
            // event = createEvent(name: 'switch', value: strValue, descriptionText: description)   // already handled ?
            break
        case 0x0E :  // (14) Power ON Behavior (Restart Status) relay_status Enum	{ "range": ["off", "on", "memory" ]} [0: 'off', 1: 'on', 2: 'last state'] 
            strValue = PowerOnBehaviorOptions[fncmd]
            description = "Power ON Behavior is ${strValue}"
            event = createEvent(name: 'powerOnBehavior', value: strValue, descriptionText: description)
            break
        case 0x0F : // (15) indicator status light_mode	Enum { "range": ["none", "relay", "pos"] }
            strValue = LightIndicatorOptions[fncmd]
            description = "Light indicator status is ${strValue}"
            event = createEvent(name: 'lightIndicatorMode', value: strValue, descriptionText: description)
            break
        case 0x10 : // (16) "Backlight Switch", 0x10 bool   backlight_switch	Boolean	"{true,false}"
            strValue = BacklightSwitchOptions[fncmd]
            description = "Backlight mode is ${strValue}"
            event = createEvent(name: 'backlightMode', value: strValue, descriptionText: description)
            break
        case 0x13 : // (19) delay-off schedule
            strValue = fncmd
            description = "delay-off schedule is ${strValue}"
            break
        case 0x1F : // (31) restart status 3
        case 0x1E : // (30) restart status 2
        case 0x1D : // (29) restart status 1 
            int gang = dp - 0x1D + 1
            description = "Restart status for gang #${gang} is ${fncmd}"
            break
        case 0x20 : // (32) unknown0x20
            strValue = fncmd
            description = "unknown0x20 is ${strValue}"
            event = createEvent(name: 'unknown0x20', value: strValue, descriptionText: description)
            break
        case 0x65 :  // (101) child lock
            strValue = ChildLockOptions[fncmd]
            description = "Child lock is ${strValue}"
            event = createEvent(name: 'childLock', value: strValue, descriptionText: description)
            break
        case 0x66 : // (102) backlight  "Backlight", 0x66, value, OK! backlight	Integer	{  "unit": "%",  "min": 0,  "max": 100,  "scale": 0,  "step": 1 }
            strValue = fncmd
            description = "Backlight level is ${strValue}"
            event = createEvent(name: 'backlightLevel', value: strValue, descriptionText: description)
            break
        case 0x67 : // (103) onColor    night_light_color	Enum	
            strValue = ColorOptions[fncmd]
            description = "onColor is ${strValue}"
            event = createEvent(name: 'onColor', value: strValue, descriptionText: description)
            break
        case 0x68 : // (104) offColor night_light_mode ?	Enum
            strValue = ColorOptions[fncmd]
            description = "offColor is ${strValue}"
            event = createEvent(name: 'offColor', value: strValue, descriptionText: description)
            break
        case 0xD1 : // (209) cycle schedule
            strValue = fncmd
            description = "cycle schedule  is ${strValue}"
            break
        case 0xD2 : // (210) random schedule
            strValue = fncmd
            description = "random schedule  is ${strValue}"
            break
        default:
            if (debugLogging) { log.warn "<b>NOT PROCESSED</b> Tuya cmd: dp=${dp} value=${fncmd} descMap.data = ${descMap?.data}" }
            break
    }
    if (event != [:]) {
        sendEvent(event)
        if (infoLogging) { log.info description }
    }
    else if (description != '') {
        if (infoLogging) { log.info description }
    }
}


private int getTuyaAttributeValue(final List<String> _data, final int index) {
    int retValue = 0
    if (_data.size() >= 6) {
        int dataLength = zigbee.convertHexToInt(_data[5 + index])
        if (dataLength == 0) { return 0 }
        int power = 1
        for (i in dataLength..1) {
            retValue = retValue + power * zigbee.convertHexToInt(_data[index + i + 5])
            power = power * 256
        }
    }
    return retValue
}

String getPACKET_ID() { return zigbee.convertToHexString(new Random().nextInt(65536), 4) }

List<String> getTuyaCommand(String dp, String dp_type, String fncmd, int tuyaCmdDefault = SETDATA) { return sendTuyaCommand(dp, dp_type, fncmd, tuyaCmdDefault) }
List<String> sendTuyaCommand(String dp, String dp_type, String fncmd, int tuyaCmdDefault = SETDATA) {
    List<String> cmds = []
    int tuyaCmd = tuyaCmdDefault
    cmds = zigbee.command(CLUSTER_TUYA, tuyaCmd, [:], delay = 201, PACKET_ID + dp + dp_type + zigbee.convertToHexString((int)(fncmd.length() / 2), 4) + fncmd )
    if (debugLogging) { "${device.displayName} getTuyaCommand (dp=$dp fncmd=$fncmd dp_type=$dp_type) = ${cmds}" }
    return cmds
}

public void tuyaTest(String dpCommand, String dpValue, String dpTypeString ) {
    String dpType   = dpTypeString == 'DP_TYPE_VALUE' ? DP_TYPE_VALUE : dpTypeString == 'DP_TYPE_BOOL' ? DP_TYPE_BOOL : dpTypeString == 'DP_TYPE_ENUM' ? DP_TYPE_ENUM : null
    String dpValHex = dpTypeString == 'DP_TYPE_VALUE' ? zigbee.convertToHexString(dpValue as int, 8) : dpValue
    if (settings?.logEnable) { log.warn "${device.displayName}  sending TEST command=${dpCommand} value=${dpValue} ($dpValHex) type=${dpType}" }
    sendZigbeeCommands( sendTuyaCommand(dpCommand, dpType, dpValHex) )
}

void syncTuyaDateTime() {
    long offset = 0
    int offsetHours = 0
    Calendar cal = Calendar.getInstance()    //it return same time as new Date()
    int hour = cal.get(Calendar.HOUR_OF_DAY)
    try {
        offset = location.getTimeZone().getOffset(new Date().getTime())
        offsetHours = (offset / 3600000) as int
        if (debugLogging) { log.debug "${device.displayName} timezone offset of current location is ${offset} (${offsetHours} hours), current hour is ${hour} h" }
    } catch (e) {
        log.error "${device.displayName} cannot resolve current location. please set location in Hubitat location setting. Setting timezone offset to zero"
    }
    //
    List<String> cmds = zigbee.command(0xEF00, 0x24, '0008' + zigbee.convertToHexString((int)(now() / 1000), 8) + zigbee.convertToHexString((int)((now() + offset) / 1000), 8))
    sendZigbeeCommands(cmds)
    if (debugLogging) { log.debug "${device.displayName} Tuya device time synchronized" }
}

void sendZigbeeCommands(List<String> cmd) {
    if (debugLogging) { log.debug "${device.displayName} sendZigbeeCommands: ${cmd}" }
    hubitat.device.HubMultiAction allActions = new hubitat.device.HubMultiAction()
    cmd.each {
        allActions.add(new hubitat.device.HubAction(it, hubitat.device.Protocol.ZIGBEE))
    }
    sendHubCommand(allActions)
}

def lastCheckin() {    // send event for heartbeat
    def now = new Date()
    sendEvent(name: 'lastCheckin', value: now)
}

def off() {
    if (infoLogging) { log.info 'Turn all switches OFF' }
    return  [
        "he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0xEF00 0x00 {0001010100010002010001000301000100}", 'delay 200',
    ]
}

def on() {
    if (infoLogging) { log.info 'Turn all switches ON' }
    return  [
        "he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0xEF00 0x00 {0001010100010102010001010301000101}", 'delay 200',
    ]
}

def refresh() {
    if (infoLogging) { log.info 'Refreshing...' }
    if (isTratoZemismartKeypad()) {
        return [queryAllTuyaDP()]
    }
    return  [
        lastCheckin()
    ]
}

private String getChildId(childDevice) {
    return childDevice.deviceNetworkId.substring(childDevice.deviceNetworkId.length() - 2)
}

void componentOn(childDevice) {
    if (debugLogging) { log.debug "component state is ON - ${childDevice} {${childDevice.deviceNetworkId}}" }
    if (infoLogging) { log.info "${childDevice} is ON" }
    String fullDataOn = '0001' + getChildId(childDevice) + '01000101'
    sendHubCommand(new HubAction("he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0xEF00 0x00 {${fullDataOn}}", Protocol.ZIGBEE))
    if (debugLogging) { log.debug { "{executed} 0x${device.deviceNetworkId} 0x${device.endpointId} 0xEF00 0x00 {${fullDataOn}}" } }
}

void componentOff(childDevice) {
    if (debugLogging) { log.debug "component state is OFF - ${childDevice} {${childDevice.deviceNetworkId}}" }
    if (infoLogging) { log.info "${childDevice} is OFF" }
    String fullDataOff = '0001' + getChildId(childDevice) + '01000100'
    sendHubCommand(new HubAction("he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0xEF00 0x00 {${fullDataOff}}", Protocol.ZIGBEE))
    if (debugLogging) { log.debug "{executed} 0x${device.deviceNetworkId} 0x${device.endpointId} 0xEF00 0x00 {${fullDataOff}}" }
}

void componentRefresh(childDevice) {
    if (debugLogging) { log.debug "component refresh ${childDevice.deviceNetworkId} ${childDevice}" }
    sendHubCommand(new HubAction("he rattr 0x${device.deviceNetworkId} 0x${device.endpointId} 0xEF00 0x00", Protocol.ZIGBEE))
    if (debugLogging) { log.debug "{executed} 0x${device.deviceNetworkId} 0x${device.endpointId} 0xEF00 0x00" }
}

void setupChildDevices() {
    if (debugLogging) { log.debug 'Parent setupChildDevices' }
    deleteObsoleteChildren()
    int buttons = 0
    switch (device.data.manufacturer) {
        case '_TZE200_amp6tsvy' :
        case '_TZE200_oisqyl4o' :
        case '_TZE200_wfxuhoea' :
        case '_TZE200_gbagoilo' :
        case '_TZE204_rzdkn5rx' :
            buttons = 1
            break
        case '_TZE200_g1ib5ldv' :
        case '_TZE200_wunufsil' :
        case '_TZE200_nh9m9emk' :
        case '_TZE200_7deq70b8' :
        case '_TZE204_zpvusbtv' :
            buttons = 2
            break
        case '_TZE200_tz32mtza' :
        case '_TZE200_kyfqmmyl' :
        case '_TZE200_go3tvswy' :
        case '_TZE200_atpwqgml' :
        case '_TZE204_rkbxtclc' :
            buttons = 3
            break
        case '_TZE200_k6jhsr0q' :
        case '_TZE200_aqnazj70' :
        case '_TZE200_1ozguk6x' :
        case '_TZE200_mexisfik' :
        case '_TZE200_1n2kyphz' :
        case '_TZE200_shkxsgis' :
        case '_TZE204_shkxsgis' :
        case '_TZE200_mua6ucdj' :
        case '_TZE204_aagrxlbd' :
            buttons = 4
            break
        case '_TZE200_9mahtqtg' :
        case '_TZE200_r731zlxk' :
        case '_TZE204_xjknlqz8' :
        case '_TZE204_4cl0dzt4' :
        case '_TYZB01_ltundz9m' :
        case '_TZ3000_jyupj3fw' :
        case '_TZE200_wnp4d4va' :
        case '_TZE204_lmgrbuwf' :   // NOVADIGITAL 
            buttons = 6
            break
        case '_TZE204_dvosyycn' :
            buttons = 6              // limited to 6 relays only, actually has 8 relays and 8 inputs
            break
        case '_TZE204_dqolcpcp' :
            buttons = 12
            break
        case '_TZE200_vhy3iakz' :
        case '_TZ3000_uim07oem' :
        default :                     // assume 4 buttons also for any unknown manufacturers codes!
            buttons = 4
            break
    }
    if (infoLogging) { log.info  "model: ${device.data.manufacturer}   buttons: $buttons" }
    createChildDevices((int)buttons)
}

void createChildDevices(int buttons) {
    if (debugLogging) { log.debug 'Parent createChildDevices' }

    if (buttons <= 1) {
        if (debugLogging) { log.debug "This device have only: $buttons button, Child devices not needed." }
        return
    }
    /* groovylint-disable-next-line UnnecessaryElseStatement */
    else {
        for (i in 1..buttons) {
            def childId = "${device.id}-0${i}"
            def existingChild = getChildDevices()?.find { it.deviceNetworkId == childId }

            if (existingChild) {
                if (infoLogging) { log.info "Child device ${childId} already exists (${existingChild})" }
            }
            else {
                if (infoLogging) { log.info "Creating device ${childId}" }
                addChildDevice('hubitat', 'Generic Component Switch', childId, [isComponent: true, name: "Switch EP0${i}", label: "${device.displayName} EP0${i}"])
            }
        }
    }
}

void deleteObsoleteChildren() {
    if (debugLogging) { log.debug 'Parent deleteChildren' }

    getChildDevices().each { child ->
        if (!child.deviceNetworkId.startsWith(device.id) || child.deviceNetworkId == "${device.id}-00") {
            if (infoLogging) { log.info "Deleting ${child.deviceNetworkId}" }
            deleteChildDevice(child.deviceNetworkId)
        }
    }
}

List<String> switchLightModeConfig() {
    List<String> cmds = []
    if (isTratoZemismartKeypad()) {
        int bl = settings?.switchLightMode == 'on' ? 1 : 0
        String dpValHex = zigbee.convertToHexString(bl as int, 2)
        cmds = getTuyaCommand('10', DP_TYPE_BOOL, dpValHex)
    }
    else {
        switch (switchLightMode) {
            case 'off':
                if (infoLogging) { log.info 'Backlight - OFF' }
                cmds = zigbee.command(0xEF00, 0x0, '00010f04000100')
                break
            case 'on':
                if (infoLogging) { log.info 'Backlight - ON' }
                cmds = zigbee.command(0xEF00, 0x0, '00010f04000101')
                break
            case 'position':
                if (infoLogging) { log.info 'Backlight - position' }
                cmds = zigbee.command(0xEF00, 0x0, '00010f04000102')
                break
        }
    }
    return cmds
}

List<String> powerOnBehaviorConfig() {
    List<String> cmds = []
    if (isTratoZemismartKeypad()) {
        int rl = settings?.powerOnBehavior == 'off' ? 0 : settings?.powerOnBehavior == 'on' ? 1 : 2
        if (debugLogging) { log.debug "Relay mode: ${rl} (${settings?.powerOnBehavior})" }
        String dpValHex = zigbee.convertToHexString(rl, 2)
        cmds = getTuyaCommand('0E', DP_TYPE_ENUM, dpValHex)
    }
    else {
        switch (powerOnBehavior) {
            case 'off':
                if (infoLogging) { log.info 'Relay state - OFF' }
                cmds = zigbee.command(0xEF00, 0x0, '00010e04000100')
                break
            case 'on':
                if (infoLogging) { log.info 'Relay state - ON' }
                cmds = zigbee.command(0xEF00, 0x0, '00010e04000101')
                break
            case 'last state':
                if (infoLogging) { log.info 'Relay state - last state' }
                cmds = zigbee.command(0xEF00, 0x0, '00010e04000102')
                break
        }
    }
    return cmds
}



void queryAllTuyaDP() {
    log.trace 'queryAllTuyaDP()'
    List<String> cmds = zigbee.command(0xEF00, 0x03)
    sendZigbeeCommands(cmds)
}

void testParse(final String description) {
    log.warn "testParse: ${description}"
    parse(description)
    log.trace '---end of testParse---'
}

void test() {
    log.trace 'test()'
    List<String> cmds = childModeConfig()
    sendZigbeeCommands(cmds)
}