/* groovylint-disable NglParseError, ImplicitReturnStatement, InsecureRandom, MethodReturnTypeRequired, MethodSize, ParameterName, PublicMethodsBeforeNonPublicMethods, StaticMethodsBeforeInstanceMethods, UnnecessaryGroovyImport, UnnecessaryObjectReferences, UnusedImport, VariableName *//**
* Tuya Zigbee Chlorine Meter- driver for Hubitat Elevation
*
* https://community.hubitat.com/t/dynamic-capabilities-commands-and-attributes-for-drivers/98342
*
* 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. 3.3.0 2024-08-03 kkossev - first test version
* ver. 3.3.1 2024-08-31 kkossev - added tuyaDataQuery; added dp 103 104 114 115 116 118 decoding; invalid freeChlorine value -1.0 (0xFFFFFFFFF) returned as 0 (zero), added automatic polling (configurable)
* ver. 3.3.2 2024-09-06 kkossev - debug is off by default; freeChlorine is divided by 10;
* ver. 3.4.0 2025-05-24 kkossev - HE platfrom version 2.4.1.x decimal preferences patch/workaround.*
* TODO:
*/
static String version() { "3.4.0" }
static String timeStamp() { "2025/05/24 6:46 PM" }
@Field static final Boolean _DEBUG = false
@Field static final Boolean _TRACE_ALL = false // trace all messages, including the spammy ones
@Field static final Boolean DEFAULT_DEBUG_LOGGING = false // disable it for production
deviceType = "MultiMeter"
@Field static final String DEVICE_TYPE = "MultiMeter"
metadata {
definition (
name: 'Tuya Zigbee Chlorine Meter',
importUrl: 'https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Tuya%20Zigbee%20Chlorine%20Meter/Tuya_Zigbee_Chlorine_Meter_lib_included.groovy',
namespace: 'kkossev', author: 'Krassimir Kossev', singleThreaded: true )
{
// no standard capabilities
attribute 'tds', 'number' // Total Dissolved Solids
attribute 'temperature', 'number' // Temperature
attribute 'battery', 'number' // Battery level remaining
attribute 'ph', 'number' // pH value
attribute 'ec', 'number' // Electrical conductivity
attribute 'orp', 'number' // Oxidation Reduction Potential value
attribute 'freeChlorine', 'number' // Free chlorine value
attribute 'backlightvalue', 'number' // Bbacklight value
attribute 'phMmax', 'number' // pH maximal value
attribute 'phMmin', 'number' // pH minimal value
attribute 'ecMmax', 'number' // Electrical Conductivity maximal value
attribute 'ecMmin', 'number' // Electrical Conductivity minimal value
attribute 'orpMmax', 'number' // Oxidation Reduction Potential maximal value
attribute 'orpMmin', 'number' // Oxidation Reduction Potential minimal value
attribute 'freeChlorineMax', 'number' // Free Chlorine maximal value
attribute 'freeChlorineMin', 'number' // Free Chlorine minimal value
attribute 'salinity', 'number' // Salt value
attribute 'backlightvalue', 'number' // Bbacklight value
// no commands
if (_DEBUG) {
command 'tuyaDataQuery'
}
// itterate through all the figerprints and add them on the fly
deviceProfilesV3.each { profileName, profileMap ->
if (profileMap.fingerprints != null) {
if (profileMap.device?.isDepricated != true) {
profileMap.fingerprints.each {
fingerprint it
}
}
}
}
}
preferences {
if (device) {
// input(name: 'info', type: 'hidden', title: "For more info, click on this link to visit the WiKi page")
}
input name: 'txtEnable', type: 'bool', title: 'Enable descriptionText logging', defaultValue: true, description: 'Enables events logging.'
input name: 'logEnable', type: 'bool', title: 'Enable debug logging', defaultValue: DEFAULT_DEBUG_LOGGING, description: 'Turns on debug logging for 24 hours.'
// the rest of the preferences are inputIt from the deviceProfileLib and from the included libraries
if (device) {
input name: 'pollingInterval', type: 'enum', title: 'Polling Interval', options: PollingIntervalOpts.options, defaultValue: PollingIntervalOpts.defaultValue, required: true, description: 'Changes how often the hub will poll the sensor.'
}
}
}
@Field static String ttStyleStr = ''
@Field static final Map PollingIntervalOpts = [
defaultValue: 300,
options : [0: 'Disabled', 5: 'Every 5 seconds (DONT DO THAT!)', 60: 'Every minute (not recommended)', 120: 'Every 2 minutes', 300: 'Every 5 minutes (default)', 600: 'Every 10 minutes', 900: 'Every 15 minutes', 1800: 'Every 30 minutes', 3600: 'Every 1 hour']
]
/*
Measures :
PH : test ranges: 0.0-14.0ph; Resolution: 0.1ph; Accuracy: ±0.1ph
CL : test ranges: 0.0-4.0mg/L; Resolution: 0.1mg/L; Accuracy: ±0.1mg/L
Salt : test ranges: 0-999ppm, 1000-9990ppm; Resolution: 1ppm, 10ppm; Accuracy: ±2% F.S
EC : test ranges: 0-2000us/c m , 2000-9990uS/c m,10.01- 19.99mS/c m; Resolution: 1uS/c m 10uS/c m 0.01mS/C M; Accuracy: ±2% F.S.
Total Dissolved Solids : test ranges: 0-999ppm,1000- 9990pp, Resolution: 1ppm 10ppm; Accuracy: ±2% F.S.
ORP : test ranges: -999mv ~+999mv; Resolution: 1mv; Accuracy:15mv
Temperature : test ranges: 0.0℃-50.0℃ 32.0℉-122.0℉; Resolution: 0.1℃/0.1℉; Accuracy: ±0.5℃
*/
@Field static final Map deviceProfilesV3 = [
// https://github.com/Koenkk/zigbee2mqtt/issues/18704
// https://community.home-assistant.io/t/pool-monitoring-device-yieryi-ble-yl01-zigbee-ph-orp-free-chlorine-salinity-etc/659545/10
// https://github.com/Koenkk/zigbee2mqtt/issues/18704#issuecomment-1732263086
// https://github.com/zigbeefordomoticz/z4d-certified-devices/blob/e65463300dda776145ca4b2953ebe162c2f60b3d/z4d_certified_devices/Certified/Tuya/TS0601-BLE-YL01.json#L7
'CHLORINE_METER_BLE_YL01' : [
description : 'BLE_YL01 Tuya Zigbee Chlorine Meter',
models : ['TS0601'],
device : [type: 'Sensor', powerSource: 'dc', isSleepy:false], // check powerSource
capabilities : ['Battery': true, 'TemperatureMeasurement': true],
preferences : ['phMmax': '106', 'phMmin': '107', 'ecMmax': '108', 'ecMmin': '109', 'orpMmax': '110', 'orpMmin': '111', 'freeChlorineMax': '112', 'freeChlorineMin': '113'],
// "Param": { "tempCompensation": 0, "ph7Compensation": 0, "ecCompensation": 0, "orpCompensation": 0 }
commands : ['resetStats':'resetStats', 'refresh':'refresh', 'initialize':'initialize', 'updateAllPreferences': 'updateAllPreferences', 'resetPreferencesToDefaults':'resetPreferencesToDefaults', 'validateAndFixPreferences':'validateAndFixPreferences', 'printFingerprints':'printFingerprints', 'printPreferences':'printPreferences'],
fingerprints : [
[profileId:'0104', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', model:'TS0601', manufacturer:'_TZE200_v1jqz5cy', deviceJoinName: 'BLE_YL01 Tuya Zigbee Chlorine Meter'],
[profileId:'0104', endpointId:'01', inClusters:'0000,0004,0005,EF00', outClusters:'0019,000A', model:'TS0601', manufacturer:'_TZE200_d9mzkhoq', deviceJoinName: 'BLE_YL01 Tuya Zigbee Chlorine Meter'],
],
tuyaDPs: [
[dp:1, name:'tds', type:'number', rw: 'ro', scale:1, unit:'ppm', description:'Total Dissolved Solids'],
[dp:2, name:'temperature', type:'decimal', rw: 'ro', scale:10, unit:'C', description:'Temperature'],
[dp:7, name:'battery', type:'number', rw: 'ro', scale:1, unit:'%', description:'Battery level remaining'],
[dp:10, name:'ph', type:'decimal', rw: 'ro', scale:100, unit:'pH', description:'pH value'], // 'pH value, if the pH value is lower than 6.5, it means that the water quality is too acidic and has impurities, and it is necessary to add disinfectant water for disinfection
[dp:11, name:'ec', type:'decimal', rw: 'ro', scale:1, unit:'µS/cm', description:'Electrical conductivity'],
[dp:101, name:'orp', type:'decimal', rw: 'ro', scale:1, unit:'mV', description:'Oxidation Reduction Potential value'], // 'Oxidation Reduction Potential value. If the ORP value is above 850mv, it means that the disinfectant has been added too much, and it is necessary to add water or change the water for neutralization. If the ORP value is below 487mv, it means that too little disinfectant has been added and the pool needs to be disinfected again'
[dp:102, name:'freeChlorine', preProc:'checkInvalidValue', type:'decimal', rw: 'ro', scale:10, unit:'mg/L', description:'Free chlorine value'], // The water in the swimming pool should be between 6.5-8ph and ORP should be between 487-840mv, and the chlorine value will be displayed normally. Chlorine will not be displayed if either value is out of range
[dp:103, name:'phCalibration1', type:'number', rw: 'ro', scale:1, unit:'', description:'pH Calibration 1'], // "67": { "sensor_type": "phCalibration1" },
[dp:104, name:'backlightStatus', type:'number', rw: 'ro', scale:1, unit:'', description:'Backlight status'], // "68": { "store_tuya_attribute": "backlight_status", "EvalExp": "(value)" },
[dp:105, name:'backlightLevel', type:'number', rw: 'ro', scale:1, unit:'', description:'Backlight level'], // "69": { "store_tuya_attribute": "backlight_level", "EvalExp": "(value)" }, dp:105
[dp:106, name:'phMmax', type:'decimal', rw: 'rw', min:0, max:20, /*defVal:14.0,*/ scale:10, unit:'pH', title:'pH maximal value'],
[dp:107, name:'phMmin', type:'decimal', rw: 'rw', min:0, max:20, /*defVal:0.0,*/ scale:10, unit:'pH', title:'pH minimal value'],
[dp:108, name:'ecMmax', type:'decimal', rw: 'rw', min:0, max:20000, /*defVal:20000.0,*/ scale:1, unit:'µS/cm', title:'Electrical Conductivity maximal value'],
[dp:109, name:'ecMmin', type:'decimal', rw: 'rw', min:0, max:100, /*defVal:0.0,*/ scale:1, unit:'µS/cm', title:'Electrical Conductivity minimal value'],
[dp:110, name:'orpMmax', type:'decimal', rw: 'rw', min:0, max:1000, /*defVal:999.0,*/ scale:1, unit:'mV', title:'Oxidation Reduction Potential maximal value'],
[dp:111, name:'orpMmin', type:'decimal', rw: 'rw', min:0, max:1000, /*defVal:0.0,*/ scale:1, unit:'mV', title:'Oxidation Reduction Potential minimal value'],
[dp:112, name:'freeChlorineMax', type:'decimal', rw: 'rw', min:0, max:15, /*defVal:20.0,*/ scale:10, unit:'mg/L', title:'Free Chlorine maximal value'],
[dp:113, name:'freeChlorineMin', type:'decimal', rw: 'rw', min:0, max:15, /*defVal:20.0,*/ scale:10, unit:'mg/L', title:'Free Chlorine minimal value'],
[dp:114, name:'phCalibration2', type:'number', rw: 'ro', scale:1, unit:'', description:'pH Calibration 2'], // "72": { "sensor_type": "phCalibration2" },
[dp:115, name:'ecCalibration', type:'number', rw: 'ro', scale:1, unit:'', description:'EC Calibration'], // "73": { "sensor_type": "ecCalibration" },
[dp:116, name:'orpCalibration', type:'number', rw: 'ro', scale:1, unit:'', description:'ORP Calibration'], // "74": { "sensor_type": "orpCalibration" },
[dp:117, name:'salinity', type:'decimal', rw: 'ro', scale:1, unit:'gg', description:'Salt value'],
[dp:118, name:'salinityCalibration', type:'number', rw: 'ro', scale:1, unit:'', description:'Salinity? Calibration ?(0x76)'], // "76": { "sensor_type": "salinityCalibration" },
],
refresh: ['refreshQueryAllTuyaDP'],
configuration : ['battery': false],
deviceJoinName: 'BLE_YL01 Tuya Zigbee Chlorine Meter'
]
// second manufacturer ?
// https://www.amazon.com/YINMIK-Chlorine-Swimming-Salinity-Inground/dp/B0C2T8YLYW
// https://github.com/Koenkk/zigbee-herdsman-converters/pull/7613
]
Number checkInvalidValue(Number value) {
if (value < 0) {
logDebug "freeChlorine Invalid value -1.0 detected, returning zero!"
return 0
}
return value
}
// called from standardProcessTuyaDP in the commonLib for each Tuya dp report in a Zigbee message
// should always return true, as we are processing all the dp reports here
boolean customProcessTuyaDp(final Map descMap, final int dp, final int dp_id, final int fncmd, final int dp_len=0) {
logDebug "customProcessTuyaDp: dp=${dp} dp_id=${dp_id} fncmd=${fncmd} dp_len=${dp_len} descMap.data = ${descMap?.data}"
if (processTuyaDPfromDeviceProfile(descMap, dp, dp_id, fncmd, dp_len) == true) {
return true // sucessfuly processed from the deviceProfile
}
logWarn "NOT PROCESSED from deviceProfile Tuya cmd: dp=${dp} value=${fncmd} descMap.data = ${descMap?.data}"
localProcessTuyaDP(descMap, dp, dp_id, fncmd, dp_len)
return true
}
void localProcessTuyaDP(final Map descMap, final int dp, final int dp_id, final int fncmd, final int dp_len) {
switch (dp) {
default :
logDebug "NOT PROCESSED Tuya cmd: dp=${dp} value=${fncmd} descMap.data = ${descMap?.data}"
break
}
}
// called from processFoundItem in the deviceProfileLib
void customProcessDeviceProfileEvent(final Map descMap, final String name, valueScaled, final String unitText, final String descText) {
logTrace "customProcessDeviceProfileEvent(${name}, ${valueScaled}) called"
Map eventMap = [name: name, value: valueScaled, unit: unitText, descriptionText: descText, type: 'physical', isStateChange: true]
switch (name) {
default :
sendEvent(name : name, value : valueScaled, unit:unitText, descriptionText: descText, type: 'physical', isStateChange: true) // attribute value is changed - send an event !
logTrace "event ${name} sent w/ value ${valueScaled}"
logInfo "${descText}" // TODO - send info log only if the value has changed? // TODO - check whether Info log will be sent also for spammy clusterAttribute ?
break
}
}
List customRefresh() {
logDebug "customRefresh()"
List cmds = []
List devProfCmds = refreshFromDeviceProfileList()
if (devProfCmds != null && !devProfCmds.isEmpty()) {
cmds += devProfCmds
}
return cmds
}
void customUpdated() {
logDebug "customUpdated()"
List cmds = []
if (settings?.forcedProfile != null) {
if (this.respondsTo('getProfileKey') == false) {
logWarn "getProfileKey() is not defined in the driver"
}
else {
logDebug "current state.deviceProfile=${state.deviceProfile}, settings.forcedProfile=${settings?.forcedProfile}, getProfileKey()=${getProfileKey(settings?.forcedProfile)}"
if (getProfileKey(settings?.forcedProfile) != state.deviceProfile) {
logInfo "changing the device profile from ${state.deviceProfile} to ${getProfileKey(settings?.forcedProfile)}"
state.deviceProfile = getProfileKey(settings?.forcedProfile)
initializeVars(fullInit = false)
resetPreferencesToDefaults(debug = true)
logInfo 'press F5 to refresh the page'
}
}
}
/* groovylint-disable-next-line EmptyElseBlock */
else {
logDebug "forcedProfile is not set"
}
final int interval = (settings?.pollingInterval as Integer) ?: 0
if (interval > 0) {
logInfo "customUpdated: scheduling polling every ${interval} seconds"
schedulePolling(interval)
}
else {
unSchedulePolling()
logInfo 'customUpdated: polling is disabled!'
}
// Itterates through all settings
cmds += updateAllPreferences() // defined in deviceProfileLib
sendZigbeeCommands(cmds)
}
/**
* Schedule polling
* @param intervalMins interval in seconds
*/
private void schedulePolling(final int intervalSecs) {
String cron = getCron( intervalSecs )
logDebug "cron = ${cron}"
schedule(cron, 'autoPoll')
}
private void unSchedulePolling() {
unschedule('autoPoll')
}
/**
* Scheduled job for polling device specific attribute(s)
*/
void autoPoll() {
logDebug 'autoPoll()...'
checkDriverVersion(state)
List cmds = []
cmds = refreshFromDeviceProfileList()
if (cmds != null && cmds != [] ) {
sendZigbeeCommands(cmds)
}
}
void customInitializeVars(final boolean fullInit=false) {
logDebug "customInitializeVars(${fullInit})"
if (state.deviceProfile == null || state.deviceProfile == '' || state.deviceProfile == 'UNKNOWN') {
setDeviceNameAndProfile('TS0601', '_TZE200_v1jqz5cy') // in deviceProfileiLib.groovy
}
if (fullInit == true) {
resetPreferencesToDefaults()
}
if (fullInit || settings?.pollingInterval == null) { device.updateSetting('pollingInterval', [value: PollingIntervalOpts.defaultValue.toString(), type: 'enum']) }
}
void customInitEvents(final boolean fullInit=false) {
logDebug "customInitEvents()"
}
// https://github.com/dresden-elektronik/deconz-rest-plugin/blob/0107459aa42f8ac5333c67f415e2482069e4ff79/device_access_fn.cpp#L825
void customParseZdoClusters(Map descMap) {
if (descMap.clusterInt == 0x0013) {
logDebug "customParseZdoClusters() - device announce"
sendZigbeeCommands(refreshQueryAllTuyaDP())
}
}
List refreshQueryAllTuyaDP() {
return queryAllTuyaDP()
}
void test(String par) {
long startTime = now()
logDebug "test() started at ${startTime}"
//parse('catchall: 0104 EF00 01 01 0040 00 7770 01 00 0000 02 01 00556701000100')
def parpar = 'catchall: 0104 EF00 01 01 0040 00 7770 01 00 0000 02 01 00556701000100'
for (int i=0; i<100; i++) {
testFunc(parpar)
}
long endTime = now()
logDebug "test() ended at ${endTime} (duration ${endTime - startTime}ms)"
}
// /////////////////////////////////////////////////////////////////// Libraries //////////////////////////////////////////////////////////////////////
// ~~~~~ start include (142) kkossev.deviceProfileLib ~~~~~
/* groovylint-disable CompileStatic, CouldBeSwitchStatement, DuplicateListLiteral, DuplicateNumberLiteral, DuplicateStringLiteral, ImplicitClosureParameter, ImplicitReturnStatement, Instanceof, LineLength, MethodCount, MethodSize, NestedBlockDepth, NoDouble, NoFloat, NoWildcardImports, ParameterName, PublicMethodsBeforeNonPublicMethods, UnnecessaryElseStatement, UnnecessaryGetter, UnnecessaryPublicModifier, UnnecessarySetter, UnusedImport */ // library marker kkossev.deviceProfileLib, line 1
library( // library marker kkossev.deviceProfileLib, line 2
base: 'driver', author: 'Krassimir Kossev', category: 'zigbee', description: 'Device Profile Library', name: 'deviceProfileLib', namespace: 'kkossev', // library marker kkossev.deviceProfileLib, line 3
importUrl: 'https://raw.githubusercontent.com/kkossev/Hubitat/refs/heads/development/Libraries/deviceProfileLib.groovy', documentationLink: 'https://github.com/kkossev/Hubitat/wiki/libraries-deviceProfileLib', // library marker kkossev.deviceProfileLib, line 4
version: '3.4.2' // library marker kkossev.deviceProfileLib, line 5
) // library marker kkossev.deviceProfileLib, line 6
/* // library marker kkossev.deviceProfileLib, line 7
* Device Profile Library // library marker kkossev.deviceProfileLib, line 8
* // library marker kkossev.deviceProfileLib, line 9
* Licensed Virtual the Apache License, Version 2.0 (the "License"); you may not use this file except // library marker kkossev.deviceProfileLib, line 10
* in compliance with the License. You may obtain a copy of the License at: // library marker kkossev.deviceProfileLib, line 11
* // library marker kkossev.deviceProfileLib, line 12
* http://www.apache.org/licenses/LICENSE-2.0 // library marker kkossev.deviceProfileLib, line 13
* // library marker kkossev.deviceProfileLib, line 14
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed // library marker kkossev.deviceProfileLib, line 15
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License // library marker kkossev.deviceProfileLib, line 16
* for the specific language governing permissions and limitations under the License. // library marker kkossev.deviceProfileLib, line 17
* // library marker kkossev.deviceProfileLib, line 18
* ver. 1.0.0 2023-11-04 kkossev - added deviceProfileLib (based on Tuya 4 In 1 driver) // library marker kkossev.deviceProfileLib, line 19
* ver. 3.0.0 2023-11-27 kkossev - fixes for use with commonLib; added processClusterAttributeFromDeviceProfile() method; added validateAndFixPreferences() method; inputIt bug fix; signedInt Preproc method; // library marker kkossev.deviceProfileLib, line 20
* ver. 3.0.1 2023-12-02 kkossev - release candidate // library marker kkossev.deviceProfileLib, line 21
* ver. 3.0.2 2023-12-17 kkossev - inputIt moved to the preferences section; setfunction replaced by customSetFunction; Groovy Linting; // library marker kkossev.deviceProfileLib, line 22
* ver. 3.0.4 2024-03-30 kkossev - more Groovy Linting; processClusterAttributeFromDeviceProfile exception fix; // library marker kkossev.deviceProfileLib, line 23
* ver. 3.1.0 2024-04-03 kkossev - more Groovy Linting; deviceProfilesV3, enum pars bug fix; // library marker kkossev.deviceProfileLib, line 24
* ver. 3.1.1 2024-04-21 kkossev - deviceProfilesV3 bug fix; tuyaDPs list of maps bug fix; resetPreferencesToDefaults bug fix; // library marker kkossev.deviceProfileLib, line 25
* ver. 3.1.2 2024-05-05 kkossev - added isSpammyDeviceProfile() // library marker kkossev.deviceProfileLib, line 26
* ver. 3.1.3 2024-05-21 kkossev - skip processClusterAttributeFromDeviceProfile if cluster or attribute or value is missing // library marker kkossev.deviceProfileLib, line 27
* ver. 3.2.0 2024-05-25 kkossev - commonLib 3.2.0 allignment; // library marker kkossev.deviceProfileLib, line 28
* ver. 3.2.1 2024-06-06 kkossev - Tuya Multi Sensor 4 In 1 (V3) driver allignment (customProcessDeviceProfileEvent); getDeviceProfilesMap bug fix; forcedProfile is always shown in preferences; // library marker kkossev.deviceProfileLib, line 29
* ver. 3.3.0 2024-06-29 kkossev - empty preferences bug fix; zclWriteAttribute delay 50 ms; added advanced check in inputIt(); fixed 'Cannot get property 'rw' on null object' bug; fixed enum attributes first event numeric value bug; // library marker kkossev.deviceProfileLib, line 30
* ver. 3.3.1 2024-07-06 kkossev - added powerSource event in the initEventsDeviceProfile // library marker kkossev.deviceProfileLib, line 31
* ver. 3.3.2 2024-08-18 kkossev - release 3.3.2 // library marker kkossev.deviceProfileLib, line 32
* ver. 3.3.3 2024-08-18 kkossev - sendCommand and setPar commands commented out; must be declared in the main driver where really needed // library marker kkossev.deviceProfileLib, line 33
* ver. 3.3.4 2024-09-28 kkossev - fixed exceptions in resetPreferencesToDefaults() and initEventsDeviceProfile() // library marker kkossev.deviceProfileLib, line 34
* ver. 3.4.0 2025-02-02 kkossev - deviceProfilesV3 optimizations (defaultFingerprint); is2in1() mod // library marker kkossev.deviceProfileLib, line 35
* ver. 3.4.1 2025-02-02 kkossev - setPar help improvements; // library marker kkossev.deviceProfileLib, line 36
* ver. 3.4.2 2025-03-24 kkossev - added refreshFromConfigureReadList() method; documentation update; getDeviceNameAndProfile uses DEVICE.description instead of deviceJoinName // library marker kkossev.deviceProfileLib, line 37
* ver. 3.4.3 2025-04-25 kkossev - HE platfrom version 2.4.1.x decimal preferences patch/workaround. // library marker kkossev.deviceProfileLib, line 38
* // library marker kkossev.deviceProfileLib, line 39
* TODO - remove the 2-in-1 patch ! // library marker kkossev.deviceProfileLib, line 40
* TODO - add updateStateUnknownDPs (from the 4-in-1 driver) // library marker kkossev.deviceProfileLib, line 41
* TODO - when [refresh], send Info logs for parameters that are not events or preferences // library marker kkossev.deviceProfileLib, line 42
* TODO: refactor sendAttribute ! sendAttribute exception bug fix for virtual devices; check if String getObjectClassName(Object o) is in 2.3.3.137, can be used? // library marker kkossev.deviceProfileLib, line 43
* TODO: add _DEBUG command (for temporary switching the debug logs on/off) // library marker kkossev.deviceProfileLib, line 44
* TODO: allow NULL parameters default values in the device profiles // library marker kkossev.deviceProfileLib, line 45
* TODO: handle preferences of a type TEXT // library marker kkossev.deviceProfileLib, line 46
* // library marker kkossev.deviceProfileLib, line 47
*/ // library marker kkossev.deviceProfileLib, line 48
static String deviceProfileLibVersion() { '3.4.3' } // library marker kkossev.deviceProfileLib, line 50
static String deviceProfileLibStamp() { '2025/04/25 12:43 PM' } // library marker kkossev.deviceProfileLib, line 51
import groovy.json.* // library marker kkossev.deviceProfileLib, line 52
import groovy.transform.Field // library marker kkossev.deviceProfileLib, line 53
import hubitat.zigbee.clusters.iaszone.ZoneStatus // library marker kkossev.deviceProfileLib, line 54
import hubitat.zigbee.zcl.DataType // library marker kkossev.deviceProfileLib, line 55
import java.util.concurrent.ConcurrentHashMap // library marker kkossev.deviceProfileLib, line 56
import groovy.transform.CompileStatic // library marker kkossev.deviceProfileLib, line 58
metadata { // library marker kkossev.deviceProfileLib, line 60
// no capabilities // library marker kkossev.deviceProfileLib, line 61
// no attributes // library marker kkossev.deviceProfileLib, line 62
/* // library marker kkossev.deviceProfileLib, line 63
// copy the following commands to the main driver, if needed // library marker kkossev.deviceProfileLib, line 64
command 'sendCommand', [ // library marker kkossev.deviceProfileLib, line 65
[name:'command', type: 'STRING', description: 'command name', constraints: ['STRING']], // library marker kkossev.deviceProfileLib, line 66
[name:'val', type: 'STRING', description: 'command parameter value', constraints: ['STRING']] // library marker kkossev.deviceProfileLib, line 67
] // library marker kkossev.deviceProfileLib, line 68
command 'setPar', [ // library marker kkossev.deviceProfileLib, line 69
[name:'par', type: 'STRING', description: 'preference parameter name', constraints: ['STRING']], // library marker kkossev.deviceProfileLib, line 70
[name:'val', type: 'STRING', description: 'preference parameter value', constraints: ['STRING']] // library marker kkossev.deviceProfileLib, line 71
] // library marker kkossev.deviceProfileLib, line 72
*/ // library marker kkossev.deviceProfileLib, line 73
preferences { // library marker kkossev.deviceProfileLib, line 74
if (device) { // library marker kkossev.deviceProfileLib, line 75
input(name: 'forcedProfile', type: 'enum', title: 'Device Profile', description: 'Manually change the Device Profile, if the model/manufacturer was not recognized automatically.
Warning! Manually setting a device profile may not always work!', options: getDeviceProfilesMap()) // library marker kkossev.deviceProfileLib, line 76
// itterate over DEVICE.preferences map and inputIt all // library marker kkossev.deviceProfileLib, line 77
if (DEVICE != null && DEVICE?.preferences != null && DEVICE?.preferences != [:] && DEVICE?.device?.isDepricated != true) { // library marker kkossev.deviceProfileLib, line 78
(DEVICE?.preferences).each { key, value -> // library marker kkossev.deviceProfileLib, line 79
Map inputMap = inputIt(key) // library marker kkossev.deviceProfileLib, line 80
if (inputMap != null && inputMap != [:]) { // library marker kkossev.deviceProfileLib, line 81
input inputMap // library marker kkossev.deviceProfileLib, line 82
} // library marker kkossev.deviceProfileLib, line 83
} // library marker kkossev.deviceProfileLib, line 84
} // library marker kkossev.deviceProfileLib, line 85
} // library marker kkossev.deviceProfileLib, line 86
} // library marker kkossev.deviceProfileLib, line 87
} // library marker kkossev.deviceProfileLib, line 88
private boolean is2in1() { return getDeviceProfile().startsWith('TS0601_2IN1') } // patch! // library marker kkossev.deviceProfileLib, line 90
public String getDeviceProfile() { state?.deviceProfile ?: 'UNKNOWN' } // library marker kkossev.deviceProfileLib, line 92
public Map getDEVICE() { deviceProfilesV3 != null ? deviceProfilesV3[getDeviceProfile()] : deviceProfilesV2 != null ? deviceProfilesV2[getDeviceProfile()] : [:] } // library marker kkossev.deviceProfileLib, line 93
public Set getDeviceProfiles() { deviceProfilesV3 != null ? deviceProfilesV3?.keySet() : deviceProfilesV2 != null ? deviceProfilesV2?.keySet() : [] } // library marker kkossev.deviceProfileLib, line 94
public List getDeviceProfilesMap() { // library marker kkossev.deviceProfileLib, line 96
if (deviceProfilesV3 == null) { // library marker kkossev.deviceProfileLib, line 97
if (deviceProfilesV2 == null) { return [] } // library marker kkossev.deviceProfileLib, line 98
return deviceProfilesV2.values().description as List // library marker kkossev.deviceProfileLib, line 99
} // library marker kkossev.deviceProfileLib, line 100
List activeProfiles = [] // library marker kkossev.deviceProfileLib, line 101
deviceProfilesV3.each { profileName, profileMap -> // library marker kkossev.deviceProfileLib, line 102
if ((profileMap.device?.isDepricated ?: false) != true) { // library marker kkossev.deviceProfileLib, line 103
activeProfiles.add(profileMap.description ?: '---') // library marker kkossev.deviceProfileLib, line 104
} // library marker kkossev.deviceProfileLib, line 105
} // library marker kkossev.deviceProfileLib, line 106
return activeProfiles // library marker kkossev.deviceProfileLib, line 107
} // library marker kkossev.deviceProfileLib, line 108
// ---------------------------------- deviceProfilesV3 helper functions -------------------------------------------- // library marker kkossev.deviceProfileLib, line 110
/** // library marker kkossev.deviceProfileLib, line 112
* Returns the profile key for a given profile description. // library marker kkossev.deviceProfileLib, line 113
* @param valueStr The profile description to search for. // library marker kkossev.deviceProfileLib, line 114
* @return The profile key if found, otherwise null. // library marker kkossev.deviceProfileLib, line 115
*/ // library marker kkossev.deviceProfileLib, line 116
public String getProfileKey(final String valueStr) { // library marker kkossev.deviceProfileLib, line 117
if (deviceProfilesV3 != null) { return deviceProfilesV3.find { _, profileMap -> profileMap.description == valueStr }?.key } // library marker kkossev.deviceProfileLib, line 118
else if (deviceProfilesV2 != null) { return deviceProfilesV2.find { _, profileMap -> profileMap.description == valueStr }?.key } // library marker kkossev.deviceProfileLib, line 119
else { return null } // library marker kkossev.deviceProfileLib, line 120
} // library marker kkossev.deviceProfileLib, line 121
/** // library marker kkossev.deviceProfileLib, line 123
* Finds the preferences map for the given parameter. // library marker kkossev.deviceProfileLib, line 124
* @param param The parameter to find the preferences map for. // library marker kkossev.deviceProfileLib, line 125
* @param debug Whether or not to output debug logs. // library marker kkossev.deviceProfileLib, line 126
* @return returns either tuyaDPs or attributes map, depending on where the preference (param) is found // library marker kkossev.deviceProfileLib, line 127
* @return empty map [:] if param is not defined for this device. // library marker kkossev.deviceProfileLib, line 128
*/ // library marker kkossev.deviceProfileLib, line 129
private Map getPreferencesMapByName(final String param, boolean debug=false) { // library marker kkossev.deviceProfileLib, line 130
Map foundMap = [:] // library marker kkossev.deviceProfileLib, line 131
if (!(param in DEVICE?.preferences)) { if (debug) { log.warn "getPreferencesMapByName: preference ${param} not defined for this device!" } ; return [:] } // library marker kkossev.deviceProfileLib, line 132
/* groovylint-disable-next-line NoDef, VariableTypeRequired */ // library marker kkossev.deviceProfileLib, line 133
def preference // library marker kkossev.deviceProfileLib, line 134
try { // library marker kkossev.deviceProfileLib, line 135
preference = DEVICE?.preferences["$param"] // library marker kkossev.deviceProfileLib, line 136
if (debug) { log.debug "getPreferencesMapByName: preference ${param} found. value is ${preference}" } // library marker kkossev.deviceProfileLib, line 137
if (preference in [true, false]) { // library marker kkossev.deviceProfileLib, line 138
// find the preference in the tuyaDPs map // library marker kkossev.deviceProfileLib, line 139
logDebug "getPreferencesMapByName: preference ${param} is boolean" // library marker kkossev.deviceProfileLib, line 140
return [:] // no maps for predefined preferences ! // library marker kkossev.deviceProfileLib, line 141
} // library marker kkossev.deviceProfileLib, line 142
if (safeToInt(preference, -1) > 0) { //if (preference instanceof Number) { // library marker kkossev.deviceProfileLib, line 143
int dp = safeToInt(preference) // library marker kkossev.deviceProfileLib, line 144
//if (debug) log.trace "getPreferencesMapByName: param ${param} preference ${preference} is number (${dp})" // library marker kkossev.deviceProfileLib, line 145
foundMap = DEVICE?.tuyaDPs.find { it.dp == dp } // library marker kkossev.deviceProfileLib, line 146
} // library marker kkossev.deviceProfileLib, line 147
else { // cluster:attribute // library marker kkossev.deviceProfileLib, line 148
//if (debug) { log.trace "${DEVICE?.attributes}" } // library marker kkossev.deviceProfileLib, line 149
foundMap = DEVICE?.attributes.find { it.at == preference } // library marker kkossev.deviceProfileLib, line 150
} // library marker kkossev.deviceProfileLib, line 151
// TODO - could be also 'true' or 'false' ... // library marker kkossev.deviceProfileLib, line 152
} catch (e) { // library marker kkossev.deviceProfileLib, line 153
if (debug) { log.warn "getPreferencesMapByName: exception ${e} caught when getting preference ${param} !" } // library marker kkossev.deviceProfileLib, line 154
return [:] // library marker kkossev.deviceProfileLib, line 155
} // library marker kkossev.deviceProfileLib, line 156
if (debug) { log.debug "getPreferencesMapByName: foundMap = ${foundMap}" } // library marker kkossev.deviceProfileLib, line 157
return foundMap // library marker kkossev.deviceProfileLib, line 158
} // library marker kkossev.deviceProfileLib, line 159
public Map getAttributesMap(String attribName, boolean debug=false) { // library marker kkossev.deviceProfileLib, line 161
Map foundMap = [:] // library marker kkossev.deviceProfileLib, line 162
List