/** * Tuya Wall Thermostat driver for Hubitat Elevation * * https://community.hubitat.com/t/beta-tuya-wall-mount-thermostat-water-electric-floor-heating-zigbee-driver/87050 * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * Credits: Jaewon Park, iquix and many others * * ver. 1.0.0 2022-01-09 kkossev - Inital version * ver. 1.0.1 2022-01-09 kkossev - modelGroupPreference working OK * ver. 1.0.2 2022-01-09 kkossev - MOES group heatingSetpoint and setpointReceiveCheck() bug fixes * ver. 1.0.3 2022-01-10 kkossev - resending heatingSetpoint max 3 retries; heatSetpoint rounding up/down; incorrect temperature reading check; min and max values for heatingSetpoint * ver. 1.0.4 2022-01-11 kkossev - reads temp. calibration for AVATTO, patch: temperatures > 50 are divided by 10!; AVATO parameters decoding; added BEOK model * ver. 1.0.5 2022-01-15 kkossev - 2E+1 bug fixed; added rxCounter, txCounter, duplicateCounter; ChildLock control; if boost (emergency) mode was on, then auto() heat() off() commands cancel it; * BRT-100 thermostatOperatingState changes on valve report; AVATTO/MOES switching from off mode to auto/heat modes fix; command 'controlMode' is now removed. * ver. 1.0.6 2022-01-16 kkossev - debug/trace commands fixes * ver. 1.0.7 2022-03-21 kkossev - added childLock attribute and events; checkDriverVersion(); removed 'Switch' capability and events; enabled 'auto' mode for all thermostat types. * ver. 1.0.8 2022-04-03 kkossev - added tempCalibration; hysteresis; minTemp and maxTemp for AVATTO and BRT-100; added Battery capability for BRT-100 * ver. 1.2.1 2022-04-05 kkossev - BRT-100 basic cluster warning supressed; tempCalibration, maxTemp, minTemp fixes; added Battery capability; 'Changed from device Web UI' desctiption in off() and heat() events. * ver. 1.2.2 2022-09-03 kkossev - AVATTO additional DP logging; * * TODO: remove calibration command! (now is as Preference parameter) * */ def version() { "1.2.2" } def timeStamp() {"2022/09/03 9:39 PM"} import groovy.json.* import groovy.transform.Field import hubitat.zigbee.zcl.DataType import hubitat.device.HubAction import hubitat.device.Protocol metadata { definition (name: "Tuya Wall Thermostat", namespace: "kkossev", author: "Krassimir Kossev", importUrl: "https://raw.githubusercontent.com/kkossev/Hubitat-Tuya-Wall-Thermostat/main/Tuya-Wall-Thermostat.groovy", singleThreaded: true ) { capability "Refresh" capability "Sensor" capability "Initialize" capability "Temperature Measurement" capability "Thermostat" //capability "ThermostatMode" capability "ThermostatHeatingSetpoint" capability "ThermostatSetpoint" capability "Battery" // BRT-100 attribute "childLock", "enum", ["off", "on"] command "calibration", ["string"] /* command "zTest", [ [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"] ] */ command "initialize" command "childLock", [ [name: "ChildLock", type: "ENUM", constraints: ["off", "on"], description: "Select Child Lock mode"] ] // (AVATTO) fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_ye5jkfsb", deviceJoinName: "AVATTO Wall Thermostat" // ME81AH // (Moes) fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_aoclfnxz", deviceJoinName: "Moes Wall Thermostat" // BHT-002 // (unknown) fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_unknown", deviceJoinName: "_TZE200_ Thermostat" // unknown // (BEOK) fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_2ekuz3dz", deviceJoinName: "Beok Wall Thermostat" // // (BRT-100 for dev tests only!) fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_b6wax7g0", deviceJoinName: "BRT-100 TRV" // BRT-100 //fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_chyvmhay", deviceJoinName: "Lidl Silvercrest" // Lidl Silvercrest } preferences { input (name: "logEnable", type: "bool", title: "Debug logging", description: "Debug information, useful for troubleshooting. Recommended value is false", defaultValue: false) input (name: "txtEnable", type: "bool", title: "Description text logging", description: "Display measured values in HE log page. Recommended value is true", defaultValue: true) input (name: "forceManual", type: "bool", title: "Force Manual Mode", description: "If the thermostat changes intto schedule mode, then it automatically reverts back to manual mode", defaultValue: false) input (name: "resendFailed", type: "bool", title: "Resend failed commands", description: "If the thermostat does not change the Setpoint or Mode as expected, then commands will be resent automatically", defaultValue: false) input (name: "minTemp", type: "number", title: "Minimim Temperature", description: "The Minimim temperature that can be sent to the device", defaultValue: 10, range: "5.0..20.0") input (name: "maxTemp", type: "number", title: "Maximum Temperature", description: "The Maximum temperature that can be sent to the device", defaultValue: 40, range: "28.0..90.0") input (name: "modelGroupPreference", title: "Select a model group. Recommended value is 'Auto detect'", /*description: "Thermostat type",*/ type: "enum", options:["Auto detect", "AVATTO", "MOES", "BEOK", "MODEL3", "BRT-100"], defaultValue: "Auto detect", required: false) input (name: "tempCalibration", type: "number", title: "Temperature Calibration", description: "Adjust measured temperature range: -9..9 C", defaultValue: 0, range: "-9.0..9.0") input (name: "hysteresis", type: "number", title: "Hysteresis", description: "Adjust switching differential range: 1..5 C", defaultValue: 1, range: "1.0..5.0") // not available for BRT-100 ! } } @Field static final Map Models = [ '_TZE200_ye5jkfsb' : 'AVATTO', // Tuya AVATTO ME81AH '_TZE200_aoclfnxz' : 'MOES', // Tuya Moes BHT series Thermostat BTH-002 '_TZE200_2ekuz3dz' : 'BEOK', // Beok thermostat '_TZE200_other' : 'MODEL3', // Tuya other models (reserved) '_TZE200_b6wax7g0' : 'BRT-100', // TRV BRT-100; ZONNSMART '_TZE200_ckud7u2l' : 'TEST2', // KKmoon Tuya; temp /10.0 '_TZE200_zion52ef' : 'TEST3', // TRV MOES => fn = "0001 > off: dp = "0204" data = "02" // off; heat: dp = "0204" data = "01" // on; auto: n/a !; setHeatingSetpoint(preciseDegrees): fn = "00" SP = preciseDegrees *10; dp = "1002" '_TZE200_c88teujp' : 'TEST3', // TRV "SEA-TR", "Saswell", model "SEA801" (to be tested) '_TZE200_xxxxxxxx' : 'UNKNOWN', '_TZE200_xxxxxxxx' : 'UNKNOWN', '' : 'UNKNOWN' // ] @Field static final Integer MaxRetries = 3 // KK TODO ! private getCLUSTER_TUYA() { 0xEF00 } private getSETDATA() { 0x00 } private getSETTIME() { 0x24 } // Tuya Commands private getTUYA_REQUEST() { 0x00 } private getTUYA_REPORTING() { 0x01 } private getTUYA_QUERY() { 0x02 } private getTUYA_STATUS_SEARCH() { 0x06 } private getTUYA_TIME_SYNCHRONISATION() { 0x24 } // tuya DP type private getDP_TYPE_RAW() { "01" } // [ bytes ] private getDP_TYPE_BOOL() { "01" } // [ 0/1 ] private getDP_TYPE_VALUE() { "02" } // [ 4 byte value ] private getDP_TYPE_STRING() { "03" } // [ N byte string ] private getDP_TYPE_ENUM() { "04" } // [ 0-255 ] private getDP_TYPE_BITMAP() { "05" } // [ 1,2,4 bytes ] as bits // Parse incoming device messages to generate events def parse(String description) { checkDriverVersion() if (state.rxCounter != null) state.rxCounter = state.rxCounter + 1 //if (settings?.logEnable) log.debug "${device.displayName} parse() descMap = ${zigbee.parseDescriptionAsMap(description)}" if (description?.startsWith('catchall:') || description?.startsWith('read attr -')) { Map descMap = zigbee.parseDescriptionAsMap(description) if (descMap?.clusterInt==CLUSTER_TUYA && descMap?.command == "24") { //getSETTIME if (settings?.logEnable) log.debug "${device.displayName} time synchronization request from device, descMap = ${descMap}" def offset = 0 try { offset = location.getTimeZone().getOffset(new Date().getTime()) //if (settings?.logEnable) log.debug "${device.displayName} timezone offset of current location is ${offset}" } catch(e) { log.error "${device.displayName} cannot resolve current location. please set location in Hubitat location setting. Setting timezone offset to zero" } def cmds = zigbee.command(CLUSTER_TUYA, SETTIME, "0008" +zigbee.convertToHexString((int)(now()/1000),8) + zigbee.convertToHexString((int)((now()+offset)/1000), 8)) // log.trace "${device.displayName} now is: ${now()}" // KK TODO - converto to Date/Time string! if (settings?.logEnable) log.debug "${device.displayName} sending time data : ${cmds}" cmds.each{ sendHubCommand(new hubitat.device.HubAction(it, hubitat.device.Protocol.ZIGBEE)) } if (state.txCounter != null) state.txCounter = state.txCounter + 1 state.old_dp = "" state.old_fncmd = "" } else if (descMap?.clusterInt==CLUSTER_TUYA && descMap?.command == "0B") { // ZCL Command Default Response String clusterCmd = descMap?.data[0] def status = descMap?.data[1] if (settings?.logEnable) log.debug "${device.displayName} device has received Tuya cluster ZCL command 0x${clusterCmd} response 0x${status} data = ${descMap?.data}" state.old_dp = "" state.old_fncmd = "" if (status != "00") { if (settings?.logEnable) log.warn "${device.displayName} ATTENTION! manufacturer = ${device.getDataValue("manufacturer")} group = ${getModelGroup()} unsupported Tuya cluster ZCL command 0x${clusterCmd} response 0x${status} data = ${descMap?.data} !!!" } } else if ((descMap?.clusterInt==CLUSTER_TUYA) && (descMap?.command == "01" || descMap?.command == "02")) { //if (descMap?.command == "02") { if (settings?.logEnable) log.warn "command == 02 !" } def transid = zigbee.convertHexToInt(descMap?.data[1]) // "transid" is just a "counter", a response will have the same transid as the command def dp = zigbee.convertHexToInt(descMap?.data[2]) // "dp" field describes the action/message of a command frame def dp_id = zigbee.convertHexToInt(descMap?.data[3]) // "dp_identifier" is device dependant def fncmd = getTuyaAttributeValue(descMap?.data) // if (dp == state.old_dp && fncmd == state.old_fncmd) { if (settings?.logEnable) log.warn "(duplicate) transid=${transid} dp_id=${dp_id} dp=${dp} fncmd=${fncmd} command=${descMap?.command} data = ${descMap?.data}" if ( state.duplicateCounter != null ) state.duplicateCounter = state.duplicateCounter +1 return } if (settings?.logEnable) log.trace " dp_id=${dp_id} dp=${dp} fncmd=${fncmd}" state.old_dp = dp state.old_fncmd = fncmd // the switch cases below default to dp_id = "01" switch (dp) { case 0x01 : // 0x01: Heat / Off DP_IDENTIFIER_THERMOSTAT_MODE_4 0x01 // mode for Moes device used with DP_TYPE_ENUM if (getModelGroup() in ['BRT-100', 'TEST2']) { processBRT100Presets( dp, fncmd ) // 0x0401 # Mode (Received value 0:Manual / 1:Holiday / 2:Temporary Manual Mode / 3:Prog) } else { // AVATTO switch (boolean) /* version 1.0.4 */ def mode = (fncmd == 0) ? "off" : "heat" if (settings?.txtEnable) log.info "${device.displayName} Thermostat mode is: ${mode} (dp=${dp}, fncmd=${fncmd})" sendEvent(name: "thermostatMode", value: mode, displayed: true) if (mode == "off") { sendEvent(name: "thermostatOperatingState", value: "idle", displayed: true) // do not store as last state! } else { sendEvent(name: "thermostatOperatingState", value: state.lastThermostatOperatingState, displayed: true) // do not store as last state! } state.lastThermostatMode = mode if (mode == state.mode) { state.mode = "" } } break case 0x02 : // Mode (LIDL) // DP_IDENTIFIER_THERMOSTAT_HEATSETPOINT 0x02 // Heatsetpoint if (settings?.logEnable) log.trace " dp_id=${dp_id} dp=${dp} fncmd=${fncmd}" if (getModelGroup() in ['BRT-100', 'TEST2']) { // BRT-100 Thermostat heatsetpoint # 0x0202 # processTuyaHeatSetpointReport( fncmd ) // target temp, in degrees (int!) break } else { // AVATTO : mode (enum) 'manual', 'program' // DP_IDENTIFIER_THERMOSTAT_MODE_2 0x02 // mode for Moe device used with DP_TYPE_ENUM if (settings?.logEnable) log.trace "device current mode = ${device.currentState('thermostatMode').value}" if (device.currentState("thermostatMode").value == "off") { if (settings?.logEnable) log.warn "ignoring 0x02 command in off mode" sendEvent(name: "thermostatOperatingState", value: "idle") break // ignore 0x02 command if thermostat was switched off !! } else { // continue below.. break statement is missing intentionaly! if (settings?.logEnable) log.trace "...continue in mode ${device.currentState('thermostatMode').value}..." } } case 0x03 : // Scheduled/Manual Mode or // Thermostat current temperature (in decidegrees) if (settings?.logEnable) log.trace "processing command dp=${dp} fncmd=${fncmd}" // TODO - use processTuyaModes3( dp, fncmd ) if (descMap?.data.size() <= 7) { def mode if (!(fncmd == 0)) { // KK inverted mode = "auto" // scheduled //log.trace "forceManual = ${settings?.forceManual}" if (settings?.forceManual == true) { if (settings?.logEnable) log.warn "calling setManualMode()" setManualMode() } else { //log.trace "setManualMode() not called!" } } else { mode = "heat" // manual } if (settings?.txtEnable) log.info "${device.displayName} Thermostat mode is: $mode (0x${fncmd}) (dp=${dp}, fncmd=${fncmd})" sendEvent(name: "thermostatMode", value: mode, displayed: true) // mode was confirmed from the Preset info data... state.lastThermostatMode = mode } else { // # 0x0203 # BRT-100 // Thermostat current temperature if (settings?.logEnable) log.trace "processTuyaTemperatureReport descMap?.size() = ${descMap?.data.size()} dp_id=${dp_id} dp=${dp} :" processTuyaTemperatureReport( fncmd ) } break case 0x04 : // BRT-100 Boost DP_IDENTIFIER_THERMOSTAT_BOOST DP_IDENTIFIER_THERMOSTAT_BOOST 0x04 // Boost for Moes processTuyaBoostModeReport( fncmd ) break case 0x05 : // BRT-100 ? if (settings?.txtEnable) log.info "${device.displayName} configuration is done. Result: 0x${fncmd}" break case 0x07 : // others Childlock status DP_IDENTIFIER_THERMOSTAT_CHILDLOCK_1 0x07 // 0x0407 > starting moving if (settings?.txtEnable) log.info "${device.displayName} valve starts moving: 0x${fncmd}" // BRT-100 00-> opening; 01-> closed! if (fncmd == 00) { sendThermostatOperatingStateEvent("heating") //sendEvent(name: "thermostatOperatingState", value: "heating", displayed: true) } else { // fncmd == 01 sendThermostatOperatingStateEvent("idle") //sendEvent(name: "thermostatOperatingState", value: "idle", displayed: true) } break case 0x08 : // DP_IDENTIFIER_WINDOW_OPEN2 0x08 // BRT-100 if (settings?.txtEnable) log.info "${device.displayName} Open window detection MODE (dp=${dp}) is: ${fncmd}" //0:function disabled / 1:function enabled break // case 0x09 : // BRT-100 unknown function case 0x0D : // 0x0D (13) BRT-100 Childlock status DP_IDENTIFIER_THERMOSTAT_CHILDLOCK_4 0x0D MOES, LIDL if (settings?.txtEnable) log.info "${device.displayName} Child Lock (dp=${dp}) is: ${fncmd}" // 0:function disabled / 1:function enabled break case 0x10 : // (16): Heating setpoint AVATTO // DP_IDENTIFIER_THERMOSTAT_HEATSETPOINT_3 0x10 // Heatsetpoint for TRV_MOE mode heat // BRT-100 Rx only? processTuyaHeatSetpointReport( fncmd ) break case 0x11 : // (17) AVATTO if (settings?.txtEnable) log.info "${device.displayName} Set temperature F is: ${fncmd}" break case 0x12 : // (18) Max Temp Limit MOES, LIDL if (getModelGroup() in ['AVATTO']) { if (settings?.txtEnable) log.info "${device.displayName} Set temperature upper limit F is: ${fncmd}" } else { // KK TODO - also Window open status (false:true) for TRVs ? DP_IDENTIFIER_WINDOW_OPEN if (settings?.txtEnable) log.info "${device.displayName} Max Temp Limit is: ${fncmd}" } break case 0x13 : // (19) Max Temp LIMIT AVATTO MOES, LIDL if (getModelGroup() in ['AVATTO']) { device.updateSetting("maxTemp", fncmd) // aka 'temperature ceiling' } if (settings?.txtEnable) log.info "${device.displayName} Max Temp Limit is: ${fncmd} C (dp=${dp}, fncmd=${fncmd})" break case 0x14 : // (20) Dead Zone Temp (hysteresis) MOES, LIDL if (getModelGroup() in ['AVATTO']) { if (settings?.txtEnable) log.info "${device.displayName} lower limit F is: ${fncmd}" } else { // KK TODO - also Valve state report : on=1 / off=0 ? DP_IDENTIFIER_THERMOSTAT_VALVE 0x14 // Valve if (settings?.txtEnable) log.info "${device.displayName} Dead Zone Temp (hysteresis) is: ${fncmd}" } break case 0x0E : // (14) BRT-100 Battery # 0x020e # battery percentage (updated every 4 hours ) case 0x15 : // (21) def battery = fncmd >100 ? 100 : fncmd if (settings?.txtEnable) log.info "${device.displayName} battery is: ${fncmd} % (dp=${dp})" getBatteryPercentageResult(fncmd*2) break case 0x17 : // (23) temperature scale for AVATTO if (settings?.txtEnable) log.info "${device.displayName} temperature scale is: ${fncmd==0?'C':'F'} (${fncmd})" break case 0x18 : // (24) : Current (local) temperature if (settings?.logEnable) log.trace "processTuyaTemperatureReport dp_id=${dp_id} dp=${dp} :" processTuyaTemperatureReport( fncmd ) break case 0x1A : // (26) AVATTO setpoint lower limit if (getModelGroup() in ['AVATTO']) { device.updateSetting("minTemp", fncmd) } if (settings?.txtEnable) log.info "${device.displayName} Min temperature limit is: ${fncmd} C (dp=${dp}, fncmd=${fncmd})" // TODO - update the minTemp preference ! break case 0x1B : // (27) temperature calibration/correction (offset in degrees) for AVATTO, Moes and Saswell processTuyaCalibration( dp, fncmd ) break case 0x1D : // (29) AVATTO if (settings?.txtEnable) log.info "${device.displayName} current temperature F is: ${fncmd}" break case 0x23 : // (35) LIDL BatteryVoltage if (settings?.txtEnable) log.info "${device.displayName} BatteryVoltage is: ${fncmd}" break case 0x24 : // (36) : current (running) operating state (valve) AVATTO (enum) 'open','close' if (settings?.txtEnable) log.info "${device.displayName} thermostatOperatingState is: ${fncmd==0 ? "heating" : "idle" } (dp=${dp}, fncmd=${fncmd})" sendThermostatOperatingStateEvent(fncmd==0 ? "heating" : "idle") break case 0x27 : // (39) AVATTO - RESET if (settings?.txtEnable) log.info "${device.displayName} thermostat reset (${fncmd})" break case 0x1E : // (30) DP_IDENTIFIER_THERMOSTAT_CHILDLOCK_3 0x1E // For Moes device case 0x28 : // (40) Child Lock DP_IDENTIFIER_THERMOSTAT_CHILDLOCK_2 0x28 ( AVATTO (boolean) ) if (settings?.txtEnable) log.info "${device.displayName} Child Lock is: ${fncmd} (dp=${dp})" sendEvent(name: "childLock", value: (fncmd == 0) ? "off" : "on" ) break case 0x2B : // (43) AVATTO Sensor 0-In 1-Out 2-Both // KK TODO if (settings?.txtEnable) log.info "${device.displayName} Sensor is: ${fncmd==0?'In':fncmd==1?'Out':fncmd==2?'In and Out':'UNKNOWN'} (${fncmd})" break case 0x2C : // temperature calibration (offset in degree) //DP_IDENTIFIER_THERMOSTAT_CALIBRATION_2 0x2C // Calibration offset used by others processTuyaCalibration( dp, fncmd ) break case 0x2D : // (45) LIDL and AVATTO ErrorStatus (bitmap) e1, e2, e3 // er1: Built-in sensor disconnected or fault with it; Er1: Built-in sensor disconnected or fault with it. if (settings?.txtEnable) log.info "${device.displayName} error code : ${fncmd}" break // case 0x62 : // (98) DP_IDENTIFIER_REPORTING_TIME 0x62 (Sensors) case 0x65 : // (101) AVATTO PID ; also LIDL ComfortTemp if (getModelGroup() in ['AVATTO']) { if (settings?.txtEnable) log.info "${device.displayName} Thermostat PID regulation point is: ${fncmd}" // Model#1 only !! } else { if (settings?.logEnable) log.info "${device.displayName} Thermostat SCHEDULE_1 data received (not processed)..." } break case 0x66 : // (102) min temperature limit; also LIDL EcoTemp if (settings?.txtEnable) log.info "${device.displayName} Min temperature limit is: ${fncmd}" break case 0x67 : // (103) max temperature limit; also LIDL AwaySetting if (getModelGroup() in ['BRT-100']) { // #0x0267 # Boost heating countdown in second (Received value [0, 0, 1, 44] for 300) if (settings?.txtEnable) log.info "${device.displayName} Boost heating countdown: ${fncmd} seconds" } else if (getModelGroup() in ['AVATTO']) { // Antifreeze mode ? if (settings?.txtEnable) log.info "${device.displayName} Antifreeze mode is ${fncmd==0?'off':'on'} (${fncmd})" } else { if (settings?.txtEnable) log.info "${device.displayName} unknown parameter is: ${fncmd} (dp=${dp}, fncmd=${fncmd}, data=${descMap?.data})" } // KK TODO - could be setpoint for some devices ? // DP_IDENTIFIER_THERMOSTAT_HEATSETPOINT_2 0x67 // Heatsetpoint for Moe ? break case 0x68 : // (104) DP_IDENTIFIER_THERMOSTAT_VALVE_2 0x68 // Valve; also LIDL TempCalibration! if (getModelGroup() in ['AVATTO']) { if (settings?.txtEnable) log.info "${device.displayName} AVATTO unknown parameter (104) is: ${fncmd}" // TODO: check AVATTO usage } else { if (settings?.txtEnable) log.info "${device.displayName} Valve position is: ${fncmd}% (dp=${dp}, fncmd=${fncmd})" // # 0x0268 # TODO - send event! (works OK with BRT-100 (values of 25 / 50 / 75 / 100) } break case 0x69 : // (105) BRT-100 temp calibration // could be also Heatsetpoint for TRV_MOE mode auto ? also LIDL if (getModelGroup() in ['BRT-100']) { processTuyaCalibration( dp, fncmd ) } else if (getModelGroup() in ['AVATTO']) { if (settings?.txtEnable) log.info "${device.displayName} AVATTO unknown parameter (105) is: ${fncmd}" // TODO: check AVATTO usage } else { log.warn "${device.displayName} (DP=0x69) ?TRV_MOES auto mode Heatsetpoint? value is: ${fncmd}" } break case 0x6A : // (106) DP_IDENTIFIER_THERMOSTAT_MODE_1 0x6A // mode used with DP_TYPE_ENUM Energy saving mode (Received value 0:off / 1:on) if (getModelGroup() in ['AVATTO']) { if (settings?.txtEnable) log.info "${device.displayName} Dead Zone temp (hysteresis) is: ${fncmd}C (dp=${dp}, fncmd=${fncmd})" device.updateSetting("hysteresis", fncmd) } else { if (settings?.txtEnable) log.info "${device.displayName} Energy saving mode? (dp=${dp}) is: ${fncmd} data = ${descMap?.data})" // 0:function disabled / 1:function enabled } break case 0x6B : // (107) DP_IDENTIFIER_TEMPERATURE 0x6B (Sensors) // BRT-100 ! if (getModelGroup() in ['AVATTO']) { if (settings?.txtEnable) log.info "${device.displayName} AVATTO unknown parameter (105) is: ${fncmd}" // TODO: check AVATTO usage } else { if (settings?.txtEnable) log.info "${device.displayName} (DP=0x6B) Energy saving mode temperature value is: ${fncmd}" // for BRT-100 # 0x026b # Energy saving mode temperature ( Received value [0, 0, 0, 15] ) } break case 0x6C : // (107) if (getModelGroup() in ['BRT-100']) { if (settings?.txtEnable) log.info "${device.displayName} (DP=0x6C) Max target temp is: ${fncmd}" // BRT-100 ( Received value [0, 0, 0, 35] ) device.updateSetting("maxTemp", fncmd) } else if (getModelGroup() in ['AVATTO']) { if (settings?.txtEnable) log.info "${device.displayName} AVATTO unknown parameter (107) is: ${fncmd}" // TODO: check AVATTO usage } else { if (settings?.txtEnable) log.info "${device.displayName} (DP=0x6C) humidity value is: ${fncmd}" // DP_IDENTIFIER_HUMIDITY 0x6C (Sensors) } // KK Tuya cmd: dp=108 value=404095046 descMap.data = [00, 08, 6C, 00, 00, 18, 06, 00, 28, 08, 00, 1C, 0B, 1E, 32, 0C, 1E, 32, 11, 00, 18, 16, 00, 46, 08, 00, 50, 17, 00, 3C] break case 0x6D : // (108) if (getModelGroup() in ['BRT-100']) { // 0x026d # Min target temp (Received value [0, 0, 0, 5]) if (settings?.txtEnable) log.info "${device.displayName} (DP=0x6D) Min target temp is: ${fncmd}" device.updateSetting("minTemp", fncmd) } else if (getModelGroup() in ['AVATTO']) { if (settings?.txtEnable) log.info "${device.displayName} AVATTO unknown parameter (108) is: ${fncmd}" // TODO: check AVATTO usage } else { // Valve position in % (also // DP_IDENTIFIER_THERMOSTAT_SCHEDULE_4 0x6D // Not finished) if (settings?.txtEnable) log.info "${device.displayName} (DP=0x6D) valve position is: ${fncmd} (dp=${dp}, fncmd=${fncmd})" } // KK TODO if (valve > 3) => On ! break case 0x6E : // (110) Low battery DP_IDENTIFIER_BATTERY 0x6E if (settings?.txtEnable) log.info "${device.displayName} Battery (DP= 0x6E) is: ${fncmd}" break case 0x70 : // (112) // Reporting DP_IDENTIFIER_REPORTING 0x70 // DP_IDENTIFIER_THERMOSTAT_SCHEDULE_2 0x70 // work days (6) if (settings?.txtEnable) log.info "${device.displayName} reporting status state : ${descMap?.data}" break //case 0x71 :// DP_IDENTIFIER_THERMOSTAT_SCHEDULE_3 0x71 // holiday = Not working day (6) // case 0x74 : // 0x74(116)- LIDL OpenwindowTemp // case 0x75 : // 0x75(117) - LIDL OpenwindowTime default : if (settings?.logEnable) log.warn "${device.displayName} NOT PROCESSED Tuya cmd: dp=${dp} value=${fncmd} descMap.data = ${descMap?.data}" break } // (dp) switch } else if (descMap?.cluster == "0000") { if (settings?.logEnable) log.debug "${device.displayName} basic cluster report : descMap = ${descMap}" } else { if (settings?.logEnable) log.warn "not parsed : "+descMap } } // if catchAll || readAttr } def processTuyaHeatSetpointReport( fncmd ) { def setpointValue def model = getModelGroup() switch (model) { case 'AVATTO' : setpointValue = fncmd break case 'MOES' : setpointValue = fncmd break case 'BEOK' : setpointValue = fncmd / 10.0 break case 'MODEL3' : setpointValue = fncmd break case 'BRT-100' : setpointValue = fncmd break case 'TEST2' : case 'TEST3' : setpointValue = fncmd / 10.0 break case 'UNKNOWN' : default : setpointValue = fncmd break } if (settings?.txtEnable) log.info "${device.displayName} heatingSetpoint is: ${setpointValue}" sendEvent(name: "heatingSetpoint", value: setpointValue as int, unit: "C", displayed: true) sendEvent(name: "thermostatSetpoint", value: setpointValue as int, unit: "C", displayed: false) // Google Home compatibility if (setpointValue == state.setpoint) { state.setpoint = 0 } } def processTuyaTemperatureReport( fncmd ) { double currentTemperatureValue def model = getModelGroup() switch (model) { case 'AVATTO' : currentTemperatureValue = fncmd break case 'MOES' : case 'BEOK' : // confirmed to be OK! case 'MODEL3' : currentTemperatureValue = fncmd / 10.0 break case 'BRT-100' : case 'TEST2' : case 'TEST3' : currentTemperatureValue = fncmd / 10.0 break default : currentTemperatureValue = fncmd break } if (currentTemperatureValue > 50 || currentTemperatureValue < 1) { log.warn "${device.displayName} invalid temperature : ${currentTemperatureValue}" // auto correct patch! currentTemperatureValue = currentTemperatureValue / 10.0 log.warn "auto correct patch for temperature!" } if (settings?.txtEnable) log.info "${device.displayName} temperature is: ${currentTemperatureValue}" sendEvent(name: "temperature", value: currentTemperatureValue, unit: "C", displayed: true) } def processTuyaCalibration( dp, fncmd ) { def temp = fncmd if (getModelGroup() in ['AVATTO'] ){ // (dp=27, fncmd=-1) device.updateSetting("tempCalibration", temp) //log.trace "AVATTO calibration" } else if (getModelGroup() in ['BRT-100'] && dp == 105) { // 0x69 device.updateSetting("tempCalibration", temp) if (settings?.logEnable) log.trace "BRT-100 calibration" } else { // "_TZE200_aoclfnxz" if (settings?.logEnable) log.trace "other calibration, getModelGroup() = ${getModelGroup()} dp=${dp} fncmd = ${fncmd}" if (temp > 2048) { temp = temp - 4096; } temp = temp / 100 // KK - check ! } if (settings?.txtEnable) log.info "${device.displayName} temperature calibration (correction) is: ${temp} (dp=${dp}, fncmd=${fncmd}) " } def processBRT100Presets( dp, data ) { if (settings?.logEnable) log.trace "processBRT100Presets fp-${dp} data=${data}" // 0x0401 # Mode (Received value 0:Manual / 1:Holiday / 2:Temporary Manual Mode / 3:Prog) // KK TODO - check why the difference for values 0 and 3 ? /* 0x0401 : 0 : Manual Mode 1 : Holiday Mode 2 : Temporary Manual Mode (will return to Schedule mode at the next schedule time) 3 : Schedule Programming Mode TRV sends those values when changing modes: 1 for Manual 0 for Schedule 2 for Temporary manual (in Schedule) 3 for Away Schedule -> [0] for attribute 0x0401 Manual -> [1] for attribute 0x0401 Temp Manual -> [2] for attribute 0x0401 Holiday -> [3] for attribute 0x0401 */ def mode def preset if (data == 0) { //programming (schedule ) mode = "auto" preset = "auto" } else if (data == 1) { //manual mode = "heat" preset = "manual" } else if (data == 2) { //temporary_manual mode = "heat" preset = "manual" } //temporary_manual else if (data == 3) { //holiday mode = "off" // BRT-100 'holiday' preset is matched to 'off' mode! preset = "holiday" } else { if (settings?.logEnable) log.warn "${device.displayName} processBRT100Presets unknown: ${data}" return; } if (state.lastThermostatMode == "emergency heat") { runIn(2, sendTuyaBoostModeOff) // also turn boost off! } sendEvent(name: "thermostatMode", value: mode, displayed: true) // mode was confirmed from the Preset info data... state.lastThermostatMode = mode // TODO - change tehrmostatPreset depending on preset ? if (settings?.txtEnable) log.info "${device.displayName} BRT-100 Presets: mode = ${mode} preset = ${preset}" } def processTuyaModes3( dp, data ) { // dp = 0x0402 : // preset for moes or mode // dp = 0x0403 : // preset for moes if (getModelGroup() in ['BRT-100', 'TEST2']) { // BRT-100 ? KK: TODO! def mode if (data == 0) { mode = "auto" } //schedule else if (data == 1) { mode = "heat" } //manual else if (data == 2) { mode = "off" } //away else { if (settings?.logEnable) log.warn "${device.displayName} processTuyaModes3: unknown mode: ${data}" return } if (settings?.txtEnable) log.info "${device.displayName} mode is: ${mode}" // TODO - - change thremostatMode depending on mode ? } else { // NOT TESTED !! def preset if (dp == 0x02) { // 0x0402 preset = "auto" } else if (dp == 0x03) { // 0x0403 preset = "program" } else { if (settings?.logEnable) log.warn "${device.displayName} processTuyaMode3: unknown preset: dp=${dp} data=${data}" return } if (settings?.txtEnable) log.info "${device.displayName} preset is: ${preset}" // TODO - - change preset ? } } def processTuyaModes4( dp, data ) { // TODO - check for model ! // dp = 0x0403 : // preset for moes if (true) { def mode if (data == 0) { mode = "holiday" } else if (data == 1) { mode = "auto" } else if (data == 2) { mode = "manual" } else if (data == 3) { mode = "manual" } else if (data == 4) { mode = "eco" } else if (data == 5) { mode = "boost" } else if (data == 6) { mode = "complex" } else { if (settings?.logEnable) log.warn "${device.displayName} processTuyaModes4: unknown mode: ${data}" return } if (settings?.txtEnable) log.info "${device.displayName} mode is: ${mode}" // TODO - - change thremostatMode depending on mode ? } else { if (settings?.logEnable) log.warn "${device.displayName} processTuyaModes4: model manufacturer ${device.getDataValue('manufacturer')} group = ${getModelGroup()}" return } } def processTuyaBoostModeReport( fncmd ) { def boostMode = fncmd == 0 ? "off" : "on" // 0:"off" : 1:"boost in progress" if (settings?.txtEnable) log.info "${device.displayName} Boost mode is: $boostMode (0x${fncmd})" if (boostMode == "on") { sendEvent(name: "thermostatMode", value: "emergency heat", displayed: false) state.lastThermostatMode = "emergency heat" sendThermostatOperatingStateEvent("heating") } else { if (device.currentState('thermostatMode').value == "emergency heat") { // restore the lastThermostatMode if (settings?.txtEnable) log.info "${device.displayName} restoring the lastThermostatMode: ${state.lastThermostatMode}" setThermostatMode(state.lastThermostatMode) } else { if (settings?.logEnable) log.debug "boost is already off - last thermostatMode was ${device.currentState('thermostatMode').value}" } } } private int getTuyaAttributeValue(ArrayList _data) { int retValue = 0 if (_data.size() >= 6) { int dataLength = _data[5] as Integer int power = 1; for (i in dataLength..1) { retValue = retValue + power * zigbee.convertHexToInt(_data[i+5]) power = power * 256 } } return retValue } def sendThermostatOperatingStateEvent( st ) { sendEvent(name: "thermostatOperatingState", value: st, displayed: true) state.lastThermostatOperatingState = st } def guessThermostatOperatingState() { try { double dTemp = Double.parseDouble(device.currentState('temperature').value) double dSet = Double.parseDouble(device.currentState('heatingSetpoint').value) if (dTemp >= dSet) { if (settings?.txtEnable) log.debug "guessing operating state is IDLE" return "idle" } else { if (settings?.txtEnable) log.debug "guessing operating state is HEAT" return "heat" } } catch (NumberFormatException e) { return "unknown" } } def sendTuyaBoostModeOff() { ArrayList cmds = [] if (settings?.txtEnable) log.info "${device.displayName} turning Boost mode off" sendThermostatOperatingStateEvent( guessThermostatOperatingState() ) //sendEvent(name: "thermostatOperatingState", value: guessThermostatOperatingState(), displayed: false) cmds = sendTuyaCommand("04", DP_TYPE_BOOL, "00") sendZigbeeCommands( cmds ) } def sendTuyaThermostatMode( mode ) { ArrayList cmds = [] def dp = "" def fn = "" def model = getModelGroup() switch (mode) { case "off" : // if (model in ['AVATTO', 'MOES', 'BEOK', 'MODEL3']) { dp = "01" fn = "00" } else if (model in ['BRT-100']) { // BRT-100: off mode is also reffered as 'holiday' or 'away' dp = "01" fn = "03" if (state.lastThermostatMode == "emergency heat") { runIn(2, sendTuyaBoostModeOff) // also turn boost off after 2 seconds! } return sendTuyaCommand(dp, DP_TYPE_ENUM, fn) // BRT-100 DP=1 needs DP_TYPE_ENUM! } else { // all other models dp = "04" fn = "00" // not tested ! } break case "heat" : // manual mode if (model in ['AVATTO', 'MOES', 'BEOK', 'MODEL3']) { // TODO - this command only does not switch off Scheduled (auto) mode ! if (device.currentState('thermostatMode').value == "off") { cmds += switchThermostatOn() } dp = "02" // was "01" fn = "00" // was "01" } else if (model in ['BRT-100']) { dp = "01" fn = "01" return sendTuyaCommand(dp, DP_TYPE_ENUM, fn) // BRT-100 DP=1 needs DP_TYPE_ENUM! } else { // all other models // not tested! dp = "04" fn = "01" // not tested ! } break case "auto" : // scheduled mode if (settings?.logEnable) log.trace "sending AUTO mode!" if (model in ['AVATTO', 'MOES', 'BEOK', 'MODEL3']) { // TODO - does not switch off manual mode ? if (device.currentState('thermostatMode').value == "off") { cmds += switchThermostatOn() } dp = "02" // was "01" fn = "01" // was "02" // return sendTuyaCommand(dp, DP_TYPE_ENUM, fn) } else if (model in ['BRT-100']) { dp = "01" fn = "00" return sendTuyaCommand(dp, DP_TYPE_ENUM, fn) // BRT-100 DP=1 needs DP_TYPE_ENUM! } else { // all other models // not tested! dp = "01" fn = "01" // not tested ! (same as heat) } break case "emergency heat" : state.mode = "emergency heat" if (model in ['BRT-100']) { // BRT-100 dp = "04" fn = "01" } else { // all other models // not tested! dp = "04" fn = "01" // not tested ! } break case "cool" : state.mode = "cool" return null break default : log.warn "Unsupported mode ${mode}" return null } cmds += sendTuyaCommand(dp, DP_TYPE_BOOL, fn) sendZigbeeCommands( cmds ) } // sends TuyaCommand and checks after 4 seconds def setThermostatMode( mode ) { if (settings?.logEnable) log.debug "${device.displayName} sending setThermostatMode(${mode})" state.mode = mode runIn(4, modeReceiveCheck) return sendTuyaThermostatMode( mode ) // must be last command! } def sendTuyaHeatingSetpoint( temperature ) { if (settings?.logEnable) log.debug "${device.displayName} sendTuyaHeatingSetpoint(${temperature})" def settemp = temperature as int // KK check! def dp = "10" def model = getModelGroup() switch (model) { case 'AVATTO' : // AVATTO - only integer setPoints! dp = "10" settemp = temperature break case 'MOES' : // MOES - 0.5 precision? ( and double the setpoint value ? ) dp = "10" settemp = temperature // KK check! break case 'BEOK' : // dp = "10" settemp = temperature * 10 break case 'MODEL3' : dp = "10" settemp = temperature break case 'BRT-100' : // BRT-100 dp = "02" settemp = temperature case 'TEST2' : case 'TEST3' : //dp = "02" settemp = temperature break default : settemp = temperature break } // iquix code //settemp += (settemp != temperature && temperature > device.currentValue("heatingSetpoint")) ? 1 : 0 // KK check ! if (settings?.logEnable) log.debug "${device.displayName} changing setpoint to ${settemp}" state.setpoint = temperature // KK was settemp !! CHECK ! runIn(4, setpointReceiveCheck) sendTuyaCommand(dp, DP_TYPE_VALUE, zigbee.convertToHexString(settemp as int, 8)) } // ThermostatHeatingSetpoint command // sends TuyaCommand and checks after 4 seconds // 1°C steps. (0.5°C setting on the TRV itself, rounded for zigbee interface) def setHeatingSetpoint( temperature ) { def previousSetpoint = device.currentState('heatingSetpoint').value as int if (settings?.logEnable) log.trace "setHeatingSetpoint temperature = ${temperature} as int = ${temperature as int} (previousSetpointt = ${previousSetpoint})" if (temperature != (temperature as int)) { if (temperature > previousSetpoint) { temperature = (temperature + 0.5 ) as int } else { temperature = temperature as int } if (settings?.logEnable) log.trace "corrected heating setpoint${temperature}" } if (settings?.maxTemp == null || settings?.minTemp == null ) { device.updateSetting("minTemp", 5); device.updateSetting("maxTemp", 28) } if (temperature > settings?.maxTemp.value ) temperature = settings?.maxTemp.value if (temperature < settings?.minTemp.value ) temperature = settings?.minTemp.value state.heatingSetPointRetry = 0 sendTuyaHeatingSetpoint( temperature ) } def setCoolingSetpoint(temperature){ if (settings?.logEnable) log.debug "${device.displayName} setCoolingSetpoint(${temperature}) called!" if (temperature != (temperature as int)) { temperature = (temperature + 0.5 ) as int if (settings?.logEnable) log.trace "corrected temperature: ${temperature}" } sendEvent(name: "coolingSetpoint", value: temperature, unit: "C", displayed: false) // setHeatingSetpoint(temperature) // KK check! } def heat(){ setThermostatMode("heat") } def off(){ setThermostatMode("off") } def on() { heat() } def setThermostatFanMode(fanMode) { sendEvent(name: "thermostatFanMode", value: "${fanMode}", descriptionText: getDescriptionText("thermostatFanMode is ${fanMode}")) } def auto() { setThermostatMode("auto") } def emergencyHeat() { setThermostatMode("emergency heat") } def cool() { setThermostatMode("off") } def fanAuto() { setThermostatFanMode("auto") } def fanCirculate() { setThermostatFanMode("circulate") } def fanOn() { setThermostatFanMode("on") } def setManualMode() { if (settings?.logEnable) log.debug "${device.displayName} setManualMode()" ArrayList cmds = [] cmds = sendTuyaCommand("02", DP_TYPE_ENUM, "00") + sendTuyaCommand("03", DP_TYPE_ENUM, "01") // iquix code sendZigbeeCommands( cmds ) } def switchThermostatOn() { if (settings?.logEnable) log.debug "${device.displayName} switching On!" ArrayList cmds = [] cmds = sendTuyaCommand("01", DP_TYPE_BOOL, "01") //sendZigbeeCommands( cmds ) return cmds } def getModelGroup() { def manufacturer = device.getDataValue("manufacturer") def modelGroup = 'UNKNOWN' if (modelGroupPreference == null) { device.updateSetting("modelGroupPreference", "Auto detect") } if (modelGroupPreference == "Auto detect") { if (manufacturer in Models) { modelGroup = Models[manufacturer] } else { modelGroup = 'UNKNOWN' } } else { modelGroup = modelGroupPreference } //if (settings?.logEnable) log.trace "${device.displayName} manufacturer ${manufacturer} group is ${modelGroup}" return modelGroup } def sendSupportedThermostatModes() { def supportedThermostatModes = "[]" switch (getModelGroup()) { case 'AVATTO' : case 'MOES' : case 'BEOK' : case 'MODEL3' : supportedThermostatModes = '[off, heat, auto]' break case 'BRT-100' : // BRT-100 supportedThermostatModes = '[off, heat, auto, emergency heat]' //supportedThermostatModes = '[off, heat, auto, emergency heat, eco, test]' break default : supportedThermostatModes = '[off, heat, auto]' break } sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, isStateChange: true, displayed: true) } // called from initialize() def installed() { if (settings?.txtEnable) log.info "installed()" sendSupportedThermostatModes() sendEvent(name: "supportedThermostatFanModes", value: ["auto"]) sendEvent(name: "thermostatMode", value: "heat", displayed: false) state.lastThermostatMode = "heat" sendThermostatOperatingStateEvent( "idle" ) //sendEvent(name: "thermostatOperatingState", value: "idle", displayed: false) sendEvent(name: "heatingSetpoint", value: 20, unit: "C", displayed: false) sendEvent(name: "coolingSetpoint", value: 20, unit: "C", displayed: false) sendEvent(name: "temperature", value: 20, unit: "C", displayed: false) sendEvent(name: "thermostatSetpoint", value: 20, unit: "C", displayed: false) // Google Home compatibility sendEvent(name: "switch", value: "on", displayed: true) state.mode = "" state.setpoint = 0 unschedule() runEvery1Minute(receiveCheck) // KK: check } def updated() { ArrayList cmds = [] if (modelGroupPreference == null) { device.updateSetting("modelGroupPreference", "Auto detect") } /* unconditional */log.info "Updating ${device.getLabel()} (${device.getName()}) model ${device.getDataValue('model')} manufacturer ${device.getDataValue('manufacturer')} modelGroupPreference = ${modelGroupPreference} (${getModelGroup()})" if (settings?.txtEnable) log.info "Force manual is ${forceManual}; Resend failed is ${resendFailed}" if (settings?.txtEnable) log.info "Debug logging is ${logEnable}; Description text logging is ${txtEnable}" if (logEnable==true) { runIn(86400, logsOff) // turn off debug logging after 24 hours } else { unschedule(logsOff) } if (getModelGroup() in ['AVATTO']) { fncmd = safeToInt( tempCalibration ) if (settings?.logEnable) log.trace "${device.displayName} changing tempCalibration to= ${fncmd}" cmds += sendTuyaCommand("1B", DP_TYPE_VALUE, zigbee.convertToHexString(fncmd as int, 8)) fncmd = safeToInt( hysteresis ) if (settings?.logEnable) log.trace "${device.displayName} changing hysteresis to= ${fncmd}" cmds += sendTuyaCommand("6A", DP_TYPE_VALUE, zigbee.convertToHexString(fncmd as int, 8)) fncmd = safeToInt( minTemp ) if (settings?.logEnable) log.trace "${device.displayName} changing minTemp to= ${fncmd}" cmds += sendTuyaCommand("1A", DP_TYPE_VALUE, zigbee.convertToHexString(fncmd as int, 8)) fncmd = safeToInt( maxTemp ) if (settings?.logEnable) log.trace "${device.displayName} changing maxTemp to= ${fncmd}" cmds += sendTuyaCommand("13", DP_TYPE_VALUE, zigbee.convertToHexString(fncmd as int, 8)) } else if (getModelGroup() in ['BRT-100']) { fncmd = safeToInt( tempCalibration ) if (settings?.logEnable) log.trace "${device.displayName} changing tempCalibration to= ${fncmd}" cmds += sendTuyaCommand("69", DP_TYPE_VALUE, zigbee.convertToHexString(fncmd as int, 8)) fncmd = safeToInt( minTemp ) if (settings?.logEnable) log.trace "${device.displayName} changing minTemp to= ${fncmd}" cmds += sendTuyaCommand("6D", DP_TYPE_VALUE, zigbee.convertToHexString(fncmd as int, 8)) fncmd = safeToInt( maxTemp ) if (settings?.logEnable) log.trace "${device.displayName} changing maxTemp to= ${fncmd}" cmds += sendTuyaCommand("6C", DP_TYPE_VALUE, zigbee.convertToHexString(fncmd as int, 8)) } /* unconditional */ log.info "Update finished" sendZigbeeCommands( cmds ) } def refresh() { ArrayList cmds = [] if (settings?.logEnable) {log.debug "${device.displayName} refresh()..."} def model = getModelGroup() switch (model) { /* case 'BRT-100' : def dp = "69" def fncmd = safeToInt( tempCalibration ) cmds += sendTuyaCommand(dp, DP_TYPE_VALUE, zigbee.convertToHexString(fncmd as int, 8)) sendZigbeeCommands( cmds ) */ default : zigbee.readAttribute(0 , 0 ) break } } def driverVersionAndTimeStamp() {version()+' '+timeStamp()} def checkDriverVersion() { if (state.driverVersion != null && driverVersionAndTimeStamp() == state.driverVersion) { } else { if (txtEnable==true) log.debug "${device.displayName} updating the settings from the current driver version ${state.driverVersion} to the new version ${driverVersionAndTimeStamp()}" initializeVars( fullInit = false ) state.driverVersion = driverVersionAndTimeStamp() } } def logInitializeRezults() { log.info "${device.displayName} manufacturer = ${device.getDataValue("manufacturer")} ModelGroup = ${getModelGroup()}" log.info "${device.displayName} Initialization finished\r version=${version()} (Timestamp: ${timeStamp()})" } // called by initialize() button void initializeVars( boolean fullInit = true ) { if (settings?.txtEnable) log.info "${device.displayName} InitializeVars()... fullInit = ${fullInit}" if (fullInit == true ) { state.clear() state.driverVersion = driverVersionAndTimeStamp() } // state.old_dp = "" state.old_fncmd = "" state.mode = "" state.setpoint = 0 state.packetID = 0 state.heatingSetPointRetry = 0 state.modeSetRetry = 0 state.rxCounter = 0 state.txCounter = 0 state.duplicateCounter = 0 // if (fullInit == true || state.lastThermostatMode == null) state.lastThermostatMode = "unknown" if (fullInit == true || state.lastThermostatOperatingState == null) state.lastThermostatOperatingState = "unknown" // if (fullInit == true || device.getDataValue("logEnable") == null) device.updateSetting("logEnable", true) if (fullInit == true || device.getDataValue("txtEnable") == null) device.updateSetting("txtEnable", true) if (fullInit == true || device.getDataValue("forceManual") == null) device.updateSetting("forceManual", false) if (fullInit == true || device.getDataValue("resendFailed") == null) device.updateSetting("resendFailed", false) if (fullInit == true || device.getDataValue("minTemp") == null) device.updateSetting("minTemp", 10) if (fullInit == true || device.getDataValue("maxTemp") == null) device.updateSetting("maxTemp", 28) if (fullInit == true || device.getDataValue("tempCalibration") == null) device.updateSetting("tempCalibration", 0) if (fullInit == true || device.getDataValue("hysteresis") == null) device.updateSetting("hysteresis", 1) // } def configure() { initialize() } def initialize() { if (true) "${device.displayName} Initialize()..." // sendEvent(name: "supportedThermostatModes", value: ["off", "cool"]) unschedule() initializeVars() installed() updated() runIn( 3, logInitializeRezults) } def modeReceiveCheck() { if (settings?.resendFailed == false ) return if (state.mode != "") { if (settings?.logEnable) log.warn "${device.displayName} modeReceiveCheck() failed" /* if (settings?.logEnable) log.debug "${device.displayName} resending mode command :"+state.mode def cmds = setThermostatMode(state.mode) cmds.each{ sendHubCommand(new hubitat.device.HubAction(it, hubitat.device.Protocol.ZIGBEE)) } */ } else { if (settings?.logEnable) log.debug "${device.displayName} modeReceiveCheck() OK" } } def setpointReceiveCheck() { if (settings?.resendFailed == false ) return if (state.setpoint != 0 ) { state.heatingSetPointRetry = state.heatingSetPointRetry + 1 if (state.heatingSetPointRetry < MaxRetries) { if (settings?.logEnable) log.warn "${device.displayName} setpointReceiveCheck(${state.setpoint}) failed" if (settings?.logEnable) log.debug "${device.displayName} resending setpoint command :"+state.setpoint sendTuyaHeatingSetpoint(state.setpoint) } else { if (settings?.logEnable) log.warn "${device.displayName} setpointReceiveCheck(${state.setpoint}) giving up retrying" } } else { if (settings?.logEnable) log.debug "${device.displayName} setpointReceiveCheck(${state.setpoint}) OK" } } // unconditionally scheduled Every1Minute from installed() .. def receiveCheck() { modeReceiveCheck() setpointReceiveCheck() } private sendTuyaCommand(dp, dp_type, fncmd) { ArrayList cmds = [] cmds += zigbee.command(CLUSTER_TUYA, SETDATA, PACKET_ID + dp + dp_type + zigbee.convertToHexString((int)(fncmd.length()/2), 4) + fncmd ) if (settings?.logEnable) log.trace "sendTuyaCommand = ${cmds}" if (state.txCounter != null) state.txCounter = state.txCounter + 1 return cmds } void sendZigbeeCommands(ArrayList cmd) { if (settings?.logEnable) {log.debug "${device.displayName} sendZigbeeCommands(cmd=$cmd)"} hubitat.device.HubMultiAction allActions = new hubitat.device.HubMultiAction() cmd.each { allActions.add(new hubitat.device.HubAction(it, hubitat.device.Protocol.ZIGBEE)) if (state.txCounter != null) state.txCounter = state.txCounter + 1 } sendHubCommand(allActions) } private getPACKET_ID() { state.packetID = ((state.packetID ?: 0) + 1 ) % 65536 return zigbee.convertToHexString(state.packetID, 4) } private getDescriptionText(msg) { def descriptionText = "${device.displayName} ${msg}" if (settings?.txtEnable) log.info "${descriptionText}" return descriptionText } def logsOff(){ log.warn "${device.displayName} debug logging disabled..." device.updateSetting("logEnable",[value:"false",type:"bool"]) } def controlMode( mode ) { ArrayList cmds = [] switch (mode) { case "manual" : cmds += sendTuyaCommand("02", DP_TYPE_ENUM, "00")// + sendTuyaCommand("03", DP_TYPE_ENUM, "01") // iquix code if (settings?.logEnable) log.trace "${device.displayName} sending manual mode : ${cmds}" break case "program" : cmds += sendTuyaCommand("02", DP_TYPE_ENUM, "01")// + sendTuyaCommand("03", DP_TYPE_ENUM, "01") // iquix code if (settings?.logEnable) log.trace "${device.displayName} sending program mode : ${cmds}" break default : break } sendZigbeeCommands( cmds ) } def childLock( mode ) { ArrayList cmds = [] def dp if (getModelGroup() in ["AVATTO"]) dp = "28" else if (getModelGroup() in ["BRT-100"]) dp = "0D" else return switch (mode) { case "off" : cmds += sendTuyaCommand(dp, DP_TYPE_BOOL, "00") break case "on" : cmds += sendTuyaCommand(dp, DP_TYPE_BOOL, "01") break default : break } sendEvent(name: "childLock", value: mode) if (settings?.logEnable) log.trace "${device.displayName} sending child lock mode : ${mode}" sendZigbeeCommands( cmds ) } def calibration( offset ) { offset = 0 ArrayList cmds = [] def dp if (getModelGroup() in ["AVATTO"]) dp = "1B" else if (getModelGroup() in ["BRT-100"]) dp = "69" else return; // callibration command returns also thermostat mode (heat), operation mode (manual), heating stetpoint and few seconds later - the temperature! cmds += sendTuyaCommand(dp, DP_TYPE_VALUE, zigbee.convertToHexString(offset as int, 8)) if (settings?.logEnable) log.trace "${device.displayName} sending calibration offset : ${offset}" sendZigbeeCommands( cmds ) } Integer safeToInt(val, Integer defaultVal=0) { return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal } Double safeToDouble(val, Double defaultVal=0.0) { return "${val}"?.isDouble() ? "${val}".toDouble() : defaultVal } def getBatteryPercentageResult(rawValue) { //if (settings?.logEnable) log.debug "${device.displayName} Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" def result = [:] if (0 <= rawValue && rawValue <= 200) { result.name = 'battery' result.translatable = true result.value = Math.round(rawValue / 2) result.descriptionText = "${device.displayName} battery is ${result.value}%" result.isStateChange = true result.unit = '%' sendEvent(result) if (settings?.txtEnable) log.info "${result.descriptionText}" } else { if (settings?.logEnable) log.warn "${device.displayName} ignoring BatteryPercentageResult(${rawValue})" } } def zTest( dpCommand, dpValue, dpTypeString ) { ArrayList cmds = [] def dpType = dpTypeString=="DP_TYPE_VALUE" ? DP_TYPE_VALUE : dpTypeString=="DP_TYPE_BOOL" ? DP_TYPE_BOOL : dpTypeString=="DP_TYPE_ENUM" ? DP_TYPE_ENUM : null def dpValHex = dpTypeString=="DP_TYPE_VALUE" ? zigbee.convertToHexString(dpValue as int, 8) : dpValue log.warn " sending TEST command=${dpCommand} value=${dpValue} ($dpValHex) type=${dpType}" switch ( getModelGroup() ) { case 'AVATTO' : case 'MOES' : case 'BEOK' : case 'MODEL3' : case 'BRT-100' : case 'TEST2' : // MOES case 'TEST3' : case 'UNKNOWN' : default : break } sendZigbeeCommands( sendTuyaCommand(dpCommand, dpType, dpValHex) ) } /* BRT-100 Zigbee network re-pair procedure: After the actuator has completed self-test, long press [X] access to interface, short press '+' to choose WiFi icon, short press [X] to confirm this option, long press [X]. WiFi icon will start flashing when in pairing mode. */