/* 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 - testing _TZE204_hwyydvqm (device is now handled in the 'Tuya Zigbee Dimmer' driver!) * revision 1.3.3 - 2024-10-23 - kkossev - added TS0601 _TZE204_lmgrbuwf (NOVADIGITAL) * revision 1.3.4 - 2025-04-07 - kkossev - added TS0601 _TZE204_g4au0afs (NOVADIGITAL); added TS0601 _TZE200_mwvfvw8g _TZE200_cduqh1l0 _TZE200_emxxanvi ; added TS0601 _TZE200_kyfqmmyl _TZE200_2hf7x9n3 _TZE204_atpwqgml _TZE200_bynnczcb * revision 1.3.5 - 2025-06-07 - kkossev - added TS0601 _TZE204_cduqh1l0 * revision 1.4.0 - 2025-08-30 - kkossev - added Moes Star Feather Smart Switches SFL02-Z (1,2,3,4 gangs); added Nova Digital Topazio Smart Switches (without the 'scene mode') * revision 1.4.1 - 2025-10-04 - kkossev - added Mercator Ikuu 5-gang switch TS0601 _TZE200_leaqthtqq @giovanni.mausisa * revision 1.4.2 - 2025-10-08 - kkossev/nickjamesdavis - added Eyzee 6-gang fingerprints _TZE204_jgujzjwh (app 4A) & _TZE200_8eazvzo6 (app 41) with 6-button detection * revision 1.4.3 - 2025-11-29 - kkossev - Moes Star Feather Smart Switches processing bug fixes, including date/time sync * revision 1.4.4 - 2026-01-05 - kkossev - added TS0601 _TZE284_ms97nkyy * revision 1.4.5 - 2026-02-04 - kkossev - adding TS0601 _TZE204_nvxorhcj @bhnoronha * revision 1.5.0 - 2026-04-15 - kkossev - fixed Moes Star Feather on()/off() commands; added all known Moes Star Feather Zigbee Wall Switches (1-4 gangs) with support for all their features, including _TZE200_z3u99qxt @smc.cross * revision 1.5.1 - 2026-06-03 - kkossev - added Quero automatização 1-gang _TZE284_nzns7udm, 2-gang _TZE284_1aqlsquf, 3-gang _TZE284_pgxndxp4; renamed _TZE284_ms97nkyy to 'Quero automatização 6-gang switch'; each Quero N-gang device creates N physical + N virtual child switches; * revision 1.5.2 - 2026-06-21 - Nathanael-Shermett - added setPreference command * revision 1.5.3 - 2026-09-30 - kkossev - added Zemismart _TZE284_xibaabmu 4-gang control and screen labels @marcusvrsilva * * */ static String version() { '1.5.3' } static String timeStamp() { '2026/09/30 08:57 AM' } 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' command 'setPreference', [[name:'preference*', type:'STRING', description:'Preference name, e.g. lightIndicatorMode'], [name:'value*', type:'STRING', description:'New value, e.g. none']] 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 fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_g4au0afs', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'NOVADIGITAL 6-gang switch' // https://community.hubitat.com/t/novadigital-tuya-6-gang-wall-switch-not-recognized-by-hubitat/152353?u=kkossev fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_mwvfvw8g', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Tuya 6-gang switch' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_cduqh1l0', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Tuya 6-gang switch' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_cduqh1l0', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Tuya 6-gang switch' // https://community.hubitat.com/t/driver-zigbee-smart-switch-6-gang-tuya/154041?u=kkossev fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_emxxanvi', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Tuya 6-gang switch' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_kyfqmmyl', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'46', deviceJoinName: 'Interruptor touch Zigbee 3 Teclas' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_2hf7x9n3', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'46', deviceJoinName: 'Interruptor touch Zigbee 3 Teclas' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_atpwqgml', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'46', deviceJoinName: 'Interruptor touch Zigbee 3 Teclas' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_bynnczcb', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'46', deviceJoinName: 'Interruptor touch Zigbee 3 Teclas' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_leaqthtqq', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'42', deviceJoinName: 'Mercator Ikuü 5-gang switch' // https://community.hubitat.com/t/driver-needed-for-a-6-gang-mercator-ikuu-zigbee-switch/142341/10?u=kkossev // Moes Star Feather Smart Switches fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_ydkqbmpt', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes Star Feather Smart Switch 1 Gang' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_z3u99qxt', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'41', deviceJoinName: 'Moes Star Feather Smart Switch 1 Gang' // https://community.hubitat.com/t/re-release-moes-zigbee-wall-switch-1-2-3-gang-tuya-ts0601-switches/138120/43?u=kkossev fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_stvgmdjz', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes Star Feather Smart Switch 1 Gang' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_uenof8jd', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes Star Feather Smart Switch 2 Gang' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_tzyy0rtq', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes Star Feather Smart Switch 2 Gang' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_hktk6hze', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes Star Feather Smart Switch 2 Gang' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_wv9ukqca', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes Star Feather Smart Switch 3 Gang' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_zo0cfekv', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes Star Feather Smart Switch 3 Gang' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_rd8cdssd', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes Star Feather Smart Switch 3 Gang' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_dq8bu0pt', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes Star Feather Smart Switch 4 Gang' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_9dhenr94', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Moes Star Feather Smart Switch 4 Gang' // Nova Digital Topazio Smart Switches fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_hmabvy81', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'42', deviceJoinName: 'Nova Digital Topazio Smart Switch 4 Gang' fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_nvxorhcj', endpointId:'01', inClusters:'0004,0005,EF00,0000', outClusters:'0019,000A', application:'4A', deviceJoinName: 'Nova Digital Topazio Smart Switch 8 Gang' // https://community.hubitat.com/t/re-release-moes-zigbee-wall-switch-1-2-3-gang-tuya-ts0601-switches/138120/28?u=kkossev // fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE204_jgujzjwh', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'4A', deviceJoinName: 'Eyzee 6-gang switch' // https://community.hubitat.com/t/re-release-moes-zigbee-wall-switch-1-2-3-gang-tuya-ts0601-switches/138120/12?u=kkossev fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE200_8eazvzo6', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', application:'41', deviceJoinName: 'Eyzee 6-gang switch' // https://community.hubitat.com/t/re-release-moes-zigbee-wall-switch-1-2-3-gang-tuya-ts0601-switches/138120/12?u=kkossev // Quero automatização Switches fingerprint profileId:"0104", model:"TS0601", manufacturer:"_TZE284_ms97nkyy", endpointId:'01', inClusters:"0000,0004,0005,EF00,0000,ED00", outClusters:"0019,000A", controllerType: "ZGB", deviceJoinName: 'Quero automatização 6-gang switch' fingerprint profileId:"0104", model:"TS0601", manufacturer:"_TZE284_nzns7udm", endpointId:'01', inClusters:"0000,0004,0005,EF00,0000,ED00", outClusters:"0019,000A", controllerType: "ZGB", deviceJoinName: 'Quero automatização 1-gang switch' fingerprint profileId:"0104", model:"TS0601", manufacturer:"_TZE284_1aqlsquf", endpointId:'01', inClusters:"0000,0004,0005,EF00,0000,ED00", outClusters:"0019,000A", controllerType: "ZGB", deviceJoinName: 'Quero automatização 2-gang switch' fingerprint profileId:"0104", model:"TS0601", manufacturer:"_TZE284_pgxndxp4", endpointId:'01', inClusters:"0000,0004,0005,EF00,0000,ED00", outClusters:"0019,000A", controllerType: "ZGB", deviceJoinName: 'Quero automatização 3-gang switch' // marcusvrsilva: https://community.hubitat.com/t/138120/56; cluster list confirmed by supplied pairing screenshot. fingerprint profileId:'0104', model:'TS0601', manufacturer:'_TZE284_xibaabmu', endpointId:'01', inClusters:'0004,0005,EF00,0000,ED00', outClusters:'0019,000A', controllerType:'ZGB', deviceJoinName:'Zemismart 4-gang screen switch' } attribute 'switchLightMode', 'enum', ['off', 'on', 'position'] attribute 'powerOnBehavior', 'enum', ['off', 'on', 'last state'] attribute 'lastCheckin', 'string' attribute 'screenLabel1', 'string' attribute 'screenLabel2', 'string' attribute 'screenLabel3', 'string' attribute 'screenLabel4', 'string' attribute 'screenMasterSwitch', 'enum', ['off', 'on'] attribute 'screenIndicatorMode', 'enum', ['off', 'on_off_status', 'switch_position'] attribute 'delayColor', 'enum', ['Red', 'Blue', 'Green', 'White', 'Yellow', 'Magenta', 'Cyan', 'WarmWhite', 'WarmYellow'] attribute 'radarConfig', 'enum', ['none', '10s', '20s', '30s', '45s', '60s'] attribute 'powerOnBehavior_l1', 'enum', ['off', 'on', 'last state'] attribute 'powerOnBehavior_l2', 'enum', ['off', 'on', 'last state'] attribute 'powerOnBehavior_l3', 'enum', ['off', 'on', 'last state'] attribute 'powerOnBehavior_l4', 'enum', ['off', 'on', 'last state'] attribute 'backlightLevel', 'number' attribute 'childLock', 'enum', ['off', 'on'] attribute 'backlightMode', 'enum', ['off', 'on'] attribute 'lightIndicatorMode', 'enum', ['none', 'lit when on', 'lit when off'] attribute 'vibrationMode', 'enum', ['Gear 0', 'Gear 1', 'Gear 2', 'Gear 3'] attribute 'switchMode_l1', 'enum', ['switch', 'scene'] attribute 'switchMode_l2', 'enum', ['switch', 'scene'] attribute 'switchMode_l3', 'enum', ['switch', 'scene'] attribute 'switchMode_l4', 'enum', ['switch', 'scene'] attribute 'inductionMode', 'enum', ['off', 'on'] 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: ("Enable info logging"), description: '', defaultValue: true) input(name: 'debugLogging', type: 'bool', title: ("Enable debug logging"), description: '', defaultValue: true) if (device) { if (isZemismartScreenSwitch()) { input(name: 'screenIndicatorMode', type: 'enum', title: 'Indicator mode', description: 'Unset leaves the device setting unchanged.', options: [off:'Off', on_off_status:'On/off status', switch_position:'Switch position'], required: false) input(name: 'backlightMode', type: 'enum', title: 'Backlight', description: 'Unset leaves the device setting unchanged.', options: ['off', 'on'], required: false) input(name: 'backlightLevel', type: 'number', title: 'Backlight brightness (%)', description: 'Whole number 0-100. Unset leaves the device setting unchanged.', range: '0..100', required: false) input(name: 'childLlock', type: 'enum', title: 'Child lock', description: 'Unset leaves the device setting unchanged.', options: ['off', 'on'], required: false) input(name: 'onColor', type: 'enum', title: 'Color when on', description: 'Unset leaves the device setting unchanged.', options: ['Red', 'Blue', 'Green', 'White', 'Yellow', 'Magenta', 'Cyan', 'WarmWhite', 'WarmYellow'], required: false) input(name: 'offColor', type: 'enum', title: 'Color when off', description: 'Unset leaves the device setting unchanged.', options: ['Red', 'Blue', 'Green', 'White', 'Yellow', 'Magenta', 'Cyan', 'WarmWhite', 'WarmYellow'], required: false) input(name: 'delayColor', type: 'enum', title: 'Color while delayed', description: 'Unset leaves the device setting unchanged. Does not start a timer.', options: ['Red', 'Blue', 'Green', 'White', 'Yellow', 'Magenta', 'Cyan', 'WarmWhite', 'WarmYellow'], required: false) input(name: 'radarConfig', type: 'enum', title: 'Radar configuration', description: 'Unset leaves the device setting unchanged.', options: ['none', '10s', '20s', '30s', '45s', '60s'], required: false) for (int gang = 1; gang <= 4; gang++) { input(name: "powerOnBehavior_l${gang}", type: 'enum', title: "Switch ${gang} power-on behavior", description: 'Unset leaves the device setting unchanged.', options: ['off', 'on', 'last state'], required: false) input(name: "screenLabel${gang}", type: 'text', title: "Display label ${gang}", description: 'Maximum 12 characters (some symbols count as two). Save Preferences to apply; removing a previously set label clears it.', defaultValue: '', required: false) } } else if (isTratoZemismartKeypad()) { input(name: 'backlightLevel', type: 'number', title: ("Led: Backlight Level"), description: ("Select led backlight level 0-100 (default: 50)"), defaultValue: 50, range: [0..100]) input(name: 'backlightMode', type: 'enum', title: ("Led: Backlight Mode"), description: ("Select backlight mode (default: on)"), options: ['off', 'on'], defaultValue: 'on') input(name: 'lightIndicatorMode', type: 'enum', title: ("Led: Indicator Mode"), 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: ("Led: On Color"), 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: ("Led: Off Color"), 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: ("Child Lock"), description: ("Select child lock mode (default: off)"), options: ['off', 'on'], defaultValue: 'off') } else if (isMoesStarFeatherSwitch()) { input(name: 'backlightMode', type: 'enum', title: ("Led: Backlight Mode"), description: ("Select backlight mode (default: on)"), options: ['off', 'on'], defaultValue: 'on') input(name: 'lightIndicatorMode', type: 'enum', title: ("Led: Indicator Mode"), description: ("Select led indicator mode (default: lit when on)"), options: ['none', 'lit when on', 'lit when off'], defaultValue: 'lit when on') input(name: 'vibrationMode', type: 'enum', title: ("Vibration Mode"), description: ("Select vibration intensity for tactile feedback (default: Gear 1)"), options: ['Gear 0', 'Gear 1', 'Gear 2', 'Gear 3'], defaultValue: 'Gear 1') input(name: 'inductionMode', type: 'enum', title: ("Induction Mode"), description: ("Enable/disable induction sensing (default: off)"), options: ['off', 'on'], defaultValue: 'off') int sfGangs = getStarFeatherGangCount() input(name: 'switchMode_l1', type: 'enum', title: ("Switch 1 Mode"), description: ("Switch or scene mode for gang 1 (default: switch)"), options: ['switch', 'scene'], defaultValue: 'switch') input(name: 'countdown_l1', type: 'number', title: ("Switch 1 Countdown Timer (s)"), description: ("Auto-off timer for gang 1, seconds, 0 = disabled (max 86400)"), defaultValue: 0, range: '0..86400') input(name: 'momentary_l1', type: 'number', title: ("Switch 1 Momentary Timer (s)"), description: ("Momentary on timer for gang 1, seconds, 0 = disabled (max 3600)"), defaultValue: 0, range: '0..3600') if (sfGangs >= 2) { input(name: 'switchMode_l2', type: 'enum', title: ("Switch 2 Mode"), description: ("Switch or scene mode for gang 2 (default: switch)"), options: ['switch', 'scene'], defaultValue: 'switch') input(name: 'countdown_l2', type: 'number', title: ("Switch 2 Countdown Timer (s)"), description: ("Auto-off timer for gang 2, seconds, 0 = disabled"), defaultValue: 0, range: '0..86400') input(name: 'momentary_l2', type: 'number', title: ("Switch 2 Momentary Timer (s)"), description: ("Momentary on timer for gang 2, seconds, 0 = disabled"), defaultValue: 0, range: '0..3600') } if (sfGangs >= 3) { input(name: 'switchMode_l3', type: 'enum', title: ("Switch 3 Mode"), description: ("Switch or scene mode for gang 3 (default: switch)"), options: ['switch', 'scene'], defaultValue: 'switch') input(name: 'countdown_l3', type: 'number', title: ("Switch 3 Countdown Timer (s)"), description: ("Auto-off timer for gang 3, seconds, 0 = disabled"), defaultValue: 0, range: '0..86400') input(name: 'momentary_l3', type: 'number', title: ("Switch 3 Momentary Timer (s)"), description: ("Momentary on timer for gang 3, seconds, 0 = disabled"), defaultValue: 0, range: '0..3600') } if (sfGangs >= 4) { input(name: 'switchMode_l4', type: 'enum', title: ("Switch 4 Mode"), description: ("Switch or scene mode for gang 4 (default: switch)"), options: ['switch', 'scene'], defaultValue: 'switch') input(name: 'countdown_l4', type: 'number', title: ("Switch 4 Countdown Timer (s)"), description: ("Auto-off timer for gang 4, seconds, 0 = disabled"), defaultValue: 0, range: '0..86400') input(name: 'momentary_l4', type: 'number', title: ("Switch 4 Momentary Timer (s)"), description: ("Momentary on timer for gang 4, seconds, 0 = disabled"), defaultValue: 0, range: '0..3600') } } else if (isQueroSwitch()) { input(name: 'backlightLevel', type: 'number', title: ("Backlight Brightness"), description: ("Set backlight brightness 0-99% (default: 50)"), defaultValue: 50, range: '0..99') } else { input(name: 'switchLightMode', type: 'enum', title: ("Switch Backlight Mode"), description: ("Select type of backlight indicator (default: Position)"), options: ['off', 'on', 'position'], defaultValue: 'position') } if (!isZemismartScreenSwitch()) { input(name: 'powerOnBehavior', type: 'enum', title: ("Switch Power On Behavior"), 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'] } boolean isZemismartScreenSwitch() { return device.getDataValue('model') == 'TS0601' && device.getDataValue('manufacturer') == '_TZE284_xibaabmu' } boolean isMoesStarFeatherSwitch() { //return true return device.getDataValue('manufacturer') in ['_TZE200_ydkqbmpt', '_TZE200_z3u99qxt', '_TZE200_stvgmdjz', '_TZE200_uenof8jd', '_TZE200_tzyy0rtq', '_TZE200_hktk6hze', '_TZE200_wv9ukqca', '_TZE200_zo0cfekv', '_TZE200_rd8cdssd', '_TZE200_dq8bu0pt', '_TZE200_9dhenr94', '_TZE200_hmabvy81'] } int getStarFeatherGangCount() { String mfr = device.getDataValue('manufacturer') if (mfr in ['_TZE200_ydkqbmpt', '_TZE200_z3u99qxt', '_TZE200_stvgmdjz']) { return 1 } if (mfr in ['_TZE200_uenof8jd', '_TZE200_tzyy0rtq', '_TZE200_hktk6hze']) { return 2 } if (mfr in ['_TZE200_wv9ukqca', '_TZE200_zo0cfekv', '_TZE200_rd8cdssd']) { return 3 } if (mfr in ['_TZE200_dq8bu0pt', '_TZE200_9dhenr94', '_TZE200_hmabvy81']) { return 4 } return 1 } boolean isQueroSwitch() { return device.getDataValue('manufacturer') in ['_TZE284_ms97nkyy', '_TZE284_nzns7udm', '_TZE284_1aqlsquf', '_TZE284_pgxndxp4'] } boolean isQuero12GangSwitch() { return device.getDataValue('manufacturer') == '_TZE284_ms97nkyy' } int getQueroGangCount() { // Returns total child device count (physical + virtual) switch (device.getDataValue('manufacturer')) { case '_TZE284_ms97nkyy': return 12 case '_TZE284_nzns7udm': return 2 case '_TZE284_1aqlsquf': return 4 case '_TZE284_pgxndxp4': return 6 default: return 1 } } int getQueroPhysicalCount() { // Returns number of physical relay channels only switch (device.getDataValue('manufacturer')) { case '_TZE284_ms97nkyy': return 6 case '_TZE284_nzns7udm': return 1 case '_TZE284_1aqlsquf': return 2 case '_TZE284_pgxndxp4': return 3 default: return 1 } } boolean is8GangNvxorhcj() { return device.getDataValue('manufacturer') == '_TZE204_nvxorhcj' } void initializeVars() { if (isZemismartScreenSwitch()) { // Optional screen configuration stays unset until selected; never impose device defaults. for (int gang = 1; gang <= 4; gang++) { if (settings["screenLabel${gang}"] == null) { device.updateSetting("screenLabel${gang}", [type:'text', value:'']) } } } else if (isTratoZemismartKeypad()) { device.updateSetting('backlightMode', [type:'enum', value:'on']) device.updateSetting('lightIndicatorMode', [type:'enum', value:'lit when on']) } else if (isMoesStarFeatherSwitch()) { device.updateSetting('backlightMode', [type:'enum', value:'on']) device.updateSetting('lightIndicatorMode', [type:'enum', value:'lit when on']) device.updateSetting('vibrationMode', [type:'enum', value:'Gear 1']) device.updateSetting('inductionMode', [type:'enum', value:'off']) device.updateSetting('switchMode_l1', [type:'enum', value:'switch']) device.updateSetting('switchMode_l2', [type:'enum', value:'switch']) device.updateSetting('switchMode_l3', [type:'enum', value:'switch']) device.updateSetting('switchMode_l4', [type:'enum', value:'switch']) device.updateSetting('countdown_l1', [type:'number', value:0]) device.updateSetting('countdown_l2', [type:'number', value:0]) device.updateSetting('countdown_l3', [type:'number', value:0]) device.updateSetting('countdown_l4', [type:'number', value:0]) device.updateSetting('momentary_l1', [type:'number', value:0]) device.updateSetting('momentary_l2', [type:'number', value:0]) device.updateSetting('momentary_l3', [type:'number', value:0]) device.updateSetting('momentary_l4', [type:'number', value:0]) } else if (isQueroSwitch()) { device.updateSetting('backlightLevel', [type:'number', value:50]) } else { device.updateSetting('switchLightMode', [type:'enum', value:'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 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 (isZemismartScreenSwitch()) { cmds += zemismartScreenConfig() + screenLabelConfig() } if (isMoesStarFeatherSwitch()) { cmds += vibrationModeConfig() + inductionModeConfig() + switchModeConfig() + countdownConfig() + momentaryConfig() } 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 (isQueroSwitch()) { // Quero automatização switches: backlight brightness 0-99% int brightness = settings?.backlightLevel != null ? settings.backlightLevel as int : 50 cmds += getTuyaCommand('65', DP_TYPE_VALUE, zigbee.convertToHexString(brightness, 8)) } if (infoLogging) { log.info 'Updated...' } sendZigbeeCommands(cmds) } def setPreference(String preference, String value) { String key = preference?.trim() String val = value?.trim() ?: '' if (!key) { log.warn 'setPreference: missing preference name' return } String type = val.isNumber() ? 'number' : (val.toLowerCase() in ['true', 'false'] ? 'bool' : 'enum') if (isZemismartScreenSwitch()) { if (key ==~ /screenLabel[1-4]/) { type = 'text' val = value ?: '' // Preserve numeric-looking text and surrounding spaces. } else if (key in ['debugLogging', 'infoLogging']) { type = 'bool' val = val.toLowerCase() } else { type = key == 'backlightLevel' ? 'number' : 'enum' } if (!validScreenPreference(key, val)) { return } } if (infoLogging) { log.info "setPreference: ${key} = ${val} (${type})" } device.updateSetting(key, [value: val, type: type]) settings[key] = isZemismartScreenSwitch() && type == 'bool' ? val == 'true' : val updated() } private static int getCLUSTER_TUYA() { 0xEF00 } private static int getSETDATA() { 0x00 } private static int getSETTIME() { 0x24 } private static String getDP_TYPE_RAW() { '00' } // [ 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}" } // Check if this is a Star Feather device - if so, skip traditional switch logic if (isMoesStarFeatherSwitch()) { parseMoesStarFeatherSwitch(descMap) // handle Star Feather specific parsing return [:] } if (isQueroSwitch()) { parse12GangSwitch(descMap) // handle Quero automatização switch parsing return [:] } if (isZemismartScreenSwitch()) { parseZemismartScreenSwitch(descMap) return [:] } 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' // _TZE204_nvxorhcj (8-gang) uses DP 0x65/0x66 for gangs 7/8 String childId = switchFunc boolean is8GangSwitchDp = is8GangNvxorhcj() && (switchFunc in ['65', '66']) if (is8GangSwitchDp) { childId = switchFunc == '65' ? '07' : '08' } if ((switchFunc in ['01', '02', '03', '04', '05', '06'] || is8GangSwitchDp) && switchAttr == '01') { // Traditional switch logic for Moes and Trato devices def cd = getChildDevice("${device.id}-${childId}") if (cd == null) { return createEvent(name: 'switch', value: switchState) } if (descMap?.command == '00') { // switch toggled cd.parse([[name: 'switch', value:switchState, descriptionText: "Child switch ${childId} turned $switchState"]]) } else if (descMap?.command in ['01', '02']) { // report switch status cd.parse([[name: 'switch', value:switchState, descriptionText: "Child switch ${childId} 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) != childId && 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() } } } } // Protocol reference: zigbee-herdsman-converters, zemismart.ts ZMS-206US-4 // at 8c6213a285159cb8ebb63436db2aef7c39565772. Scope is the exact fingerprint above. void parseZemismartScreenSwitch(Map descMap) { String command = descMap.command?.toUpperCase() if (descMap.sourceEndpoint != null && descMap.sourceEndpoint != '01') { return } // Global ZCL default responses (0B) must never be interpreted as Tuya data. if (descMap.isClusterSpecific == false || command == '0B') { return } if (command == '24') { syncTuyaDateTime() return } if (command == '25') { // ZCL UINT16 payloadSize is little-endian, followed by gateway-connected = 1. sendZigbeeCommands(zigbee.command(CLUSTER_TUYA, 0x25, [destEndpoint:1], 200, '010001')) return } if (!(command in ['00', '01', '02', '05', '06'])) { return } List data = descMap.data if (data == null || data.size() < 2 || data.any { !(it instanceof String) || !(it ==~ /[0-9a-fA-F]{2}/) }) { if (debugLogging) { log.debug 'Screen switch: invalid Tuya payload' } return } // Decode the complete frame first: a truncated trailing record cannot publish partial state. List records = [] int offset = 2 while (offset < data.size()) { if (data.size() - offset < 4) { if (debugLogging) { log.debug 'Screen switch: truncated DP header' } return } int dp = Integer.parseInt(data[offset], 16) int dpType = Integer.parseInt(data[offset + 1], 16) int length = Integer.parseInt(data[offset + 2], 16) * 256 + Integer.parseInt(data[offset + 3], 16) offset += 4 if (length > data.size() - offset) { if (debugLogging) { log.debug "Screen switch: truncated DP ${dp}, declared length ${length}" } return } records.add([dp:dp, type:dpType, bytes:new ArrayList(data.subList(offset, offset + length))]) offset += length } Map relayUpdates = [:] Map reportedUpdates = [:] records.each { record -> processZemismartScreenDP(descMap, record.dp, record.type, record.bytes, relayUpdates, reportedUpdates) } if (!relayUpdates.isEmpty()) { // Child currentValue can lag within and between event calls; retain the last reports. Map reported = [:] for (int gang = 1; gang <= 4; gang++) { String key = String.format('%02d', gang) reported[key] = state.screenRelayStates?.get(key) ?: getChildDevice("${device.id}-${key}")?.currentValue('switch') } reported.putAll(relayUpdates) state.screenRelayStates = reported relayUpdates.each { key, value -> def child = getChildDevice("${device.id}-${key}") if (child) { child.parse([[name:'switch', value:value, descriptionText:"${child.displayName} is ${value}"]]) } else { log.warn "Screen switch: missing child ${key}; run Initialize" } } updateParentSwitchState(null, null, reported) } String masterState = reportedUpdates.screenMasterSwitch boolean queryRelays = masterState != null && masterState != state.screenMasterState && relayUpdates.isEmpty() // Record the master report before querying, so an identical response cannot cause a loop. if (masterState != null) { state.screenMasterState = masterState } reportedUpdates.each { name, value -> sendEvent(name:name, value:value) if (infoLogging) { log.info "${device.displayName} ${name} is ${value}" } } if (queryRelays) { sendZigbeeCommands(zigbee.command(CLUSTER_TUYA, 0x03, [destEndpoint:1])) } } void processZemismartScreenDP(final Map descMap, final int dp, final int dpType, final List dpData, Map relayUpdates, Map reportedUpdates) { String attributeName = null Map options = [:] switch (dp) { case 0x01: case 0x02: case 0x03: case 0x04: if (dpType == 1 && dpData.size() == 1 && dpData[0] in ['00', '01']) { relayUpdates[String.format('%02d', dp)] = dpData[0] == '01' ? 'on' : 'off' } else if (debugLogging) { log.debug "Screen switch: invalid relay DP ${dp}, type ${dpType}, bytes ${dpData}" } return case 0x0D: case 0x10: case 0x65: attributeName = dp == 0x0D ? 'screenMasterSwitch' : dp == 0x10 ? 'backlightMode' : 'childLock' if (dpType == 1 && dpData.size() == 1 && dpData[0] in ['00', '01']) { reportedUpdates[attributeName] = dpData[0] == '01' ? 'on' : 'off' } else if (debugLogging) { log.debug "Screen switch: invalid ${attributeName}, type ${dpType}, bytes ${dpData}" } return case 0x0F: attributeName = 'screenIndicatorMode' options = ScreenIndicatorOptions break case 0x13: case 0x67: case 0x68: attributeName = dp == 0x13 ? 'delayColor' : dp == 0x67 ? 'onColor' : 'offColor' options = ColorOptions break case 0x1D: case 0x1E: case 0x1F: case 0x20: attributeName = "powerOnBehavior_l${dp - 28}".toString() options = PowerOnBehaviorOptions break case 0x6F: attributeName = 'radarConfig' options = RadarConfigOptions break case 0x66: if (dpType == 2 && dpData.size() == 4 && Long.parseLong(dpData.join(''), 16) <= 100) { reportedUpdates.backlightLevel = Long.parseLong(dpData.join(''), 16) as int } else if (debugLogging) { log.debug "Screen switch: invalid brightness, type ${dpType}, bytes ${dpData}" } return case 0x07: case 0x08: case 0x09: case 0x0A: if (dpType == 2 && dpData.size() == 4) { if (debugLogging) { log.debug "Screen switch: countdown ${dp - 6} = ${Long.parseLong(dpData.join(''), 16)} seconds (report only)" } } else if (debugLogging) { log.debug "Screen switch: invalid countdown DP ${dp}, type ${dpType}, bytes ${dpData}" } return case 0x0E: case 0x18: case 0xC9: if (debugLogging) { log.debug "Screen switch: diagnostic DP ${dp}, type ${dpType}, bytes ${dpData} (report only)" } return case 0x69: case 0x6A: case 0x6B: case 0x6C: if (dpType != 0) { if (debugLogging) { log.debug "Screen switch: invalid label DP ${dp}, type ${dpType}, bytes ${dpData}" } return } byte[] bytes = dpData.collect { Integer.parseInt(it, 16) as byte } as byte[] String text = new String(bytes, 'UTF-8') // A round trip rejects malformed UTF-8 instead of publishing replacement characters. if (screenTextHex(text).equalsIgnoreCase(dpData.join(''))) { reportedUpdates["screenLabel${dp - 104}".toString()] = text } else if (debugLogging) { log.debug "Screen switch: invalid UTF-8 in DP ${dp}" } return default: if (debugLogging) { log.debug "Screen switch: ignored DP ${dp}, type ${dpType}, bytes ${dpData}" } return } if (dpType == 4 && dpData.size() == 1 && options.containsKey(Integer.parseInt(dpData[0], 16))) { reportedUpdates[attributeName] = options[Integer.parseInt(dpData[0], 16)] } else if (debugLogging) { log.debug "Screen switch: invalid ${attributeName}, type ${dpType}, bytes ${dpData}" } } private String screenTextHex(String text) { return text.getBytes('UTF-8').collect { String.format('%02X', it & 0xFF) }.join('') } private boolean validScreenText(String text) { if (text == null || text.length() > 12) { return false } for (int i = 0; i < text.length(); i++) { char ch = text.charAt(i) if (Character.isHighSurrogate(ch)) { if (i + 1 >= text.length() || !Character.isLowSurrogate(text.charAt(i + 1))) { return false } i++ } else if (Character.isLowSurrogate(ch)) { return false } } return true } 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 } } void parseMoesStarFeatherSwitch(Map descMap) { int dataLen = descMap?.data.size() if (dataLen <= 5) { if (descMap.command == '0B') { if (debugLogging) { log.debug "Star Feather command acknowledgment received" } return } if (descMap.command == '24') { syncTuyaDateTime() return } if (debugLogging) { log.warn "parseMoesStarFeatherSwitch: unprocessed short Tuya command response: dp_id=${descMap?.data[3]} dp=${descMap?.data[2]} data=${descMap?.data}) size=${dataLen}" } 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) // processStarFeatherDP(descMap, dp, dp_id, fncmd) i = i + fncmd_len + 4 } } void parse12GangSwitch(Map descMap) { if (descMap?.data == null) { //if (debugLogging) { log.warn "parse12GangSwitch: descMap.data is null = ${descMap}" } if (debugLogging) { log.debug "parse12GangSwitch: check-in" } return } int dataLen = descMap.data.size() if (dataLen <= 5) { if (descMap.command == '0B') { if (debugLogging) { log.debug "Quero switch command acknowledgment received" } return } if (descMap.command == '24') { syncTuyaDateTime() return } if (descMap.command == '11') { if (debugLogging) { log.debug "parse12GangSwitch: unhandled command 0x11 data=${descMap?.data}, ignored" } return } if (debugLogging) { log.warn "parse12GangSwitch: unprocessed short Tuya command response: dp_id=${descMap?.data[3]} dp=${descMap?.data[2]} data=${descMap?.data}) size=${dataLen}" } 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) // process12GangDP(descMap, dp, dp_id, fncmd) i = i + fncmd_len + 4 } } @Field static final Map SwitchStateOptions = [0: 'off', 1: 'on'] @Field static final Map ChildLockOptions = [0: 'off', 1: 'on'] // 0x65 @Field static final Map PowerOnBehaviorOptions = [0: 'off', 1: 'on', 2: 'last state'] // 0x0E 'Turn power Off', 'Turn power On', 'Restore previous state'], @Field static final Map BacklightSwitchOptions = [0: 'off', 1: 'on'] // 0x10 @Field static final Map LightIndicatorOptions = [0: 'none', 1: 'lit when on', 2: 'lit when off'] // 0x0F @Field static final Map ColorOptions = [0: 'Red', 1: 'Blue', 2: 'Green', 3: 'White', 4: 'Yellow', 5: 'Magenta', 6: 'Cyan', 7: 'WarmWhite', 8: 'WarmYellow'] @Field static final Map ScreenIndicatorOptions = [0: 'off', 1: 'on_off_status', 2: 'switch_position'] @Field static final Map RadarConfigOptions = [0: 'none', 1: '10s', 2: '20s', 3: '30s', 4: '45s', 5: '60s'] 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 "NOT PROCESSED 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 } } } void processStarFeatherDP(final Map descMap, final int dp, final int dp_id, final int fncmd, final int dp_len=0) { if (debugLogging) { log.debug "processStarFeatherDP: dp=${dp} dp_id=${dp_id} fncmd=${fncmd} dp_len=${dp_len}" } Map event = [:] String description = '' String strValue = '' switch (dp) { case 0x01 : // (1) scene_1 action if (debugLogging) { log.debug "Scene 1 action triggered" } sendEvent(name: 'pushed', value: 1, descriptionText: 'Scene 1 button pushed', isStateChange: true) break case 0x02 : // (2) scene_2 action if (debugLogging) { log.debug "Scene 2 action triggered" } sendEvent(name: 'pushed', value: 2, descriptionText: 'Scene 2 button pushed', isStateChange: true) break case 0x03 : // (3) scene_3 action if (debugLogging) { log.debug "Scene 3 action triggered" } sendEvent(name: 'pushed', value: 3, descriptionText: 'Scene 3 button pushed', isStateChange: true) break case 0x04 : // (4) scene_4 action if (debugLogging) { log.debug "Scene 4 action triggered" } sendEvent(name: 'pushed', value: 4, descriptionText: 'Scene 4 button pushed', isStateChange: true) break case 0x12 : // (18) mode_l1 - Switch1 mode strValue = fncmd == 0 ? 'switch' : 'scene' description = "Switch 1 mode is ${strValue}" event = createEvent(name: 'switchMode_l1', value: strValue, descriptionText: description) break case 0x13 : // (19) mode_l2 - Switch2 mode strValue = fncmd == 0 ? 'switch' : 'scene' description = "Switch 2 mode is ${strValue}" event = createEvent(name: 'switchMode_l2', value: strValue, descriptionText: description) break case 0x14 : // (20) mode_l3 - Switch3 mode strValue = fncmd == 0 ? 'switch' : 'scene' description = "Switch 3 mode is ${strValue}" event = createEvent(name: 'switchMode_l3', value: strValue, descriptionText: description) break case 0x15 : // (21) mode_l4 - Switch4 mode strValue = fncmd == 0 ? 'switch' : 'scene' description = "Switch 4 mode is ${strValue}" event = createEvent(name: 'switchMode_l4', value: strValue, descriptionText: description) break case 0x18 : // (24) state_l1 - Switch 1 state strValue = fncmd == 1 ? 'on' : 'off' def cd1 = getChildDevice("${device.id}-01") if (cd1 != null) { description = "Child switch 1 is ${strValue}" cd1.parse([[name: 'switch', value: strValue, descriptionText: description]]) } else { sendEvent(name: 'switch', value: strValue, descriptionText: description) if (debugLogging) { log.debug description } } updateParentSwitchState('01', strValue) break case 0x19 : // (25) state_l2 - Switch 2 state strValue = fncmd == 1 ? 'on' : 'off' def cd2 = getChildDevice("${device.id}-02") if (cd2 != null) { description = "Child switch 2 is ${strValue}" cd2.parse([[name: 'switch', value: strValue, descriptionText: description]]) } updateParentSwitchState('02', strValue) break case 0x1A : // (26) state_l3 - Switch 3 state strValue = fncmd == 1 ? 'on' : 'off' def cd3 = getChildDevice("${device.id}-03") if (cd3 != null) { description = "Child switch 3 is ${strValue}" cd3.parse([[name: 'switch', value: strValue, descriptionText: description]]) } updateParentSwitchState('03', strValue) break case 0x1B : // (27) state_l4 - Switch 4 state strValue = fncmd == 1 ? 'on' : 'off' def cd4 = getChildDevice("${device.id}-04") if (cd4 != null) { description = "Child switch 4 is ${strValue}" cd4.parse([[name: 'switch', value: strValue, descriptionText: description]]) } updateParentSwitchState('04', strValue) break case 0x1E : // (30) countdown_l1 - Timer 1 case 0x1F : // (31) countdown_l2 - Timer 2 case 0x20 : // (32) countdown_l3 - Timer 3 case 0x21 : // (33) countdown_l4 - Timer 4 int gang = dp - 0x1E + 1 description = "Gang #${gang} countdown timer is ${fncmd} seconds" break case 0x24 : // (36) backlight_mode strValue = BacklightSwitchOptions[fncmd] description = "Backlight mode is ${strValue}" event = createEvent(name: 'backlightMode', value: strValue, descriptionText: description) break case 0x25 : // (37) indicator_status strValue = fncmd == 0 ? 'none' : fncmd == 1 ? 'lit when on' : 'lit when off' description = "Light indicator status is ${strValue}" event = createEvent(name: 'lightIndicatorMode', value: strValue, descriptionText: description) break case 0x26 : // (38) power_on_behavior strValue = PowerOnBehaviorOptions[fncmd] description = "Power ON Behavior is ${strValue}" event = createEvent(name: 'powerOnBehavior', value: strValue, descriptionText: description) break case 0x67 : // (103) induction_mode strValue = fncmd == 1 ? 'on' : 'off' description = "Induction mode is ${strValue}" event = createEvent(name: 'inductionMode', value: strValue, descriptionText: description) break case 0x68 : // (104) vibration_mode strValue = "Gear ${fncmd}" description = "Vibration mode is ${strValue}" event = createEvent(name: 'vibrationMode', value: strValue, descriptionText: description) break case 0x69 : // (105) momentary_1 case 0x6A : // (106) momentary_2 case 0x6B : // (107) momentary_3 case 0x6C : // (108) momentary_4 int gang = dp - 0x69 + 1 description = "Gang #${gang} momentary timer is ${fncmd} seconds" break default: if (debugLogging) { log.warn "NOT PROCESSED Star Feather Tuya cmd: dp=${dp} value=${fncmd} descMap.data = ${descMap?.data}" } break } if (event != [:]) { sendEvent(event) if (infoLogging) { log.info description } } else if (description != '') { if (debugLogging) { log.debug description } } } void process12GangDP(final Map descMap, final int dp, final int dp_id, final int fncmd, final int dp_len=0) { if (debugLogging) { log.debug "process12GangDP: dp=${dp} dp_id=${dp_id} fncmd=${fncmd} dp_len=${dp_len}" } Map event = [:] String description = '' String strValue = '' switch (dp) { case 0x18 : // (24) state_l1 - Physical Switch 1 case 0x19 : // (25) state_l2 - Physical Switch 2 case 0x1A : // (26) state_l3 - Physical Switch 3 case 0x1B : // (27) state_l4 - Physical Switch 4 case 0x1C : // (28) state_l5 - Physical Switch 5 case 0x1D : // (29) state_l6 - Physical Switch 6 int switchNum = dp - 0x18 + 1 if (!isQuero12GangSwitch() && isQueroSwitch() && switchNum > getQueroPhysicalCount()) { if (debugLogging) { log.debug "unknown dp ${dp}" } break } strValue = fncmd == 1 ? 'on' : 'off' String childId = String.format('%02d', switchNum) def cd = getChildDevice("${device.id}-${childId}") if (cd != null) { description = "Child switch ${switchNum} is ${strValue}" cd.parse([[name: 'switch', value: strValue, descriptionText: description]]) if (debugLogging) { log.debug description } } updateParentSwitchState(childId, strValue) break // example : catchall: 0104 EF00 01 01 0040 00 CA52 01 00 0000 02 01 00D60704000100 case 0x05 : // (5) virtual scene switch 1 - Quero 1/2/3-channel only case 0x06 : // (6) virtual scene switch 2 - Quero 2/3-channel only if (!isQuero12GangSwitch() && isQueroSwitch()) { int physCount = getQueroPhysicalCount() int sceneOffset = dp - 0x05 + 1 // 0x05->1, 0x06->2 int virtualChildNum = physCount + sceneOffset String childId = String.format('%02d', virtualChildNum) def cd = getChildDevice("${device.id}-${childId}") if (cd != null) { if (fncmd == 0) { String currentState = cd.currentValue('switch') ?: 'off' strValue = currentState == 'on' ? 'off' : 'on' description = "Virtual scene switch ${virtualChildNum} toggled to ${strValue}" } else if (fncmd == 3) { strValue = 'on' description = "Virtual scene switch ${virtualChildNum} turned on" } else if (fncmd == 2) { strValue = 'off' description = "Virtual scene switch ${virtualChildNum} turned off" } else { if (debugLogging) { log.debug "Virtual scene switch ${virtualChildNum}: unexpected fncmd=${fncmd}" } break } cd.parse([[name: 'switch', value: strValue, descriptionText: description]]) if (debugLogging) { log.debug description } } updateParentSwitchState(childId, strValue) } else { if (debugLogging) { log.debug "unknown dp ${dp}" } } break case 0x07 : // (7) action_1 - Virtual Scene Switch 7 (12-gang) / virtual scene 3 (Quero 3-channel) case 0x08 : // (8) action_2 - Virtual Scene Switch 8 case 0x09 : // (9) action_3 - Virtual Scene Switch 9 case 0x0A : // (10) action_4 - Virtual Scene Switch 10 case 0x0B : // (11) action_5 - Virtual Scene Switch 11 case 0x0C : // (12) action_6 - Virtual Scene Switch 12 if (!isQuero12GangSwitch() && isQueroSwitch()) { // Quero 3-channel: DP 0x07 = virtual scene 3 -> child (physicalCount+3) int physCount3 = getQueroPhysicalCount() int virtualChildNum3 = physCount3 + 3 String childId3 = String.format('%02d', virtualChildNum3) def cd3 = getChildDevice("${device.id}-${childId3}") if (cd3 != null) { if (fncmd == 0) { String currentState = cd3.currentValue('switch') ?: 'off' strValue = currentState == 'on' ? 'off' : 'on' description = "Virtual scene switch ${virtualChildNum3} toggled to ${strValue}" } else if (fncmd == 3) { strValue = 'on' description = "Virtual scene switch ${virtualChildNum3} turned on" } else if (fncmd == 2) { strValue = 'off' description = "Virtual scene switch ${virtualChildNum3} turned off" } else { if (debugLogging) { log.debug "Virtual scene switch ${virtualChildNum3}: unexpected fncmd=${fncmd}" } break } cd3.parse([[name: 'switch', value: strValue, descriptionText: description]]) if (debugLogging) { log.debug description } } updateParentSwitchState(childId3, strValue) break } int sceneNum = dp - 0x07 + 7 // Map to switches 7-12 (12-gang) String childId = String.format('%02d', sceneNum) def cd = getChildDevice("${device.id}-${childId}") if (cd != null) { if (fncmd == 0) { // Physical press: toggle the state // example : catchall: 0104 EF00 01 01 0040 00 CA52 01 00 0000 02 01 00D60704000100 String currentState = cd.currentValue('switch') ?: 'off' strValue = currentState == 'on' ? 'off' : 'on' description = "Virtual scene switch ${sceneNum} toggled to ${strValue}" } else if (fncmd == 3) { // Command ON (dpCmd 03) strValue = 'on' description = "Virtual scene switch ${sceneNum} turned on" } else if (fncmd == 2) { // Command OFF (dpCmd 02) strValue = 'off' description = "Virtual scene switch ${sceneNum} turned off" } else { if (debugLogging) { log.debug "Virtual scene switch ${sceneNum}: unexpected fncmd=${fncmd}" } break } cd.parse([[name: 'switch', value: strValue, descriptionText: description]]) if (debugLogging) { log.debug description } } updateParentSwitchState(childId, strValue) break case 0x65 : // (101) backlight_brightness (0-99%) strValue = fncmd.toString() description = "Backlight brightness is ${strValue}%" event = createEvent(name: 'backlightLevel', value: strValue, descriptionText: description) break case 0x24 : // (36) state_l1 for Quero 1/2/3-gang; unknown for 12-gang case 0x25 : // (37) state_l2 for Quero 2/3-gang; unknown for 12-gang case 0x26 : // (38) state_l3 for Quero 3-gang; unknown for 12-gang if (!isQuero12GangSwitch()) { int chanNum = dp - 0x24 + 1 if (chanNum > getQueroPhysicalCount()) { if (debugLogging) { log.debug "unknown dp ${dp}" } break } strValue = fncmd == 1 ? 'on' : 'off' String childId = String.format('%02d', chanNum) def cd = getChildDevice("${device.id}-${childId}") if (cd != null) { description = "Child switch ${chanNum} is ${strValue}" cd.parse([[name: 'switch', value: strValue, descriptionText: description]]) if (debugLogging) { log.debug description } } updateParentSwitchState(childId, strValue) } else { if (debugLogging) { log.debug "unknown dp ${dp}" } } break case 0x12 : // (18) unknown_dp_18 case 0x13 : // (19) unknown_dp_19 case 0x14 : // (20) unknown_dp_20 case 0x15 : // (21) unknown_dp_21 case 0x16 : // (22) unknown_dp_22 case 0x17 : // (23) unknown_dp_23 case 0x1E : // (30) unknown_dp_30 case 0x1F : // (31) unknown_dp_31 case 0x20 : // (32) unknown_dp_32 case 0x21 : // (33) unknown_dp_33 case 0x27 : // (39) unknown_dp_39 case 0x28 : // (40) unknown_dp_40 case 0x29 : // (41) unknown_dp_41 case 0x2A : // (42) unknown_dp_42 case 0x2B : // (43) unknown_dp_43 case 0x2C : // (44) unknown_dp_44 if (debugLogging) { log.debug "unknown dp ${dp}" } break default: if (debugLogging) { log.warn "NOT PROCESSED 12-gang Tuya cmd: dp=${dp} value=${fncmd} descMap.data = ${descMap?.data}" } break } if (event != [:]) { sendEvent(event) if (infoLogging) { log.info description } } else if (description != '') { if (debugLogging) { log.debug description } } } void updateParentSwitchState(String childId = null, String newState = null, Map reportedStates = [:]) { def cdsOn = 0 // count number of switches on getChildDevices().each { child -> String childSwitchId = getChildId(child) // If this is the child being updated, use the new state; otherwise query current state String switchState = (childId != null && childSwitchId == childId) ? newState : child.currentValue('switch') if (reportedStates.containsKey(childSwitchId)) { switchState = reportedStates[childSwitchId] } if (switchState == 'on') { cdsOn++ } } // A multi-DP batch may arrive before the platform has updated child currentValue(). if (cdsOn == 0 && reportedStates.values().any { it == null }) { return } String parentState = cdsOn > 0 ? 'on' : 'off' if (device.currentValue('switch') != parentState) { if (debugLogging) { log.debug "Parent Switch turned ${parentState.toUpperCase()} (number of child switches on = ${cdsOn})" } sendEvent(name: 'switch', value: parentState) } else { if (debugLogging) { log.debug "Parent Switch remains ${parentState.toUpperCase()} (number of child switches on = ${cdsOn})" } } } private int getTuyaAttributeValue(final List _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 getTuyaCommand(String dp, String dp_type, String fncmd, int tuyaCmdDefault = SETDATA) { return sendTuyaCommand(dp, dp_type, fncmd, tuyaCmdDefault) } List sendTuyaCommand(String dp, String dp_type, String fncmd, int tuyaCmdDefault = SETDATA) { List 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" } // String payloadSize = isZemismartScreenSwitch() ? '0800' : '0008' List cmds = zigbee.command(0xEF00, 0x24, payloadSize + 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 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' } if (isZemismartScreenSwitch()) { List cmds = getTuyaCommand('0D', DP_TYPE_BOOL, '00') cmds += ['delay 1000'] cmds += zigbee.command(CLUSTER_TUYA, 0x03, [destEndpoint:1]) sendZigbeeCommands(cmds) return } if (is8GangNvxorhcj()) { List cmds = [] ['01', '02', '03', '04', '05', '06', '65', '66'].each { String dpHex -> cmds += getTuyaCommand(dpHex, DP_TYPE_BOOL, '00') } return cmds } if (isQuero12GangSwitch()) { // 12-gang switch: Turn off first 6 physical relays (DPs 24-29 / 0x18-0x1D) in a single command return [ "he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0xEF00 0x00 {0001180100010019010001001A010001001B010001001C010001001D01000100}", 'delay 200', ] } if (isQueroSwitch()) { // Quero automatização 1/2/3-channel: turn off physical relays only (DPs 0x18-0x1A) List cmds = [] int physical = getQueroPhysicalCount() for (int g = 1; g <= physical; g++) { cmds += getTuyaCommand(zigbee.convertToHexString(0x18 + g - 1, 2), DP_TYPE_BOOL, '00') } sendZigbeeCommands(cmds) return } if (isMoesStarFeatherSwitch()) { List cmds = [] int gangs = getStarFeatherGangCount() for (int g = 1; g <= gangs; g++) { cmds += getTuyaCommand(zigbee.convertToHexString(0x18 + g - 1, 2), DP_TYPE_BOOL, '00') } sendZigbeeCommands(cmds) return } return [ "he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0xEF00 0x00 {0001010100010002010001000301000100}", 'delay 200', ] } def on() { if (infoLogging) { log.info 'Turn all switches ON' } if (isZemismartScreenSwitch()) { List cmds = getTuyaCommand('0D', DP_TYPE_BOOL, '01') cmds += ['delay 1000'] cmds += zigbee.command(CLUSTER_TUYA, 0x03, [destEndpoint:1]) sendZigbeeCommands(cmds) return } if (is8GangNvxorhcj()) { List cmds = [] ['01', '02', '03', '04', '05', '06', '65', '66'].each { String dpHex -> cmds += getTuyaCommand(dpHex, DP_TYPE_BOOL, '01') } return cmds } if (isQuero12GangSwitch()) { // 12-gang switch: Turn on first 6 physical relays (DPs 24-29 / 0x18-0x1D) in a single command return [ "he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0xEF00 0x00 {0001180100010119010001011A010001011B010001011C010001011D01000101}", 'delay 200', ] } if (isQueroSwitch()) { // Quero automatização 1/2/3-channel: turn on physical relays only (DPs 0x18-0x1A) List cmds = [] int physical = getQueroPhysicalCount() for (int g = 1; g <= physical; g++) { cmds += getTuyaCommand(zigbee.convertToHexString(0x18 + g - 1, 2), DP_TYPE_BOOL, '01') } sendZigbeeCommands(cmds) return } if (isMoesStarFeatherSwitch()) { List cmds = [] int gangs = getStarFeatherGangCount() for (int g = 1; g <= gangs; g++) { cmds += getTuyaCommand(zigbee.convertToHexString(0x18 + g - 1, 2), DP_TYPE_BOOL, '01') } sendZigbeeCommands(cmds) return } return [ "he cmd 0x${device.deviceNetworkId} 0x${device.endpointId} 0xEF00 0x00 {0001010100010102010001010301000101}", 'delay 200', ] } def refresh() { if (infoLogging) { log.info 'Refreshing...' } //if (isTratoZemismartKeypad() || isMoesStarFeatherSwitch() || is12GangSwitch() || is8GangNvxorhcj()) { 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" } if (isZemismartScreenSwitch()) { String childId = getChildId(childDevice) if (!(childId in ['01', '02', '03', '04'])) { return } sendZigbeeCommands(getTuyaCommand(childId, DP_TYPE_BOOL, '01')) } else if (isMoesStarFeatherSwitch()) { // Star Feather switches use different data points: 0x18(24), 0x19(25), 0x1A(26), 0x1B(27) String childId = getChildId(childDevice) int dpValue = 0x18 + (childId as int) - 1 // Convert to hex: 01->0x18, 02->0x19, etc. String dpHex = zigbee.convertToHexString(dpValue, 2) List cmds = getTuyaCommand(dpHex, DP_TYPE_BOOL, '01') sendZigbeeCommands(cmds) } else if (is8GangNvxorhcj()) { // 8-gang special mapping: gangs 7/8 are DP 0x65/0x66 String childId = getChildId(childDevice) String dpHex = (childId as int) <= 6 ? childId : (childId == '07' ? '65' : '66') List cmds = getTuyaCommand(dpHex, DP_TYPE_BOOL, '01') sendZigbeeCommands(cmds) } else if (isQuero12GangSwitch()) { // 12-gang switch: Switches 1-6 use DPs 24-29 (0x18-0x1D), Switches 7-12 use DPs 7-12 (0x07-0x0C) String childId = getChildId(childDevice) int switchNum = childId as int int dpValue String dpType String dpCmd if (switchNum <= 6) { dpValue = 0x18 + switchNum - 1 // Physical relays: 01->0x18, 02->0x19, ... 06->0x1D dpType = DP_TYPE_BOOL dpCmd = '01' } else { dpValue = 0x07 + switchNum - 7 // Virtual scenes: 07->0x07, 08->0x08, ... 12->0x0C dpType = DP_TYPE_ENUM dpCmd = '03' // Use '03' for virtual switch ON command } String dpHex = zigbee.convertToHexString(dpValue, 2) List cmds = getTuyaCommand(dpHex, dpType, dpCmd) sendZigbeeCommands(cmds) } else if (isQueroSwitch()) { // Quero automatização 1/2/3-channel: physical 01..N->DPs 0x18-0x1A (BOOL); virtual (N+1)..2N->DPs 0x05-0x07 (ENUM) String childId = getChildId(childDevice) int childNum = childId as int int physical = getQueroPhysicalCount() int dpValue String dpType String dpCmd if (childNum <= physical) { dpValue = 0x18 + childNum - 1 dpType = DP_TYPE_BOOL dpCmd = '01' } else { dpValue = 0x05 + (childNum - physical) - 1 dpType = DP_TYPE_ENUM dpCmd = '03' } String dpHex = zigbee.convertToHexString(dpValue, 2) List cmds = getTuyaCommand(dpHex, dpType, dpCmd) sendZigbeeCommands(cmds) } else { // Original method for other switches 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" } if (isZemismartScreenSwitch()) { String childId = getChildId(childDevice) if (!(childId in ['01', '02', '03', '04'])) { return } sendZigbeeCommands(getTuyaCommand(childId, DP_TYPE_BOOL, '00')) } else if (isMoesStarFeatherSwitch()) { // Star Feather switches use different data points: 0x18(24), 0x19(25), 0x1A(26), 0x1B(27) String childId = getChildId(childDevice) int dpValue = 0x18 + (childId as int) - 1 // Convert to hex: 01->0x18, 02->0x19, etc. String dpHex = zigbee.convertToHexString(dpValue, 2) List cmds = getTuyaCommand(dpHex, DP_TYPE_BOOL, '00') sendZigbeeCommands(cmds) } else if (is8GangNvxorhcj()) { // 8-gang special mapping: gangs 7/8 are DP 0x65/0x66 String childId = getChildId(childDevice) String dpHex = (childId as int) <= 6 ? childId : (childId == '07' ? '65' : '66') List cmds = getTuyaCommand(dpHex, DP_TYPE_BOOL, '00') sendZigbeeCommands(cmds) } else if (isQuero12GangSwitch()) { // 12-gang switch: Switches 1-6 use DPs 24-29 (0x18-0x1D), Switches 7-12 use DPs 7-12 (0x07-0x0C) String childId = getChildId(childDevice) int switchNum = childId as int int dpValue String dpType String dpCmd if (switchNum <= 6) { dpValue = 0x18 + switchNum - 1 // Physical relays: 01->0x18, 02->0x19, ... 06->0x1D dpType = DP_TYPE_BOOL dpCmd = '00' } else { dpValue = 0x07 + switchNum - 7 // Virtual scenes: 07->0x07, 08->0x08, ... 12->0x0C dpType = DP_TYPE_ENUM dpCmd = '02' // Use '02' for virtual switch OFF command } String dpHex = zigbee.convertToHexString(dpValue, 2) List cmds = getTuyaCommand(dpHex, dpType, dpCmd) sendZigbeeCommands(cmds) } else if (isQueroSwitch()) { // Quero automatização 1/2/3-channel: physical 01..N->DPs 0x18-0x1A (BOOL); virtual (N+1)..2N->DPs 0x05-0x07 (ENUM) String childId = getChildId(childDevice) int childNum = childId as int int physical = getQueroPhysicalCount() int dpValue String dpType String dpCmd if (childNum <= physical) { dpValue = 0x18 + childNum - 1 dpType = DP_TYPE_BOOL dpCmd = '00' } else { dpValue = 0x05 + (childNum - physical) - 1 dpType = DP_TYPE_ENUM dpCmd = '02' } String dpHex = zigbee.convertToHexString(dpValue, 2) List cmds = getTuyaCommand(dpHex, dpType, dpCmd) sendZigbeeCommands(cmds) } else { // Original method for other switches 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 (isZemismartScreenSwitch()) { refresh(); return } 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' : case '_TZE200_ydkqbmpt' : // Moes Star Feather 1 Gang case '_TZE200_z3u99qxt' : // Moes Star Feather 1 Gang case '_TZE200_stvgmdjz' : // Moes Star Feather 1 Gang buttons = 1 break case '_TZE200_g1ib5ldv' : case '_TZE200_wunufsil' : case '_TZE200_nh9m9emk' : case '_TZE200_7deq70b8' : case '_TZE204_zpvusbtv' : case '_TZE200_uenof8jd' : // Moes Star Feather 2 Gang case '_TZE200_tzyy0rtq' : // Moes Star Feather 2 Gang case '_TZE200_hktk6hze' : // Moes Star Feather 2 Gang / Nova Digital TPZ-2 buttons = 2 break case '_TZE200_tz32mtza' : case '_TZE200_kyfqmmyl' : case '_TZE200_go3tvswy' : case '_TZE200_atpwqgml' : case '_TZE204_rkbxtclc' : case '_TZE200_2hf7x9n3' : case '_TZE204_atpwqgml' : case '_TZE200_bynnczcb' : case '_TZE200_wv9ukqca' : // Moes Star Feather 3 Gang case '_TZE200_zo0cfekv' : // Moes Star Feather 3 Gang case '_TZE200_rd8cdssd' : // Moes Star Feather 3 Gang / Nova Digital TPZ-3 buttons = 3 break case '_TZE200_k6jhsr0q' : case '_TZE284_xibaabmu' : // Zemismart 4-gang screen switch case '_TZE200_aqnazj70' : case '_TZE200_1ozguk6x' : case '_TZE200_mexisfik' : case '_TZE200_1n2kyphz' : case '_TZE200_shkxsgis' : case '_TZE204_shkxsgis' : case '_TZE200_mua6ucdj' : case '_TZE204_aagrxlbd' : case '_TZE200_dq8bu0pt' : // Moes Star Feather 4 Gang case '_TZE200_9dhenr94' : // Moes Star Feather 4 Gang case '_TZE200_hmabvy81' : // Moes Star Feather 4 Gang / Nova Digital TPZ-4 buttons = 4 break case '_TZE200_leaqthtqq' : // Mercator Ikuu 5-gang switch buttons = 5 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 case '_TZE204_g4au0afs' : // NOVADIGITAL case '_TZE200_mwvfvw8g' : case '_TZE200_cduqh1l0' : case '_TZE204_cduqh1l0' : case '_TZE200_emxxanvi' : case '_TZE204_jgujzjwh' : // Eyzee 6-gang switch case '_TZE200_8eazvzo6' : // Eyzee 6-gang switch buttons = 6 break case '_TZE204_dvosyycn' : buttons = 6 // limited to 6 relays only, actually has 8 relays and 8 inputs break case '_TZE204_nvxorhcj' : // 8 Nova Digital Topazio Smart Switch 8 Gang buttons = 8 break case '_TZE204_dqolcpcp' : // 6 relays and 6 scene switches (virtual) https://github.com/Koenkk/zigbee-herdsman-converters/blob/e9132fc65cc0bcbef60a5793976ba192fcc845ac/src/devices/qa.ts#L239 buttons = 12 break case '_TZE284_ms97nkyy' : // Quero automatização 6-gang switch - 6 physical relays (DPs 24-29) + 6 virtual scene switches (DPs 7-12) buttons = 12 break case '_TZE284_nzns7udm' : // Quero automatização 1-gang switch (1 physical + 1 virtual = 2) buttons = 2 break case '_TZE284_1aqlsquf' : // Quero automatização 2-gang switch (2 physical + 2 virtual = 4) buttons = 4 break case '_TZE284_pgxndxp4' : // Quero automatização 3-gang switch (3 physical + 3 virtual = 6) buttons = 6 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}-${String.format('%02d', 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 EP${String.format('%02d', i)}", label: "${device.displayName} EP${String.format('%02d', 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) } } } private Map screenPreferenceOptions(String key) { if (key == 'screenIndicatorMode') { return ScreenIndicatorOptions } if (key == 'radarConfig') { return RadarConfigOptions } if (key in ['backlightMode', 'childLlock']) { return SwitchStateOptions } if (key in ['onColor', 'offColor', 'delayColor']) { return ColorOptions } if (key ==~ /powerOnBehavior_l[1-4]/) { return PowerOnBehaviorOptions } return [:] } private boolean validScreenPreference(String key, Object value) { boolean valid = false if (key ==~ /screenLabel[1-4]/) { valid = value instanceof String && validScreenText(value) } else if (key in ['debugLogging', 'infoLogging']) { valid = value?.toString() in ['true', 'false'] } else if (key == 'backlightLevel') { try { BigDecimal brightness = new BigDecimal(value.toString()) valid = brightness >= 0 && brightness <= 100 && brightness.stripTrailingZeros().scale() <= 0 } catch (Exception ignored) { valid = false } } else { valid = screenPreferenceOptions(key).containsValue(value) } if (!valid) { log.warn "Screen switch: unsupported preference or invalid value ${key} = ${value}" } return valid } List zemismartScreenConfig() { List cmds = [] if (!isZemismartScreenSwitch()) { return cmds } [backlightLevel:'66', childLlock:'65', onColor:'67', offColor:'68', delayColor:'13', radarConfig:'6F'].each { key, dp -> def value = settings[key] if (value != null && value != '' && validScreenPreference(key, value)) { if (key == 'backlightLevel') { cmds += getTuyaCommand(dp, DP_TYPE_VALUE, zigbee.convertToHexString(new BigDecimal(value.toString()).intValue(), 8)) } else { int code = screenPreferenceOptions(key).find { it.value == value }.key cmds += getTuyaCommand(dp, key == 'childLlock' ? DP_TYPE_BOOL : DP_TYPE_ENUM, zigbee.convertToHexString(code, 2)) } if (infoLogging) { log.info "Setting ${key} to ${value}" } } } return cmds } List screenLabelConfig() { List cmds = [] if (!isZemismartScreenSwitch()) { return cmds } Map requests = [:] + (state.screenLabelRequests ?: [:]) for (int gang = 1; gang <= 4; gang++) { String key = gang.toString() String text = settings["screenLabel${gang}"] ?: '' // Unconfigured blanks leave factory labels alone. Clearing a previously sent label is explicit. if (!requests.containsKey(key) && text.trim().isEmpty()) { continue } if (requests[key] == text && device.currentValue("screenLabel${gang}") == text) { continue } if (!validScreenText(text)) { log.warn "Invalid display label ${gang}: maximum 12 UTF-16 units of valid text" continue } cmds += getTuyaCommand(zigbee.convertToHexString(104 + gang, 2), DP_TYPE_RAW, screenTextHex(text)) requests[key] = text if (infoLogging) { log.info "Setting display label ${gang} to ${text}" } } state.screenLabelRequests = requests // Requested values remain separate from screenLabelN attributes populated by reports. return cmds } List switchLightModeConfig() { List cmds = [] if (isZemismartScreenSwitch()) { [screenIndicatorMode:'0F', backlightMode:'10'].each { key, dp -> def value = settings[key] if (value != null && value != '' && validScreenPreference(key, value)) { int code = screenPreferenceOptions(key).find { it.value == value }.key cmds += getTuyaCommand(dp, key == 'backlightMode' ? DP_TYPE_BOOL : DP_TYPE_ENUM, zigbee.convertToHexString(code, 2)) if (infoLogging) { log.info "Setting ${key} to ${value}" } } } return 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 if (isMoesStarFeatherSwitch()) { // Star Feather uses separate DPs for backlight and indicator - treat independently // Handle backlight mode (DP 0x24) int backlightValue = settings?.backlightMode == 'on' ? 1 : 0 if (infoLogging) { log.info "Star Feather - Setting backlight mode: ${settings?.backlightMode}" } cmds += getTuyaCommand('24', DP_TYPE_BOOL, zigbee.convertToHexString(backlightValue, 2)) // Handle indicator mode (DP 0x25) int indicatorValue = settings?.lightIndicatorMode == 'none' ? 0 : settings?.lightIndicatorMode == 'lit when on' ? 1 : 2 if (infoLogging) { log.info "Star Feather - Setting indicator mode: ${settings?.lightIndicatorMode}" } cmds += getTuyaCommand('25', DP_TYPE_ENUM, zigbee.convertToHexString(indicatorValue, 2)) } else { switch (settings?.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 powerOnBehaviorConfig() { List cmds = [] if (isZemismartScreenSwitch()) { for (int gang = 1; gang <= 4; gang++) { String key = "powerOnBehavior_l${gang}" def value = settings[key] if (value != null && value != '' && validScreenPreference(key, value)) { int code = PowerOnBehaviorOptions.find { it.value == value }.key cmds += getTuyaCommand(zigbee.convertToHexString(28 + gang, 2), DP_TYPE_ENUM, zigbee.convertToHexString(code, 2)) if (infoLogging) { log.info "Setting ${key} to ${value}" } } } return 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 if (isMoesStarFeatherSwitch()) { int rl = settings?.powerOnBehavior == 'off' ? 0 : settings?.powerOnBehavior == 'on' ? 1 : 2 if (debugLogging) { log.debug "Star Feather Power-on behavior: ${rl} (${settings?.powerOnBehavior})" } String dpValHex = zigbee.convertToHexString(rl, 2) cmds = getTuyaCommand('26', DP_TYPE_ENUM, dpValHex) // DP 0x26 for Star Feather switches } 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 } List vibrationModeConfig() { List cmds = [] if (isMoesStarFeatherSwitch()) { // Extract gear number from "Gear X" format int gearNumber = 1 // default if (settings?.vibrationMode) { String gearStr = settings?.vibrationMode?.replaceAll(/[^0-9]/, '') gearNumber = gearStr ? (gearStr as int) : 1 } if (infoLogging) { log.info "Star Feather - Setting vibration mode: ${settings?.vibrationMode}" } String dpValHex = zigbee.convertToHexString(gearNumber, 2) cmds = getTuyaCommand('68', DP_TYPE_ENUM, dpValHex) } return cmds } List inductionModeConfig() { List cmds = [] if (isMoesStarFeatherSwitch()) { int inductionValue = settings?.inductionMode == 'on' ? 1 : 0 if (infoLogging) { log.info "Star Feather - Setting induction mode: ${settings?.inductionMode}" } String dpValHex = zigbee.convertToHexString(inductionValue, 2) cmds = getTuyaCommand('67', DP_TYPE_BOOL, dpValHex) } return cmds } List switchModeConfig() { List cmds = [] if (!isMoesStarFeatherSwitch()) { return cmds } int gangs = getStarFeatherGangCount() for (int g = 1; g <= gangs; g++) { def val = settings["switchMode_l${g}"] if (val != null) { int modeValue = val == 'scene' ? 1 : 0 String dpHex = zigbee.convertToHexString(0x12 + g - 1, 2) if (infoLogging) { log.info "Star Feather - Setting switch ${g} mode: ${val}" } cmds += getTuyaCommand(dpHex, DP_TYPE_ENUM, zigbee.convertToHexString(modeValue, 2)) } } return cmds } List countdownConfig() { List cmds = [] if (!isMoesStarFeatherSwitch()) { return cmds } int gangs = getStarFeatherGangCount() for (int g = 1; g <= gangs; g++) { def val = settings["countdown_l${g}"] if (val != null) { int seconds = val as int String dpHex = zigbee.convertToHexString(0x1E + g - 1, 2) if (infoLogging) { log.info "Star Feather - Setting switch ${g} countdown: ${seconds}s" } cmds += getTuyaCommand(dpHex, DP_TYPE_VALUE, zigbee.convertToHexString(seconds, 8)) } } return cmds } List momentaryConfig() { List cmds = [] if (!isMoesStarFeatherSwitch()) { return cmds } int gangs = getStarFeatherGangCount() for (int g = 1; g <= gangs; g++) { def val = settings["momentary_l${g}"] if (val != null) { int seconds = val as int String dpHex = zigbee.convertToHexString(0x69 + g - 1, 2) if (infoLogging) { log.info "Star Feather - Setting switch ${g} momentary timer: ${seconds}s" } cmds += getTuyaCommand(dpHex, DP_TYPE_VALUE, zigbee.convertToHexString(seconds, 8)) } } return cmds } void queryAllTuyaDP() { log.trace 'queryAllTuyaDP()' List cmds = [] if (isZemismartScreenSwitch()) { // Tuya configureMagicPacket, followed by the normal DP query through refresh(). cmds += zigbee.readAttribute(0x0000, [0x0004, 0x0000, 0x0001, 0x0005, 0x0007, 0xFFFE], [destEndpoint:1], 1000) } 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 cmds = childModeConfig() sendZigbeeCommands(cmds) }