/**
* Tuya Temperature Humidity Illuminance LCD Display with a Clock
*
* 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.
*
* ver. 1.0.0 2022-01-02 kkossev - Inital test version
* ver. 1.0.1 2022-02-05 kkossev - Added Zemismart ZXZTH fingerprint; added _TZE200_locansqn; Fahrenheit scale + rounding; temperatureScaleParameter; temperatureSensitivity; minTempAlarm; maxTempAlarm
* ver. 1.0.2 2022-02-06 kkossev - Tuya commands refactoring; TS0222 T/H poll on illuminance change (EP2); modelGroupPreference bug fix; dyncamic parameters
* ver. 1.0.3 2022-02-13 kkossev - _TZE200_c7emyjom fingerprint added;
* ver. 1.0.4 2022-02-20 kkossev - Celsius/Fahrenheit correction for TS0601_Tuya devices
* ver. 1.0.5 2022-04-25 kkossev - added TS0601_AUBESS (illuminance only); ModelGroup is shown in State Variables
* ver. 1.0.6 2022-05-09 kkossev - new model 'TS0201_LCZ030' (_TZ3000_qaaysllp)
* ver. 1.0.7 2022-06-09 kkossev - new model 'TS0601_Contact'(_TZE200_pay2byax); illuminance unit changed to 'lx; Bug fix - all settings were reset back in to the defaults on hub reboot
* ver. 1.0.8 2022-08-13 kkossev - _TZE200_pay2byax bug fixes; '_TZE200_locansqn' (TS0601_Haozee) bug fixes; removed degrees symbol from the logs; removed temperatureScaleParameter'preference (use HE scale setting); decimal/number bug fixes;
* added temperature and humidity offesets; configured parameters (including C/F HE scale) are sent to the device when paired again to HE; added Minimum time between temperature and humidity reports;
* ver. 1.0.9 2022-10-02 kkossev - configure _TZ2000_a476raq2 reporting time; added TS0601 _TZE200_bjawzodf; code cleanup
* ver. 1.0.10 2022-10-11 kkossev - '_TZ3000_itnrsufe' reporting configuration bug fix?; reporting configuration result Info log; added Sonoff SNZB-02 fingerprint; reportingConfguration is sent on pairing to HE;
* ver. 1.0.11 2022-10-31 kkossev - added _TZE200_whkgqxse; fingerprint correction; _TZ3000_bguser20 _TZ3000_fllyghyj _TZ3000_yd2e749y _TZ3000_6uzkisv2
* ver. 1.1.0 2022-12-18 kkossev - added _info_ attribute; delayed reporting configuration when the sleepy device wakes up; excluded TS0201 model devices in the delayed configuration; _TZE200_locansqn fingerprint correction and max reporting periods formula correction
* added TS0601_Soil _TZE200_myd45weu ; added _TZE200_znbl8dj5 _TZE200_a8sdabtg _TZE200_qoy0ekbd
* ver. 1.1.1 2023-01-14 kkossev - added _TZ3000_ywagc4rj TS0201_TH; bug fix: negative temperatures not calculated correctly;
* ver. 1.2.0 2023-01-15 kkossev - parsing multiple DP received in one command;
* ver. 1.2.1 2023-01-15 kkossev - _TZE200_locansqn fixes;_TZ3000_bguser20 correct model;
* ver. 1.3.0 2023-02-02 kkossev - healthStatus; added capability 'Health Check'
* ver. 1.3.1 2023-02-10 kkossev - added RH3052 TUYATEC-gqhxixyk
* ver. 1.3.2 2023-03-04 kkossev - added TS0601 _TZE200_zl1kmjqx _TZE200_qyflbnbj, added TS0201 _TZ3000_dowj6gyi and _TZ3000_8ybe88nf
* ver. 1.3.3 2023-04-23 kkossev - _TZE200_znbl8dj5 inClusters correction; ignored invalid humidity values; implemented ping() and rtt (round-trip-time) attribute;
* ver. 1.3.4 2023-04-24 kkossev - send rtt 'timeout' if ping() fails; added resetStats command; added individual stat.stats counters for T/H/I/battery; configuration possible loop bug fix;
* ver. 1.3.5 2023-05-28 kkossev - sendRttEvent exception fixed; added _TZE200_cirvgep4 in TS0601_Tuya group; fingerprint correction; battery reports are capped to 100% and not ignored;
* ver. 1.3.6 2023-06-10 kkossev - added _TZE200_yjjdcqsq to TS0601_Tuya group;
* ver. 1.3.7 2023-08-02 vpjuslin -Yet another name for Tuya soil sensor: _TZE200_ga1maeof
* ver. 1.3.8 2023-08-17 kkossev - added OWON THS317-ET for tests; added TS0201 _TZ3000_rdhukkmi; added TS0222 _TYZB01_ftdkanlj
* ver. 1.3.9 2023-09-29 kkossev - added Sonoff SNZB-02P; added TS0201 _TZ3210_ncw88jfq; moved _TZE200_yjjdcqsq and _TZE200_cirvgep4 to a new group 'TS0601_Tuya_2'; added _TZE204_upagmta9, added battery state 'low', 'medium', 'high'
*
* TODO: add TS0601 _TZE200_khx7nnka in a new TUYA_LIGHT device profile : https://community.hubitat.com/t/simple-smart-light-sensor/110341/16?u=kkossev @Pradeep
* TODO: healthStatus periodic job is not started.
* TODO: _TZ3000_qaaysllp frequent illuminance reports - check configuration; add minimum time between lux reports parameter!
* TODO: TS0201 - bindings are sent, even if nothing to configure?
* TODO: add Batteryreporting time configuration (like in the TS004F driver)
*
*/
def version() { "1.3.9" }
def timeStamp() {"2023/09/29 10:54 PM"}
import groovy.json.*
import groovy.transform.Field
import hubitat.zigbee.zcl.DataType
import hubitat.device.HubAction
import hubitat.device.Protocol
@Field static final Boolean _DEBUG = false
metadata {
definition (name: "Tuya Temperature Humidity Illuminance LCD Display with a Clock", namespace: "kkossev", author: "Krassimir Kossev", importUrl: "https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Tuya%20Temperature%20Humidity%20Illuminance%20LCD%20Display%20with%20a%20Clock/Tuya_Temperature_Humidity_Illuminance_LCD_Display_with_a_Clock.groovy", singleThreaded: true ) {
capability "Refresh"
capability "Sensor"
capability "Battery"
capability "TemperatureMeasurement"
capability "RelativeHumidityMeasurement"
capability "IlluminanceMeasurement"
//capability "ContactSensor" // uncomment for _TZE200_pay2byax contact w/ illuminance sensor
//capability "MotionSensor" // uncomment for SiHAS multi sensor
capability "Health Check"
if (_DEBUG == true) {
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 "test", [[name:"test", type: "STRING", description: "test", constraints: ["STRING"]]]
}
command "resetStats", [[name: "Reset statistics" ]]
command "initialize", [[name: "Manually initialize the device after switching drivers. \n\r ***** Will load device default values! *****" ]]
attribute "_info", "string" // when defined as attribute, will be shown on top of the 'Current States' list ...
attribute "healthStatus", "enum", ["unknown", "offline", "online"]
attribute "rtt", "number"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_lve3dvpy", deviceJoinName: "Tuya Temperature Humidity Illuminance LCD Display with a Clock"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_c7emyjom", deviceJoinName: "Tuya Temperature Humidity Illuminance LCD Display with a Clock"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_locansqn", deviceJoinName: "Haozee Temperature Humidity Illuminance LCD Display with a Clock" // https://de.aliexpress.com/item/1005003634353180.html
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_bq5c8xfe", deviceJoinName: "Haozee Temperature Humidity Illuminance LCD Display with a Clock"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0402,0405", outClusters:"0019", model:"TS0201", manufacturer:"_TZ2000_hjsgdkfl", deviceJoinName: "AVATTO S-H02"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0402,0405", outClusters:"0019", model:"TS0201", manufacturer:"_TZ2000_a476raq2", deviceJoinName: "Tuya Temperature Humidity LCD display"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0400,E002", outClusters:"0019,000A", model:"TS0201", manufacturer:"_TZ3000_qaaysllp", deviceJoinName: "NAS-TH02B LCZ030 T/H/I/LCD" // Neo Coolcam ? // NOT TESTED!
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0400", outClusters:"0019,000A", model:"TS0222", manufacturer:"_TYZB01_kvwjujy9", deviceJoinName: "MOES ZSS-ZK-THL"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0400", outClusters:"0019,000A", model:"TS0222", manufacturer:"_TYZB01_ftdkanlj", deviceJoinName: "MOES ZSS-ZK-THL"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0400,0001,0500", outClusters:"0019,000A", model:"TS0222", manufacturer:"_TYZB01_4mdqxxnn", deviceJoinName: "Tuya Illuminance Sensor TS0222_2"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0400,0001,0500", outClusters:"0019,000A", model:"TS0222", manufacturer:"_TZ3000_lfa05ajd", deviceJoinName: "Zemismart ZXZTH"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_pisltm67", deviceJoinName: "AUBESS Light Sensor S-LUX-ZB"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TYST11_pisltm67", deviceJoinName: "AUBESS Light Sensor S-LUX-ZB"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0500,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_pay2byax", deviceJoinName: "Tuya Contact and Illuminance Sensor"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0003,0402,0405,E002,0000", outClusters:"0003,0019,000A", model:"TS0201", manufacturer:"_TZ3000_itnrsufe", deviceJoinName: "Tuya temperature and humidity sensor RCTW1Z"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0003,0402,0405,E002,0000", outClusters:"0003,0019,000A", model:"TS0201", manufacturer:"_TZ3000_ywagc4rj", deviceJoinName: "Tuya temperature and humidity sensor" // https://community.hubitat.com/t/release-tuya-temperature-humidity-illuminance-lcd-display-with-a-clock/88093/211?u=kkossev
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0402,0405,EF00,0000", outClusters:"0019,000A", model:"TS0201", manufacturer:"_TZ3210_ncw88jfq", deviceJoinName: "Tuya temperature and humidity sensor" // https://community.hubitat.com/t/release-tuya-temperature-humidity-illuminance-lcd-display-with-a-clock/88093/211?u=kkossev
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0004,0005,0402,0405,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_bjawzodf", deviceJoinName: "Tuya like Temperature Humidity LCD Display" // https://de.aliexpress.com/item/4000739457722.html?gatewayAdapt=glo2deu
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_bjawzodf", deviceJoinName: "Tuya like Temperature Humidity LCD Display" // https://de.aliexpress.com/item/4000739457722.html?gatewayAdapt=glo2deu
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_zl1kmjqx", deviceJoinName: "Tuya Temperature Humidity sensor MIR-TE100-TY" // https://www.aliexpress.com/item/1005002836127648.html
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_qyflbnbj", deviceJoinName: "Tuya Temperature Humidity sensor MIR-TE100-TY" // https://www.aliexpress.com/item/1005002836127648.html
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_myd45weu", deviceJoinName: "Tuya Temperature Humidity Soil Monitoring Sensor" // https://www.aliexpress.com/item/1005004979025740.html
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_ga1maeof", deviceJoinName: "Tuya Temperature Humidity Soil Monitoring Sensor" // https://www.aliexpress.com/item/1005004979025740.html
// model: 'ZG-227ZL',
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0004,0005,0402,0405,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_qoy0ekbd", deviceJoinName: "Tuya Temperature Humidity LCD Display" // not tested
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0004,0005,0402,0405,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_a8sdabtg", deviceJoinName: "Tuya Temperature Humidity (no screen)" // https://community.hubitat.com/t/new-temp-humidity-device-not-working-correctly-generic-zigbee-th-driver/109725?u=kkossev
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_cirvgep4", deviceJoinName: "Tuya Temperature Humidity LCD Display with a Clock" //https://community.hubitat.com/t/release-tuya-temperature-humidity-illuminance-lcd-display-with-a-clock-w-healthstatus/88093/308?u=kkossev
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0402,0405,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_znbl8dj5", deviceJoinName: "Tuya Temperature Humidity" // kk
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_yjjdcqsq", deviceJoinName: "Tuya Temperature Humidity" // kk
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_9yapgbuv", deviceJoinName: "Tuya Temperature Humidity"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_upagmta9", deviceJoinName: "Tuya Temperature Humidity"
// kk
//
fingerprint profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_whkgqxse", deviceJoinName: "Tuya Zigbee Temperature Humidity Sensor With Backlight" // https://www.aliexpress.com/item/1005003980647546.html
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0003,0402,0405,0000", outClusters:"0003,0019,000A", model:"TS0201", manufacturer:"_TZ3000_bguser20", deviceJoinName: "Tuya Temperature Humidity sensor WSD500A"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0003,0402,0405,0000", outClusters:"0003,0019,000A", model:"TS0201", manufacturer:"_TZ3000_xr3htd96", deviceJoinName: "Tuya Temperature Humidity sensor WSD500A"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0003,0402,0405,0000", outClusters:"0003,0019,000A", model:"TS0201", manufacturer:"_TZ3000_fllyghyj", deviceJoinName: "Tuya Temperature Humidity sensor" // not tested
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0003,0402,0405,0000", outClusters:"0003,0019,000A", model:"TS0201", manufacturer:"_TZ3000_yd2e749y", deviceJoinName: "Tuya Temperature Humidity sensor" // not tested
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0003,0402,0405,0000", outClusters:"0003,0019,000A", model:"TS0201", manufacturer:"_TZ3000_6uzkisv2", deviceJoinName: "Tuya Temperature Humidity sensor" // not tested
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0003,0402,0405,0000", outClusters:"0003,0019,000A", model:"TS0201", manufacturer:"_TZ3000_dowj6gyi", deviceJoinName: "Tuya Temperature Humidity sensor" // not tested
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0003,0402,0405,0000", outClusters:"0003,0019,000A", model:"TS0201", manufacturer:"_TZ3000_8ybe88nf", deviceJoinName: "Tuya Temperature Humidity sensor" // https://community.hubitat.com/t/release-tuya-temperature-humidity-illuminance-lcd-display-with-a-clock-w-healthstatus/88093/262?u=kkossev
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0003,0402,0405,0000", outClusters:"0003,0019,000A", model:"TS0201", manufacturer:"_TZ3000_rdhukkmi", deviceJoinName: "Tuya Temperature Humidity sensor" // https://community.hubitat.com/t/tuya-humidity-temperature-sensor/76635/55?u=kkossev
//
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0402,0405", outClusters:"0003,0402,0405", model:"RH3052", manufacturer:"TUYATEC-gqhxixyk", deviceJoinName: "TUYATEC RH3052 Motion Sensor" // https://community.hubitat.com/t/moes-zigbee-3-0-temp-humidity-sensor-driver/112318?u=kkossev
//
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0003,0402,0405,0001", outClusters:"0003", model:"TH01", manufacturer:"eWeLink", deviceJoinName: "Sonoff Temperature and Humidity Sensor SNZB-02"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0003,0402,0405,0001", outClusters:"0003", model:"TH01", manufacturer:"SONOFF", deviceJoinName: "Sonoff Temperature and Humidity Sensor SNZB-02"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0020,0402,0405,FC57,FC11", outClusters:"0019", model:"SNZB-02D", manufacturer:"SONOFF", deviceJoinName: "Sonoff Temperature and Humidity Sensor SNZB-02D"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0020,0402,0405,FC57,FC11", outClusters:"0019", model:"SNZB-02D", manufacturer:"eWeLink", deviceJoinName: "Sonoff Temperature and Humidity Sensor SNZB-02D"
fingerprint profileId:"0104", endpointId:"01", inClusters:"0001,0020,0000,0003,0402", outClusters:"0003", model:"THS317-ET", manufacturer:"OWON", deviceJoinName: "OWON Temperature sensor" //https://community.hubitat.com/t/newbie-help-with-owon-ths317-et-multi-sensor/122671/5?u=kkossev
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0020,0402,0405,FC57,FC11", outClusters:"0019", model:"SNZB-02P", manufacturer:"eWeLink", deviceJoinName: "Sonoff Temperature and Humidity Sensor SNZB-02P" // https://community.hubitat.com/t/release-tuya-temperature-humidity-illuminance-lcd-display-with-a-clock-w-healthstatus/88093/435?u=kkossev
fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0020,0402,0405,FC57,FC11", outClusters:"0019", model:"SNZB-02P", manufacturer:"SONOFF", deviceJoinName: "Sonoff Temperature and Humidity Sensor SNZB-02P"
}
preferences {
//input description: "Once you change values on this page, the attribute value \"needUpdate\" will show \"YES\" until all configuration parameters are updated.", title: "Settings", displayDuringSetup: false, type: "paragraph", element: "paragraph"
input (name: "logEnable", type: "bool", title: "Debug logging", description: "Debug information, useful for troubleshooting. Recommended value is false", defaultValue: true)
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: "modelGroupPreference", type: "enum", title: "Model Group", description:"Recommended value is Auto detect", defaultValue: 0, options:
["Auto detect":"Auto detect", "TS0601_Tuya":"TS0601_Tuya", "TS0601_Tuya_2":"TS0601_Tuya_2", "TS0601_Haozee":"TS0601_Haozee", "TS0601_AUBESS":"TS0601_AUBESS", "TS0201":"TS0201", "TS0222":"TS0222", 'TS0201_LCZ030': 'TS0201_LCZ030',
"TS0222_2":"TS0222_2", "TS0201_TH":"TS0201_TH", "TS0601_Soil":"TS0601_Soil", "Zigbee NON-Tuya":"Zigbee NON-Tuya", "OWON":"OWON"])
input (name: "advancedOptions", type: "bool", title: "Advanced options", description: "May not be supported by all devices!", defaultValue: false)
if (advancedOptions == true) {
if (isConfigurableSleepyDevice()) {
input (title: "To configure a sleepy device, try any of the methods below :", description: " * Rapidly change the temperature or the humidity
* Remove the battery for at least 1 minute
* Pair the device again to HE", type: "paragraph", element: "paragraph")
}
configParams.each {
//log.warn "it.value.input.limit = ${it.value.input.limit}"
if (it.value.input.limit == null || 'ALL' in it.value.input.limit || getModelGroup() in it.value.input.limit) {
//log.trace "it = ${it.value.input.limit}"
input it.value.input
}
}
}
}
}
@Field static Map configParams = [
0: [input: [name: "temperatureOffset", type: "decimal", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", defaultValue: 0.0, range: "-100.0..100.0",
limit:['ALL']]],
1: [input: [name: "humidityOffset", type: "decimal", title: "Humidity offset", description: "Enter a percentage to adjust the humidity.", defaultValue: 0.0, range: "-100.0..100.0",
limit:['ALL']]],
2: [input: [name: "temperatureSensitivity", type: "decimal", title: "Temperature Sensitivity", description: "Temperature change for reporting, "+"\u00B0"+"C", defaultValue: 0.5, range: "0.1..50.0",
limit:['TS0601_Tuya', 'TS0601_Haozee', 'TS0201_TH', "Zigbee NON-Tuya"]]],
3: [input: [name: "humiditySensitivity", type: "number", title: "Humidity Sensitivity", description: "Humidity change for reporting, %", defaultValue: 5, range: "1..50",
limit:['TS0601_Tuya', 'TS0601_Haozee', 'TS0201_TH', "Zigbee NON-Tuya"]]],
4: [input: [name: "illuminanceSensitivity", type: "number", title: "Illuminance Sensitivity", description: "Illuminance change for reporting, %", defaultValue: 12, range: "10..100", // TS0222 "MOES ZSS-ZK-THL"
limit:['TS0222']]],
5: [input: [name: "minTempAlarmPar", type: "decimal", title: "Minimum Temperature Alarm", description: "Minimum Temperature Alarm, C", defaultValue: 0.0, range: "-20.0..60.0",
limit:['TS0601_Tuya', /*'TS0601_Haozee',*/ 'TS0201_LCZ030']]],
6: [input: [name: "maxTempAlarmPar", type: "decimal", title: "Maximum Temperature Alarm", description: "Maximum Temperature Alarm, C", defaultValue: 39.0, range: "-20.0..60.0",
limit:['TS0601_Tuya', /*'TS0601_Haozee',*/ 'TS0201_LCZ030']]],
7: [input: [name: "minHumidityAlarmPar", type: "number", title: "Minimal Humidity Alarm", description: "Minimum Humidity Alarm, %", defaultValue: 20, range: "0..100", // 'TS0601_Haozee' only!
limit:[/*'TS0601_Haozee',*/ /*'TS0201_LCZ030'*/]]],
8: [input: [name: "maxHumidityAlarmPar", type: "number", title: "Maximum Humidity Alarm", description: "Maximum Humidity Alarm, %", defaultValue: 60, range: "0..100", // 'TS0601_Haozee' only!
limit:[/*'TS0601_Haozee',*/ /*'TS0201_LCZ030'*/]]],
9: [input: [name: "minReportingTimeTemp", type: "number", title: "Minimum time between temperature reports", description: "Minimum time between temperature reporting, seconds", defaultValue: 10, range: "1..3600",
limit:['ALL']]],
10: [input: [name: "maxReportingTimeTemp", type: "number", title: "Maximum time between temperature reports", description: "Maximum time between temperature reporting, seconds", defaultValue: 3600, range: "10..43200",
limit:['TS0601_Haozee', 'TS0201_TH', "Zigbee NON-Tuya"]]],
11: [input: [name: "minReportingTimeHumidity", type: "number", title: "Minimum time between humidity reports", description: "Minimum time between humidity reporting, seconds", defaultValue: 10, range: "1..3600",
limit:['ALL']]],
12: [input: [name: "maxReportingTimeHumidity", type: "number", title: "Maximum time between humidity reports", description: "Maximum time between humidity reporting, seconds", defaultValue: 3600, range: "10..43200",
limit:['TS0601_Haozee', 'TS0201_TH', "Zigbee NON-Tuya"]]],
13: [input: [name: "alarmTempPar", type: "enum", title: "Temperature Alarm", description:"Temperature Alarm", defaultValue: 0, options: [0:"Below min temp", 1:"Over max temp", 2:"off"],
limit:[/*'TS0201_LCZ030'*/]]],
14: [input: [name: "alarmHumidityPar", type: "enum", title: "Humidity Alarm", description:"Temperature Alarm", defaultValue: 0, options: [0:"Below min hum.", 1:"Over max hum", 2:"off"],
limit:[/*'TS0201_LCZ030'*/]]]
]
@Field static final Map Models = [
'_TZE200_lve3dvpy' : 'TS0601_Tuya', // Tuya Temperature Humidity LCD Display with a Clock
'_TZE200_c7emyjom' : 'TS0601_Tuya', // Tuya Temperature Humidity LCD Display with a Clock
'_TZE200_whkgqxse' : 'TS0601_Tuya', // Tuya Zigbee Temperature Humidity Sensor With Backlight https://www.aliexpress.com/item/1005003980647546.html
'_TZE200_a8sdabtg' : 'TS0601_Tuya', // Tuya Zigbee Temperature Humidity Sensor - no display! https://www.amazon.de/gp/product/B09NKCDXT9 - TODO !
'_TZE200_qoy0ekbd' : 'TS0601_Tuya', // https://www.aliexpress.com/item/1005004896603070.html - TODO !
'_TZE200_znbl8dj5' : 'TS0601_Tuya', // https://www.aliexpress.com/item/1005004116638127.html - TODO !
'_TZE200_zl1kmjqx' : 'TS0601_Tuya', // https://www.aliexpress.com/item/1005002836127648.html
'_TZE200_qyflbnbj' : 'TS0601_Tuya', // not tested
'_TZE200_cirvgep4' : 'TS0601_Tuya_2', // https://www.aliexpress.com/item/1005005198387789.html
//'_TZE200_yjjdcqsq' : 'TS0601_Tuya', // https://community.hubitat.com/t/release-tuya-temperature-humidity-illuminance-lcd-display-with-a-clock-w-healthstatus/88093/389?u=kkossev
'_TZE200_yjjdcqsq' : 'TS0601_Tuya_2', // https://community.hubitat.com/t/release-tuya-temperature-humidity-illuminance-lcd-display-with-a-clock-w-healthstatus/88093/446?u=kkossev
'_TZE200_9yapgbuv' : 'TS0601_Tuya_2', // not tested
'_TZE200_locansqn' : 'TS0601_Haozee', // Haozee Temperature Humidity Illuminance LCD Display with a Clock
'_TZE200_bq5c8xfe' : 'TS0601_Haozee', //
'_TZE200_nnrfa68v' : 'TS0601_Haozee', // not tested NOUS E6 https://noussmart.pl/product/e6.html
'_TZE200_pisltm67' : 'TS0601_AUBESS', // illuminance only sensor
'_TZ2000_a476raq2' : 'TS0201', // KK
'_TZ3000_lfa05ajd' : 'TS0201', // Zemismart ZXZTH
'_TZ2000_xogb73am' : 'TS0201',
'_TZ2000_avdnvykf' : 'TS0201',
'_TYZB01_a476raq2' : 'TS0201',
'_TYZB01_hjsgdkfl' : 'TS0201',
'_TZ2000_hjsgdkfl' : 'TS0201', // "AVATTO S-H02"
'_TZ3000_bguser20' : 'TS0201', // Model WSD500A
'_TZ3000_xr3htd96' : 'TS0201', // Model WSD500A
'_TZ3000_fllyghyj' : 'TS0201',
'_TZ3000_yd2e749y' : 'TS0201',
'_TZ3000_6uzkisv2' : 'TS0201',
'_TZ3000_dowj6gyi' : 'TS0201',
'_TZ3000_8ybe88nf' : 'TS0201',
'_TZ3000_rdhukkmi' : 'TS0201',
'TUYATEC-gqhxixyk' : 'TS0201', // model RH3052
'_TZ3000_qaaysllp' : 'TS0201_LCZ030', // NAS-TH02B / NEO Coolcam ? - T/H/I - testing! // https://github.com/Datakg/tuya/blob/53e33ae7767aedbb5d2138f2a31798badffd80d2/zhaquirks/tuya/ts0201_neo.py
'_TYZB01_kvwjujy9' : 'TS0222', // "MOES ZSS-ZK-THL" e-Ink display
'_TYZB01_ftdkanlj' : 'TS0222', // https://community.hubitat.com/t/release-tuya-temperature-humidity-illuminance-lcd-display-with-a-clock-w-healthstatus/88093/419?u=kkossev
'_TYZB01_4mdqxxnn' : 'TS0222_2', // illuminance only sensor
'_TZE200_pay2byax' : 'TS0601_Contact', // Contact and illuminance sensor
'_TZ3000_itnrsufe' : 'TS0201_TH', // Temperature and humidity sensor; // reports both battery voltage and perceintage; cluster 0xE002, attr 0xE00B: 0-Celsius, 1: Fahrenheit ( 0x30 ENUM)
'_TZ3000_ywagc4rj' : 'TS0201_TH', // https://community.hubitat.com/t/release-tuya-temperature-humidity-illuminance-lcd-display-with-a-clock/88093/210?u=kkossev
'_TZ3210_ncw88jfq' : 'TS0201_TH', // https://community.hubitat.com/t/release-tuya-temperature-humidity-illuminance-lcd-display-with-a-clock-w-healthstatus/88093/436?u=kkossev
'_TZE200_myd45weu' : 'TS0601_Soil', // Soil monitoring sensor
'_TZE200_ga1maeof' : 'TS0601_Soil', // Soil monitoring sensor
'eWeLink' : 'Zigbee NON-Tuya', // Sonoff Temperature and Humidity Sensor SNZB-02, SNZB-02D, SNZB-02P
'SONOFF' : 'Zigbee NON-Tuya', // Sonoff Temperature and Humidity Sensor SNZB-02, SNZB-02D, SNZB-02P
'ShinaSystem' : 'Zigbee NON-Tuya', // USM-300Z
'OWON' : 'OWON', // model:"THS317-ET", manufacturer:"OWON"
'' : 'UNKNOWN',
'ALL' : 'ALL',
'TEST' : 'TEST'
]
@Field static final Map deviceProfilesV3 = [
"SONOFF_TEMP_HUMI" : [
models : ["TH01", "SNZB-02D", "SNZB-02P"],
manufacturers : ["eWeLink", "SONOFF"],
fingerprints : [
[profileId:"0104", endpointId:"01", inClusters:"0000,0003,0402,0405,0001", outClusters:"0003", model:"TH01", manufacturer:"eWeLink", deviceJoinName: "Sonoff Temperature and Humidity Sensor SNZB-02"],
[profileId:"0104", endpointId:"01", inClusters:"0000,0003,0402,0405,0001", outClusters:"0003", model:"TH01", manufacturer:"SONOFF", deviceJoinName: "Sonoff Temperature and Humidity Sensor SNZB-02"],
[profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0020,0402,0405,FC57,FC11", outClusters:"0019", model:"SNZB-02D", manufacturer:"eWeLink", deviceJoinName: "Sonoff Temperature and Humidity Sensor SNZB-02D"],
[profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0020,0402,0405,FC57,FC11", outClusters:"0019", model:"SNZB-02D", manufacturer:"SONOFF", deviceJoinName: "Sonoff Temperature and Humidity Sensor SNZB-02D"],
[profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0020,0402,0405,FC57,FC11", outClusters:"0019", model:"SNZB-02P", manufacturer:"eWeLink", deviceJoinName: "Sonoff Temperature and Humidity Sensor SNZB-02P"],
[profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0020,0402,0405,FC57,FC11", outClusters:"0019", model:"SNZB-02P", manufacturer:"SONOFF", deviceJoinName: "Sonoff Temperature and Humidity Sensor SNZB-02P"]
],
deviceJoinName: "Sonoff Temperature and Humidity Sensor",
capabilities : ["battery": true],
attributes : ["healthStatus": "unknown", "powerSource": "battery"],
configuration : ["battery": false],
preferences : [
"powerOnBehaviour" : [ name: "powerOnBehaviour", type: "enum", title: "Power-On Behaviour", description:"Select Power-On Behaviour", defaultValue: "2", options: ['0': 'closed', '1': 'open', '2': 'last state']] //,
]
]
]
def isConfigurableSleepyDevice() { getModelGroup() in ['Zigbee NON-Tuya', 'TS0201_TH'] }
@Field static final Integer MaxRetries = 3
@Field static final Integer ConfigTimer = 15
@Field static final Integer presenceCountTreshold = 4
@Field static final Integer defaultMinReportingTime = 10
@Field static final Integer REFRESH_TIMER = 3000
@Field static final Integer COMMAND_TIMEOUT = 10
@Field static final Integer MAX_PING_MILISECONDS = 10000 // rtt more than 10 seconds will be ignored
@Field static String UNKNOWN = "UNKNOWN"
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()
Map statsMap = stringToJsonMap(state.stats); try {statsMap['rxCtr']++ } catch (e) {statsMap['rxCtr']=1}; state.stats = mapToJsonString(statsMap)
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 == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) {
if (descMap.attrInt == 0x0021) {
getBatteryPercentageResult(Integer.parseInt(descMap.value,16))
} else if (descMap.attrInt == 0x0020){
//log.trace "descMap.attrInt == 0x0020"
getBatteryVoltageResult(Integer.parseInt(descMap.value, 16))
}
else {
log.warn "unparesed attrint $descMap.attrInt"
}
}
else if (descMap.cluster == "0400" && descMap.attrId == "0000") {
def rawLux = Integer.parseInt(descMap.value,16)
if (device.getDataValue("manufacturer") in ["ShinaSystem"]) {
illuminanceEventLux( rawLux )
}
else {
illuminanceEvent( rawLux )
}
if (getModelGroup() == 'TS0222') {
pollTS0222()
}
}
else if (descMap.cluster == "0400" && descMap.attrId == "F001") { //MOES ZSS-ZK-THL, also TS0201 Neo Coolcam!
def raw = Integer.parseInt(descMap.value,16)
if (settings?.txtEnable) log.info "${device.displayName} illuminance sensitivity is ${raw} Lux"
device.updateSetting("illuminanceSensitivity", [value:raw, type:"number"])
}
else if (descMap.cluster == "0402" && descMap.attrId == "0000") {
if (getModelGroup() != 'TS0222_2') {
def raw = Integer.parseInt(descMap.value,16)
if (raw > 32767) {
//Here we deal with negative values
raw = raw - 65536
}
temperatureEvent( raw / 100.0 )
}
else {
if (settings?.logEnable) log.warn "${device.displayName} Ignoring ${getModelGroup()} temperature event"
}
}
else if (descMap.cluster == "0405" && descMap.attrId == "0000") {
def raw = Integer.parseInt(descMap.value,16)
if (getModelGroup() != 'TS0201_TH') {
humidityEvent( raw / 100.0 )
}
else {
humidityEvent( raw / 10.0 ) // also _TZE200_bjawzodf, _TZE200_zl1kmjqx ?
}
}
else if (descMap.cluster == "0406" && descMap.attrId == "0000") { // OWON, SiHAS
def raw = Integer.parseInt(descMap.value,16)
motionEvent( raw & 0x01 )
}
else if (descMap.cluster == "0000" && descMap.attrId == "0001") { // ping
// descMap = [raw:0D310100000A01002004, dni:0D31, endpoint:01, cluster:0000, size:0A, attrId:0001, encoding:20, command:01, value:04, clusterInt:0, attrInt:1]
logDebug "Tuya check-in message (attribute ${descMap.attrId} reported: ${descMap?.value})"
def now = new Date().getTime()
Map lastTxMap = stringToJsonMap(state.lastTx)
def timeRunning = now.toInteger() - (lastTxMap.pingTime ?: '0').toInteger()
if (timeRunning < MAX_PING_MILISECONDS && timeRunning > 0) {
sendRttEvent()
}
unschedule('deviceCommandTimeout')
}
else if (descMap?.clusterInt == CLUSTER_TUYA) {
processTuyaCluster( descMap )
}
else if (descMap?.clusterId == "0013") { // device announcement, profileId:0000
logInfo "device announcement"
try {statsMap['rejoins']++ } catch (e) {statsMap['rejoins']=1}; state.stats = mapToJsonString(statsMap)
if (getModelGroup() == 'TS0222') {
configure()
}
}
else if (descMap.isClusterSpecific == false && descMap.command == "01" ) { //global commands read attribute response
def status = descMap.data[2]
if (status == "86") {
if (settings?.logEnable) log.warn "${device.displayName} Cluster ${descMap.clusterId} read attribute - NOT SUPPORTED!\r ${descMap}"
}
else {
if (settings?.logEnable) log.warn "${device.displayName} UNPROCESSED Global Command : ${descMap}"
}
}
else if (descMap.profileId == "0000") { //zdo
parseZDOcommand(descMap)
}
else if (descMap.clusterId != null && descMap.profileId == "0104") { // ZHA global command
parseZHAcommand(descMap)
}
else {
if (settings?.logEnable) log.debug "${device.displayName} NOT PARSED : ${descMap}"
}
} // if 'catchall:' or 'read attr -'
else {
if (settings?.logEnable) log.debug "${device.displayName} UNPROCESSED parse() descMap = ${zigbee.parseDescriptionAsMap(description)}"
}
//
if (isPendingConfig()) {
ConfigurationStateMachine()
}
}
def parseZHAcommand( Map descMap) {
Map lastRxMap = stringToJsonMap(state.lastRx)
Map lastTxMap = stringToJsonMap(state.lastTx)
switch (descMap.command) {
case "01" : //read attribute response. If there was no error, the successful attribute reading would be processed in the main parse() method.
def status = descMap.data[2]
def attrId = descMap.data[1] + descMap.data[0]
if (status == "86") {
if (logEnable==true) log.warn "${device.displayName} Read attribute response: unsupported Attributte ${attrId} cluster ${clusterId}"
}
else {
if (logEnable==true) log.debug "${device.displayName} Read attribute response: status code ${status} Attributte ${attrId} cluster ${descMap.clusterId}"
}
break
case "04" : //write attribute response
if (logEnable==true) log.info "${device.displayName} Received Write Attribute Response for cluster:${descMap.clusterId} , data=${descMap.data} (Status: ${descMap.data[0]=="00" ? 'Success' : 'Failure'})"
break
case "07" : // Configure Reporting Response
if (logEnable==true) log.info "${device.displayName} Received Configure Reporting Response for cluster:${descMap.clusterId} , data=${descMap.data} (Status: ${descMap.data[0]=="00" ? 'Success' : 'Failure'})"
// Status: Unreportable Attribute (0x8c)
break
case "09" : // Command: Read Reporting Configuration Response (0x09)
def status = zigbee.convertHexToInt(descMap.data[0]) // Status: Success (0x00)
def attr = zigbee.convertHexToInt(descMap.data[3])*256 + zigbee.convertHexToInt(descMap.data[2]) // Attribute: OnOff (0x0000)
if (status == 0) {
def dataType = zigbee.convertHexToInt(descMap.data[4]) // Data Type: Boolean (0x10)
def min = zigbee.convertHexToInt(descMap.data[6])*256 + zigbee.convertHexToInt(descMap.data[5])
def max = zigbee.convertHexToInt(descMap.data[8]+descMap.data[7])
def delta = 0
if (descMap.data.size() == 11) {
delta = zigbee.convertHexToInt(descMap.data[10]+descMap.data[9])
}
else if (descMap.data.size() == 10) {
delta = zigbee.convertHexToInt(descMap.data[9])
}
else {
if (logEnable==true) log.debug "${device.displayName} descMap.data.size = ${descMap.data.size()}"
}
logDebug "Received Read Reporting Configuration response (0x09) for cluster:${descMap.clusterId} attribite:${descMap.data[3]+descMap.data[2]}, data=${descMap.data} (Status: ${descMap.data[0]=="00" ? 'Success' : 'Failure'}) min=${min} max=${max} delta=${delta}"
String attributeName
if (descMap.clusterId == "0405") {
attributeName = "humidity"
lastRxMap.humiCfg = min.toString() + "," + max.toString() + "," + delta.toString()
if (lastRxMap.humiCfg == lastTxMap.humiCfg) {
lastTxMap.humiCfgOK = true
}
}
else if (descMap.clusterId == "0402") {
attributeName = "temperature"
lastRxMap.tempCfg = min.toString() + "," + max.toString() + "," + delta.toString()
if (lastRxMap.tempCfg == lastTxMap.tempCfg) {
lastTxMap.tempCfgOK = true
}
}
else if (descMap.clusterId == "0001") {
attributeName = "battery %"
}
else {
attributeName = descMap.clusterId
}
if (lastTxMap.humiCfgOK == true && lastTxMap.tempCfgOK == true) {
logDebug "both T&H configured!"
lastTxMap.cfgFailure = false
}
if (txtEnable==true) {
log.info "${device.displayName} Reporting Configuration Response for ${attributeName} (status: ${descMap.data[0]=="00" ? 'Success' : 'Failure'}) is: min=${min} max=${max} delta=${delta}"
}
}
else { // failure
if (logEnable==true) log.info "${device.displayName} Not Found (0x8b) Read Reporting Configuration Response for cluster:${descMap.clusterId} attribite:${descMap.data[3]+descMap.data[2]}, data=${descMap.data} (Status: ${descMap.data[0]=="00" ? 'Success' : 'Failure'})"
}
break
case "0B" : // ZCL Default Response
def status = descMap.data[1]
if (status != "00") {
if (logEnable==true) log.info "${device.displayName} Received ZCL Default Response to Command ${descMap.data[0]} for cluster:${descMap.clusterId} , data=${descMap.data} (Status: ${descMap.data[1]=="00" ? 'Success' : 'Failure'})"
}
break
default :
if (logEnable==true) log.warn "${device.displayName} Unprocessed global command: cluster=${descMap.clusterId} command=${descMap.command} attrId=${descMap.attrId} value=${descMap.value} data=${descMap.data}"
break
}
state.lastRx = mapToJsonString(lastRxMap)
state.lastTx = mapToJsonString(lastTxMap)
}
def parseZDOcommand( Map descMap ) {
switch (descMap.clusterId) {
case "0006" :
if (logEnable) log.info "${device.displayName} Received match descriptor request, data=${descMap.data} (Sequence Number:${descMap.data[0]}, Input cluster count:${descMap.data[5]} Input cluster: 0x${descMap.data[7]+descMap.data[6]})"
break
case "0013" : // device announcement
if (logEnable) log.info "${device.displayName} Received device announcement, data=${descMap.data} (Sequence Number:${descMap.data[0]}, Device network ID: ${descMap.data[2]+descMap.data[1]}, Capability Information: ${descMap.data[11]})"
break
case "8004" : // simple descriptor response
if (logEnable) log.info "${device.displayName} Received simple descriptor response, data=${descMap.data} (Sequence Number:${descMap.data[0]}, status:${descMap.data[1]}, lenght:${hubitat.helper.HexUtils.hexStringToInt(descMap.data[4])}"
parseSimpleDescriptorResponse( descMap )
break
case "8005" : // endpoint response
if (logEnable) log.info "${device.displayName} Received endpoint response: cluster: ${descMap.clusterId} (endpoint response) endpointCount = ${ descMap.data[4]} endpointList = ${descMap.data[5]}"
break
case "8021" : // bind response
if (logEnable) log.info "${device.displayName} Received bind response, data=${descMap.data} (Sequence Number:${descMap.data[0]}, Status: ${descMap.data[1]=="00" ? 'Success' : 'Failure'})"
break
case "8022" : // unbind response
if (logEnable) log.info "${device.displayName} Received unbind response, data=${descMap.data} (Sequence Number:${descMap.data[0]}, Status: ${descMap.data[1]=="00" ? 'Success' : 'Failure'})"
break
case "8034" : // leave response
if (logEnable) log.info "${device.displayName} Received leave response, data=${descMap.data}"
break
case "8038" : // Management Network Update Notify
if (logEnable) log.info "${device.displayName} Received Management Network Update Notify, data=${descMap.data}"
break
default :
if (logEnable) log.warn "${device.displayName} Unprocessed ZDO command: cluster=${descMap.clusterId} command=${descMap.command} attrId=${descMap.attrId} value=${descMap.value} data=${descMap.data}"
break // 2022/09/16
}
}
def processTuyaCluster( descMap ) {
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) {
if (settings?.logEnable) 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))
// TODO : send raw command without 'need confirmation' frame control !
if (settings?.logEnable) log.trace "${device.displayName} now is: ${now()}" // KK TODO - convert 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
}
else if (descMap?.clusterInt==CLUSTER_TUYA && descMap?.command == "0B") { // ZCL Command Default Response
String clusterCmd = descMap?.data[0]
def status = descMap?.data[1]
logDebug "Tuya cluster confirmation for command 0x${clusterCmd} response 0x${status} data = ${descMap?.data}"
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"))
{
def 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
for (int i = 0; i < (dataLen-4); ) {
def dp = zigbee.convertHexToInt(descMap?.data[2+i]) // "dp" field describes the action/message of a command frame
def dp_id = zigbee.convertHexToInt(descMap?.data[3+i]) // "dp_identifier" is device dependant
def fncmd_len = zigbee.convertHexToInt(descMap?.data[5+i])
def fncmd = getTuyaAttributeValue(descMap?.data, i) //
if (settings?.logEnable) log.trace "${device.displayName} dp_id=${dp_id} dp=${dp} fncmd=${fncmd} fncmd_len=${fncmd_len} (index=${i})"
processTuyaDP( descMap, dp, dp_id, fncmd)
i = i + fncmd_len + 4;
//log.warn "next index is : ${i}"
}
//log.warn "##### end of parsing ####"
} // if (descMap?.command == "01" || descMap?.command == "02")
}
def processTuyaDP( descMap, dp, dp_id, fncmd) {
switch (dp) {
case 0x01 : // temperature in C for most models
//
if (getModelGroup() == "TS0601_Contact") {
def value = fncmd == 0 ? "closed" : "open" // inverted!
sendEvent("name": "contact", "value": value)
if (settings?.txtEnable) log.info "${device.displayName} Contact is ${value}"
}
else if (getModelGroup() != "TS0601_AUBESS") { // temperature in C, including 'TS0601_Tuya_2'
if (fncmd > 32767) {
//Here we deal with negative values
fncmd = fncmd - 65536
}
temperatureEvent( fncmd / 10.0 )
}
else {
def lomihi = fncmd == 0 ? "low" : fncmd == 1 ? "medium" : fncmd == 2 ? "high" : "unknown"
if (settings?.logEnable) log.debug "${device.displayName} Tuya illuminance status is: ${lomihi} (dp_id=${dp_id} dp=${dp} fncmd=${fncmd})"
}
break
case 0x02 : // humidity % for most of the models; 'TS0601_Contact'illuminance; 'TS0601_Contact'0 battery %
if (getModelGroup() == 'TS0601_AUBESS') {
illuminanceEventLux( safeToInt( fncmd ) )
}
else if (getModelGroup() == "TS0601_Contact") {
getBatteryPercentageResult(fncmd * 2)
}
else {
if (device.getDataValue("manufacturer") in ["_TZE200_bjawzodf", "_TZE200_zl1kmjqx"]) {
humidityEvent( (fncmd / 10.0) as int )
}
else {
humidityEvent( fncmd ) // including 'TS0601_Tuya_2'
}
}
break
case 0x03 : // humidity or illuminance or battery state
if (getModelGroup() in ['TS0601_Soil']) {
logDebug "Soil Sensor humidity raw = ${fncmd}"
humidityEvent( fncmd )
}
else if (getModelGroup() in ['TS0601_Tuya_2']) {
logDebug "battery_state (0x03) is ${fncmd}" // ['low', 'medium', 'high']
def rawValue = 0
if (fncmd == 2) rawValue = 100 // Battery High
else if (fncmd == 1) rawValue = 66 // Battery Medium
else if (fncmd == 0) rawValue = 33 // Battery Low
getBatteryPercentageResult(rawValue*2)
}
else { // _TZE200_zl1kmjqx link quality?
illuminanceEvent(fncmd)
}
break
case 0x04 : // battery
getBatteryPercentageResult(fncmd * 2)
if (settings?.txtEnable) log.info "${device.displayName} battery is $fncmd %"
break
case 0x05 : // Soil Monitor
if (fncmd > 32767) {
// not good for the plants ...
fncmd = fncmd - 65536
}
temperatureEvent( fncmd )
break
case 0x09: // temp. scale 1=Fahrenheit 0=Celsius (TS0601 Tuya and Haoze) TS0601_Tuya does not change the symbol on the LCD ! // including 'TS0601_Tuya_2'
if (settings?.logEnable) log.info "${device.displayName} Temperature scale reported by device is: ${fncmd == 1 ? 'Fahrenheit' :'Celsius' }" // {'celsius': new Enum(0), 'fahrenheit': new Enum(1)}
break
case 0x0A: // (10) Max. Temp Alarm, Value / 10 (both TS0601_Tuya and TS0601_Haozee)
if (((safeToDouble(settings?.maxTempAlarmPar)*10.0 as int) == (fncmd as int)) || (getModelGroup() in ['TS0601_Haozee'])) {
if (settings?.logEnable) log.info "${device.displayName} reported temperature alarm upper limit ${fncmd/10.0 as double} C"
}
else {
if (settings?.logEnable) log.warn "${device.displayName} warning: temperature alarm upper limit reported by the device (${fncmd/10.0 as double} C) differs from the preference setting (${settings?.maxTempAlarmPar} C)"
}
break
case 0x0B: // (11) Min. Temp Alarm, Value / 10 (both TS0601_Tuya and TS0601_Haozee)
if (((safeToDouble(settings?.minTempAlarmPar)*10.0 as int) == (fncmd as int)) || (getModelGroup() in ['TS0601_Haozee'])) {
if (settings?.logEnable) log.info "${device.displayName} reported temperature alarm lower limit ${fncmd/10.0 as double} C"
}
else {
if (settings?.logEnable) log.warn "${device.displayName} warning: temperature alarm lower limit reported by the device (${fncmd/10.0 as double} C) differs from the preference setting (${settings?.minTempAlarmPar} C)"
}
break
case 0x0C: // (12) Max?. Humidity Alarm (Haozee only?)
if (settings?.logEnable) log.info "${device.displayName} humidity alarm upper limit is ${fncmd} "
break
case 0x0D: // (13) Min?. Humidity Alarm (Haozee only?)
if (settings?.logEnable) log.info "${device.displayName} humidity alarm lower limit is ${fncmd} "
//device.updateSetting("minHumidityAlarmPar", [value:fncmd, type:"number"])
break
case 0x0E: // (14) Temperature Alarm 0 = low alarm? 1 = high alarm? 2 = alarm cleared
if (getModelGroup() in ['TS0601_Soil']) {
if (settings?.txtEnable) log.info "${device.displayName} battery_state (0x0E) is ${fncmd}"
}
else {
if (fncmd == 1) {
if (settings?.txtEnable) log.info "${device.displayName} Minimal Temperature Alarm (0x0E=${fncmd}) is active"
}
else if (fncmd == 0) { // TS0601_Haozee only?
if (settings?.txtEnable) log.info "${device.displayName} Maximal Temperature Alarm (0x0E=${fncmd}) is active"
}
else if (fncmd == 2 ) {
if (getModelGroup() in ['TS0601_Haozee']) {
if (settings?.txtEnable) log.info "${device.displayName} Maximal Temperature Alarm (0x0E=${fncmd}) is inactive"
}
else {
if (settings?.txtEnable) log.info "${device.displayName} Minimal Temperature Alarm (0x0E=${fncmd}) is inactive"
}
}
else {
if (settings?.txtEnable) log.warn "${device.displayName} Temperature Alarm (0x0E) UNKNOWN value ${fncmd}" // 1 if alarm (lower alarm) ? 2 if lower alam is cleared
}
}
break
case 0x0F: // humidity Alarm 0 = low alarm? 1 = high alarm? 2 = alarm cleared (Haozee only?)
if (getModelGroup() in ['TS0601_Soil']) {
getBatteryPercentageResult(fncmd * 2)
}
else {
if (fncmd == 1) {
if (settings?.txtEnable) log.info "${device.displayName} Minimal Humidity Alarm (0x0F=${fncmd}) is active"
}
else if (fncmd == 0) {
if (settings?.txtEnable) log.info "${device.displayName} Maximal Humidity Alarm (0x0F=${fncmd}) is active"
}
else if (fncmd == 2 ) {
if (settings?.txtEnable) log.info "${device.displayName} Humidity Alarm (0x0F=${fncmd}) is inactive"
}
else {
if (settings?.logEnable) log.warn "${device.displayName} Temperature Alarm (0x0E) UNKNOWN value ${fncmd}" // 1 if alarm (lower alarm) ? 2 if lower alam is cleared
}
}
break
case 0x11 : // (17) temperature max reporting interval, default 120 min (Haozee only) // maxReportingTimeTemp
if (settings?.maxReportingTimeTemp == ((fncmd*60/2.5) as int)) {
if (settings?.logEnable) log.info "${device.displayName} reported temperature max reporting interval ${((fncmd*60/2.5) as int)} seconds"
}
else {
if (settings?.logEnable) log.warn "${device.displayName} warning: temperature max reporting interval reported by the device (${((fncmd*60/2.5) as int)}s) differs from the preference setting (${settings?.maxReportingTimeTemp}s)"
}
break
case 0x12 : // (18) humidity max reporting interval, default 120 min (Haozee only)
if (settings?.maxReportingTimeHumidity == ((fncmd*60/2.5) as int)) {
if (settings?.logEnable) log.info "${device.displayName} reported humidity max reporting interval ${((fncmd*60/2.5) as int)} seconds"
}
else {
if (settings?.logEnable) log.warn "${device.displayName} warning: humidity max reporting interval reported by the device (${((fncmd*60/2.5) as int)}s) differs from the preference setting (${settings?.maxReportingTimeHumidity}s)"
}
break
case 0x13 : // (19) temperature sensitivity(value/2/10) default 0.3C ( divide / 2 for Haozee only?)
if ((safeToDouble(settings?.temperatureSensitivity)*20.0 as int) == (fncmd as int)) {
if (settings?.logEnable) log.info "${device.displayName} reported temperature sensitivity ${fncmd/20.0} C"
}
else {
if (settings?.logEnable) log.warn "${device.displayName} warning: temperature sensitivity reported by the device (${fncmd/20.0}) differs from the preference setting (${settings?.temperatureSensitivity})"
}
break
case 0x14 : // (20) humidity sensitivity default 3% (Haozee only)
if (settings?.humiditySensitivity == fncmd) {
if (settings?.logEnable) log.info "${device.displayName} reported humidity sensitivity ${fncmd} %"
}
else {
if (settings?.logEnable) log.warn "${device.displayName} warning: humidity sensitivity reported by the device (${fncmd}%) differs from the preference setting (${settings?.humiditySensitivity}%)"
}
break
case 0x65 : // (101)
illuminanceEventLux( safeToInt( fncmd ) ) // _TZE200_pay2byax
break
default :
if (settings?.logEnable) log.warn "${device.displayName} NOT PROCESSED Tuya cmd: dp=${dp} value=${fncmd} descMap.data = ${descMap?.data}"
break
}
}
private int getTuyaAttributeValue(ArrayList _data, index) {
int retValue = 0
if (_data.size() >= 6) {
int dataLength = _data[5+index] as Integer
int power = 1;
for (i in dataLength..1) {
retValue = retValue + power * zigbee.convertHexToInt(_data[index+i+5])
power = power * 256
}
}
return retValue
}
def getModelGroup() {
def manufacturer = device.getDataValue("manufacturer")
def modelGroup = 'UNKNOWN'
if (modelGroupPreference == null) {
device.updateSetting("modelGroupPreference", [value:"Auto detect", type:"enum"])
}
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 temperatureEvent( temperature, isDigital=false ) {
def map = [:]
Map statsMap = stringToJsonMap(state.stats); try {statsMap['tempCtr']++ } catch (e) {statsMap['tempCtr']=1}; state.stats = mapToJsonString(statsMap)
map.name = "temperature"
def Scale = location.temperatureScale
if (Scale == "F") {
temperature = (temperature * 1.8) + 32
map.unit = "\u00B0"+"F"
}
else {
map.unit = "\u00B0"+"C"
}
def tempCorrected = temperature + safeToDouble(settings?.temperatureOffset)
map.value = Math.round((tempCorrected - 0.05) * 10) / 10
map.type = isDigital == true ? "digital" : "physical"
map.isStateChange = true
map.descriptionText = "${map.name} is ${tempCorrected} ${map.unit}"
Map lastRxMap = stringToJsonMap(state.lastRx)
def timeElapsed = Math.round((now() - lastRxMap['tempTime'])/1000)
Integer timeRamaining = (minReportingTimeTemp - timeElapsed) as Integer
if (timeElapsed >= minReportingTimeTemp) {
if (settings?.txtEnable) {log.info "${device.displayName} ${map.descriptionText}"}
unschedule("sendDelayedEventTemp") //get rid of stale queued reports
lastRxMap['tempTime'] = now()
sendEvent(map)
}
else { // queue the event
map.type = "delayed"
if (settings?.logEnable) log.debug "${device.displayName} DELAYING ${timeRamaining} seconds event : ${map}"
runIn(timeRamaining, 'sendDelayedEventTemp', [overwrite: true, data: map])
}
state.lastRx = mapToJsonString(lastRxMap)
}
private void sendDelayedEventTemp(Map map) {
if (settings?.txtEnable) {log.info "${device.displayName} ${map.descriptionText} (${map.type})"}
Map lastRxMap = stringToJsonMap(state.lastRx); try {lastRxMap['tempTime'] = now()} catch (e) {lastRxMap['tempTime']=now()-(minReportingTimeTemp * 2000)}; state.lastRx = mapToJsonString(lastRxMap)
sendEvent(map)
}
def humidityEvent( humidity, isDigital=false ) {
def map = [:]
Map statsMap = stringToJsonMap(state.stats); try {statsMap['humiCtr']++ } catch (e) {statsMap['humiCtr']=1}; state.stats = mapToJsonString(statsMap)
double humidityAsDouble = safeToDouble(humidity) + safeToDouble(settings?.humidityOffset)
if (humidityAsDouble <= 0.0 || humidityAsDouble > 100.0) {
logWarn "ignored invalid humidity ${humidity} (${humidityAsDouble})"
return
}
map.value = Math.round(humidityAsDouble)
map.name = "humidity"
map.unit = "% RH"
map.type = isDigital == true ? "digital" : "physical"
map.isStateChange = true
map.descriptionText = "${map.name} is ${humidityAsDouble.round(1)} ${map.unit}"
Map lastRxMap = stringToJsonMap(state.lastRx)
def timeElapsed = Math.round((now() - lastRxMap['humiTime'])/1000)
Integer timeRamaining = (minReportingTimeHumidity - timeElapsed) as Integer
if (timeElapsed >= minReportingTimeHumidity) {
if (settings?.txtEnable) {log.info "${device.displayName} ${map.descriptionText}"}
unschedule("sendDelayedEventHumi")
lastRxMap['humiTime'] = now()
sendEvent(map)
}
else { // queue the event
map.type = "delayed"
if (settings?.logEnable) log.debug "${device.displayName} DELAYING ${timeRamaining} seconds event : ${map}"
runIn(timeRamaining, 'sendDelayedEventHumi', [overwrite: true, data: map])
}
state.lastRx = mapToJsonString(lastRxMap)
}
private void sendDelayedEventHumi(Map map) {
if (settings?.txtEnable) {log.info "${device.displayName} ${map.descriptionText} (${map.type})"}
//state.lastHumi = now()
Map lastRxMap = stringToJsonMap(state.lastRx); try {lastRxMap['humiTime'] = now()} catch (e) {lastRxMap['humiTime']=now()-(minReportingTimeHumidity * 2000)}; state.lastRx = mapToJsonString(lastRxMap)
sendEvent(map)
}
def switchEvent( value ) {
def map = [:]
map.name = "switch"
map.value = value
map.descriptionText = "${device.displayName} switch is ${value}"
if (settings?.txtEnable) {log.info "${map.descriptionText}"}
sendEvent(map)
}
def motionEvent( value ) {
def map = [:]
map.name = "motion"
map.value = value ? 'active' : 'inactive'
map.descriptionText = "${device.displayName} motion is ${map.value}"
if (settings?.txtEnable) {log.info "${map.descriptionText}"}
sendEvent(map)
}
def illuminanceEvent( illuminance, isDigital=false ) {
Map statsMap = stringToJsonMap(state.stats); try {statsMap['illumCtr']++ } catch (e) {statsMap['illumCtr']=1}; state.stats = mapToJsonString(statsMap)
def lux = illuminance > 0 ? Math.round(Math.pow(10,(illuminance/10000))) : 0
sendEvent("name": "illuminance", "value": lux, "type": isDigital == true ? 'digital':'physical', "unit": "lx")
if (settings?.txtEnable) log.info "$device.displayName illuminance is ${lux} Lux"
}
def illuminanceEventLux( Integer lux, isDigital=false ) {
Map statsMap = stringToJsonMap(state.stats); try {statsMap['illumCtr']++ } catch (e) {statsMap['illumCtr']=1}; state.stats = mapToJsonString(statsMap)
sendEvent("name": "illuminance", "value": lux, "type": isDigital == true ? 'digital':'physical', "unit": "lx")
if (settings?.txtEnable) log.info "$device.displayName illuminance is ${lux} Lux"
}
// called from initialize() and when installed as a new device
def installed() {
sendEvent(name: "_info", value: "installed", isStateChange: true)
if (settings?.txtEnable) log.info "${device.displayName} installed()..."
unschedule()
initializeVars(fullInit = true )
}
//
def updated() {
ArrayList cmds = []
Map lastRxMap = stringToJsonMap(state.lastRx)
Map lastTxMap = stringToJsonMap(state.lastTx)
state.modelGroup = getModelGroup()
logInfo "Updating ${device.getLabel()} (${device.getName()}) model ${device.getDataValue('model')} manufacturer ${device.getDataValue('manufacturer')} modelGroupPreference = ${modelGroupPreference} (${getModelGroup()})"
logInfo "Debug logging is ${logEnable}; Description text logging is ${txtEnable}"
if (logEnable) {
runIn(86400, "logsOff", [overwrite: true, misfire: "ignore"]) // turn off debug logging after 30 minutes
logInfo "Debug logging will be turned off after 24 hours"
}
else {
unschedule("logsOff")
}
Integer fncmd
if (getModelGroup() in ['TS0601_Tuya','TS0601_Haozee']) {
Integer intValue = ((safeToDouble(settings?.temperatureSensitivity )) * 20.0) as int
if (settings?.logEnable) log.trace "${device.displayName} setting temperatureSensitivity to ${(intValue as Double)/20.0} C"
cmds += sendTuyaCommand("13", DP_TYPE_VALUE, zigbee.convertToHexString(intValue as int, 8))
}
if (getModelGroup() in ['TS0601_Tuya','TS0601_Haozee','TS0201_LCZ030']) {
if (location.temperatureScale == "C") { // Celsius
cmds += sendTuyaCommand("09", DP_TYPE_ENUM, "00")
if (settings?.logEnable) log.trace "${device.displayName} setting temperature scale to Celsius: ${cmds}"
}
else if (location.temperatureScale == "F") { // Fahrenheit
cmds += sendTuyaCommand("09", DP_TYPE_ENUM, "01")
if (settings?.logEnable) log.trace "${device.displayName} setting temperature scale to Fahrenheit: ${cmds}"
}
else {
if (settings?.logEnable) log.warn "${device.displayName} temperatureScaleParameter does NOT MATCH! (${location.temperatureScale})"
}
}
if (getModelGroup() in ['TS0601_Tuya','TS0201_LCZ030']) {
fncmd = (safeToDouble( maxTempAlarmPar ) * 10) as int
if (settings?.logEnable) log.trace "${device.displayName} setting maxTempAlarm to ${fncmd/10.0 as double} C"
cmds += sendTuyaCommand("0A", DP_TYPE_VALUE, zigbee.convertToHexString(fncmd as int, 8))
fncmd = (safeToDouble( minTempAlarmPar ) * 10) as int
if (settings?.logEnable) log.trace "${device.displayName} setting minTempAlarm to ${fncmd/10.0 as double} C"
cmds += sendTuyaCommand("0B", DP_TYPE_VALUE, zigbee.convertToHexString(fncmd as int, 8))
}
if (getModelGroup() in ['TS0601_Haozee']) {
Integer intValue = settings?.humiditySensitivity as int
if (settings?.logEnable) log.trace "${device.displayName} setting humiditySensitivity to ${intValue} %"
cmds += sendTuyaCommand("14", DP_TYPE_VALUE, zigbee.convertToHexString(intValue as int, 8))
//
intValue = ((settings?.maxReportingTimeTemp * 2.5) as int) / 60
if (settings?.logEnable) log.trace "${device.displayName} setting Temperature Max reporting time to ${(intValue/2.5) as int} minutes"
cmds += sendTuyaCommand("11", DP_TYPE_VALUE, zigbee.convertToHexString(intValue as int, 8))
//
intValue = ((settings?.maxReportingTimeHumidity *2.5) as int) / 60
if (settings?.logEnable) log.trace "${device.displayName} setting Humidity Max reporting time to ${(intValue/2.5) as int} minutes"
cmds += sendTuyaCommand("12", DP_TYPE_VALUE, zigbee.convertToHexString(intValue as int, 8))
/*
fncmd = safeToInt( maxHumidityAlarmPar )
if (settings?.logEnable) log.trace "${device.displayName} changing maxHumidityAlarm to= ${fncmd}"
cmds += sendTuyaCommand("0C", DP_TYPE_VALUE, zigbee.convertToHexString(fncmd as int, 8))
fncmd = safeToInt( minHumidityAlarmPar )
if (settings?.logEnable) log.trace "${device.displayName} changing minHumidityAlarm to= ${fncmd}"
cmds += sendTuyaCommand("0D", DP_TYPE_VALUE, zigbee.convertToHexString(fncmd as int, 8))
*/
}
if (getModelGroup() in ['TS0601_Haozee']) {
// TODO - write attribute 0xF001, cluster 0x400
}
if (getModelGroup() in ['OWON']) {
cmds += initializeDevice()
}
if (isConfigurableSleepyDevice()) { // ["Zigbee NON-Tuya", "TS0201_TH"]
lastTxMap.tempCfg = (settings?.minReportingTimeTemp as int).toString() + "," + (settings?.maxReportingTimeTemp as int).toString() + "," + ((settings?.temperatureSensitivity * 100) as int).toString()
lastTxMap.humiCfg = (settings?.minReportingTimeHumidity as int).toString() + "," + (settings?.maxReportingTimeHumidity as int).toString() + "," + ((settings?.humiditySensitivity *100) as int).toString()
if (lastTxMap.tempCfg != lastRxMap.tempCfg) {
cmds += zigbee.configureReporting(0x0402, 0x0000, DataType.INT16, settings?.minReportingTimeTemp as int, settings?.maxReportingTimeTemp as int, (settings?.temperatureSensitivity * 100) as int, [:], 200)
log.info "configure temperature reporting (${lastTxMap.tempCfg}) pending ..."
lastTxMap.tempCfgOK = false
}
else {
logDebug "Temperature reporting already configured (${lastTxMap.tempCfg}), skipping ..."
lastTxMap.tempCfgOK = true
}
if (lastTxMap.humiCfg != lastRxMap.humiCfg) {
cmds += zigbee.configureReporting(0x0405, 0x0000, DataType.UINT16, settings?.minReportingTimeHumidity as int, settings?.maxReportingTimeHumidity as int, (settings?.humiditySensitivity *100) as int, [:], 200)
log.info "configure humidity reporting (${lastTxMap.humiCfg}) pending ..."
lastTxMap.humiCfgOK = false
}
else {
logDebug "Humidity reporting already configured (${lastTxMap.humiCfg}), skipping ..."
lastTxMap.humiCfgOK = true
}
cmds += zigbee.configureReporting(0x0001, 0x0021, DataType.UINT8, 10, 1440, 0x01, [:], 200)
cmds += zigbee.reportingConfiguration(0x0402, 0x0000, [:], 250)
cmds += zigbee.reportingConfiguration(0x0405, 0x0000, [:], 250)
cmds += zigbee.reportingConfiguration(0x0001, 0x0021, [:], 250)
}
def pendingConfig = lastTxMap.tempCfgOK == true ? 0 : 1
pendingConfig += lastTxMap.humiCfgOK == true ? 0 : 1
if (isConfigurableSleepyDevice()) { // ['Zigbee NON-Tuya', 'TS0201_TH']
if (pendingConfig != 0 ) {
logInfo "pending ${pendingConfig} reporting configurations"
updateInfo("Pending ${pendingConfig} configuration(s). Wake up the device!")
lastTxMap.cfgFailure = false
}
else {
logInfo "no changed configuration parameters to be sent to the device."
}
// try reading the reporting configuration anyway ...
}
state.lastTx = mapToJsonString(lastTxMap)
sendZigbeeCommands( cmds )
}
def isPendingConfig() {
Map lastTxMap = stringToJsonMap(state.lastTx)
if (lastTxMap.cfgFailure == true || ( lastTxMap.tempCfgOK == true && lastTxMap.humiCfgOK == true)) {
return false
}
else {
return true
}
}
// called from parse() when any packet is received from the awaken device ...
def ConfigurationStateMachine() {
if (!isConfigurableSleepyDevice()) {
return
}
Map lastTxMap = stringToJsonMap(state.lastTx)
if (lastTxMap.cfgFailure == true ) {
updateInfo("configuration failure")
unschedule("configTimer")
return
}
def configState = state.configState
logDebug "ConfigurationStateMachine configState = ${configState}"
switch (configState) {
case 0 : // idle
if (isPendingConfig()) {
logDebug "configuration pending ..."
updateInfo("sending the reporting configuration...")
lastTxMap.cfgTimer = ConfigTimer
updated()
runIn(1, "configTimer" , [overwrite: true, misfire: "ignore"])
configState = 1
}
else {
logWarn "ConfigurationStateMachine called without isPendingConfig?"
unschedule("configTimer")
}
break
case 1 : // waiting 10 seconds for acknowledge from the device // TODO - process config ERRORS !!!
if (!isPendingConfig()) {
updateInfo("configured")
lastTxMap.cfgTimer = 0
configState = 0
unschedule("configTimer")
}
else if (lastTxMap.cfgTimer == null || lastTxMap.cfgTimer == 0) { // timeout
logDebug "Timeout!"
updateInfo("Timeout!")
lastTxMap.cfgTimer = 0
unschedule("configTimer")
configState = 0 // try again next time a packet is received from the device..
}
else {
logDebug "config confirmation still pending ... lastTxMap.cfgTimer is ${lastTxMap.cfgTimer}"
}
break
default :
logWarn "ConfigurationStateMachine() unknown state ${configState}"
unschedule("configTimer")
configState = 0
break
}
state.configState = configState
state.lastTx = mapToJsonString(lastTxMap)
}
// scheduled from ConfigurationStateMachine
def configTimer() {
Map lastTxMap = stringToJsonMap(state.lastTx)
logDebug "configTimer() callled"
if (lastTxMap.cfgTimer != null) {
if (!isPendingConfig()) {
logDebug "configuration is successful! "
ConfigurationStateMachine()
}
else {
lastTxMap.cfgTimer = lastTxMap.cfgTimer - 1
if (lastTxMap.cfgTimer >= 0 ) {
state.lastTx = mapToJsonString(lastTxMap) // flush the timer!
ConfigurationStateMachine()
runIn(1, "configTimer" /*, [overwrite: true, misfire: "ignore"]*/)
logDebug "scheduling again configTimer = ${lastTxMap.cfgTimer}"
}
else {
logDebug "configTimer expired! Do not restart it."
lastTxMap.cfgFailure = true
}
}
}
else {
lastTxMap.cfgTimer = 0
}
state.lastTx = mapToJsonString(lastTxMap)
}
def pollTS0222() {
List cmds = []
cmds += zigbee.readAttribute(0x0001, 0x0021, [:], delay=200) // Battery Percent
cmds += "he rattr 0x${device.deviceNetworkId} 0x02 0x0402 0x0000 {}" //, "delay 200",
cmds += "he rattr 0x${device.deviceNetworkId} 0x02 0x0405 0x0000 {}" //, "delay 200",
sendZigbeeCommands(cmds)
}
def refresh() {
checkDriverVersion()
if (getModelGroup() == 'TS0222') {
pollTS0222()
}
else if (true /*getModelGroup() in ['TS0201_TH']*/) {
List cmds = []
cmds += zigbee.readAttribute(0x0001, 0x0021, [:], delay=200)
cmds += zigbee.readAttribute(0x0402, 0x0000, [:], delay=200)
cmds += zigbee.readAttribute(0x0405, 0x0000, [:], delay=200)
sendZigbeeCommands( cmds )
}
else {
logInfo "refresh() is not implemented for this sleepy Zigbee device"
}
}
def ping() {
logInfo 'ping...'
scheduleCommandTimeoutCheck()
Map lastTxMap = stringToJsonMap(state.lastTx)
lastTxMap.pingTime = new Date().getTime()
state.lastTx = mapToJsonString(lastTxMap)
sendZigbeeCommands( zigbee.readAttribute(zigbee.BASIC_CLUSTER, 0x01, [:], 0) )
}
def sendRttEvent() {
def now = new Date().getTime()
Map lastTxMap = stringToJsonMap(state.lastTx)
def timeRunning = now.toInteger() - (lastTxMap.pingTime ?: '0').toInteger()
def descriptionText = "Round-trip time is ${timeRunning} (ms)"
logInfo "${descriptionText}"
sendEvent(name: "rtt", value: timeRunning, descriptionText: descriptionText, unit: "ms", isDigital: true)
}
private void scheduleCommandTimeoutCheck(int delay = COMMAND_TIMEOUT) {
runIn(delay, 'deviceCommandTimeout')
}
void deviceCommandTimeout() {
logWarn 'no response received (sleepy device or offline?)'
sendEvent(name: "rtt", value: 'timeout', descriptionText: 'no response received', unit: "", isDigital: true)
}
def driverVersionAndTimeStamp() {version()+' '+timeStamp()}
def checkDriverVersion() {
if (state.driverVersion == null || driverVersionAndTimeStamp() != state.driverVersion) {
logInfo "updating the settings from the current driver version ${state.driverVersion} to the new version ${driverVersionAndTimeStamp()}"
initializeVars( fullInit = false )
//
if (state.rxCounter != null) state.remove("rxCounter")
if (state.txCounter != null) state.remove("txCounter")
if (state.packetID != null) state.remove("packetID")
if (state.lastRx == null || state.stats == null || state.lastTx == null) {
resetStats()
}
//
state.driverVersion = driverVersionAndTimeStamp()
}
}
def resetStats() {
Map stats = [
date : new Date().format('yyyy-MM-dd', location.timeZone),
rxCtr : 0,
txCtr : 0,
rejoins: 0
]
Map lastRx = [
tempTime : now() - defaultMinReportingTime * 1000,
humiTime : now() - defaultMinReportingTime * 1000,
tempCfg : '-1,-1,-1',
humiCfg : '-1,-1,-1'
]
Map lastTx = [
tempCfg : '-1,-1,-1',
humiCfg : '-1,-1,-1',
tempCfgOK : false,
humiCfgOK : false,
cfgFailure : false,
cfgTimer : 0
]
state.stats = mapToJsonString( stats )
state.lastRx = mapToJsonString( lastRx )
state.lastTx = mapToJsonString( lastTx )
if (txtEnable==true) log.info "${device.displayName} Statistics were reset. Press F5 to refresh the device page"
}
def logInitializeRezults() {
if (settings?.txtEnable) log.info "${device.displayName} manufacturer = ${device.getDataValue("manufacturer")} ModelGroup = ${getModelGroup()}"
if (settings?.txtEnable) log.info "${device.displayName} Initialization finished\r version=${version()} (Timestamp: ${timeStamp()})"
}
// called by initialize() button
void initializeVars(boolean fullInit = true ) {
log.info "${device.displayName} InitializeVars()... fullInit = ${fullInit}"
if (fullInit == true ) {
state.clear()
resetStats()
state.driverVersion = driverVersionAndTimeStamp()
}
state.configState = 0 // reset the configuration state machine
if (fullInit == true || settings?.modelGroupPreference == null) device.updateSetting("modelGroupPreference", [value:"Auto detect", type:"enum"])
if (fullInit == true || settings?.logEnable == null) device.updateSetting("logEnable", true)
if (fullInit == true || settings?.txtEnable == null) device.updateSetting("txtEnable", true)
if (fullInit == true || settings?.temperatureOffset == null) device.updateSetting("temperatureOffset", [value:0.0, type:"decimal"])
if (fullInit == true || settings?.humidityOffset == null) device.updateSetting("humidityOffset", [value:0.0, type:"decimal"])
if (fullInit == true || settings?.advancedOptions == null) device.updateSetting("advancedOptions", false)
if (fullInit == true || settings?.temperatureSensitivity == null) device.updateSetting("temperatureSensitivity", [value:0.5, type:"decimal"])
if (fullInit == true || settings?.humiditySensitivity == null) device.updateSetting("humiditySensitivity", [value:5, type:"number"])
if (fullInit == true || settings?.illuminanceSensitivity == null) device.updateSetting("illuminanceSensitivity", [value:12, type:"number"])
if (fullInit == true || settings?.minTempAlarmPar == null) device.updateSetting("minTempAlarmPar", [value:0.0, type:"decimal"])
if (fullInit == true || settings?.maxTempAlarmPar == null) device.updateSetting("maxTempAlarmPar", [value:39.0, type:"decimal"])
if (fullInit == true || settings?.minHumidityAlarmPar == null) device.updateSetting("minHumidityAlarmPar", [value:20, type:"number"])
if (fullInit == true || settings?.maxHumidityAlarmPar == null) device.updateSetting("maxHumidityAlarmPar", [value:60, type:"number"])
if (fullInit == true || settings?.minReportingTimeTemp == null) device.updateSetting("minReportingTimeTemp", [value:10, type:"number"])
if (fullInit == true || settings?.maxReportingTimeTemp == null) device.updateSetting("maxReportingTimeTemp", [value:3600, type:"number"])
if (fullInit == true || settings?.minReportingTimeHumidity == null) device.updateSetting("minReportingTimeHumidity", [value:10, type:"number"])
if (fullInit == true || settings?.maxReportingTimeHumidity == null) device.updateSetting("maxReportingTimeHumidity", [value:3600, type:"number"])
if (fullInit == true || state.notPresentCounter == null) state.notPresentCounter = 0
//
if (fullInit == true || state.modelGroup == null) state.modelGroup = getModelGroup()
//if (fullInit == true || state.lastTemp == null) state.lastTemp = now() - defaultMinReportingTime * 1000
//if (fullInit == true || state.lastHumi == null) state.lastHumi = now() - defaultMinReportingTime * 1000
}
/**
* initializes the device
* Invoked from configure()
* @return zigbee commands
*/
def initializeDevice() {
ArrayList cmds = []
logInfo 'initializeDevice...'
if (getModelGroup() == 'OWON') { // https://github.com/Koenkk/zigbee-herdsman-converters/blob/e8750f6f2a34a3a6ae87f61e989a00964fb1107f/devices/owon.js
// It seem this device have 2 version, one using the endpoint 0x01 and one other using the endpoint 0x03
// https://github.com/dresden-elektronik/deconz-rest-plugin/issues/5738#issuecomment-1579521543
// there is a firmware bug in the OWON THS317-ET which leads to a reported temperature of 327.67°C if the real sensor temperature is near -20°C e.g. in a fridge.
cmds += ["zdo bind 0x${device.deviceNetworkId} 0x01 0x01 0x0001 {${device.zigbeeId}} {}", "delay 200",]
cmds += ["zdo bind 0x${device.deviceNetworkId} 0x01 0x01 0x0402 {${device.zigbeeId}} {}", "delay 200",]
cmds += ["zdo bind 0x${device.deviceNetworkId} 0x03 0x01 0x0001 {${device.zigbeeId}} {}", "delay 200",]
cmds += ["zdo bind 0x${device.deviceNetworkId} 0x03 0x01 0x0402 {${device.zigbeeId}} {}", "delay 200",]
cmds += zigbee.configureReporting(0x0001, 0x0021, DataType.UINT8, 60, 3600, 0x01, [:], 200)
cmds += zigbee.configureReporting(0x0402, 0x0000, DataType.INT16, 60, 300, 0x32, [:], 200) // or delta = 0x14
cmds += zigbee.reportingConfiguration(0x0001, 0x0021, [:], 250)
cmds += zigbee.reportingConfiguration(0x0402, 0x0000, [:], 250)
}
//
if (cmds == []) {
cmds = ["delay 299",]
}
return cmds
}
def tuyaBlackMagic() {
List cmds = []
cmds += zigbee.readAttribute(0x0000, [0x0004, 0x000, 0x0001, 0x0005, 0x0007, 0xfffe], [:], delay=200) // Cluster: Basic, attributes: Man.name, ZLC ver, App ver, Model Id, Power Source, attributeReportingStatus
cmds += zigbee.writeAttribute(0x0000, 0xffde, 0x20, 0x13, [:], delay=200) // was commented out ver 1.0.10 2022/11/10; returned back ver 1.20 01/15/2023
return cmds
}
def configure() {
if (settings?.txtEnable) log.info "${device.displayName} configure().."
List cmds = []
cmds += tuyaBlackMagic()
cmds += initializeDevice()
sendZigbeeCommands(cmds)
runIn(1, updated) // send the default or previously configured preference parameters during the Zigbee pairing process..
}
// NOT called when the driver is initialized as a new device, because the Initialize capability is NOT declared!
def initialize() {
log.info "${device.displayName} Initialize()..."
unschedule()
initializeVars(fullInit = true)
installed()
configure()
runIn( 3, logInitializeRezults)
}
private sendTuyaCommand(dp, dp_type, fncmd) {
ArrayList cmds = []
cmds += zigbee.command(CLUSTER_TUYA, SETDATA, [:], delay=200, PACKET_ID + dp + dp_type + zigbee.convertToHexString((int)(fncmd.length()/2), 4) + fncmd )
if (settings?.logEnable) log.trace "${device.displayName} sendTuyaCommand = ${cmds}"
return cmds
}
void sendZigbeeCommands(ArrayList cmd) {
if (settings?.logEnable) {log.trace "${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
}
Map statsMap = stringToJsonMap(state.stats); try {statsMap['txCtr']++ } catch (e) {statsMap['txCtr']=1}; state.stats = mapToJsonString(statsMap)
sendHubCommand(allActions)
}
private getPACKET_ID() {
return zigbee.convertToHexString(new Random().nextInt(65536), 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 getBatteryPercentageResult(rawValue) {
logDebug "getBatteryPercentageResult: rawValue = ${rawValue} -> ${rawValue / 2}%"
def result = [:]
Map statsMap = stringToJsonMap(state.stats); try {statsMap['battCtr']++ } catch (e) {statsMap['battCtr']=1}; state.stats = mapToJsonString(statsMap)
if (rawValue < 0) { rawValue = 0; logWarn "batteryPercentage rawValue corrected to ${rawValue}" }
if (rawValue >200 ) { rawValue = 200; logWarn "batteryPercentage rawValue corrected to ${rawValue}" }
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 = "%"
result.type = 'physical'
sendEvent(result)
}
private Map getBatteryVoltageResult(rawValue) {
logDebug "getBatteryVoltageResult: volts = ${(double)rawValue / 10.0}"
Map statsMap = stringToJsonMap(state.stats); try {statsMap['battCtr']++ } catch (e) {statsMap['battCtr']=1}; state.stats = mapToJsonString(statsMap)
def linkText = getLinkText(device)
def result = [:]
def volts = rawValue / 10
if (!(rawValue == 0 || rawValue == 255)) {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
def roundedPct = Math.round(pct * 100)
if (roundedPct <= 0)
roundedPct = 1
result.value = Math.min(100, roundedPct)
result.descriptionText = "${linkText} battery is ${result.value}%"
result.name = 'battery'
result.isStateChange = true
result.type = 'physical'
result.unit = "%"
sendEvent(result)
}
else {
if (settings?.logEnable) log.warn "${device.displayName} ignoring BatteryResult(${rawValue})"
}
}
// called when any event was received from the Zigbee device in parse() method..
def setPresent() {
if ((device.currentValue("healthStatus") ?: "unknown") != "online") {
sendHealthStatusEvent("online")
powerSourceEvent() // sent ony once now - 2023-01-31
if (settings?.txtEnable) log.info "${device.displayName} is present"
if (!isRadar()) {
if (device.currentValue('battery', true) == 0 ) {
if (state.lastBattery != null && safeToInt(state.lastBattery) != 0) {
sendBatteryEvent(safeToInt(state.lastBattery), isDigital=true)
}
}
}
}
state.notPresentCounter = 0
}
def deviceHealthCheck() {
state.notPresentCounter = (state.notPresentCounter ?: 0) + 1
if (state.notPresentCounter > presenceCountTreshold) {
if ((device.currentValue("healthStatus", true) ?: "unknown") != "offline" ) {
sendHealthStatusEvent("offline")
if (settings?.txtEnable) log.warn "${device.displayName} is not present!"
powerSourceEvent("unknown")
if (!(device.currentValue('motion', true) in ['inactive', '?'])) {
handleMotion(false, isDigital=true)
if (settings?.txtEnable) log.warn "${device.displayName} forced motion to 'inactive"
}
if (safeToInt(device.currentValue('battery', true)) != 0) {
if (settings?.txtEnable) log.warn "${device.displayName} forced battery to '0 %"
sendBatteryEvent( 0, isDigital=true )
}
}
}
else {
if (logEnable) log.debug "${device.displayName} deviceHealthCheck - online (notPresentCounter=${state.notPresentCounter})"
}
}
def sendHealthStatusEvent(value) {
sendEvent(name: "healthStatus", value: value, descriptionText: "${device.displayName} healthStatus set to $value")
}
String mapToJsonString( Map map) {
if (map==null || map==[:]) return ""
String str = JsonOutput.toJson(map)
return str
}
Map stringToJsonMap( String str) {
if (str==null) return [:]
def jsonSlurper = new JsonSlurper()
def map = jsonSlurper.parseText( str )
return map
}
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 logDebug(msg) {
if (settings?.logEnable) {
log.debug "${device.displayName} " + msg
}
}
def logInfo(msg) {
if (settings?.txtEnable) {
log.info "${device.displayName} " + msg
}
}
def logWarn(msg) {
if (settings?.logEnable) {
log.warn "${device.displayName} " + msg
}
}
def updateInfo(msg= ' ') {
sendEvent(name: "_info" , value: msg, isStateChange: false)
}
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
if (settings?.logEnable) log.warn "${device.displayName} sending TEST command=${dpCommand} value=${dpValue} ($dpValHex) type=${dpType}"
sendZigbeeCommands( sendTuyaCommand(dpCommand, dpType, dpValHex) )
}
def test( String description) {
log.warn "parising : ${description}"
parse( description)
}