/** * Tuya Multi Sensor 4 In 1 driver for Hubitat * * https://community.hubitat.com/t/alpha-tuya-zigbee-multi-sensor-4-in-1/92441 * * 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-04-16 kkossev - Inital test version * ver. 1.0.1 2022-04-18 kkossev - IAS cluster multiple TS0202, TS0210 and RH3040 Motion Sensors fingerprints; ignore repeated motion inactive events * ver. 1.0.2 2022-04-21 kkossev - setMotion command; state.HashStringPars; advancedOptions: ledEnable (4in1); all DP info logs for 3in1!; _TZ3000_msl6wxk9 and other TS0202 devices inClusters correction * ver. 1.0.3 2022-05-05 kkossev - '_TZE200_ztc6ggyl' 'Tuya ZigBee Breath Presence Sensor' tests; Illuminance unit changed to 'lx' * ver. 1.0.4 2022-05-06 kkossev - DeleteAllStatesAndJobs; added isHumanPresenceSensorAIR(); isHumanPresenceSensorScene(); isHumanPresenceSensorFall(); convertTemperatureIfNeeded * ver. 1.0.5 2022-06-11 kkossev - _TZE200_3towulqd +battery; 'Reset Motion to Inactive' made explicit option; sensitivity and keepTime for IAS sensors (TS0202-tested OK) and TS0601(not tested); capability "PowerSource" used as presence * ver. 1.0.6 2022-07-10 kkossev - battery set to 0% and motion inactive when the device goes OFFLINE; * ver. 1.0.7 2022-07-17 kkossev - _TZE200_ikvncluo (MOES) and _TZE200_lyetpprm radars; scale fadingTime and detectionDelay by 10; initialize() will resets to defaults; radar parameters update bug fix; removed unused states and attributes for radars * ver. 1.0.8 2022-07-24 kkossev - _TZE200_auin8mzr (HumanPresenceSensorAIR) unacknowledgedTime; setLEDMode; setDetectionMode commands and vSensitivity; oSensitivity, vacancyDelay preferences; _TZE200_9qayzqa8 (black sensor) Attributes: motionType; preferences: inductionTime; targetDistance. * ver. 1.0.9 2022-08-11 kkossev - degrees Celsius symbol bug fix; added square black radar _TZE200_0u3bj3rc support, temperatureOffset bug fix; decimal/number type prferences bug fix * ver. 1.0.10 2022-08-15 kkossev - added Lux threshold parameter; square black radar LED configuration is resent back when device is powered on; round black PIR sensor powerSource is set to DC; added OWON OCP305 Presence Sensor * ver. 1.0.11 2022-08-22 kkossev - IAS devices initialization improvements; presence threshold increased to 4 hours; 3in1 exceptions bug fixes; 3in1 and 4in1 exceptions bug fixes; * ver. 1.0.12 2022-09-05 kkossev - added _TZE200_wukb7rhc MOES radar * ver. 1.0.13 2022-09-25 kkossev - added _TZE200_jva8ink8 AUBESS radar; 2-in-1 Sensitivity setting bug fix * ver. 1.0.14 2022-10-31 kkossev - added Bond motion sensor ZX-BS-J11W fingerprint for tests * ver. 1.0.15 2022-12-03 kkossev - OWON 0x0406 cluster binding; added _TZE204_ztc6ggyl _TZE200_ar0slwnd _TZE200_sfiy5tfs _TZE200_mrf6vtua (was wrongly 3in1) mmWave radards; * ver. 1.0.16 2022-12-10 kkossev - _TZE200_3towulqd (2-in-1) motion detection inverted; excluded from IAS group; * ver. 1.1.0 2022-12-25 kkossev - SetPar() command; added 'Send Event when parameters change' option; code cleanup; added _TZE200_holel4dk; added 4-in-1 _TZ3210_rxqls8v0, _TZ3210_wuhzzfqg * ver. 1.1.1 2023-01-08 kkossev - illuminance event bug fix; fadingTime minimum value 0.5; SetPar command shows in the UI the list of all possible parameters; _TZ3000_6ygjfyll bug fix; * ver. 1.2.0 2023-02-07 kkossev - healthStatus; supressed repetative Radar detection delay and Radar fading time Info messages in the logs; logsOff missed when hub is restarted bug fix; capability 'Health Check'; _TZE200_3towulqd (2in1) new firmware versions fix for motion; * ver. 1.2.1 2023-02-10 kkossev - reverted the unsuccessful changes made in the latest 1.2.0 version for _TZE200_3towulqd (2in1); added _TZE200_v6ossqfy as BlackSquareRadar; removed the wrongly added TUYATEC T/H sensor... * ver. 1.2.2 2023-03-18 kkossev - typo in a log transaction fixed; added TS0202 _TZ3000_kmh5qpmb as a 3-in-1 type device'; added _TZE200_xpq2rzhq radar; bug fix in setMotion() * ver. 1.3.0 2023-03-22 kkossev -'_TYST11_7hfcudw5' moved to 3-in-1 group; added deviceProfiles; fixed initializaiton missing on the first pairing; added batteryVoltage; added tuyaVersion; added delayed battery event; * removed state.lastBattery; caught sensitivity par exception; fixed forcedProfile was not set automatically on Initialize; * ver. 1.3.1 2023-03-29 kkossev - added 'invertMotion' option; 4in1 (Fantem) Refresh Tuya Magic; invertMotion is set to true by default for _TZE200_3towulqd; * ver. 1.3.2 2023-04-17 kkossev - 4-in-1 parameter for adjusting the reporting time; supressed debug logs when ignoreDistance is flipped on; 'Send Event when parameters change' parameter is removed (events are always sent when there is a change); fadingTime and detectionDelay change was not logged and not sent as an event; * ver. 1.3.3 2023-05-14 kkossev - code cleanup; added TS0202 _TZ3210_cwamkvua [Motion Sensor and Scene Switch]; added _TZE204_sooucan5 radar in a new TS0601_YXZBRB58_RADAR group (for tests); added reportingTime4in1 to setPar command options; * ver. 1.3.4 2023-05-19 kkossev - added _TZE204_sxm7l9xa mmWave radar to TS0601_YXZBRB58_RADAR group; isRadar() bug fix; * ver. 1.3.5 2023-05-28 kkossev - fixes for _TZE200_lu01t0zlTS0601_RADAR_MIR-TY-FALL mmWave radar (only the basic Motion and radarSensitivity is supported for now). * ver. 1.3.6 2023-06-25 kkossev - chatty radars excessive debug logging bug fix * ver. 1.3.7 2023-07-27 kkossev - fixes for _TZE204_sooucan5; moved _TZE204_sxm7l9xa to a new Device Profile TS0601_SXM7L9XA_RADAR; added TS0202 _TZ3040_bb6xaihh _TZ3040_wqmtjsyk; added _TZE204_qasjif9e radar; * ver. 1.4.0 2023-08-06 kkossev - added new TS0225 _TZE200_hl0ss9oa 24GHz radar (TS0225_HL0SS9OA_RADAR); added basic support for the new TS0601 _TZE204_sbyx0lm6 radar w/ relay; added Hive MOT003; added sendCommand; added TS0202 _TZ3040_6ygjfyll * ver. 1.4.1 2023-08-15 kkossev - TS0225_HL0SS9OA_RADAR ignoring ZCL illuminance and IAS motion reports; added radarAlarmMode, radarAlarmVolume, radarAlarmTime, Radar Static Detection Minimum Distance; added TS0225_AWARHUSB_RADAR TS0225_EGNGMRZH_RADAR * ver. 1.4.2 2023-08-15 kkossev - 'Tuya Motion Sensor and Scene Switch' driver clone (Button capabilities enabled) * ver. 1.4.3 2023-08-17 kkossev - TS0225 _TZ3218_awarhusb device profile changed to TS0225_LINPTECH_RADAR; cluster 0xE002 parser; added TS0601 _TZE204_ijxvkhd0 to TS0601_IJXVKHD0_RADAR; added _TZE204_dtzziy1e, _TZE200_ypprdwsl _TZE204_xsm7l9xa; YXZBRB58 radar illuminance and fadingTime bug fixes; added new TS0225_2AAELWXK_RADAR profile * ver. 1.4.4 2023-08-18 kkossev - Method too large: Script1.processTuyaCluster ... :( TS0225_LINPTECH_RADAR: myParseDescriptionAsMap & swapOctets(); deleteAllCurrentStates(); TS0225_2AAELWXK_RADAR preferences configuration and commands; added Illuminance correction coefficient; code cleanup * ver. 1.4.5 2023-08-26 kkossev - reduced debug logs; * ver. 1.5.0 2023-08-27 kkossev - added TS0601 _TZE204_yensya2c radar; refactoring: deviceProfilesV2: tuyaDPs; unknownDPs; added _TZE204_clrdrnya; _TZE204_mhxn2jso; 2in1: _TZE200_1ibpyhdc, _TZE200_bh3n6gk8; added TS0202 _TZ3000_jmrgyl7o _TZ3000_hktqahrq _TZ3000_kmh5qpmb _TZ3040_usvkzkyn; added TS0601 _TZE204_kapvnnlk new device profile TS0601_KAPVNNLK_RADAR * ver. 1.5.1 2023-09-09 kkossev - _TZE204_kapvnnlk fingerprint and DPs correction; added 2AAELWXK preferences; TS0225_LINPTECH_RADAR known preferences using E002 cluster * ver. 1.5.2 2023-09-14 kkossev - TS0601_IJXVKHD0_RADAR ignore dp1 dp2; Distance logs changed to Debug; Refresh() updates driver version; * ver. 1.5.3 2023-09-30 kkossev - humanMotionState re-enabled for TS0225_HL0SS9OA_RADAR; tuyaVersion is updated on Refresh; LINPTECH: added existance_time event; illuminance parsing exception changed to debug level; leave_time changed to fadingTime; fadingTime configuration * * ver. 1.6.0 2023-10-08 kkossev - (dev. branch) major refactoring of the preferences input; all preference settings are reset to defaults when changing device profile; added 'all' attribute; present state 'motionStarted' in a human-readable form. * setPar and sendCommand major refactoring +parameters changed from enum to string; TS0601_KAPVNNLK_RADAR parameters support; * ver. 1.6.1 2023-10-12 kkossev - (dev. branch) TS0601_KAPVNNLK_RADAR TS0225_HL0SS9OA_RADAR TS0225_2AAELWXK_RADAR TS0601_RADAR_MIR-HE200-TY TS0601_YXZBRB58_RADAR TS0601_SXM7L9XA_RADAR TS0601_IJXVKHD0_RADAR TS0601_YENSYA2C_RADAR TS0601_SBYX0LM6_RADAR TS0601_PIR_AIR TS0601_PIR_PRESENCE refactoring; radar enum preferences; * ver. 1.6.2 2023-10-14 kkossev - (dev. branch) LINPTECH preferences changed to enum type; enum preferences - set defaultValue; TS0601_PIR_PRESENCE - preference inductionTime changed to fadingTime, humanMotionState sent as event; TS0225_2AAELWXK_RADAR - preferences setting; _TZE204_ijxvkhd0 fixes; Linptech fixes; added radarAlarmMode radarAlarmVolume; * ver. 1.6.3 2023-10-15 kkossev - (dev. branch) setPar() and preferences updates bug fixes; automatic fix for preferences which type was changed between the versions, including bool; * ver. 1.6.4 2023-10-18 kkossev - (dev. branch) added TS0601 _TZE204_e5m9c5hl to SXM7L9XA profile; added a bunch of new manufacturers to SBYX0LM6 profile; * ver. 1.6.5 2023-10-23 kkossev - (dev. branch) bugfix: setPar decimal values for enum types; added SONOFF_SNZB-06P_RADAR; added SIHAS_USM-300Z_4_IN_1; added SONOFF_MOTION_IAS; TS0202_MOTION_SWITCH _TZ3210_cwamkvua refactoring; luxThreshold hardcoded to 0 and not configurable!; do not try to input preferences of a type bool * TS0601_2IN1 refactoring; added keepTime and sensitivity attributes for PIR sensors; added _TZE200_ppuj1vem 3-in-1; TS0601_3IN1 refactoring; added _TZ3210_0aqbrnts 4in1; * ver. 1.6.6 2023-11-02 kkossev - _TZE204_ijxvkhd0 staticDetectionSensitivity bug fix; SONOFF radar clusters binding; assign profile UNKNOWN for unknown devices; SONOFF radar cluster FC11 attr 2001 processing as occupancy; TS0601_IJXVKHD0_RADAR sensitivity as number; number type pars are scalled also!; _TZE204_ijxvkhd0 sensitivity settings changes; added preProc function; TS0601_IJXVKHD0_RADAR - removed multiplying by 10 * ver. 1.6.7 2023-11-09 kkossev - (dev. branch) divideBy10 fix for TS0601_IJXVKHD0_RADAR; added new TS0202_MOTION_IAS_CONFIGURABLE group * ver. 1.6.8 2023-11-20 kkossev - SONOFF SNZB-06P RADAR bug fixes; added radarSensitivity and fadingTime preferences; update parameters for Tuya radars bug fix; * * TODO: if isSleepy - store in state.cmds and send when the device wakes up! (on both update() and refresh() * TODO: TS0202_MOTION_IAS missing sensitivity and retrigger time settings bug fix; * TODO: handle preferences of a type TEXT * TODO: add Sensitivity Levels Presets * TODO: W.I.P. TS0202_4IN1 refactoring * TODO: TS0601_3IN1 - process Battery/USB powerSource change events! (0..4) * TODO: when device rejoins the network, read the battry percentage again! * TODO: check why only voltage is reported for SONOFF_MOTION_IAS; * TODO: hide motionKeepTime and motionSensitivity for SONOFF_MOTION_IAS; * TODO: Black Square Radar validateAndFixPreferences: map not found for preference indicatorLight * TODO: quickRef * TODO: command for black radar LED * TODO: TS0225_2AAELWXK_RADAR dont see an attribute as mentioned that shows the distance at which the motion was detected. - https://community.hubitat.com/t/the-new-tuya-human-presence-sensors-ts0225-tze200-hl0ss9oa-tze200-2aaelwxk-have-actually-5-8ghz-modules-inside/122283/294?u=kkossev * TODO: TS0225_2AAELWXK_RADAR led setting not working - https://community.hubitat.com/t/the-new-tuya-human-presence-sensors-ts0225-tze200-hl0ss9oa-tze200-2aaelwxk-have-actually-5-8ghz-modules-inside/122283/294?u=kkossev * TODO: do not show errors/warnings for new settings ie breath , led etc if the preferences setsetare not set and saved - https://community.hubitat.com/t/the-new-tuya-human-presence-sensors-ts0225-tze200-hl0ss9oa-tze200-2aaelwxk-have-actually-5-8ghz-modules-inside/122283/294?u=kkossev * TODO: delete all previous preferencies when changing the device profile! * TODO: Linptech spammyDPsToIgnore[] ! * TODO: radars - ignore the change of the presence/motion being turned off when changing parameters for a period of 10 seconds ? * TODO: add rtt measurement for ping() * TODO: TS0225_HL0SS9OA_RADAR - add presets * TOOD: Tuya 2in1 illuminance_interval (dp=102) ! * TODO: humanMotionState - add preference: enum "disabled", "enabled", "enabled w/ timing" ...; add delayed event * TODO: publish examples of SetPar usage : https://community.hubitat.com/t/4-in-1-parameter-for-adjusting-reporting-time/115793/12?u=kkossev * TODO: ignore invalid humidity reprots (>100 %) * TODO: use getKeepTimeOpts() for processing dp=0x0A (10) keep time ! ( 2-in-1 time is wrong) * TODO: add to state 'last battery' the time when the battery was last reported. * TODO: check the bindings commands in configure() * TODO: implement getActiveEndpoints() */ def version() { "1.6.8" } def timeStamp() {"2023/11/20 1:13 PM"} import groovy.json.* import groovy.transform.Field import hubitat.device.HubAction import hubitat.device.Protocol import hubitat.zigbee.clusters.iaszone.ZoneStatus import hubitat.zigbee.zcl.DataType import java.util.ArrayList import java.util.concurrent.ConcurrentHashMap @Field static final Boolean _DEBUG = false @Field static final Boolean _TRACE_ALL = false // trace all messages, including the spammy onese metadata { definition (name: "Tuya Multi Sensor 4 In 1", namespace: "kkossev", author: "Krassimir Kossev", importUrl: "https://raw.githubusercontent.com/kkossev/Hubitat/development/Drivers/Tuya%20Multi%20Sensor%204%20In%201/Tuya%20Multi%20Sensor%204%20In%201.groovy", singleThreaded: true ) { capability "Sensor" //capability "Configuration" capability "Battery" capability "MotionSensor" capability "TemperatureMeasurement" capability "RelativeHumidityMeasurement" capability "IlluminanceMeasurement" capability "TamperAlert" capability "PowerSource" capability "HealthCheck" capability "Refresh" //capability "PushableButton" // uncomment for TS0202 _TZ3210_cwamkvua [Motion Sensor and Scene Switch] //capability "DoubleTapableButton" //capability "HoldableButton" //attribute "occupancy", "enum", ["occupied", "unoccupied"] // https://developer.smartthings.com/docs/devices/capabilities/capabilities-reference // https://developer.smartthings.com/capabilities/occupancySensor attribute "all", "string" attribute "batteryVoltage", "number" attribute "healthStatus", "enum", ["offline", "online"] attribute "distance", "number" // Tuya Radar attribute "unacknowledgedTime", "number" // AIR models attribute "existance_time", "number" // BlackSquareRadar & LINPTECH attribute "leave_time", "number" // BlackSquareRadar only attribute" pushed", "number" // TS0202 _TZ3210_cwamkvua [Motion Sensor and Scene Switch] attribute "keepTime", "enum", ["10 seconds", "30 seconds","60 seconds", "120 seconds"] attribute "sensitivity", "enum", ["low", "medium","high"] attribute "radarSensitivity", "number" attribute "staticDetectionSensitivity", "number" // added 10/29/2023 attribute "detectionDelay", "decimal" attribute "fadingTime", "decimal" attribute "minimumDistance", "decimal" attribute "maximumDistance", "decimal" attribute "radarStatus", "enum", ["checking", "check_success", "check_failure", "others", "comm_fault", "radar_fault"] attribute "humanMotionState", "enum", ["none", "moving", "small_move", "stationary", "presence", "peaceful", "large_move"] attribute "radarAlarmMode", "enum", ["0 - arm", "1 - off", "2 - alarm", "3 - doorbell"] attribute "radarAlarmVolume", "enum", ["0 - low", "1 - medium", "2 - high", "3 - mute"] command "configure", [[name: "Configure the sensor after switching drivers"]] command "initialize", [[name: "Initialize the sensor after switching drivers. \n\r ***** Will load device default values! *****" ]] command "setMotion", [[name: "setMotion", type: "ENUM", constraints: ["No selection", "active", "inactive"], description: "Force motion active/inactive (for tests)"]] command "refresh", [[name: "May work for some DC/mains powered sensors only"]] command "setPar", [ [name:"par", type: "STRING", description: "preference parameter name", constraints: ["STRING"]], [name:"val", type: "STRING", description: "preference parameter value", constraints: ["STRING"]] ] command "sendCommand", [[name: "sendCommand", type: "STRING", constraints: ["STRING"], description: "send Tuya Radar commands"]] if (_DEBUG == true) { command "testTuyaCmd", [ [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 "testParse", [[name:"val", type: "STRING", description: "description", constraints: ["STRING"]]] command "test", [[name:"val", type: "STRING", description: "preference parameter value", constraints: ["STRING"]]] } deviceProfilesV2.each { profileName, profileMap -> if (profileMap.fingerprints != null) { profileMap.fingerprints.each { fingerprint it } } } } preferences { if (advancedOptions == true || advancedOptions == false) { // Groovy ... :) //input (name: "quickref", type: "hidden", title: "$ttStyleStrQuick Reference v${version()}") input (name: "txtEnable", type: "bool", title: "Description text logging", description: "Display sensor states on HE log page. The recommended value is true", defaultValue: true) input (name: "logEnable", type: "bool", title: "Debug logging", description: "Debug information, useful for troubleshooting. The recommended value is false", defaultValue: true) if (("motionReset" in DEVICE?.preferences) && (DEVICE?.preferences.motionReset == true)) { input (name: "motionReset", type: "bool", title: "Reset Motion to Inactive", description: "Software Reset Motion to Inactive after timeout. Recommended value is false", defaultValue: false) if (motionReset.value == true) { input ("motionResetTimer", "number", title: "Motion Reset Timer", description: "After motion is detected, wait ___ second(s) until resetting to inactive state. Default = 60 seconds", range: "0..7200", defaultValue: 60) } } if (false) { // TODO! input ("temperatureOffset", "decimal", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", defaultValue: 0.0) input ("humidityOffset", "decimal", title: "Humidity offset", description: "Enter a percentage to adjust the humidity.", range: "-50..50", defaultValue: 0.0) } } if (("reportingTime4in1" in DEVICE?.preferences)) { // 4in1() input ("reportingTime4in1", "number", title: "4-in-1 Reporting Time", description: "4-in-1 Reporting Time configuration, minutes.
0 will enable real-time (10 seconds) reporting!
", range: "0..7200", defaultValue: DEFAULT_REPORTING_4IN1) } if (("ledEnable" in DEVICE?.preferences)) { // 4in1() input (name: "ledEnable", type: "bool", title: "Enable LED", description: "Enable LED blinking when motion is detected (4in1 only)", defaultValue: true) } if (advancedOptions == true || advancedOptions == false) { if ((DEVICE?.capabilities?.IlluminanceMeasurement == true) && (DEVICE?.preferences.luxThreshold != false)) { input ("luxThreshold", "number", title: "Lux threshold", description: "Minimum change in the lux which will trigger an event", range: "0..999", defaultValue: 5) input name: "illuminanceCoeff", type: "decimal", title: "Illuminance Correction Coefficient", description: "Illuminance correction coefficient, range (0.10..10.00)", range: "0.10..10.00", defaultValue: 1.00 } } if (("DistanceMeasurement" in DEVICE?.capabilities)) { input (name: "ignoreDistance", type: "bool", title: "Ignore distance reports", description: "If not used, ignore the distance reports received every 1 second!", defaultValue: true) } // itterate over DEVICE.preferences map and inputIt all! (DEVICE.preferences).each { key, value -> if (inputIt(key) != null) { input inputIt(key) } } if (false) { if ("textLargeMotion" in DEVICE?.preferences) { input (name: 'textLargeMotion', type: 'text', title: "Motion Detection Settigs ⇨", description: "Settings for movement types such as walking, trotting, fast running, circling, jumping and other movements ") } if ("textSmallMotion" in DEVICE?.preferences) { input (name: 'textSmallMotion', type: 'text', title: "Small Motion Detection Settigs ⇒", description: "Settings for small movement types such as tilting the head, waving, raising the hand, flicking the body, playing with the mobile phone, turning over the book, etc.. ") } if ("textStaticDetection" in DEVICE?.preferences) { input (name: 'textStaticDetection', type: 'text', title: "Static Detection Settigs ⇨", description: "The sensor can detect breathing within a certain range to determine people presence in the detection area (for example, while sleeping or reading).") } } input (name: "advancedOptions", type: "bool", title: "Advanced Options", description: "Enables showing the advanced options/preferences. Hit F5 in the browser to refresh the Preferences list
.May not work for all device types!
", defaultValue: false) if (advancedOptions == true) { input (name: "forcedProfile", type: "enum", title: "Device Profile", description: "Forcely change the Device Profile, if the model/manufacturer was not recognized automatically.
Warning! Manually setting a device profile may not always work!
", options: getDeviceProfilesMap()) if ("Battery" in DEVICE?.capabilities) { input (name: "batteryDelay", type: "enum", title: "Battery Events Delay", description:"Select the Battery Events Delay
(default is no delay)
", options: delayBatteryOpts.options, defaultValue: delayBatteryOpts.defaultValue) } if ("invertMotion" in DEVICE?.preferences) { input (name: "invertMotion", type: "bool", title: "Invert Motion Active/Not Active", description: "Some Tuya motion sensors may report the motion active/inactive inverted...", defaultValue: false) } } input (name: "allStatusTextEnable", type: "bool", title: "Enable 'all' Status Attribute Creation?", description: "Status attribute for Devices/Rooms", defaultValue: false) } } @Field static final Boolean _IGNORE_ZCL_REPORTS = true @Field static final String UNKNOWN = 'UNKNOWN' @Field static final Map blackRadarLedOptions = [ "0" : "Off", "1" : "On" ] // HumanPresenceSensorAIR @Field static final Map TS0225humanMotionState = [ "0": "none", "1": "moving", "2": "small_move", "3": "stationary" ] @Field static String ttStyleStr = "" /* LivingRoom Bedroom Washroom Aisle Kitchen Motion detection distance - 800 500 300 900 1000 Motion detection sensitivity - 6x 6x 5x 8x 6x Small motion detection distance - 600 400 400 400 600 Small motion detection sensitivity 9 6 8 5 8 Static detection distance 600 600 400 0 0 Static detection sensitivity 9x 9x 8x 8x 8x */ // TODO - remove all the usused sensitivity and keepTime static maps! @Field static final Map sensitivityOpts = [ defaultValue: 2, options: [0: 'low', 1: 'medium', 2: 'high']] @Field static final Map keepTime4in1Opts = [ defaultValue: 0, options: [0: '10 seconds', 1: '30 seconds', 2: '60 seconds', 3: '120 seconds', 4: '240 seconds', 5: '480 seconds']] @Field static final Map keepTime2in1Opts = [ defaultValue: 0, options: [0: '10 seconds', 1: '30 seconds', 2: '60 seconds', 3: '120 seconds']] @Field static final Map keepTime3in1Opts = [ defaultValue: 0, options: [0: '30 seconds', 1: '60 seconds', 2: '120 seconds']] @Field static final Map keepTimeIASOpts = [ defaultValue: 0, options: [0: '30 seconds', 1: '60 seconds', 2: '120 seconds']] @Field static final Map powerSourceOpts = [ defaultValue: 0, options: [0: 'unknown', 1: 'mains', 2: 'mains', 3: 'battery', 4: 'dc', 5: 'emergency mains', 6: 'emergency mains']] @Field static final Map delayBatteryOpts = [ defaultValue: 0, options: [0: 'No delay', 30: '30 seconds', 3600: '1 hour', 14400: '4 hours', 28800: '8 hours', 43200: '12 hours']] def getKeepTimeOpts() { return is4in1() ? keepTime4in1Opts : is3in1() ? keepTime3in1Opts : is2in1() ? keepTime2in1Opts : keepTimeIASOpts} @Field static final Integer presenceCountTreshold = 4 @Field static final Integer defaultPollingInterval = 3600 @Field static final Integer DEFAULT_REPORTING_4IN1 = 5 // time in minutes def getDeviceGroup() { state.deviceProfile ?: "UNKNOWN" } def getDEVICE() { deviceProfilesV2[getDeviceGroup()] } def getDeviceProfiles() { deviceProfilesV2.keySet() } def getDeviceProfilesMap() {deviceProfilesV2.values().description as List} def is4in1() { return getDeviceGroup().contains("TS0202_4IN1") } def is3in1() { return getDeviceGroup().contains("TS0601_3IN1") } def is2in1() { return getDeviceGroup().contains("TS0601_2IN1") } def isMotionSwitch() { return getDeviceGroup().contains("TS0202_MOTION_SWITCH") } def isIAS() { DEVICE?.device?.isIAS == true } //def isTS0601_PIR() { (DEVICE.device?.type == "PIR") && (("keepTime" in DEVICE.preferences) || ("sensitivity" in DEVICE.preferences)) } //def isConfigurable() { return isIAS() } // TS0202 models ['_TZ3000_mcxw5ehu', '_TZ3000_msl6wxk9'] def getTemperatureDiv() { isSiHAS() ? 100.0 : 10.0 } // temperatureEvent def getHumidityDiv() { isSiHAS() ? 100.0 : 1.0 } // humidityEvent def isZY_M100Radar() { return getDeviceGroup().contains("TS0601_TUYA_RADAR") } def isBlackPIRsensor() { return getDeviceGroup().contains("TS0601_PIR_PRESENCE") } def isBlackSquareRadar() { return getDeviceGroup().contains("TS0601_BLACK_SQUARE_RADAR") } def isHumanPresenceSensorAIR() { return getDeviceGroup().contains("TS0601_PIR_AIR") } // isHumanPresenceSensorScene() removed in version 1.6.1 //def isHumanPresenceSensorFall() { return getDeviceGroup().contains("TS0601_RADAR_MIR-TY-FALL") } // NOT USED ver 1.6.1 def isYXZBRB58radar() { return getDeviceGroup().contains("TS0601_YXZBRB58_RADAR") } def isSXM7L9XAradar() { return getDeviceGroup().contains("TS0601_SXM7L9XA_RADAR") } def isIJXVKHD0radar() { return getDeviceGroup().contains("TS0601_IJXVKHD0_RADAR") } def isHL0SS9OAradar() { return getDeviceGroup().contains("TS0225_HL0SS9OA_RADAR") } def is2AAELWXKradar() { return getDeviceGroup().contains("TS0225_2AAELWXK_RADAR") } // same as HL0SS9OA, but another set of DPs def isSBYX0LM6radar() { return getDeviceGroup().contains("TS0601_SBYX0LM6_RADAR") } def isLINPTECHradar() { return getDeviceGroup().contains("TS0225_LINPTECH_RADAR") } def isEGNGMRZHradar() { return getDeviceGroup().contains("TS0225_EGNGMRZH_RADAR") } def isKAPVNNLKradar() { return getDeviceGroup().contains("TS0601_KAPVNNLK_RADAR") } def isSONOFF() { return getDeviceGroup().contains("SONOFF_SNZB-06P_RADAR") } def isSiHAS() { return getDeviceGroup().contains("SIHAS_USM-300Z_4_IN_1") } // TODO - check if DPs are declared in the device profiles and remove this function def isChattyRadarReport(descMap) { if ((isZY_M100Radar() || isSBYX0LM6radar()) && (settings?.ignoreDistance == true) ) { return (descMap?.clusterId == "EF00" && (descMap.command in ["01", "02"]) && descMap.data?.size > 2 && descMap.data[2] == "09") } else if ((isYXZBRB58radar() || isSXM7L9XAradar()) && (settings?.ignoreDistance == true)) { return (descMap?.clusterId == "EF00" && (descMap.command in ["01", "02"]) && descMap.data?.size > 2 && descMap.data[2] == "6D") } /* TODO - check ! else if (isKAPVNNLKradar() && settings?.ignoreDistance == true) { return (descMap?.clusterId == "EF00" && (descMap.command in ["01", "02"]) && descMap.data?.size > 2 && descMap.data[2] == "13") } */ // if (isHL0SS9OAradar()) { // humanMotionState // return (descMap?.clusterId == "EF00" && (descMap.command in ["01", "02"]) && descMap.data?.size > 2 && descMap.data[2] == "0B") // } else { return false } } @Field static final Map deviceProfilesV2 = [ // is4in1() "TS0202_4IN1" : [ description : "Tuya 4in1 (motion/temp/humi/lux) sensor", models : ["TS0202"], // model: 'ZB003-X' vendor: 'Fantem' device : [type: "PIR", isIAS:true, powerSource: "dc", isSleepy:false], // check powerSource capabilities : ["MotionSensor": true, "TemperatureMeasurement": true, "RelativeHumidityMeasurement": true, "IlluminanceMeasurement": true, "tamper": true, "Battery": true], preferences : ["motionReset":true, "reportingTime4in1":true, "ledEnable":true, "keepTime":true, "sensitivity":true], commands : ["reportingTime4in1":"reportingTime4in1", "resetStats":"resetStats", 'refresh':'refresh', "initialize":"initialize", "updateAllPreferences": "updateAllPreferences", "resetPreferencesToDefaults":"resetPreferencesToDefaults", "validateAndFixPreferences":"validateAndFixPreferences"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0500,EF00", outClusters:"0019,000A", model:"TS0202", manufacturer:"_TZ3210_zmy9hjay", deviceJoinName: "Tuya TS0202 Multi Sensor 4 In 1"], // pairing: double click! [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0500,EF00", outClusters:"0019,000A", model:"5j6ifxj", manufacturer:"_TYST11_i5j6ifxj", deviceJoinName: "Tuya TS0202 Multi Sensor 4 In 1"], [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0500,EF00", outClusters:"0019,000A", model:"hfcudw5", manufacturer:"_TYST11_7hfcudw5", deviceJoinName: "Tuya TS0202 Multi Sensor 4 In 1"], [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0500,EF00", outClusters:"0019,000A", model:"TS0202", manufacturer:"_TZ3210_rxqls8v0", deviceJoinName: "Tuya TS0202 Multi Sensor 4 In 1"], // not tested [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0500,EF00", outClusters:"0019,000A", model:"TS0202", manufacturer:"_TZ3210_wuhzzfqg", deviceJoinName: "Tuya TS0202 Multi Sensor 4 In 1"], // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars/92441/282?u=kkossev [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0500,EF00", outClusters:"0019,000A", model:"TS0202", manufacturer:"_TZ3210_0aqbrnts", deviceJoinName: "Tuya TS0202 Multi Sensor 4 In 1 is-thpl-zb"] ], tuyaDPs: [ [dp:1, name:'motion', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", description:'Motion'], // ??? check ^^ [dp:5, name:'tamper', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"clear", 1:"detected"] , unit:"", description:'Tamper detection'], // ??? IAS cluster is used instead? {occupancy: (zoneStatus & 1) > 0, tamper: (zoneStatus & 4) > 0}; [dp:9, name:"sensitivity", type:"enum", rw: "rw", min:0, max:2, defaultValue:"2", unit:"", map:[0:"low", 1:"medium", 2:"high"], title:"Sensitivity", description:"PIR sensor sensitivity (update at the time motion is activated)"], // check ^^^^^ hasOwnProperty('currentZoneSensitivityLevel') .read('ssIasZone', ['currentZoneSensitivityLevel', 61441, 'zoneStatus'] [dp:10, name:"keepTime", type:"enum", rw: "rw", min:0, max:4, defaultValue:"0", unit:"seconds", map:[0:"0 seconds", 1:"30 seconds", 2:"60 seconds", 3:"120 seconds", 3:"240 seconds", 4:"480 seconds"], title:"Keep Time", description:"PIR keep time in seconds (update at the time motion is activated)"], // check ^^^^^ hasOwnProperty('61441') [dp:25, name:'battery2', type:"number", rw: "ro", min:0, max:100, defaultValue:100, step:1, scale:1, unit:"%", description:'Remaining battery 2 in %'], // ^^^TODO^^^ [dp:102, name:'reportingTime4in1',type:"number", rw: "ro", min:0, max:1440, defaultValue:10, step:5, scale:1, unit:"minutes", title:"Reporting Interval", description:'Reporting interval in minutes'], [dp:104, name:'tempCalibration', type:"decimal", rw:"ro", min:-2.0, max:2.0, defaultValue:0.0, step:1, scale:10, unit:"deg.", title:"Temperature Calibration", description:'Temperature calibration (-2.0...2.0)'], // ^^^TODO^^ pre-process negative values ! [dp:105, name:'humiCalibration', type:"number", rw: "ro", min:-15, max:15, defaultValue:0, step:1, scale:1, unit:"%RH", title:"Huidity Calibration", description:'Humidity Calibration'], [dp:106, name:'illumCalibration',type:"number", rw: "ro", min:-20, max:20, defaultValue:0, step:1, scale:1, unit:"Lx", title:"Illuminance Calibration", description:'Illuminance calibration in lux/i>'], [dp:107, name:'temperature', type:"decimal", rw: "ro", min:-20.0, max:80.0, defaultValue:0.0, step:1, scale:10, unit:"deg.", description:'Temperature'], [dp:108, name:'humidity', type:"number", rw: "ro", min:1, max:100, defaultValue:100, step:1, scale:1, unit:"%RH", description:'Humidity'], [dp:109, name:'pirSensorEnable', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"1", step:1, scale:1, map:[0:"disabled", 1:"enabled"] , unit:"", title:"MoPIR Sensor Enable", description:'Enable PIR sensor'], [dp:110, name:'battery', type:"number", rw: "ro", min:0, max:100, defaultValue:100, step:1, scale:1, unit:"%", description:'Battery level'], // ^^^TODO^^^ [dp:111, name:'ledEnable', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"disabled", 1:"enabled"] , unit:"", title:"LED Enable", description:'Enable LED'], [dp:112, name:'reportingEnable', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"disabled", 1:"enabled"] , unit:"", title:"Reporting Enable", description:'Enable reporting'], ], deviceJoinName: "Tuya Multi Sensor 4 In 1", configuration : ["battery": false] ], // is3in1() "TS0601_3IN1" : [ // https://szneo.com/en/products/show.php?id=239 // https://www.banggood.com/Tuya-Smart-Linkage-ZB-Motion-Sensor-Human-Infrared-Detector-Mobile-Phone-Remote-Monitoring-PIR-Sensor-p-1858413.html?cur_warehouse=CN description : "Tuya 3in1 (Motion/Temp/Humi) sensor", models : ["TS0601"], device : [type: "PIR", powerSource: "dc", isSleepy:false], // powerSource changes batt/DC dynamically! capabilities : ["MotionSensor": true, "TemperatureMeasurement": true, "RelativeHumidityMeasurement": true, "tamper": true, "Battery": true], preferences : ["motionReset":true], commands : ["resetStats":"resetStats", 'refresh':'refresh', "initialize":"initialize", "updateAllPreferences": "updateAllPreferences", "resetPreferencesToDefaults":"resetPreferencesToDefaults", "validateAndFixPreferences":"validateAndFixPreferences"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_7hfcudw5", deviceJoinName: "Tuya NAS-PD07 Multi Sensor 3 In 1"], [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_ppuj1vem", deviceJoinName: "Tuya NAS-PD07 Multi Sensor 3 In 1"] ], tuyaDPs: [ [dp:101, name:'motion', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", description:'Motion'], [dp:102, name:'battery', type:"number", rw: "ro", min:0, max:100, defaultValue:100, step:1, scale:1, unit:"%", description:'Battery level'], // ^^^TODO^^^ [dp:103, name:'tamper', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"clear", 1:"detected"] , unit:"", description:'Tamper detection'], [dp:104, name:'temperature', type:"decimal", rw: "ro", min:-20.0, max:80.0, defaultValue:0.0, step:1, scale:10, unit:"deg.", description:'Temperature'], [dp:105, name:'humidity', type:"number", rw: "ro", min:1, max:100, defaultValue:100, step:1, scale:1, unit:"%RH", description:'Humidity'], [dp:106, name:'tempScale', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"Celsius", 1:"Fahrenheit"] , unit:"", description:'Temperature scale'], [dp:107, name:'minTemp', type:"number", rw: "ro", min:-20, max:80, defaultValue:0, step:1, scale:1, unit:"deg.", description:'Minimal temperature'], [dp:108, name:'maxTemp', type:"number", rw: "ro", min:-20, max:80, defaultValue:0, step:1, scale:1, unit:"deg.", description:'Maximal temperature'], [dp:109, name:'minHumidity', type:"number", rw: "ro", min:0, max:100, defaultValue:0, step:1, scale:1, unit:"%RH", description:'Minimal humidity'], [dp:110, name:'maxHumidity', type:"number", rw: "ro", min:0, max:100, defaultValue:0, step:1, scale:1, unit:"%RH", description:'Maximal humidity'], [dp:111, name:'tempAlarm', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", description:'Temperature alarm'], [dp:112, name:'humidityAlarm', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", description:'Humidity alarm'], [dp:113, name:'alarmType', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"type0", 1:"type1"] , unit:"", description:'Alarm type'], ], deviceJoinName: "Tuya Multi Sensor 3 In 1", configuration : ["battery": false] ], // is2in1() "TS0601_2IN1" : [ // https://github.com/Koenkk/zigbee-herdsman-converters/blob/bf32ce2b74689328048b407e56ca936dc7a54a0b/src/devices/tuya.ts#L4568 description : "Tuya 2in1 (Motion and Illuminance) sensor", models : ["TS0601"], device : [type: "PIR", isIAS:false, powerSource: "battery", isSleepy:true], capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "Battery": true], preferences : ["motionReset":true, "invertMotion":true, "keepTime":"10", "sensitivity":"9"], commands : ["resetStats":"resetStats", 'refresh':'refresh', "initialize":"initialize", "updateAllPreferences": "updateAllPreferences", "resetPreferencesToDefaults":"resetPreferencesToDefaults", "validateAndFixPreferences":"validateAndFixPreferences"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_3towulqd", deviceJoinName: "Tuya 2 in 1 Zigbee Mini PIR Motion Detector + Bright Lux ZG-204ZL"], // https://www.aliexpress.com/item/1005004095233195.html [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_bh3n6gk8", deviceJoinName: "Tuya 2 in 1 Zigbee Mini PIR Motion Detector + Bright Lux ZG-204ZL"], // https://community.hubitat.com/t/tze200-bh3n6gk8-motion-sensor-not-working/123213?u=kkossev [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_1ibpyhdc", deviceJoinName: "Tuya 2 in 1 Zigbee Mini PIR Motion Detector + Bright Lux ZG-204ZL"] // ], tuyaDPs: [ [dp:1, name:'motion', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", description:'Motion'], [dp:4, name:'battery', type:"number", rw: "ro", min:0, max:100, defaultValue:100, step:1, scale:1, unit:"%", title:"Battery level", description:'Battery level'], [dp:9, name:"sensitivity", type:"enum", rw: "rw", min:0, max:2, defaultValue:"2", unit:"", map:[0:"low", 1:"medium", 2:"high"], title:"Sensitivity", description:"PIR sensor sensitivity (update at the time motion is activated)"], [dp:10, name:"keepTime", type:"enum", rw: "rw", min:0, max:3, defaultValue:"0", unit:"seconds", map:[0:"10 seconds", 1:"30 seconds", 2:"60 seconds", 3:"120 seconds"], title:"Keep Time", description:"PIR keep time in seconds (update at the time motion is activated)"], [dp:12, name:'illuminance', type:"number", rw: "ro", min:0, max:1000, defaultValue:0, step:1, scale:1, unit:"lx", title:"illuminance", description:'illuminance'], [dp:102, name:'illuminance_interval', type:"number", rw: "rw", min:1, max:720, defaultValue:1, step:1, scale:1, unit:"minutes", title:"Illuminance Interval", description:'Brightness acquisition interval (update at the time motion is activated)'], ], deviceJoinName: "Tuya Multi Sensor 2 In 1", configuration : ["battery": false] ], "TS0202_MOTION_IAS" : [ description : "Tuya TS0202 Motion sensor (IAS)", models : ["TS0202","RH3040"], device : [type: "PIR", isIAS:true, powerSource: "battery", isSleepy:true], capabilities : ["MotionSensor": true, "Battery": true], preferences : ["motionReset":true, "keepTime":false, "sensitivity":false], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,0003,0001,0500", outClusters:"0000,0003,0001,0500", model:"TS0202", manufacturer:"_TYZB01_dl7cejts", deviceJoinName: "Tuya TS0202 Motion Sensor"], // KK model: 'ZM-RT201'// 5 seconds (!) reset period for testing [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3000_mmtwjmaq", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3000_otvn3lne", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TYZB01_jytabjkb", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TYZB01_ef5xlc9q", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TYZB01_vwqnz1sn", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TYZB01_2b8f6cio", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZE200_bq5c8xfe", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TYZB01_qjqgmqxr", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TYZB01_zwvaj5wy", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3000_bsvqrxru", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TYZB01_tv3wxhcz", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TYZB01_hqbdru35", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3000_tiwq83wk", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3000_ykwcwxmz", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3000_hgu1dlak", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3000_hktqahrq", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3000_jmrgyl7o", deviceJoinName: "Tuya TS0202 Motion Sensor"], // not tested! //https://zigbee.blakadder.com/Luminea_ZX-5311.html [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"WHD02", manufacturer:"_TZ3000_kmh5qpmb", deviceJoinName: "Tuya TS0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3040_usvkzkyn", deviceJoinName: "Tuya TS0202 Motion Sensor"], // not tested // https://www.amazon.ae/Rechargeable-Detector-Security-Devices-Required/dp/B0BKKJ48QH [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0500", model:"RH3040", manufacturer:"TUYATEC-53o41joc", deviceJoinName: "TUYATEC RH3040 Motion Sensor"], // 60 seconds reset period [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0500", model:"RH3040", manufacturer:"TUYATEC-b5g40alm", deviceJoinName: "TUYATEC RH3040 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0500", model:"RH3040", manufacturer:"TUYATEC-deetibst", deviceJoinName: "TUYATEC RH3040 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0500", model:"RH3040", manufacturer:"TUYATEC-bd5faf9p", deviceJoinName: "Nedis/Samotech RH3040 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0500", model:"RH3040", manufacturer:"TUYATEC-zn9wyqtr", deviceJoinName: "Samotech RH3040 Motion Sensor"], // vendor: 'Samotech', model: 'SM301Z' [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0500", model:"RH3040", manufacturer:"TUYATEC-b3ov3nor", deviceJoinName: "Zemismart RH3040 Motion Sensor"], // vendor: 'Nedis', model: 'ZBSM10WT' [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0500", model:"RH3040", manufacturer:"TUYATEC-2gn2zf9e", deviceJoinName: "TUYATEC RH3040 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0500,0B05", outClusters:"0019", model:"TY0202", manufacturer:"_TZ1800_fcdjzz3s", deviceJoinName: "Lidl TY0202 Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0500,0B05,FCC0", outClusters:"0019,FCC0", model:"TY0202", manufacturer:"_TZ3000_4ggd8ezp", deviceJoinName: "Bond motion sensor ZX-BS-J11W"], // https://community.hubitat.com/t/what-driver-to-use-for-this-motion-sensor-zx-bs-j11w-or-ty0202/103953/4 [profileId:"0104", endpointId:"01", inClusters:"0001,0003,0004,0500,0000", outClusters:"0004,0006,1000,0019,000A", model:"TS0202", manufacturer:"_TZ3040_bb6xaihh", deviceJoinName: "Tuya TS0202 Motion Sensor"], // https://github.com/Koenkk/zigbee2mqtt/issues/17364 [profileId:"0104", endpointId:"01", inClusters:"0001,0003,0004,0500,0000", outClusters:"0004,0006,1000,0019,000A", model:"TS0202", manufacturer:"_TZ3040_wqmtjsyk", deviceJoinName: "Tuya TS0202 Motion Sensor"], // not tested [profileId:"0104", endpointId:"01", inClusters:"0001,0003,0004,0500,0000", outClusters:"0004,0006,1000,0019,000A", model:"TS0202", manufacturer:"_TZ3000_h4wnrtck", deviceJoinName: "Tuya TS0202 Motion Sensor"] // not tested ], deviceJoinName: "Tuya TS0202 Motion Sensor", configuration : ["battery": false] ], "TS0202_MOTION_IAS_CONFIGURABLE" : [ description : "Tuya TS0202 Motion sensor (IAS) configurable", models : ["TS0202"], device : [type: "PIR", isIAS:true, powerSource: "battery", isSleepy:true], capabilities : ["MotionSensor": true, "Battery": true], preferences : ["motionReset":true, "keepTime":"0x0500:0xF001", "sensitivity":"0x0500:0x0013"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3000_mcxw5ehu", deviceJoinName: "Tuya TS0202 ZM-35H-Q Motion Sensor"], // TODO: PIR sensor sensitivity and PIR keep time in seconds ['30', '60', '120'] [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3000_msl6wxk9", deviceJoinName: "Tuya TS0202 ZM-35H-Q Motion Sensor"], // TODO: fz.ZM35HQ_attr ['30', '60', '120'] [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3040_msl6wxk9", deviceJoinName: "Tuya TS0202 ZM-35H-Q Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3040_fwxuzcf4", deviceJoinName: "Tuya TS0202 ZM-35H-Q Motion Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3000_6ygjfyll", deviceJoinName: "Tuya TS0202 Motion Sensor"], // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars/92441/289?u=kkossev [profileId:"0104", endpointId:"01", inClusters:"0001,0500,0003,0000", outClusters:"1000,0006,0019,000A", model:"TS0202", manufacturer:"_TZ3040_6ygjfyll", deviceJoinName: "Tuya TS0202 Motion Sensor"], // https://community.hubitat.com/t/tuya-motion-sensor-driver/72000/54?u=kkossev ], attributes: [ [at:"0x0500:0x0013", name:"sensitivity", type:"enum", rw: "rw", min:0, max:2, defaultValue:"2", unit:"", map:[0:"low", 1:"medium", 2:"high"], title:"Sensitivity", description:"PIR sensor sensitivity (update at the time motion is activated)"], [at:"0x0500:0xF001", name:"keepTime", type:"enum", rw: "rw", min:0, max:2, defaultValue:"0", unit:"seconds", map:[0:"30 seconds", 1:"60 seconds", 2:"120 seconds"], title:"Keep Time", description:"PIR keep time in seconds (update at the time motion is activated)"], ], deviceJoinName: "Tuya TS0202 Motion Sensor configurable", configuration : ["battery": false] ], // isMotionSwitch() "TS0202_MOTION_SWITCH": [ description : "Tuya Motion Sensor and Scene Switch", models : ["TS0202"], device : [type: "PIR", isIAS:true, powerSource: "battery", isSleepy:true], capabilities : ["MotionSensor":true, "IlluminanceMeasurement":true, "switch":true, "Battery":true], preferences : ["motionReset":true, "keepTime":false, "sensitivity":false, "luxThreshold":false], // keepTime is hardcoded 60 seconds, no sensitivity configuration fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0001,0500,EF00,0000", outClusters:"0019,000A", model:"TS0202", manufacturer:"_TZ3210_cwamkvua", deviceJoinName: "Tuya Motion Sensor and Scene Switch"] // vendor: 'Linkoze', model: 'LKMSZ001' ], tuyaDPs: [ [dp:101, name:'pushed', type:"enum", rw: "ro", min:0, max:2, defaultValue:"0", step:1, scale:1, map:[0:"pushed", 1:"doubleTapped", 2:"held"] , unit:"", title:"Presence state", description:'Presence state'], [dp:102, name:'illuminance', type:"number", rw: "ro", min:0, max:1, defaultValue:0, step:1, scale:1, unit:"lx", title:"illuminance", description:'illuminance'], ], deviceJoinName: "Tuya Motion Sensor and Scene Switch", configuration : ["battery": false] ], "TS0601_PIR_PRESENCE" : [ // isBlackPIRsensor() // https://github.com/zigpy/zha-device-handlers/issues/1618 description : "Tuya PIR Human Motion Presence Sensor (Black)", models : ["TS0601"], device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "Battery": true], preferences : ["fadingTime":"102","targetDistance":"105"], commands : ["resetStats":"resetStats", "resetPreferencesToDefaults":"resetPreferencesToDefaults"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_9qayzqa8", deviceJoinName: "Smart PIR Human Motion Presence Sensor (Black)"] // https://www.aliexpress.com/item/1005004296422003.html ], tuyaDPs: [ // TODO - defaults !! [dp:102, name:'fadingTime', type:"number", rw: "rw", min:24, max:300 , defaultValue:24, step:1, scale:1, unit:"seconds", title:"Fading time", description:'Fading(Induction) time'], [dp:105, name:'targetDistance', type:"enum", rw: "rw", min:0, max:9 , defaultValue:"6", step:1, scale:1, map:[0:"0.5 m", 1:"1.0 m", 2:"1.5 m", 3:"2.0 m", 4:"2.5 m", 5:"3.0 m", 6:"3.5 m", 7:"4.0 m", 8:"4.5 m", 9:"5.0 m"] , unit:"meters", title:"Target Distance", description:'Target Distance'], [dp:119, name:'motion', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", title:"Presence state", description:'Presence state'], [dp:141, name:'humanMotionState', type:"enum", rw: "ro", min:0, max:4 , defaultValue:"0", step:1, scale:1, map:[0:"none", 1:"presence", 2:"peaceful", 3:"small_move", 4:"large_move"] , unit:"", title:"Presence state", description:'Presence state'], ], deviceJoinName: "Tuya PIR Human Motion Presence Sensor LQ-CG01-RDR", configuration : ["battery": false] ], "TS0601_PIR_AIR" : [ // isHumanPresenceSensorAIR() - Human presence sensor AIR (PIR sensor!) - o_sensitivity, v_sensitivity, led_status, vacancy_delay, light_on_luminance_prefer, light_off_luminance_prefer, mode, luminance_level, reference_luminance, vacant_confirm_time description : "Tuya PIR Human Motion Presence Sensor AIR", models : ["TS0601"], device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "Battery": true], // TODO - check if battery powered? preferences : ["vacancyDelay":"103", "ledStatusAIR":"110", "detectionMode":"104", "vSensitivity":"101", "oSensitivity":"102", "lightOnLuminance":"107", "lightOffLuminance":"108" ], commands : ["resetStats":"resetStats"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_auin8mzr", deviceJoinName: "Tuya PIR Human Motion Presence Sensor AIR"] // Tuya LY-TAD-K616S-ZB ], tuyaDPs: [ // TODO - defaults !! [dp:101, name:'vSensitivity', type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", step:1, scale:1, map:[0:"Speed Priority", 1:"Standard", 2:"Accuracy Priority"] , unit:"-", title:"vSensitivity options", description:'V-Sensitivity mode'], [dp:102, name:'oSensitivity', type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", step:1, scale:1, map:[0:"Sensitive", 1:"Normal", 2:"Cautious"] , unit:"", title:"oSensitivity options", description:'O-Sensitivity mode'], [dp:103, name:'vacancyDelay', type:"number", rw: "rw", min:0, max:1000, defaultValue:10, step:1, scale:1, unit:"seconds", title:"Vacancy Delay", description:'Vacancy Delay'], [dp:104, name:'detectionMode', type:"enum", rw: "rw", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"General Model", 1:"Temporary Stay", 2:"Basic Detecton", 3:"PIR Sensor Test"] , unit:"", title:"Detection Mode", description:'Detection Mode'], [dp:105, name:'unacknowledgedTime', type:"number", rw: "ro", min:0, max:9 , defaultValue:7, step:1, scale:1, unit:"seconds", description:'unacknowledgedTime'], [dp:106, name:'illuminance', type:"number", rw: "ro", min:0, max:2000, defaultValue:0, step:1, scale:1, unit:"lx", title:"illuminance", description:'illuminance'], [dp:107, name:'lightOnLuminance', type:"number", rw: "rw", min:0, max:2000, defaultValue:0, step:1, scale:1, unit:"lx", title:"lightOnLuminance", description:'lightOnLuminance'], // Ligter, Medium, ... ?// TODO =- check range 0 - 10000 ? [dp:108, name:'lightOffLuminance', type:"number", rw: "rw", min:0, max:2000, defaultValue:0, step:1, scale:1, unit:"lx", title:"lightOffLuminance", description:'lightOffLuminance'], [dp:109, name:'luminanceLevel', type:"number", rw: "ro", min:0, max:2000, defaultValue:0, step:1, scale:1, unit:"lx", title:"luminanceLevel", description:'luminanceLevel'], // Ligter, Medium, ... ? [dp:110, name:'ledStatusAIR', type:"enum", rw: "rw", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0: "Switch On", 1:"Switch Off", 2: "Default"] , unit:"", title:"LED status", description:'Led status switch'], ], deviceJoinName: "Tuya PIR Human Motion Presence Sensor AIR", configuration : ["battery": false] ], "SONOFF_MOTION_IAS" : [ description : "Sonoff/eWeLink Motion sensor", models : ["eWeLink"], device : [type: "PIR", isIAS:true, powerSource: "battery", isSleepy:true], // very sleepy !! capabilities : ["MotionSensor": true, "Battery": true], preferences : ["motionReset":true, "keepTime":false, 'sensitivity':false], // just enable or disable showing the motionReset preference, no link to tuyaDPs or attributes map! fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,0003,0500,0001", outClusters:"0003", model:"ms01", manufacturer:"eWeLink", deviceJoinName: "eWeLink Motion Sensor"], // for testL 60 seconds re-triggering period! [profileId:"0104", endpointId:"01", inClusters:"0000,0003,0500,0001", outClusters:"0003", model:"msO1", manufacturer:"eWeLink", deviceJoinName: "eWeLink Motion Sensor"], // second variant [profileId:"0104", endpointId:"01", inClusters:"0000,0003,0500,0001", outClusters:"0003", model:"MS01", manufacturer:"eWeLink", deviceJoinName: "eWeLink Motion Sensor"] // third variant ], deviceJoinName: "Sonoff/eWeLink Motion sensor", configuration : [ "0x0001":[["bind":true], ["reporting":"0x21, 0x20, 3600, 7200, 0x02"]], // TODO - use the reproting values "0x0500":[["bind":false], ["sensitivity":false], ["keepTime":false]], // TODO - use in update function ] // battery percentage, min 3600, max 7200, UINT8, delta 2 ], "NONTUYA_MOTION_IAS" : [ description : "Other OEM Motion sensors (IAS)", models : ["MOT003","XXX"], device : [type: "PIR", isIAS:true, powerSource: "battery", isSleepy:true], capabilities : ["MotionSensor": true, "Battery": true], preferences : ["motionReset":true, "keepTime":"0x0500:0xF001", "sensitivity":"0x0500:0x0013"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,0001,0003,0020,0400,0402,0500", outClusters:"0019", model:"MOT003", manufacturer:"HiveHome.com", deviceJoinName: "Hive Motion Sensor"] // https://community.hubitat.com/t/hive-motion-sensors-can-we-get-custom-driver-sorted/108177?u=kkossev ], attributes: [ [at:"0x0500:0x0013", name:"sensitivity", type:"enum", rw: "rw", min:0, max:2, defaultValue:"2", unit:"", map:[0:"low", 1:"medium", 2:"high"], title:"Sensitivity", description:"PIR sensor sensitivity (update at the time motion is activated)"], [at:"0x0500:0xF001", name:"keepTime", type:"enum", rw: "rw", min:0, max:2, defaultValue:"0", unit:"seconds", map:[0:"30 seconds", 1:"60 seconds", 2:"120 seconds"], title:"Keep Time", description:"PIR keep time in seconds (update at the time motion is activated)"], ], deviceJoinName: "Other OEM Motion sensor (IAS)", configuration : ["battery": false] ], "---" : [ description : "--------------------------------------", models : [], fingerprints : [], ], // ------------------------------------------- mmWave Radars ------------------------------------------------// "TS0601_TUYA_RADAR" : [ // isZY_M100Radar() // spammy devices! description : "Tuya Human Presence mmWave Radar ZY-M100", models : ["TS0601"], device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "DistanceMeasurement":true], preferences : ["radarSensitivity":"2", "detectionDelay":"101", "fadingTime":"102", "minimumDistance":"3", "maximumDistance":"4"], commands : ["resetStats":"resetStats"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_ztc6ggyl", deviceJoinName: "Tuya ZigBee Breath Presence Sensor ZY-M100"], // KK [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_ztc6ggyl", deviceJoinName: "Tuya ZigBee Breath Presence Sensor ZY-M100"], // KK [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_ikvncluo", deviceJoinName: "Moes TuyaHuman Presence Detector Radar 2 in 1"], // jw970065 [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_lyetpprm", deviceJoinName: "Tuya ZigBee Breath Presence Sensor"], [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_wukb7rhc", deviceJoinName: "Moes Smart Human Presence Detector"], // https://www.moeshouse.com/collections/smart-sensor-security/products/smart-zigbee-human-presence-detector-pir-mmwave-radar-detection-sensor-ceiling-mount [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_jva8ink8", deviceJoinName: "AUBESS Human Presence Detector"], // https://www.aliexpress.com/item/1005004262109070.html [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_mrf6vtua", deviceJoinName: "Tuya Human Presence Detector"], // not tested [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_ar0slwnd", deviceJoinName: "Tuya Human Presence Detector"], // not tested [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_sfiy5tfs", deviceJoinName: "Tuya Human Presence Detector"], // not tested [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_holel4dk", deviceJoinName: "Tuya Human Presence Detector"], // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars/92441/280?u=kkossev [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_xpq2rzhq", deviceJoinName: "Tuya Human Presence Detector"], // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars-w-healthstatus/92441/432?u=kkossev [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_qasjif9e", deviceJoinName: "Tuya Human Presence Detector"], // [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_xsm7l9xa", deviceJoinName: "Tuya Human Presence Detector"] // ], tuyaDPs: [ [dp:1, name:'motion', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", title:"Presence state", description:'Presence state'], [dp:2, name:'radarSensitivity', type:"number", rw: "rw", min:0, max:9 , defaultValue:7, step:1, scale:1, unit:"", title:"Radar sensitivity", description:'Sensitivity of the radar'], [dp:3, name:'minimumDistance', type:"decimal", rw: "rw", min:0.0, max:10.0, defaultValue:0.1, step:1, scale:100, unit:"meters", title:"Minimim detection distance", description:'Minimim (near) detection distance'], [dp:4, name:'maximumDistance', type:"decimal", rw: "rw", min:0.0, max:10.0, defaultValue:6.0, step:1, scale:100, unit:"meters", title:"Maximum detection distance", description:'Maximum (far) detection distance'], [dp:6, name:'radarStatus', type:"enum", rw: "ro", min:0, max:5 , defaultValue:"1", step:1, scale:1, map:[ 0:"checking", 1:"check_success", 2:"check_failure", 3:"others", 4:"comm_fault", 5:"radar_fault"] , unit:"TODO", title:"Radar self checking status", description:'Radar self checking status'], // radarSeradarSelfCheckingStatus[fncmd.toString()] [dp:9, name:"distance", type:"decimal", rw: "ro", min:0.0, max:10.0 , defaultValue:0.0, step:1, scale:100, unit:"meters", title:"Distance", description:'detected distance'], [dp:101, name:'detectionDelay', type:"decimal", rw: "rw", min:0.0, max:10.0, defaultValue:0.2, step:1, scale:10, unit:"seconds", title:"Detection delay", description:'Presence detection delay timer'], [dp:102, name:'fadingTime', type:"decimal", rw: "rw", min:0.5, max:500.0, defaultValue:60.0, step:1, scale:10, unit:"seconds", title:"Fading time", description:'Presence inactivity delay timer'], // aka 'nobody time' [dp:103, name:'debugCLI', type:"number", rw: "ro", min:0, max:99999, defaultValue:0, step:1, scale:1, unit:"?", title:"debugCLI", description:'debug CLI'], [dp:104, name:'illuminance', type:"number", rw: "ro", min:0, max:2000, defaultValue:0, step:1, scale:1, unit:"lx", title:"illuminance", description:'illuminance'], ], spammyDPsToIgnore : [9], spammyDPsToNotTrace : [9, 103], deviceJoinName: "Tuya Human Presence Detector ZY-M100", configuration : [:] // status: completed ], "TS0601_KAPVNNLK_RADAR" : [ // 24GHz spammy radar w/ battery backup - no illuminance! description : "Tuya TS0601_KAPVNNLK 24GHz Radar", // https://www.amazon.com/dp/B0CDRBX1CQ?psc=1&ref=ppx_yo2ov_dt_b_product_details // https://www.aliexpress.com/item/1005005834366702.html // https://github.com/Koenkk/zigbee2mqtt/issues/18632 models : ["TS0601"], // https://www.aliexpress.com/item/1005005858609756.html // https://www.aliexpress.com/item/1005005946786561.html // https://www.aliexpress.com/item/1005005946931559.html device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "DistanceMeasurement":true, "HumanMotionState":true], preferences : ["radarSensitivity":"15", "fadingTime":"12", "maximumDistance":"13", "smallMotionDetectionSensitivity":"16"], commands : ["resetStats":"resetStats", "initialize":"initialize", "updateAllPreferences": "updateAllPreferences"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_kapvnnlk", deviceJoinName: "Tuya 24 GHz Human Presence Detector NEW"] // https://community.hubitat.com/t/tuya-smart-human-presence-sensor-micromotion-detect-human-motion-detector-zigbee-ts0601-tze204-sxm7l9xa/111612/71?u=kkossev ], tuyaDPs: [ [dp:1, name:'motion', type:"enum", rw: "ro", min:0, max:1 , defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", title:"Presence state", description:'Presence state'], [dp:11, name:"humanMotionState", type:"enum", rw: "ro", min:0, max:2, defaultValue:"0", map:[0:"none", 1:"small_move", 2:"large_move"], description:'Human motion state'], // "none", "small_move", "large_move"] [dp:12, name:'fadingTime', type:"number", rw: "rw", min:3, max:600, defaultValue:60, step:1, scale:1, unit:"seconds", title:"Fading time", description:'Presence inactivity delay timer'], // aka 'nobody time' [dp:13, name:'maximumDistance', type:"decimal", rw: "rw", min:1.5, max:6.0, defaultValue:4.0, step:75, scale:100, unit:"meters", title:"Maximum detection distance", description:'Maximum (far) detection distance'], // aka 'Large motion detection distance' [dp:15, name:'radarSensitivity', type:"number", rw: "rw", min:0, max:7 , defaultValue:5, step:1, scale:1, unit:"", title:"Radar sensitivity", description:'Large motion detection sensitivity of the radar'], [dp:16 , name:'smallMotionDetectionSensitivity', type:"number", rw: "rw", min:0, max:7, defaultValue:5, step:1, scale:1, unit:"", title:"Small motion sensitivity", description:'Small motion detection sensitivity'], [dp:19, name:"distance", type:"decimal", rw: "ro", min:0.0, max:10.0, defaultValue:0.0, step:1, scale:100, unit:"meters", title:"Distance", description:'detected distance'], [dp:101, name:'batteryLevel', type:"number", rw: "ro", min:0, max:100, defaultValue:100, step:1, scale:1, unit:"%", title:"Battery level", description:'Battery level'] ], spammyDPsToIgnore : [19], spammyDPsToNotTrace : [19], deviceJoinName: "Tuya 24 GHz Human Presence Detector NEW", configuration : [:] ], // https://github.com/Koenkk/zigbee-herdsman-converters/blob/f277bef2f84d50aea70c25261db0c2ded84b7396/src/devices/tuya.ts#L4164 "TS0601_RADAR_MIR-HE200-TY" : [ // Human presence sensor radar 'MIR-HE200-TY' - illuminance, presence, occupancy, motion_speed, motion_direction, radar_sensitivity, radar_scene ('default', 'area', 'toilet', 'bedroom', 'parlour', 'office', 'hotel') description : "Tuya Human Presence Sensor MIR-HE200-TY", models : ["TS0601"], device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true], preferences : ["radarSensitivity":"2", "tumbleSwitch":"105", "tumbleAlarmTime":"106", /*"radarScene":"112",*/ "fallSensitivity":"118"], commands : ["resetStats":"resetStats"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_vrfecyku", deviceJoinName: "Tuya Human presence sensor MIR-HE200-TY"], [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_lu01t0zl", deviceJoinName: "Tuya Human presence sensor with fall function"], [profileId:"0104", endpointId:"01", inClusters:"0000,0004,0005,EF00", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_ypprdwsl", deviceJoinName: "Tuya Human presence sensor with fall function"] ], tuyaDPs: [ [dp:1, name:'motion', type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", title:"Presence state", description:'Presence state'], [dp:2, name:'radarSensitivity', type:"number", rw: "rw", min:0, max:10, defaultValue:7, step:1, scale:1, unit:"", title:"Radar sensitivity", description:'Sensitivity of the radar'], [dp:102, name:'motionState', type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", title:"Motion state", description:'Motion state (occupancy)'], [dp:103, name:'illuminance', type:"number", rw: "ro", min:0, max:2000, defaultValue:0, step:1, scale:1, unit:"lx", title:"illuminance", description:'illuminance'], [dp:105, name:'tumbleSwitch', type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", step:1, scale:1, map:[0:"OFF", 1:"ON"] , unit:"", title:"Tumble status switch", description:'Tumble status switch'], [dp:106, name:'tumbleAlarmTime', type:"number", rw: "rw", min:1, max:5, defaultValue:1, step:1, scale:1, unit:"minutes", title:"Tumble alarm time", description:'Tumble alarm time'], [dp:112, name:'radarScene', type:"enum", rw: "rw", min:0, max:6, defaultValue:"0", step:1, scale:1, map:[ 0:"default", 1:"area", 2:"toilet", 3:"bedroom", 4:"parlour", 5:"office", 6:"hotel"] , unit:"-", title:"Radar Presets", description:'Presets for sensitivity for presence and movement'], [dp:114, name:'motionDirection', type:"enum", rw: "ro", min:0, max:2, defaultValue:"0", step:1, scale:1, map:[ 0:"standing_still", 1:"moving_forward", 2:"moving_backward"] , unit:"-", title:"Movement direction", description:'direction of movement from the point of view of the radar'], [dp:115, name:'motionSpeed', type:"number", rw: "ro", min:0, max:9999, defaultValue:0, step:1, scale:1, unit:"-", title:"Movement speed", description:'Speed of movement'], [dp:116, name:'fallDownStatus', type:"enum", rw: "ro", min:0, max:2, defaultValue:"0", step:1, scale:1, map:[ 0:"none", 1:"maybe_fall", 2:"fall"] , unit:"-", title:"Fall down status", description:'Fall down status'], //[dp:117, name:'staticDwellAalarm', type:"text", rw: "ro", min:0, max:9999, defaultValue:0, step:1, scale:1, unit:"-", title:"Static dwell alarm", description:'Static dwell alarm'], [dp:118, name:'fallSensitivity', type:"number", rw: "rw", min:1, max:10, defaultValue:7, step:1, scale:1, unit:"", title:"Fall sensitivity", description:'Fall sensitivity of the radar'], ], deviceJoinName: "Tuya Human Presence Sensor MIR-HE200-TY", configuration : [:] ], "TS0601_BLACK_SQUARE_RADAR" : [ // // 24GHz Big Black Square Radar w/ annoying LED // isBlackSquareRadar() description : "Tuya Black Square Radar", models : ["TS0601"], device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor":true], preferences : ["indicatorLight":103], commands : ["resetStats":"resetStats"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_0u3bj3rc", deviceJoinName: "24GHz Black Square Human Presence Radar w/ LED"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_v6ossqfy", deviceJoinName: "24GHz Black Square Human Presence Radar w/ LED"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_mx6u6l4y", deviceJoinName: "24GHz Black Square Human Presence Radar w/ LED"] ], tuyaDPs: [ [dp:1, name:"motion", type:"enum", rw: "ro", min:0, max:1, defaultValue: "0", map:[0:"inactive", 1:"active"], description:'Presence'], [dp:101, name:'existance_time', type:"number", rw: "ro", min:0, max:9999, scale:1, unit:"minutes", description:'Shows the presence duration in minutes'], [dp:102, name:'leave_time', type:"number", rw: "ro", min:0, max:9999, scale:1, unit:"minutes", description:'Shows the duration of the absence in minutes'], [dp:103, name:'indicatorLight', type:"enum", rw: "rw", min:0, max:1, defaultValue: "0", map:[0:"OFF", 1:"ON"], title:"Indicator Light", description:'Turns the onboard LED on or off'] ], spammyDPsToIgnore : [103], // we don't need to know the LED status every 4 seconds! spammyDPsToNotTrace : [1, 101, 102, 103], // very spammy device - 4 packates are sent every 4 seconds! deviceJoinName: "24GHz Black Square Human Presence Radar w/ LED", ], "TS0601_YXZBRB58_RADAR" : [ // Seller: shenzhenshixiangchuangyeshiyey Manufacturer: Shenzhen Eysltime Intelligent LTD Item model number: YXZBRB58 isYXZBRB58radar() description : "Tuya YXZBRB58 Radar", models : ["TS0601"], device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "DistanceMeasurement":true], // https://github.com/Koenkk/zigbee2mqtt/issues/18318 preferences : ["radarSensitivity":"2", "detectionDelay":"103", "fadingTime":"102", "minimumDistance":"3", "maximumDistance":"4"], commands : ["resetStats":"resetStats"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_sooucan5", deviceJoinName: "Tuya Human Presence Detector YXZBRB58"] // https://www.amazon.com/dp/B0BYDCY4YN ], tuyaDPs: [ // TODO - use already defined DPs and preferences !! [dp:1, name:"motion", type:"enum", rw: "ro", min:0, max:2, defaultValue:"0", map:[0:"inactive", 1:"active"], description:'Presence state'], [dp:2, name:'radarSensitivity', type:"number", rw: "rw", min:0, max:9 , defaultValue:7, scale:1, unit:"", title:"Radar Sensitivity", description:'Sensitivity of the radar'], [dp:3, name:'minimumDistance', type:"decimal", rw: "rw", min:0.0, max:10.0, defaultValue:0.5, scale:100, unit:"meters", title:'Minimum distance', description:'Minimum detection distance'], [dp:4, name:'maximumDistance', type:"decimal", rw: "rw", min:0.0, max:10.0, defaultValue:6.0, scale:100, unit:"meters", title:'Maximum distance', description:'Maximum detection distance'], [dp:101, name:'illuminance', type:"number", rw: "ro", scale:1, unit:"lx", description:'Illuminance'], [dp:102, name:'fadingTime', type:"number", rw: "rw", min:5, max:1500, defaultValue:60, step:1, scale:1, unit:"seconds", title:"Fading time", description:'Presence inactivity timer, seconds'], [dp:103, name:'detectionDelay', type:"decimal", rw: "rw", min:0.0, max:10.0, defaultValue:1.0, scale:10, unit:"seconds", title:'Detection delay', description:'Detection delay'], [dp:104, name:'radar_scene', type:"enum", rw: "rw", min:0, max:4, defaultValue:"0", map:[0:"default", 1:"bathroom", 2:"bedroom", 3:"sleeping"], description:'Presets for sensitivity for presence and movement'], // https://github.com/kirovilya/zigbee-herdsman-converters/blob/b9bb6695fdf5d26ab4195cca9fcb1f2bd73afa71/src/devices/tuya.ts [dp:105, name:"distance", type:"decimal", rw: "ro", min:0.0, max:10.0, scale:100, unit:"meters", description:'Distance'] ], // https://github.com/zigpy/zha-device-handlers/issues/2429 spammyDPsToIgnore : [105], spammyDPsToNotTrace : [105], deviceJoinName: "Tuya Human Presence Detector YXZBRB58", // https://www.aliexpress.com/item/1005005764168560.html configuration : [:] ], // isSXM7L9XAradar() // https://github.com/dresden-elektronik/deconz-rest-plugin/issues/6998#issuecomment-1612113340 "TS0601_SXM7L9XA_RADAR" : [ // https://gist.github.com/Koenkk/9295fc8afcc65f36027f9ab4d319ce64 description : "Tuya Human Presence Detector SXM7L9XA", // https://github.com/zigpy/zha-device-handlers/issues/2378#issuecomment-1558777494 models : ["TS0601"], // https://github.com/wzwenzhi/Wenzhi-ZigBee2mqtt/tree/main device : [type: "radar", powerSource: "dc", isSleepy:false], // https://github.com/wzwenzhi/Wenzhi-ZigBee2mqtt/blob/main/wenzhi_tuya_M100-230908.js capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "DistanceMeasurement":true], preferences : ["radarSensitivity":"106", "detectionDelay":"111", "fadingTime":"110", "minimumDistance":"108", "maximumDistance":"107"], commands : ["resetStats":"resetStats"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_sxm7l9xa", deviceJoinName: "Tuya Human Presence Detector SXM7L9XA"], // https://www.aliexpress.com/item/1005004788260949.html // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars-w-healthstatus/92441/539?u=kkossev [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_e5m9c5hl", deviceJoinName: "Tuya Human Presence Detector WZ-M100"] // https://www.aliexpress.com/item/1005004788260949.html // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars-w-healthstatus/92441/745?u=kkossev ], tuyaDPs: [ [dp:104, name:'illuminance', type:"number", rw: "ro", scale:1, unit:"lx", description:'illuminance'], [dp:105, name:"motion", type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", map:[0:"inactive", 1:"active"], description:'Presence state'], [dp:106, name:'radarSensitivity', type:"number", rw: "rw", min:1, max:10 , defaultValue:7, scale:1, unit:"", title:'Motion sensitivity', description:'Motion sensitivity'], [dp:107, name:'maximumDistance', type:"decimal", rw: "rw", min:0.0, max:9.5, defaultValue:6.0, scale:100, unit:"meters", title:'Maximum distance', description:'Max detection distance'], [dp:108, name:'minimumDistance', type:"decimal", rw: "rw", min:0.0, max:9.5, defaultValue:0.5, scale:100, unit:"meters", title:'Minimum distance', description:'Min detection distance'], // TODO - check DP! [dp:109, name:"distance", type:"decimal", rw: "ro", min:0.0, max:10.0, scale:100, unit:"meters", description:'Distance'], [dp:110, name:'fadingTime', type:"decimal", rw: "rw", min:0.5, max:150.0, defaultValue:60.0, step:5, scale:10, unit:"seconds", title: "Fading time", description:'Presence inactivity timer'], [dp:111, name:'detectionDelay', type:"decimal", rw: "rw", min:0.0, max:10.0, defaultValue:0.5, scale:10, unit:"seconds", title: "Detection delay", description:'Detection delay'] ], spammyDPsToIgnore : [109], spammyDPsToNotTrace : [109], deviceJoinName: "Tuya Human Presence Detector SXM7L9XA", configuration : [:], ], // isIJXVKHD0radar() '24G MmWave radar human presence motion sensor' "TS0601_IJXVKHD0_RADAR" : [ description : "Tuya Human Presence Detector IJXVKHD0", // https://github.com/Koenkk/zigbee-herdsman-converters/blob/5acadaf16b0e85c1a8401223ddcae3d31ce970eb/src/devices/tuya.ts#L5747 models : ["TS0601"], device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "DistanceMeasurement":true], preferences : ["radarSensitivity":"106", "staticDetectionSensitivity":"111", "fadingTime":"110", "maximumDistance":"107"], commands : ["resetStats":"resetStats"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_ijxvkhd0", deviceJoinName: "Tuya Human Presence Detector ZY-M100-24G"] // ], tuyaDPs: [ // https://github.com/Koenkk/zigbee2mqtt/issues/18237 [dp:1, name:"unknownDp1", type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", map:[0:"inactive", 1:"active"], description:'unknown state dp1'], [dp:2, name:"unknownDp2", type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", map:[0:"inactive", 1:"active"], description:'unknown state dp2'], [dp:104, name:'illuminance', type:"number", rw: "ro", scale:1, unit:"lx", description:'illuminance'], [dp:105, name:"humanMotionState", type:"enum", rw: "ro", min:0, max:2, defaultValue:"0", map:[0:"none", 1:"present", 2:"moving"], description:'Presence state'], [dp:106, name:'radarSensitivity', preProc:"divideBy10", type:"number", rw: "rw", min:1, max:9, defaultValue:2 , scale:1, unit:"", title:'Motion sensitivity', description:'Radar motion sensitivity
1 is highest, 9 is lowest!
'], [dp:107, name:'maximumDistance', type:"decimal", rw: "rw", min:1.5, max:5.5, defaultValue:5.5, scale:100, unit:"meters", title:'Maximum distance', description:'Max detection distance'], [dp:109, name:'distance', type:"decimal", rw: "ro", min:0.0, max:10.0, defaultValue:0.0, scale:100, unit:"meters", description:'Target distance'], [dp:110, name:'fadingTime', type:"number", rw: "rw", min:1, max:1500, defaultValue:5, scale:1, unit:"seconds", title:'', description:'Delay (fading) time'], [dp:111, name:'staticDetectionSensitivity', preProc:"divideBy10", type:"number", rw: "rw", min:1, max:9, defaultValue:3, scale:1, unit:"", title:'Static detection sensitivity', description:'Presence sensitivity
1 is highest, 9 is lowest!
'], [dp:112, name:'motion', type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", title:"Presence state", description:'Presence state'], [dp:123, name:"presence", type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", map:[0:"none", 1:"presence"], description:'Presence'] // TODO -- check if used? ], spammyDPsToIgnore : [109,9], // dp 9 test spammyDPsToNotTrace : [109, 104], // illuminance reporting is extremly spammy ! deviceJoinName: "Tuya Human Presence Detector ZY-M100-24G", configuration : [:] ], /* SmartLife radarSensitivity staticDetectionSensitivity L1 7 9 L2 6 7 L3 4 6 L4 2 4 L5 2 3 */ "TS0601_YENSYA2C_RADAR" : [ // Loginovo Zigbee Mmwave Human Presence Sensor (rectangular) // TODO: update thread first post description : "Tuya Human Presence Detector YENSYA2C", // https://github.com/Koenkk/zigbee2mqtt/issues/18646 models : ["TS0601"], // https://www.aliexpress.com/item/1005005677110270.html device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "DistanceMeasurement":true], preferences : ["radarSensitivity":"101", "presence_time":"12","detectionDelay":"102", "fadingTime":"116", "minimumDistance": "111", "maximumDistance":"112"], commands : ["resetStats":"resetStats"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_yensya2c", deviceJoinName: "Tuya Human Presence Detector YENSYA2C"], // [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_mhxn2jso", deviceJoinName: "Tuya Human Presence Detector New"] // https://github.com/Koenkk/zigbee2mqtt/issues/18623 // ^^^similar DPs, but not a full match? ^^^ TODO - make a new profile ? // https://raw.githubusercontent.com/kvazis/training/master/z2m_converters/converters/_TZE204_mhxn2jso.js ], tuyaDPs: [ [dp:1, name:'motion', type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", title:"Presence state", description:'Presence state'], [dp:12, name:"presence_time", type:"decimal", rw: "rw", min:0.1, max:360.0, defaultValue:0.5, scale:10, unit:"seconds", title:'Presence time', description:'Presence time'], // for _TZE204_mhxn2jso [dp:19, name:"distance", type:"decimal", rw: "ro", min:0.0, max:10.0, defaultValue:0.0, scale:100, unit:"meters", description:'Distance'], [dp:20, name:"illuminance", type:"number", rw: "ro", min:0, max:10000, scale:1, unit:"lx", description:'illuminance'], [dp:101, name:"radarSensitivity", type:"number", rw: "rw", min:0, max:10, defaultValue:7, scale:1, unit:"", title:'Radar sensitivity', description:'Radar sensitivity'], [dp:102, name:"detectionDelay", type:"decimal", rw: "rw", min:0.5, max:360.0, defaultValue:1.0, scale:10, unit:"seconds", title:'Delay time', description:'Presence detection delay time'], [dp:111, name:"minimumDistance", type:"decimal", rw: "rw", min:0.0, max:10.0, defaultValue:0.1, scale:100, unit:"meters", title:'Minimum distance', description:'Breath detection minimum distance'], [dp:112, name:"maximumDistance", type:"decimal", rw: "rw", min:0.5, max:10.0, defaultValue:7.0, scale:100, unit:"meters", title:'Maximum distance', description:'Breath detection maximum distance'], [dp:113, name:"breathe_flag", type:"enum", rw: "rw"], [dp:114, name:"small_flag", type:"enum", rw: "rw"], [dp:115, name:"large_flag", type:"enum", rw: "rw"], [dp:116, name:"fadingTime", type:"number", rw: "rw", min:0, max:3600, defaultValue:30, scale:1, unit:"seconds", title:'Fading time', description:'Presence (fading) delay time'] ], spammyDPsToIgnore : [19], spammyDPsToNotTrace : [19], deviceJoinName: "Tuya Human Presence Detector YENSYA2C", configuration : [:] ], // the new 5.8 GHz radar w/ humanMotionState and a lot of configuration options, 'not-so-spammy' ! - pedestal mount form-factor "TS0225_HL0SS9OA_RADAR" : [ description : "Tuya TS0225_HL0SS9OA Radar", // https://www.aliexpress.com/item/1005005761971083.html models : ["TS0225"], device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "HumanMotionState":true], preferences : ["presenceKeepTime":"12", "ledIndicator":"24", "radarAlarmMode":"105", "radarAlarmVolume":"102", "radarAlarmTime":"101", \ /*"textLargeMotion":"NONE",*/ "motionFalseDetection":"112", "motionDetectionSensitivity":"15", "motionMinimumDistance":"106", "motionDetectionDistance":"13", \ /*"textSmallMotion":"NONE",*/ "smallMotionDetectionSensitivity":"16", "smallMotionMinimumDistance":"107", "smallMotionDetectionDistance":"14", \ /*"textStaticDetection":"NONE",*/ "breatheFalseDetection":"115", "staticDetectionSensitivity":"104", "staticDetectionMinimumDistance":"108", "staticDetectionDistance":"103" \ ], commands : ['resetSettings':'resetSettings', 'moveSelfTest':'moveSelfTest', 'smallMoveSelfTest':'smallMoveSelfTest', 'breatheSelfTest':'breatheSelfTest', \ "resetStats":"resetStats", "initialize":"initialize", "updateAllPreferences": "updateAllPreferences" ], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,0003,0500,E002,EF00", outClusters:"0019,000A", model:"TS0225", manufacturer:"_TZE200_hl0ss9oa", deviceJoinName: "Tuya TS0225_HL0SS9OA Human Presence Detector"] // https://www.aliexpress.com/item/1005004788260949.html // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars-w-healthstatus/92441/539?u=kkossev ], tuyaDPs: [ // W.I.P - use already defined DPs and preferences !! TODO - verify teh default values ! [dp:1, name:'motion', type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", title:"Presence state", description:'Presence state'], [dp:11, name:"humanMotionState", type:"enum", rw: "ro", min:0, max:3, defaultValue:"0", map:[0:"none", 1:"large", 2:"small", 3:"static"], description:'Human motion state'], [dp:12, name:'presenceKeepTime', type:"number", rw: "rw", min:5, max:3600, defaultValue:30, step:1, scale:1, unit:"seconds", title:'Presence keep time', description:'Presence keep time'], [dp:13, name:'motionDetectionDistance', type:"decimal", rw: "rw", min:0.0, max:10.0, defaultValue:6.0, step:1, scale:100, unit:"meters", title:"Motion Detection Distance", description:'Large motion detection distance, meters'], //dt: "UINT16" [dp:14, name:'smallMotionDetectionDistance', type:"decimal", rw: "rw", min:0.0, max:6.0, defaultValue:5.0, step:1, scale:100, unit:"meters", title:'Small motion detection distance', description:'Small motion detection distance'], [dp:15, name:'motionDetectionSensitivity', type:"number", rw: "rw", min:0, max:10, defaultValue:7, step:1, scale:1, unit:"", title:"Motion Detection Sensitivity", description:'Large motion detection sensitivity'], // dt: "UINT8" aka Motionless Detection Sensitivity [dp:16, name:'smallMotionDetectionSensitivity', type:"number", rw: "rw", min:0, max:10 , defaultValue:7, step:1, scale:1, unit:"", title:'Small motion detection sensitivity', description:'Small motion detection sensitivity'], [dp:20, name:'illuminance', type:"number", rw: "ro", scale:10, unit:"lx", description:'Illuminance'], [dp:24, name:"ledIndicator", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"0 - OFF", 1:"1 - ON"], title:'LED indicator mode', description:'LED indicator mode'], [dp:101, name:'radarAlarmTime', type:"number", rw: "rw", min:0, max:60 , defaultValue:1, step:1, scale:1, unit:"seconds", title:'Alarm time', description:'Alarm time'], [dp:102, name:"radarAlarmVolume", type:"enum", rw: "rw", min:0, max:3, defaultValue:"3", map:[0:"0 - low", 1:"1 - medium", 2:"2 - high", 3:"3 - mute"], title:'Alarm volume', description:'Alarm volume'], [dp:103, name:'staticDetectionDistance', type:"decimal", rw: "rw", min:0.0, max:6.0, defaultValue:4.0, step:1, scale:100, unit:"meters", title:'Static detection distance', description:'Static detection distance'], [dp:104, name:'staticDetectionSensitivity', type:"number", rw: "rw", min:0, max:10, defaultValue:7, step:1, scale:1, unit:"", title: "Static Detection Sensitivity", description:'Static detection sensitivity'], // dt: "UINT8", aka Motionless Detection Sensitivity [dp:105, name:"radarAlarmMode", type:"enum", rw: "rw", min:0, max:3, defaultValue:"1", map:[0:"0 - arm", 1:"1 - off", 2:"2 - alarm", 3:"3 - doorbell"], title:'Alarm mode', description:'Alarm mode'], [dp:106, name:'motionMinimumDistance', type:"decimal", rw: "rw", min:0.0, max:6.0, defaultValue:0.5, step:1, scale:100, unit:"meters", title:'Motion minimum distance', description:'Motion minimum distance'], [dp:107, name:'smallMotionMinimumDistance', type:"decimal", rw: "rw", min:0.0, max:6.0, defaultValue:0.5, step:1, scale:100, unit:"meters", title:'Small Motion Minimum Distance', description:'Small Motion Minimum Distance'], [dp:108, name:'staticDetectionMinimumDistance', type:"decimal", rw: "rw", min:0.0, max:6.0, defaultValue:0.5, step:1, scale:100, unit:"meters", title:'Static detection minimum distance', description:'Static detection minimum distance'], [dp:109, name:'checkingTime', type:"decimal", rw: "ro", scale:10, unit:"seconds", description:'Checking time'], [dp:110, name:"radarStatus", type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", map:[0:"0 - disabled", 1:"1 - enabled"], description:'Radar small move self-test'], [dp:111, name:"radarStatus", type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", map:[0:"0 - disabled", 1:"1 - enabled"], description:'Radar breathe self-test'], [dp:112, name:"motionFalseDetection", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"0 - disabled", 1:"1 - enabled"], title:'Motion false detection', description:'Motion false detection'], [dp:113, name:"radarReset", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"0 - disabled", 1:"1 - enabled"], description:'Radar reset'], [dp:114, name:"radarStatus", type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", map:[0:"0 - disabled", 1:"1 - enabled"], description:'Radar move self-test'], [dp:115, name:"breatheFalseDetection", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"0 - disabled", 1:"1 - enabled"], title:'Breathe false detection', description:'Breathe false detection'], [dp:116, name:'existance_time', type:"number", rw: "ro", min:0, max:60 , scale:1, unit:"seconds", description:'Radar presence duration'], // not received [dp:117, name:'leave_time', type:"number", rw: "ro", min:0, max:60 , scale:1, unit:"seconds", description:'Radar absence duration'], // not received [dp:118, name:'radarDurationStatus', type:"number", rw: "ro", min:0, max:60 , scale:1, unit:"seconds", description:'Radar duration status'] // not received ], spammyDPsToIgnore : [], spammyDPsToNotTrace : [11], deviceJoinName: "Tuya TS0225_HL0SS9OA Human Presence Detector", configuration : [:] ], // the new 5.8GHz radar w/ humanMotionState and a lot of configuration options, 'not-so-spammy' ! - wall mount form-factor is2AAELWXKradar() "TS0225_2AAELWXK_RADAR" : [ // https://github.com/Koenkk/zigbee2mqtt/issues/18612 description : "Tuya TS0225_2AAELWXK 5.8 GHz Radar", // https://community.hubitat.com/t/the-new-tuya-24ghz-human-presence-sensor-ts0225-tze200-hl0ss9oa-finally-a-good-one/122283/72?u=kkossev models : ["TS0225"], // ZG-205Z https://github.com/Koenkk/zigbee-herdsman-converters/blob/38bf79304292c380dc8366966aaefb71ca0b03da/src/devices/tuya.ts#L4793 device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "HumanMotionState":true], // TODO - preferences and DPs !!!!!!!!!!!!!!!!!!!! preferences : ["presenceKeepTime":"102", "ledIndicator":"107", "radarAlarmMode":"117", "radarAlarmVolume":"116", "radarAlarmTime":"115", \ /*"textLargeMotion":"NONE",*/ "motionFalseDetection":"103", "motionDetectionSensitivity":"2", "motionMinimumDistance":"3", "motionDetectionDistance":"4", \ /*"textSmallMotion":"NONE",*/ "smallMotionDetectionSensitivity":"105", "smallMotionMinimumDistance":"110", "smallMotionDetectionDistance":"104", \ /*"textStaticDetection":"NONE",*/ "breatheFalseDetection":"113", "staticDetectionSensitivity":"109", /*"staticDetectionMinimumDistance":"108",*/ "staticDetectionDistance":"108" \ ], commands : ['resetSettings':'resetSettings', 'moveSelfTest':'moveSelfTest', 'smallMoveSelfTest':'smallMoveSelfTest', 'breatheSelfTest':'breatheSelfTest', \ "resetStats":"resetStats", "initialize":"initialize", "updateAllPreferences": "updateAllPreferences", "resetPreferencesToDefaults":"resetPreferencesToDefaults", "validateAndFixPreferences":"validateAndFixPreferences" \ ], fingerprints : [ // reports illuminance and motion using clusters 0x400 and 0x500 ! [profileId:"0104", endpointId:"01", inClusters:"0000,0003,0500,E002,EF00,EE00,E000,0400", outClusters:"0019,000A", model:"TS0225", manufacturer:"_TZE200_2aaelwxk", deviceJoinName: "Tuya TS0225_2AAELWXK 24Ghz Human Presence Detector"] // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars-w-healthstatus/92441/539?u=kkossev ], tuyaDPs: [ // W.I.P. - use already defined DPs and preferences !! [dp:1, name:'motion', type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", title:"Presence state", description:'Presence state'], [dp:2, name:'motionDetectionSensitivity', type:"number", rw: "rw", min:0, max:10, defaultValue:7, step:1, scale:1, unit:"", title:"Motion Detection Sensitivity", description:'Large motion detection sensitivity'], // dt: "UINT8" aka Motionless Detection Sensitivity [dp:3, name:'motionMinimumDistance', type:"decimal", rw: "rw", min:0.0, max:6.0, defaultValue:0.5, step:1, scale:100, unit:"meters", title:'Motion minimum distance', description:'Motion minimum distance'], [dp:4, name:'motionDetectionDistance', type:"decimal", rw: "rw", min:0.0, max:10.0, defaultValue:6.0, step:1, scale:100, unit:"meters", title:"Motion Detection Distance", description:'Large motion detection distance, meters'], //dt: "UINT16" [dp:101, name:"humanMotionState", type:"enum", rw: "ro", min:0, max:3, defaultValue:"0", map:[0:"none", 1:"large", 2:"small", 3:"static"], description:'Human motion state'], [dp:102, name:'presenceKeepTime', type:"number", rw: "rw", min:5, max:3600, defaultValue:30, step:1, scale:1, unit:"seconds", title:'Presence keep time', description:'Presence keep time'], [dp:103, name:"motionFalseDetection", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"0 - disabled", 1:"1 - enabled"], title:'Motion false detection', description:'Disable/enable Motion false detection'], [dp:104, name:'smallMotionDetectionDistance', type:"decimal", rw: "rw", min:0.0, max:6.0, defaultValue:5.0, step:1, scale:100, unit:"meters", title:'Small motion detection distance', description:'Small motion detection distance'], [dp:105, name:'smallMotionDetectionSensitivity', type:"number", rw: "rw", min:0, max:10 , defaultValue:7, step:1, scale:1, unit:"", title:'Small motion detection sensitivity', description:'Small motion detection sensitivity'], [dp:106, name:'illuminance', type:"number", rw: "ro", scale:10, unit:"lx", description:'Illuminance'], [dp:107, name:"ledIndicator", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"0 - OFF", 1:"1 - ON"], title:'LED indicator', description:'LED indicator mode'], [dp:108, name:'staticDetectionDistance', type:"decimal", rw: "rw", min:0.0, max:6.0, defaultValue:4.0, step:1, scale:100, unit:"meters", title:'Static detection distance', description:'Static detection distance'], [dp:109, name:'staticDetectionSensitivity', type:"number", rw: "rw", min:0, max:10, defaultValue:7, step:1, scale:1, unit:"", title:"Static Detection Sensitivity", description:'Static detection sensitivity'], // dt: "UINT8", aka Motionless Detection Sensitivity [dp:110, name:'smallMotionMinimumDistance', type:"decimal", rw: "rw", min:0.0, max:6.0, defaultValue:0.5, step:1, scale:100, unit:"meters", title:'Small Motion Minimum Distance', description:'Small Motion Minimum Distance'], //[dp:111, name:'staticDetectionMinimumDistance', type:"decimal", rw: "rw", min:0.0, max:6.0, defaultValue:0.5, step:1, scale:100, unit:"meters", title:'Static detection minimum distance', description:'Static detection minimum distance'], [dp:112, name:"radarReset", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"0 - disabled", 1:"1 - enabled"], description:'Radar reset'], [dp:113, name:"breatheFalseDetection", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"0 - disabled", 1:"1 - enabled"], title:'Breathe false detection', description:'Disable/enable Breathe false detection'], [dp:114, name:'checkingTime', type:"decimal", rw: "ro", scale:10, unit:"seconds", description:'Checking time'], [dp:115, name:'radarAlarmTime', type:"number", rw: "rw", min:0, max:60 , defaultValue:1, step:1, scale:1, unit:"seconds", title:'Alarm time', description:'Alarm time'], [dp:116, name:"radarAlarmVolume", type:"enum", rw: "rw", min:0, max:3, defaultValue:"3", map:[0:"0 - low", 1:"1 - medium", 2:"2 - high", 3:"3 - mute"], title:'Alarm volume', description:'Alarm volume'], [dp:117, name:"radarAlarmMode", type:"enum", rw: "rw", min:0, max:3, defaultValue:"1", map:[0:"0 - arm", 1:"1 - off", 2:"2 - alarm", 3:"3 - doorbell"], title:'Alarm mode', description:'Alarm mode'], [dp:118, name:"radarStatus", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"0 - disabled", 1:"1 - enabled"], description:'Radar small move self-test'], [dp:119, name:"radarStatus", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"0 - disabled", 1:"1 - enabled"], description:'Radar breathe self-test'], [dp:120, name:"radarStatus", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"0 - disabled", 1:"1 - enabled"], description:'Radar move self-test'] /* [dp:116, name:'existance_time', type:"number", rw: "ro", min:0, max:60 , scale:1, unit:"seconds", description:'Radar presence duration'], // not received [dp:117, name:'leave_time', type:"number", rw: "ro", min:0, max:60 , scale:1, unit:"seconds", description:'Radar absence duration'], // not received [dp:118, name:'radarDurationStatus', type:"number", rw: "ro", min:0, max:60 , scale:1, unit:"seconds", description:'Radar duration status'] // not received */ ], deviceJoinName: "Tuya TS0225_2AAELWXK 5.8 Ghz Human Presence Detector", configuration : [:] ], // isSBYX0LM6radar() // https://github.com/Koenkk/zigbee-herdsman-converters/issues/5930#issuecomment-1662456347 "TS0601_SBYX0LM6_RADAR" : [ // _TZE204_sbyx0lm6 TS0601 model: 'MTG075-ZB-RL', '5.8G Human presence sensor with relay', description : "Tuya Human Presence Detector SBYX0LM6", // https://github.com/vit-um/hass/blob/main/zigbee2mqtt/tuya_h_pr.js models : ["TS0601"], // https://github.com/Koenkk/zigbee-herdsman-converters/issues/5930 https://github.com/Koenkk/zigbee-herdsman-converters/issues/5930#issuecomment-1651270524 device : [type: "radar", powerSource: "dc", isSleepy:false], // https://github.com/wzwenzhi/Wenzhi-ZigBee2mqtt/blob/main/ts0601_radar_X75-X25-230705.js capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "DistanceMeasurement":true], preferences : ["radarSensitivity":"2", "minimumDistance":"3", "maximumDistance":"4", "detectionDelay":"101", "fadingTime":"102", "entrySensitivity":"105", "entryDistanceIndentation":"106", "breakerMode":"107", \ "breakerStatus":"108", "statusIndication":"109", "illuminThreshold":"110", "breakerPolarity":"111", "blockTime":"112" ], commands : ['resetSettings':'resetSettings'], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_sbyx0lm6", deviceJoinName: "Tuya 5.8GHz Human Presence Detector MTG075-ZB-RL"], // https://www.aliexpress.com/item/1005004788260949.html // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars-w-healthstatus/92441/539?u=kkossev [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_sbyx0lm6", deviceJoinName: "Tuya 5.8GHz Human Presence Detector MTG075-ZB-RL"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_dtzziy1e", deviceJoinName: "Tuya 24GHz Human Presence Detector MTG275-ZB-RL"], // https://www.aliexpress.com/item/1005004788260949.html // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars-w-healthstatus/92441/539?u=kkossev [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_dtzziy1e", deviceJoinName: "Tuya 24GHz Human Presence Detector MTG275-ZB-RL"], // https://www.aliexpress.com/item/1005004788260949.html // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars-w-healthstatus/92441/539?u=kkossev [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_clrdrnya", deviceJoinName: "Tuya Human Presence Detector MTG235-ZB-RL"], // https://www.aliexpress.com/item/1005005865536713.html // https://github.com/Koenkk/zigbee2mqtt/issues/18677?notification_referrer_id=NT_kwDOAF5zfrI3NDQ1Mzc2NTAxOjYxODk5NTA [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_clrdrnya", deviceJoinName: "Tuya Human Presence Detector MTG235-ZB-RL"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_cfcznfbz", deviceJoinName: "Tuya Human Presence Detector MTG075-ZB2"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_iaeejhvf", deviceJoinName: "Tuya Human Presence Detector MTG075-ZB2-RL"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_mtoaryre", deviceJoinName: "Tuya Human Presence Detector MTG035-ZB2-RL"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_8s6jtscb", deviceJoinName: "Tuya Human Presence Detector MTG035-ZB2"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_rktkuel1", deviceJoinName: "Tuya Human Presence Detector MTD065-ZB2"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_mp902om5", deviceJoinName: "Tuya Human Presence Detector MTG075-ZB"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_mp902om5", deviceJoinName: "Tuya Human Presence Detector MTG075-ZB"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_w5y5slkq", deviceJoinName: "Tuya Human Presence Detector MTG275-ZB"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_w5y5slkq", deviceJoinName: "Tuya Human Presence Detector MTG275-ZB"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_xnaqu2pc", deviceJoinName: "Tuya Human Presence Detector MTD065-ZB"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_xnaqu2pc", deviceJoinName: "Tuya Human Presence Detector MTD065-ZB"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_wk7seszg", deviceJoinName: "Tuya Human Presence Detector MTG235-ZB"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_wk7seszg", deviceJoinName: "Tuya Human Presence Detector MTG235-ZB"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_0wfzahlw", deviceJoinName: "Tuya Human Presence Detector MTD021-ZB"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_0wfzahlw", deviceJoinName: "Tuya Human Presence Detector MTD021-ZB"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_pfayrzcw", deviceJoinName: "Tuya Human Presence Detector MTG035-ZB-RL"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_pfayrzcw", deviceJoinName: "Tuya Human Presence Detector MTG035-ZB-RL"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE200_z4tzr0rg", deviceJoinName: "Tuya Human Presence Detector MTG035-ZB"], [profileId:"0104", endpointId:"01", inClusters:"0004,0005,EF00,0000", outClusters:"0019,000A", model:"TS0601", manufacturer:"_TZE204_z4tzr0rg", deviceJoinName: "Tuya Human Presence Detector MTG035-ZB"] ], tuyaDPs: [ [dp:1, name:'motion', type:"enum", rw: "ro", min:0, max:1, defaultValue:"0", step:1, scale:1, map:[0:"inactive", 1:"active"] , unit:"", title:"Presence state", description:'Presence state'], [dp:2, name:'radarSensitivity', type:"number", rw: "rw", min:1, max:9, defaultValue:5, step:1, scale:1, unit:"", title:"Radar sensitivity", description:'Sensitivity of the radar'], [dp:3, name:'minimumDistance', type:"decimal", rw: "rw", min:0.0, max:10.0, defaultValue:0.1, step:10, scale:100, unit:"meters", title:"Minimum distance", description:'Shield range of the radar'], // was shieldRange [dp:4, name:'maximumDistance', type:"decimal", rw: "rw", min:1.5, max:10.0, defaultValue:7.0, step:10, scale:100, unit:"meters", title:"Maximum distance", description:'Detection range of the radar'], // was detectionRange [dp:6, name:'radarStatus', type:"enum", rw: "ro", min:0, max:5 , defaultValue:"1", step:1, scale:1, map:[ 0:"checking", 1:"check_success", 2:"check_failure", 3:"others", 4:"comm_fault", 5:"radar_fault"] , unit:"", title:"Radar self checking status", description:'Radar self checking status'], [dp:9, name:"distance", type:"decimal", rw: "ro", min:0.0, max:10.0, defaultValue:0.0, step:1, scale:100, unit:"meters", description:'detected distance'], [dp:101, name:'detectionDelay', type:"decimal", rw: "rw", min:0.0, max:1.0, defaultValue:0.2, step:1, scale:10, unit:"seconds", title:"Detection delay", description:'Entry filter time'], [dp:102, name:'fadingTime', type:"decimal", rw: "rw", min:0.5, max:150.0, defaultValue:30.0, step:1, scale:10, unit:"seconds", title:"Fading time", description:'Presence inactivity delay timer'], // aka 'nobody time' [dp:103, name:'debugCLI', type:"number", rw: "ro", min:0, max:99999, defaultValue:0, step:1, scale:1, unit:"?", title:"debugCLI", description:'debug CLI'], [dp:104, name:'illuminance', type:"number", rw: "ro", min:0, max:2000, defaultValue:0, step:1, scale:10, unit:"lx", title:"illuminance", description:'illuminance'], // divideBy10 ! [dp:105, name:'entrySensitivity', type:"number", rw: "rw", min:1, max:9, defaultValue:5, step:1, scale:1, unit:"", title:"Entry sensitivity", description:'Radar entry sensitivity'], [dp:106, name:'entryDistanceIndentation', type:"decimal", rw: "rw", min:0.0, max:10.0, defaultValue:6.0, step:10, scale:100, unit:"meters", title:"Entry distance indentation", description:'Entry distance indentation'], // aka 'Detection range reduce when unoccupied' [dp:107, name:"breakerMode", type:"enum", rw: "rw", min:0, max:3, defaultValue:"0", map:[0:"standalone", 1:"local", 2:"manual", 3:"unavailable"], title:'Breaker mode', description:'Status Breaker mode: standalone is external, local is auto'], [dp:108, name:"breakerStatus", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"OFF", 1:"ON"], title:'Breaker status', description:'on/off state of the switch'], [dp:109, name:"statusIndication", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"OFF", 1:"ON"], title:'Status indication', description:'Led backlight when triggered'], [dp:110, name:'illuminThreshold', type:"decimal", rw: "rw", min:0.0, max:420.0, defaultValue:100.0, step:1, scale:10, unit:"lx", title:"Illuminance Threshold", description:'Illumination threshold for switching on'], [dp:111, name:"breakerPolarity", type:"enum", rw: "rw", min:0, max:1, defaultValue:"0", map:[0:"NC", 1:"NO"], title:'Breaker polarity', description:'Normally open / normally closed factory setting'], [dp:112, name:'blockTime', type:"number", rw: "rw", min:0, max:100, defaultValue:30, step:1, scale:1, unit:"seconds", title:"Block time'", description:'Sensor inhibition time after presence or relay state changed'], // aka 'nobody time' [dp:113, name:'parameterSettingResult', type:"enum", rw: "ro", min:0, max:6 , defaultValue:"1", step:1, scale:1, map:[ 0:"none", 1:"invalid detection range reduce", 2:"invalid minimum detection range", 3:"invalid maximum detection range", 4:"switch unavailable", 5:"invalid inhibition time", 6:"switch polarity unsupported"] , unit:"", description:'Config error'], [dp:114, name:'factoryParameters', type:"number", rw: "ro", scale:1, unit:"-", description:'Factory Reset'], [dp:115, name:'sensor', type:"enum", rw: "ro", min:0, max:2, defaultValue:"0", step:1, scale:1, map:[0:"on", 1:"off", 2:"report occupy", 3:"report unoccupy"] , unit:"", description:'Sensor state'], ], spammyDPsToIgnore : [9], spammyDPsToNotTrace : [9], deviceJoinName: "Tuya Human Presence Detector SBYX0LM6", configuration : [:] ], // isLINPTECHradar() "TS0225_LINPTECH_RADAR" : [ // https://github.com/Koenkk/zigbee2mqtt/issues/18637 description : "Tuya TS0225_LINPTECH 24GHz Radar", // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars-w-healthstatus/92441/646?u=kkossev models : ["TS0225"], device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "DistanceMeasurement":true], preferences : ["fadingTime":"101", "motionDetectionDistance":"0xE002:0xE00B","motionDetectionSensitivity":"0xE002:0xE004", "staticDetectionSensitivity":"0xE002:0xE005"], fingerprints : [ // https://www.amazon.com/dp/B0C7C6L66J?ref=ppx_yo2ov_dt_b_product_details&th=1 [profileId:"0104", endpointId:"01", inClusters:"0000,0003,0004,0005,E002,4000,EF00,0500", outClusters:"0019,000A", model:"TS0225", manufacturer:"_TZ3218_awarhusb", deviceJoinName: "Tuya TS0225_LINPTECH 24Ghz Human Presence Detector"] // https://www.aliexpress.com/item/1005004788260949.html // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars-w-healthstatus/92441/539?u=kkossev ], commands : ["resetStats":"resetStats", 'refresh':'refresh', "initialize":"initialize", "updateAllPreferences": "updateAllPreferences", "resetPreferencesToDefaults":"resetPreferencesToDefaults", "validateAndFixPreferences":"validateAndFixPreferences"], tuyaDPs: [ // the tuyaDPs revealed from iot.tuya.com are actually not used by the device! The only exception is dp:101 [dp:101, name:'fadingTime', type:"number", rw: "rw", min:1, max:9999, defaultValue:10, step:1, scale:1, unit:"seconds", title: "Fading time", description:'Presence inactivity timer, seconds'] // aka 'nobody time' ], attributes: [ // LINPTECH / MOES are using a custom cluster 0xE002 for the settings (except for the fadingTime), ZCL cluster 0x0400 for illuminance (malformed reports!) and the IAS cluster 0x0500 for motion detection [at:"0xE002:0xE001", name:'existance_time', type:"number", dt: "UINT16", rw: "ro", min:0, max:65535, step:1, scale:1, unit:"minutes", title: "Existance time/b>", description:'existance (presence) time, recommended value is > 10 seconds!'], // aka Presence Time [at:"0xE002:0xE004", name:'motionDetectionSensitivity', type:"enum", dt: "UINT8", rw: "rw", min:1, max:5, defaultValue:"4", step:1, scale:1, map:[1: "1 - low", 2: "2 - medium low", 3: "3 - medium", 4: "4 - medium high", 5: "5 - high"], unit:"", title: "Motion Detection Sensitivity", description:'Large motion detection sensitivity'], // aka Motionless Detection Sensitivity [at:"0xE002:0xE005", name:'staticDetectionSensitivity', type:"enum", dt: "UINT8", rw: "rw", min:1, max:5, defaultValue:"3", step:1, scale:1, map:[1: "1 - low", 2: "2 - medium low", 3: "3 - medium", 4: "4 - medium high", 5: "5 - high"], unit:"", title: "Static Detection Sensitivity", description:'Static detection sensitivity'], // aka Motionless Detection Sensitivity [at:"0xE002:0xE00A", name:"distance", type:"decimal", dt: "UINT16", rw: "ro", min:0.0, max:6.0, defaultValue:0.0, scale:100, unit:"meters", title: "Distance", description:'Measured distance'], // aka Current Distance [at:"0xE002:0xE00B", name:'motionDetectionDistance', type:"enum", dt: "UINT16", rw: "rw", min:0.75, max:6.0, defaultValue:"4.50", step:75, scale:100, map:["0.75": "0.75 meters","1.50": "1.50 meters", "2.25": "2.25 meters", "3.00": "3.00 meters", "3.75": "3.75 meters", "4.50": "4.50 meters", "5.25": "5.25 meters", "6.00" : "6.00 meters"], unit:"meters", title: "Motion Detection Distance", description:'Large motion detection distance, meters'] // aka Far Detection ], spammyDPsToIgnore : [19], // TODO spammyDPsToNotTrace : [19], // TODO deviceJoinName: "Tuya TS0225_LINPTECH 24Ghz Human Presence Detector", configuration : [:] ], // no-name 240V AC ceiling radar presence sensor "TS0225_EGNGMRZH_RADAR" : [ // https://github.com/sprut/Hub/issues/2489 description : "Tuya TS0225_EGNGMRZH 24GHz Radar", // isEGNGMRZHradar() models : ["TS0225"], device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "DistanceMeasurement":true], preferences : ["radarSensitivity":"101", "presence_time":"12","detectionDelay":"102", "fadingTime":"116", "minimumDistance": "111", "maximumDistance":"112"], commands : ["resetStats":"resetStats"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,,0500,1000,EF00,0003,0004,0008", outClusters:"0019,000A", model:"TS0225", manufacturer:"_TZFED8_egngmrzh", deviceJoinName: "Tuya TS0225_EGNGMRZH 24Ghz Human Presence Detector"] // https://www.aliexpress.com/item/1005004788260949.html // https://community.hubitat.com/t/release-tuya-zigbee-multi-sensor-4-in-1-pir-motion-sensors-and-mmwave-presence-radars-w-healthstatus/92441/539?u=kkossev ], // uses IAS for occupancy! tuyaDPs: [ [dp:101, name:"illuminance", type:"number", rw: "ro", min:0, max:10000, scale:1, unit:"lx"], // https://github.com/Koenkk/zigbee-herdsman-converters/issues/6001 [dp:103, name:"distance", type:"decimal", rw: "ro", min:0.0, max:10.0, defaultValue:0.0, scale:10, unit:"meters"], [dp:104, name:"unknown 104 0x68", type:"number", rw: "ro"], //68 [dp:105, name:"unknown 105 0x69", type:"number", rw: "ro"], //69 [dp:109, name:"unknown 109 0x6D", type:"number", rw: "ro"], //6D [dp:110, name:"unknown 110 0x6E", type:"number", rw: "ro"], //6E [dp:111, name:"unknown 111 0x6F", type:"number", rw: "ro"], //6F [dp:114, name:"unknown 114 0x72", type:"number", rw: "ro"], //72 [dp:115, name:"unknown 115 0x73", type:"number", rw: "ro"], //73 [dp:116, name:"unknown 116 0x74", type:"number", rw: "ro"], //74 [dp:118, name:"unknown 118 0x76", type:"number", rw: "ro"], //76 [dp:119, name:"unknown 119 0x77", type:"number", rw: "ro"] //77 ], spammyDPsToIgnore : [103], spammyDPsToNotTrace : [103], deviceJoinName: "Tuya TS0225_AWARHUSB 24Ghz Human Presence Detector", configuration : ["battery": false] ], // "OWON_OCP305_RADAR" : [ description : "OWON OCP305 Radar", models : ["OCP305"], device : [type: "radar", powerSource: "dc", isSleepy:false], capabilities : ["MotionSensor": true, "Battery": true], preferences : [:], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,0003,0406", outClusters:"0003", model:"OCP305", manufacturer:"OWON"] ], deviceJoinName: "OWON OCP305 Radar", configuration : ["0x0406":"bind"] ], // isSONOFF() "SONOFF_SNZB-06P_RADAR" : [ description : "SONOFF SNZB-06P RADAR", models : ["SONOFF"], device : [type: "radar", powerSource: "dc", isIAS:false, isSleepy:false], capabilities : ["MotionSensor": true], preferences : ["fadingTime":"0x0406:0x0020", "radarSensitivity":"0x0406:0x0022"], commands : ["resetStats":"resetStats", 'refresh':'refresh', "initialize":"initialize", "updateAllPreferences": "updateAllPreferences", "resetPreferencesToDefaults":"resetPreferencesToDefaults", "validateAndFixPreferences":"validateAndFixPreferences"], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,0003,0406,0500,FC57,FC11", outClusters:"0003,0019", model:"SNZB-06P", manufacturer:"SONOFF", deviceJoinName: "SONOFF SNZB-06P RADAR"] // https://community.hubitat.com/t/sonoff-zigbee-human-presence-sensor-snzb-06p/126128/14?u=kkossev ], attributes: [ [at:"0x0406:0x0022", name:"radarSensitivity", type:"enum", rw: "rw", min:1, max:3, defaultValue:"2", unit:"", map:[1:"low", 2:"medium", 3:"high"], title:"Radar Sensitivity", description:"Radar Sensitivity"], [at:"0x0406:0x0020", name:"fadingTime", type:"enum", rw: "rw", min:10, max:999, defaultValue:"60", unit:"seconds", map:[10:"10 seconds", 30:"30 seconds", 60:"60 seconds", 120:"120 seconds", 300:"300 seconds"], title:"Fading Time", description:"Radar fading time in seconds"], ], deviceJoinName: "SONOFF SNZB-06P RADAR", configuration : ["0x0406":"bind", "0x0FC57":"bind"/*, "0xFC11":"bind"*/] ], // isSiHAS() "SIHAS_USM-300Z_4_IN_1" : [ description : "SiHAS USM-300Z 4-in-1", models : ["ShinaSystem"], device : [type: "radar", powerSource: "battery", isIAS:false, isSleepy:false], capabilities : ["MotionSensor": true, "TemperatureMeasurement": true, "RelativeHumidityMeasurement": true, "IlluminanceMeasurement": true, "Battery": true], preferences : [:], fingerprints : [ [profileId:"0104", endpointId:"01", inClusters:"0000,0400,0003,0406,0402,0001,0405,0500", outClusters:"0004,0003,0019", model:"USM-300Z", manufacturer:"ShinaSystem", deviceJoinName: "SiHAS MultiPurpose Sensor"] ], commands : ["resetStats":"resetStats", 'refresh':'refresh', "initialize":"initialize", "updateAllPreferences": "updateAllPreferences", "resetPreferencesToDefaults":"resetPreferencesToDefaults", "validateAndFixPreferences":"validateAndFixPreferences"], tuyaDPs : [:], attributes : [:], deviceJoinName: "SiHAS USM-300Z 4-in-1", //configuration : ["0x0406":"bind"] // TODO !! configuration : [:] ], "UNKNOWN" : [ // the Device Profile key (shown in the State Variables) description : "Unknown device", // the Device Profile description (shown in the Preferences) models : ["UNKNOWN"], // used to match a Device profile if the individuak fingerprints do not match device : [ type: "PIR", // 'PIR' or 'radar' isIAS:true, // define it for PIR sensors only! powerSource: "dc", // determines the powerSource value - can be 'battery', 'dc', 'mains' isSleepy:false // determines the update and ping behaviour ], capabilities : ["MotionSensor": true, "IlluminanceMeasurement": true, "Battery": true], preferences : ["motionReset":true], //fingerprints : [ // [profileId:"0104", endpointId:"01", inClusters:"0000,0003,0406", outClusters:"0003", model:"model", manufacturer:"manufacturer"] //], tuyaDPs: [ [ dp:1, name:"motion", type:"enum", rw: "ro", min:0, max:1, map:[0:"inactive", 1:"active"], description:'Motion state' ] ], deviceJoinName: "Unknown device", // used during the inital pairing, if no individual fingerprint deviceJoinName was found configuration : ["battery": true], batteries : "unknown" ] ] // ---------------------------------- deviceProfilesV2 helper functions -------------------------------------------- /** * Returns the profile key for a given profile description. * @param valueStr The profile description to search for. * @return The profile key if found, otherwise null. */ def getProfileKey(String valueStr) { def key = null deviceProfilesV2.each { profileName, profileMap -> if (profileMap.description.equals(valueStr)) { key = profileName } } return key } /** * Finds the preferences map for the given parameter. * @param param The parameter to find the preferences map for. * @param debug Whether or not to output debug logs. * @return returns either tuyaDPs or attributes map, depending on where the preference (param) is found * @return null if param is not defined for this device. */ def getPreferencesMap( String param, boolean debug=false ) { Map foundMap = [:] if (!(param in DEVICE.preferences)) { if (debug) log.warn "getPreferencesMap: preference ${param} not defined for this device!" return null } def preference try { preference = DEVICE.preferences["$param"] if (debug) log.debug "getPreferencesMap: preference ${param} found. value is ${preference}" if (preference in [true, false]) { // find the preference in the tuyaDPs map logDebug "getPreferencesMap: preference ${param} is boolean" return null // no maps for predefined preferences ! } if (preference.isNumber()) { // find the preference in the tuyaDPs map int dp = safeToInt(preference) def dpMaps = DEVICE.tuyaDPs foundMap = dpMaps.find { it.dp == dp } } else { // cluster:attribute if (debug) log.trace "${DEVICE.attributes}" def dpMaps = DEVICE.tuyaDPs foundMap = DEVICE.attributes.find { it.at == preference } } // TODO - could be also 'true' or 'false' ... } catch (Exception e) { if (debug) log.warn "getPreferencesMap: exception ${e} caught when getting preference ${param} !" return null } if (debug) log.debug "getPreferencesMap: foundMap = ${foundMap}" return foundMap } /** * Resets the device preferences to their default values. * @param debug A boolean indicating whether to output debug information. */ def resetPreferencesToDefaults(boolean debug=false ) { Map preferences = DEVICE?.preferences if (preferences == null) { logDebug "Preferences not found!" return } Map parMap = [:] preferences.each{ parName, mapValue -> if (debug) log.trace "$parName $mapValue" // TODO - could be also 'true' or 'false' ... if (mapValue in [true, false]) { logDebug "Preference ${parName} is predefined -> (${mapValue})" // TODO - set the predefined value /* if (debug) log.info "par ${parName} defaultValue = ${parMap.defaultValue}" device.updateSetting("${parMap.name}",[value:parMap.defaultValue, type:parMap.type]) */ return // continue } // find the individual preference map parMap = getPreferencesMap(parName, false) if (parMap == null) { logDebug "Preference ${parName} not found in tuyaDPs or attributes map!" return // continue } // parMap = [at:0xE002:0xE005, name:staticDetectionSensitivity, type:number, dt:UINT8, rw:rw, min:0, max:5, step:1, scale:1, unit:x, title:Static Detection Sensitivity, description:Static detection sensitivity] if (parMap.defaultValue == null) { logDebug "no default value for preference ${parName} !" return // continue } if (debug) log.info "par ${parName} defaultValue = ${parMap.defaultValue}" device.updateSetting("${parMap.name}",[value:parMap.defaultValue, type:parMap.type]) } logInfo "Preferences reset to default values" } @Field static final Map IAS_ATTRIBUTES = [ // Zone Information 0x0000: 'zone state', 0x0001: 'zone type', 0x0002: 'zone status', // Zone Settings 0x0010: 'CIE addr', // EUI64 0x0011: 'Zone Id', // uint8 0x0012: 'Num zone sensitivity levels supported', // uint8 0x0013: 'Current zone sensitivity level', // uint8 0xF001: 'Current zone keep time' // enum8 ? ] @Field static final Map ZONE_TYPE = [ 0x0000: 'Standard CIE', 0x000D: 'Motion Sensor', 0x0015: 'Contact Switch', 0x0028: 'Fire Sensor', 0x002A: 'Water Sensor', 0x002B: 'Carbon Monoxide Sensor', 0x002C: 'Personal Emergency Device', 0x002D: 'Vibration Movement Sensor', 0x010F: 'Remote Control', 0x0115: 'Key Fob', 0x021D: 'Key Pad', 0x0225: 'Standard Warning Device', 0x0226: 'Glass Break Sensor', 0x0229: 'Security Repeater', 0xFFFF: 'Invalid Zone Type' ] @Field static final Map ZONE_STATE = [ 0x00: 'Not Enrolled', 0x01: 'Enrolled' ] 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) { Map descMap = [:] checkDriverVersion() if (state.rxCounter != null) state.rxCounter = state.rxCounter + 1 setPresent() //logDebug "parse (${device.getDataValue('manufacturer')}, ${driverVersionAndTimeStamp()}) description = ${description}" if (description?.startsWith('zone status') || description?.startsWith('zone report')) { logDebug "parse: zone status: $description" if ((isHL0SS9OAradar() || is2AAELWXKradar()) && _IGNORE_ZCL_REPORTS == true) { logDebug "ignored IAS zone status" return } else { parseIasMessage(description) // TS0202 Motion sensor } } else if (description?.startsWith('enroll request')) { logDebug "parse: enroll request: $description" /* The Zone Enroll Request command is generated when a device embodying the Zone server cluster wishes to be enrolled as an active alarm device. It must do this immediately it has joined the network (during commissioning). */ if (settings?.logEnable) log.info "${device.displayName} Sending IAS enroll response..." ArrayList cmds = zigbee.enrollResponse(300) + zigbee.readAttribute(0x0500, 0x0000, [:], delay=201) logDebug "enroll response: ${cmds}" sendZigbeeCommands( cmds ) } else if (description?.startsWith('catchall:') || description?.startsWith('read attr -')) { try { descMap = myParseDescriptionAsMap(description) } catch (e) { logDebug "parse: exception '${e}' caught while processing description ${description}" return } if ((isChattyRadarReport(descMap) || isSpammyDPsToIgnore(descMap)) && (_TRACE_ALL != true)) { // do not even log these spammy distance reports ... return } // if (!isSpammyDPsToNotTrace(descMap) || (_TRACE_ALL == true)) { logDebug "parse: (${device.getDataValue('manufacturer')}, ${(getDeviceGroup())}, ${driverVersionAndTimeStamp()}) descMap = ${descMap} description = ${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){ sendBatteryVoltageEvent(Integer.parseInt(descMap.value, 16)) } else { logDebug "power cluster not parsed attrint $descMap.attrInt" } } else if (descMap.cluster == "0400" && descMap.attrId == "0000") { def rawLux = Integer.parseInt(descMap.value,16) if ((isHL0SS9OAradar() || is2AAELWXKradar()) && _IGNORE_ZCL_REPORTS == true) { logDebug "ignored ZCL illuminance report (raw:Lux=${rawLux})" return } else { // including isLINPTECHradar if (isSiHAS()) { logDebug "parse: illuminanceEventLux report (raw:Lux=${rawLux})" illuminanceEventLux( rawLux ) } else { logDebug "parse: illuminanceEvent report (raw:Lux=${rawLux})" illuminanceEvent( rawLux ) } } } else if (descMap.cluster == "0402" && descMap.attrId == "0000") { def raw = Integer.parseInt(descMap.value,16) temperatureEvent( raw / getTemperatureDiv()) } else if (descMap.cluster == "0405" && descMap.attrId == "0000") { def raw = Integer.parseInt(descMap.value,16) humidityEvent( raw / getHumidityDiv()) } else if (descMap.cluster == "0406") { // OWON and SONOFF if (descMap.attrId == "0000") { def raw = Integer.parseInt(descMap.value,16) handleMotion( raw & 0x01 ) } else if (descMap.attrId == "0020") { def value = zigbee.convertHexToInt(descMap.value) sendEvent("name": "fadingTime", "value": value, "unit": "seconds", "type": "physical", "descriptionText": "fading time is ${value} seconds") logDebug "Cluster ${descMap.cluster} Attribute ${descMap.attrId} (fadingTime) value is ${value} (0x${descMap.value} seconds)" } else if (descMap.attrId == "0022") { def value = zigbee.convertHexToInt(descMap.value) sendEvent("name": "radarSensitivity", "value": value, "unit": "", "type": "physical", "descriptionText": "radar sensitivity is ${value}") logDebug "Cluster ${descMap.cluster} Attribute ${descMap.attrId} (radarSensitivity) value is ${value} (0x${descMap.value})" } else { logDebug "UNPROCESSED Cluster ${descMap.cluster} Attribute ${descMap.attrId} value is ${descMap.value} (0x${descMap.value})" } } else if (descMap?.clusterInt == CLUSTER_TUYA) { processTuyaCluster( descMap ) } else if (descMap.cluster == "E002") { processE002Cluster( descMap ) } else if (descMap.profileId == "0000") { // zdo parseZDOcommand(descMap) } else if (descMap?.cluster == "0000" && descMap?.attrId == "0001") { if (settings?.logEnable) log.info "${device.displayName} Tuya check-in (application version is ${descMap?.value})" def application = device.getDataValue("application") if (application == null || (application as String) != (descMap?.value as String)) { device.updateDataValue("application", descMap?.value) logInfo "application version set to ${descMap?.value}" updateTuyaVersion() } } else if (descMap?.cluster == "0000" && descMap?.attrId == "0004") { if (settings?.logEnable) log.info "${device.displayName} received device manufacturer ${descMap?.value}" } else if (descMap?.cluster == "0000" && descMap?.attrId == "0007") { //def value = descMap?.value == "00" ? "battery" : descMap?.value == "01" ? "mains" : descMap?.value == "03" ? "battery" : descMap?.value == "04" ? "dc" : "unknown" def powerSourceReported = powerSourceOpts.options[descMap?.value as int] logInfo "reported Power source ${powerSourceReported} (${descMap?.value})" // the powerSource reported by the device is very often not correct ... if (DEVICE.device?.powerSource != null) { powerSourceReported = DEVICE.device?.powerSource logDebug "forcing the powerSource to ${powerSourceReported}" } else if (is4in1() || ((DEVICE.device?.type == "radar") || isHumanPresenceSensorAIR() || isBlackPIRsensor() )) { // for radars force powerSource 'dc' powerSourceReported = powerSourceOpts.options[04] // force it to dc ! } powerSourceEvent( powerSourceReported ) } else if (descMap?.cluster == "0000" && descMap?.attrId == "FFDF") { logDebug "Tuya check-in (cluster revision=${descMap?.value})" } else if (descMap?.cluster == "0000" && descMap?.attrId == "FFE2") { logDebug "Tuya AppVersion is ${descMap?.value}" } else if (descMap?.cluster == "0000" && (descMap?.attrId in ["FFE0", "FFE1", "FFE3", "FFE4"])) { logDebug "Tuya unknown attribute ${descMap?.attrId} value is ${descMap?.value}" } else if (descMap?.cluster == "0000" && descMap?.attrId == "FFFE") { logDebug "Tuya attributeReportingStatus (attribute FFFE) value is ${descMap?.value}" } else if (descMap?.cluster == "0500" && descMap?.command in ["01", "0A"] ) { //IAS read attribute response //if (settings?.logEnable) log.debug "${device.displayName} IAS read attribute ${descMap?.attrId} response is ${descMap?.value}" if (descMap?.attrId == "0000") { def value = Integer.parseInt(descMap?.value, 16) logInfo "IAS Zone State repot is '${ZONE_STATE[value]}' (${value})" } else if (descMap?.attrId == "0001") { def value = Integer.parseInt(descMap?.value, 16) logInfo "IAS Zone Type repot is '${ZONE_TYPE[value]}' (${value})" } else if (descMap?.attrId == "0002") { logDebug "IAS Zone status repoted: descMap=${descMap} value= ${Integer.parseInt(descMap?.value, 16)}" handleMotion(Integer.parseInt(descMap?.value, 16)) } else if (descMap?.attrId == "0010") { logDebug "IAS Zone Address received (bitmap = ${descMap?.value})" } else if (descMap?.attrId == "0011") { logDebug "IAS Zone ID: ${descMap.value}" } else if (descMap?.attrId == "0012") { logDebug "IAS Num zone sensitivity levels supported: ${descMap.value}" } else if (descMap?.attrId == "0013") { def value = Integer.parseInt(descMap?.value, 16) logInfo "IAS Current Zone Sensitivity Level = ${sensitivityOpts.options[value]} (${value})" device.updateSetting("settings.sensitivity", [value:value.toString(), type:"enum"]) } else if (descMap?.attrId == "F001") { // [raw:7CC50105000801F02000, dni:7CC5, endpoint:01, cluster:0500, size:08, attrId:F001, encoding:20, command:0A, value:00, clusterInt:1280, attrInt:61441] def value = Integer.parseInt(descMap?.value, 16) def str = getKeepTimeOpts().options[value] logInfo "Current IAS Zone Keep-Time = ${str} (${value})" device.updateSetting("keepTime", [value: value.toString(), type: 'enum']) } else { logDebug "Zone status attribute ${descMap?.attrId}: NOT PROCESSED ${descMap}" } } // if IAS read attribute response else if (descMap?.clusterId == "0500" && descMap?.command == "04") { //write attribute response (IAS) logDebug "IAS write attribute response is ${descMap?.data[0] == '00' ? 'success' : 'FAILURE'}" } else if (descMap?.cluster == "FC11" && descMap?.command in ["01", "0A"] ) { // descMap = [raw:0DD001FC110801202001, dni:0DD0, endpoint:01, cluster:FC11, size:08, attrId:2001, encoding:20, command:0A, value:01, clusterInt:64529, attrInt:8193] if (descMap?.attrId == "2001") { logDebug "FC11 attribute 2001 value is ${descMap?.value}" //occupancyEvent( Integer.parseInt(descMap?.value, 16) ) } else { logDebug "FC11 attribute ${descMap?.attrId}: NOT PROCESSED descMap=${descMap}" } } else if (descMap?.command == "04") { // write attribute response (other) logDebug "write attribute response is ${descMap?.data[0] == '00' ? 'success' : 'FAILURE'}" } else if (descMap?.command == "0B") { // default command response String commandId = descMap.data[0] String status = "0x${descMap.data[1]}" logDebug "zigbee default command response cluster: ${clusterLookup(descMap.clusterInt)} command: 0x${commandId} status: ${descMap.data[1]== '00' ? 'success' : 'FAILURE'} (${status})" } else if (descMap?.command == "00" && descMap?.clusterId == "8021" ) { // bind response logDebug "bind response, data=${descMap.data} (Sequence Number:${descMap.data[0]}, Status: ${descMap.data[1]=="00" ? 'success' : 'FAILURE'})" } else { logDebug "NOT PARSED : descMap = ${descMap} description = ${description}" } } // if 'catchall:' or 'read attr -' else if (description.startsWith('raw')) { // description=raw:11E201E0020A0AE0219F00, dni:11E2, endpoint:01, cluster:E002, size:0A, attrId:E00A, encoding:21, command:0A, value:009F, clusterInt:57346, attrInt:57354 /* descMap = [:] descMap += description.replaceAll('\\[|\\]', '').split(',').collectEntries { entry -> def pair = entry.split(':') [(pair.first().trim()): pair.last().trim()] } */ descMap = myParseDescriptionAsMap(description) logDebug "parsed raw : cluster=${descMap.cluster} clusterId=${descMap.clusterId} attrId=${descMap.attrId} descMap=${descMap}" if (descMap != null) { if (descMap.cluster == "E002") { // TODO !!!!!!!!!!! check if this is correct : cluster vs clusterId processE002Cluster( descMap ) } else if (descMap.profileId == "0000") { // zdo parseZDOcommand(descMap) } if (descMap.cluster == "0005") { logDebug "unprocessed cluster ${descMap.cluster} description = ${description} descMap=${descMap}" } else { logWarn "UNPROCESSED RAW cluster ${descMap.cluster} description = ${description} descMap=${descMap}" } } else { logWarn "CAN NOT PARSE RAW cluster description = ${description} descMap=${descMap}" } } else { logDebug " UNPROCESSED description = ${description} descMap = ${zigbee.parseDescriptionAsMap(description)}" } } Map myParseDescriptionAsMap( String description ) { def descMap = [:] try { descMap = zigbee.parseDescriptionAsMap(description) return descMap // all OK! } catch (e1) { logDebug "exception ${e1} caught while processing parseDescriptionAsMap myParseDescriptionAsMap description: ${description}" // try alternative custom parsing descMap = [:] try { descMap += description.replaceAll('\\[|\\]', '').split(',').collectEntries { entry -> def pair = entry.split(':') [(pair.first().trim()): pair.last().trim()] } if (descMap.value != null) { descMap.value = zigbee.swapOctets(descMap.value) } } catch (e2) { logWarn "exception ${e2} caught while parsing using an alternative method myParseDescriptionAsMap description: ${description}" return [:] } logDebug "alternative method parsing success: descMap=${descMap}" } return descMap } def parseZDOcommand( Map descMap ) { switch (descMap.clusterId) { // TODO - add ZDO 0005 case "0006" : if (settings?.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 (settings?.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 (settings?.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 def endpointCount = descMap.data[4] def endpointList = descMap.data[5] if (settings?.logEnable) log.info "${device.displayName} zdo command: cluster: ${descMap.clusterId} (endpoint response) endpointCount = ${endpointCount} endpointList = ${endpointList}" break case "8021" : // bind response if (settings?.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 request if (settings?.logEnable) log.info "${device.displayName} zdo command: cluster: ${descMap.clusterId} (unbind request)" break case "8034" : //leave response if (settings?.logEnable) log.info "${device.displayName} zdo command: cluster: ${descMap.clusterId} (leave response)" break default : if (settings?.logEnable) log.warn "${device.displayName} Unprocessed ZDO command: cluster=${descMap.clusterId} command=${descMap.command} attrId=${descMap.attrId} value=${descMap.value} data=${descMap.data}" } } // TODO - refactoring def processE002Cluster( descMap ) { // raw:11E201E0020A0AE0219F00, dni:11E2, endpoint:01, cluster:E002, size:0A, attrId:E00A, encoding:21, command:0A, value:009F, clusterInt:57346, attrInt:57354 def value = zigbee.convertHexToInt(descMap.value) switch (descMap.attrId) { case "E001" : // the existance_time in minutes, UINT16 sendEvent("name": "existance_time", "value": value, "unit": "minutes", "type": "physical", "descriptionText": "Presence is active for ${value} minutes") logDebug "Cluster ${descMap.cluster} Attribute ${descMap.attrId} (existance_time) value is ${value} (0x${descMap.value} minutes)" break case "E004" : // value:05 // motionDetectionSensitivity, UINT8 // raw:F2EF01E0020804E02004, dni:F2EF, endpoint:01, cluster:E002, size:08, attrId:E004, encoding:20, command:0A, value:04, clusterInt:57346, attrInt:57348 if (settings?.logEnable == true || settings?.motionDetectionSensitivity != (value as int)) { logInfo "received LINPTECH radar motionDetectionSensitivity : ${value}"} else {logDebug "skipped ${settings?.motionDetectionSensitivity} == ${value as int}"} device.updateSetting("motionDetectionSensitivity", [value:value as String , type:"enum"]) sendEvent("name": "radarSensitivity", "value": value, "unit": "meters", "type": "physical", "descriptionText": "motionDetectionSensitivity is ${value}") break case "E005" : // value:05 // staticDetectionSensitivity, UINT8 // raw:F2EF01E0020805E02005, dni:F2EF, endpoint:01, cluster:E002, size:08, attrId:E005, encoding:20, command:0A, value:05, clusterInt:57346, attrInt:57349 if (settings?.logEnable == true || settings?.staticDetectionSensitivity != (value as int)) { logInfo "received LINPTECH radar staticDetectionSensitivity : ${value}"} else {logDebug "skipped ${settings?.staticDetectionSensitivity} == ${value as int}"} device.updateSetting("staticDetectionSensitivity", [value:value as String , type:"enum"]) sendEvent("name": "staticDetectionSensitivity", "value": value, "unit": "", "type": "physical", "descriptionText": "staticDetectionSensitivity is ${value}") break case "E00A" : // value:009F, 6E, 2E, .....00B6 0054 - distance, UINT16 if (settings?.ignoreDistance == false) { logDebug "LINPTECH radar target distance is ${value/100} m" sendEvent(name : "distance", value : value/100, unit : "m") } break case "E00B" : // value:value:600 -- motionDetectionDistance, UINT16 // raw:F2EF01E0020A0BE021C201, dni:F2EF, endpoint:01, cluster:E002, size:0A, attrId:E00B, encoding:21, command:0A, value:01C2, clusterInt:57346, attrInt:57355 Integer settingsScaled = (safeToDouble(settings?.motionDetectionDistance) * 100) as int Float valueFloat = value/100F logDebug "motionDetectionDistance raw = ${value} settings = ${settings?.motionDetectionDistance} settingsScaled = ${settingsScaled} valueFloat=${valueFloat} isEqual = ${settingsScaled == value}" if (settings?.logEnable == true || (settingsScaled != value)) {logInfo "received LINPTECH radar Motion Detection Distance : ${value/100} m"} // format valueFloat to 2 decimals String formatted = String.format("%.2f", valueFloat) device.updateSetting("motionDetectionDistance", [value:formatted, type:"enum"]) sendEvent("name": "maximumDistance", "value": formatted, "unit": "meters", "type": "physical", "descriptionText": "motionDetectionDistance is ${value} meters") break default : logWarn "Unprocessed cluster 0xE002 command ${descMap.command} attrId ${descMap.attrId} value ${value} (0x${descMap.value})" break } } def processTuyaCluster( descMap ) { if (descMap?.clusterInt==CLUSTER_TUYA && descMap?.command == "24") { //getSETTIME logDebug "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)) logDebug "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] if (!(isHL0SS9OAradar() || is2AAELWXKradar())) { logDebug "device has received Tuya cluster ZCL command 0x${clusterCmd} response 0x${status} data = ${descMap?.data}" } if (status != "00" && !(isHL0SS9OAradar() || is2AAELWXKradar())) { logDebug "ATTENTION! manufacturer = ${device.getDataValue("manufacturer")} 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"|| descMap?.command == "06")) { try { def transid = zigbee.convertHexToInt(descMap?.data[1]) // "transid" is just a "counter", a response will have the same transid as the command def dp = zigbee.convertHexToInt(descMap?.data[2]) // "dp" field describes the action/message of a command frame def dp_id = zigbee.convertHexToInt(descMap?.data[3]) // "dp_identifier" is device dependant def fncmd = getTuyaAttributeValue(descMap?.data) // def dp_len = zigbee.convertHexToInt(descMap?.data[5]) // the length of the DP - 1 or 4 ... This is NOT the DP type!! updateStateTuyaDPs(descMap, dp, dp_id, fncmd, dp_len) processTuyaDP(descMap, dp, dp_id, fncmd, dp_len) } catch (e) { logWarn "catched exception ${e} while processing descMap: ${descMap}" return } } // Tuya commands '01' and '02' else if (descMap?.clusterInt==CLUSTER_TUYA && descMap?.command == "11" ) { // dont'know what command "11" means, it is sent by the square black radar when powered on. Will use it to restore the LED on/off configuration :) logDebug "Tuya descMap?.command = ${descMap?.command} descMap.data = ${descMap?.data}" if (("indicatorLight" in DEVICE.preferences)) { if (settings?.indicatorLight != null) { ArrayList cmds = [] def value = safeToInt(indicatorLight.value) def dpValHex = zigbee.convertToHexString(value as int, 2) cmds += sendTuyaCommand("67", DP_TYPE_BOOL, dpValHex) // TODO - refactor! if (settings?.logEnable) log.info "${device.displayName} restoring indicator light to : ${blackRadarLedOptions[value.toString()]} (${value})" sendZigbeeCommands( cmds ) } } else { logDebug "unhandled Tuya command 11 descMap: ${descMap}" // TODO - sent also but Tuya radars MY-Z100 30 seconds after power-on !! } } else { logDebug "NOT PROCESSED Tuya descMap?.command = ${descMap?.command} cmd: dp=${dp} value=${fncmd} descMap.data = ${descMap?.data}" } } // // called from processTuyaCluster for every Tuya device, prior to processing the DP // updates state.tuyaDPs, increasing the DP count and updating the DP value // void updateStateTuyaDPs(descMap, dp, dp_id, fncmd, dp_len) { if (state.tuyaDPs == null) state.tuyaDPs = [:] if (dp == null || dp < 0 || dp > 255) return def value = fncmd // value def len = dp_len // data length in bytes def counter = 1 // counter if (state.tuyaDPs["$dp"] != null) { try { counter = safeToInt((state.tuyaDPs["$dp"][2] ?: 0)) + 1 } catch (e) { counter = 1 } } def upd = [value, len, counter] if (state.tuyaDPs["$dp"] == null) { // new dp in the list state.tuyaDPs["$dp"] = upd def tuyaDPsSorted = (state.tuyaDPs.sort { a, b -> return (a.key as int <=> b.key as int) }) // HE sorts the state Map elements as String ... state.tuyaDPs = tuyaDPsSorted } else { // just update the existing dp value state.tuyaDPs["$dp"] = upd } } // // called from processTuyaDPfromDeviceProfile if the DP was not found for the particular device profile // updates state.unknownDPs, increasing the DP count and updating the DP value // void updateStateUnknownDPs(descMap, dp, dp_id, fncmd, dp_len) { if (state.unknownDPs == null) state.unknownDPs = [] if (dp == null || dp < 0 || dp > 255) return if (state.unknownDPs == [] || !(dp in state.unknownDPs)) { List list = state.unknownDPs as List list.add(dp) list = list.sort() state.unknownDPs = list } else { // log.trace "state.unknownDPs= ${state.unknownDPs}" } } // // called from parse() // returns: true - do not process this message if the spammy DP is defined in the spammyDPsToIgnore element of the active Device Profule // false - the processing can continue // boolean isSpammyDPsToIgnore(descMap) { if (!(descMap?.clusterId == "EF00" && (descMap?.command in ["01", "02"]))) { return false } if (descMap?.data?.size <= 2) { return false } Integer dp = zigbee.convertHexToInt(descMap.data[2]) def spammyList = deviceProfilesV2[getDeviceGroup()].spammyDPsToIgnore return (spammyList != null && (dp in spammyList) && ((settings?.ignoreDistance ?: false) == true)) } // // called from processTuyaDP(), processTuyaDPfromDeviceProfile() // returns: true - do not generate Debug log messages if the chatty DP is defined in the spammyDPsToNotTrace element of the active Device Profule // false - debug logs can be generated // boolean isSpammyDPsToNotTrace(descMap) { if (!(descMap?.clusterId == "EF00" && (descMap?.command in ["01", "02"]))) { return false } if (descMap?.data?.size <= 2) { return false } Integer dp = zigbee.convertHexToInt(descMap.data[2]) def spammyList = deviceProfilesV2[getDeviceGroup()].spammyDPsToNotTrace return (spammyList != null && (dp in spammyList)) } def compareAndConvertStrings(foundItem, tuyaValue, hubitatValue) { String convertedValue = tuyaValue boolean isEqual = ((tuyaValue as String) == (hubitatValue as String)) // because the events(attributes) are always strings return [isEqual, convertedValue] } def compareAndConvertNumbers(foundItem, tuyaValue, hubitatValue) { Integer convertedValue if (foundItem.scale == null || foundItem.scale == 0 || foundItem.scale == 1) { // compare as integer convertedValue = tuyaValue as int } else { convertedValue = ((tuyaValue as double) / (foundItem.scale as double)) as int } boolean isEqual = ((convertedValue as int) == (hubitatValue as int)) return [isEqual, convertedValue] } def compareAndConvertDecimals(foundItem, tuyaValue, hubitatValue) { Double convertedValue if (foundItem.scale == null || foundItem.scale == 0 || foundItem.scale == 1) { convertedValue = tuyaValue as double } else { convertedValue = (tuyaValue as double) / (foundItem.scale as double) } isEqual = Math.abs((convertedValue as double) - (hubitatValue as double)) < 0.001 return [isEqual, convertedValue] } def compareAndConvertTuyaToHubitatPreferenceValue(foundItem, fncmd, preference) { if (foundItem == null || fncmd == null || preference == null) { return [true, "none"] } if (foundItem.type == null) { return [true, "none"] } boolean isEqual def tuyaValueScaled // could be integer or float switch (foundItem.type) { case "bool" : // [0:"OFF", 1:"ON"] case "enum" : // [0:"inactive", 1:"active"] (isEqual, tuyaValueScaled) = compareAndConvertNumbers(foundItem, safeToInt(fncmd), safeToInt(preference)) //logDebug "compareAndConvertTuyaToHubitatPreferenceValue: preference = ${preference} type=${foundItem.type} foundItem=${foundItem.name} isEqual=${isEqual} preferenceValue=${preferenceValue} tuyaValueScaled=${tuyaValueScaled} fncmd=${fncmd}" break case "value" : // depends on foundItem.scale case "number" : (isEqual, tuyaValueScaled) = compareAndConvertNumbers(foundItem, safeToInt(fncmd), safeToInt(preference)) //log.warn "tuyaValue=${tuyaValue} tuyaValueScaled=${tuyaValueScaled} preferenceValue = ${preference} isEqual=${isEqual}" break case "decimal" : (isEqual, tuyaValueScaled) = compareAndConvertDecimals(foundItem, safeToDouble(fncmd), safeToDouble(preference)) //logDebug "comparing as float tuyaValue=${tuyaValue} foundItem.scale=${foundItem.scale} tuyaValueScaled=${tuyaValueScaled} to preferenceValue = ${preference}" break default : logDebug "compareAndConvertTuyaToHubitatPreferenceValue: unsupported type %{foundItem.type}" return [true, "none"] // fallback - assume equal } if (isEqual == false) { logDebug "compareAndConvertTuyaToHubitatPreferenceValue: preference = ${preference} type=${foundItem.type} foundItem=${foundItem.name} isEqual=${isEqual} tuyaValueScaled=${tuyaValueScaled} (scale=${foundItem.scale}) fncmd=${fncmd}" } // return [isEqual, tuyaValueScaled] } // // called from processTuyaDPfromDeviceProfile() // compares the value of the DP foundItem against a Preference with the same name // returns: (two results!) // isEqual : true - if the Tuya DP value equals to the DP calculated value (no need to update the preference) // : true - if a preference with the same name does not exist (no preference value to update) // isEqual : false - the reported DP value is different than the corresponding preference (the preference needs to be updated!) // // hubitatEventValue - the converted DP value, scaled (divided by the scale factor) to match the corresponding preference type value // // TODO: refactor! // def compareAndConvertTuyaToHubitatEventValue(foundItem, fncmd, doNotTrace=false) { if (foundItem == null) { return [true, "none"] } if (foundItem.type == null) { return [true, "none"] } def hubitatEventValue // could be integer or float or string boolean isEqual switch (foundItem.type) { case "bool" : // [0:"OFF", 1:"ON"] case "enum" : // [0:"inactive", 1:"active"] (isEqual, hubitatEventValue) = compareAndConvertStrings(foundItem, foundItem.map[fncmd as int] ?: "unknown", device.currentValue(foundItem.name) ?: "unknown") break case "value" : // depends on foundItem.scale case "number" : (isEqual, hubitatEventValue) = compareAndConvertNumbers(foundItem, safeToInt(fncmd), safeToInt(device.currentValue(foundItem.name))) break case "decimal" : (isEqual, hubitatEventValue) = compareAndConvertDecimals(foundItem, safeToDouble(fncmd), safeToDouble(device.currentValue(foundItem.name))) break default : logDebug "compareAndConvertTuyaToHubitatEventValue: unsupported dpType %{foundItem.type}" return [true, "none"] // fallback - assume equal } //if (!doNotTrace) log.trace "foundItem=${foundItem.name} isEqual=${isEqual} attrValue=${attrValue} fncmd=${fncmd} foundItem.scale=${foundItem.scale } valueScaled=${valueScaled} " return [isEqual, hubitatEventValue] } def preProc(foundItem, fncmd_orig) { def fncmd = fncmd_orig if (foundItem == null) { return fncmd } if (foundItem.preProc == null) { return fncmd } String preProcFunction = foundItem.preProc //logDebug "preProc: foundItem.preProc = ${preProcFunction}" // check if preProc method exists if (!this.respondsTo(preProcFunction)) { logDebug "preProc: function ${preProcFunction} not found" return fncmd_orig } // execute the preProc function try { fncmd = "$preProcFunction"(fncmd_orig) } catch (e) { logWarn "preProc: Exception '${e}'caught while processing $preProcFunction($fncmd_orig) (val=${fncmd}))" return fncmd_orig } //logDebug "setFunction result is ${fncmd}" return fncmd } /** * Processes a Tuya DP (Data Point) received from the device, based on the device profile and its defined Tuya DPs. * If a preference exists for the DP, it updates the preference value and sends an event if the DP is declared as an attribute. * If no preference exists for the DP, it logs the DP value as an info message. * If the DP is spammy (not needed for anything), it does not perform any further processing. * * @param descMap The description map of the received DP. * @param dp The value of the received DP. * @param dp_id The ID of the received DP. * @param fncmd The command of the received DP. * @param dp_len The length of the received DP. * @return true if the DP was processed successfully, false otherwise. */ boolean processTuyaDPfromDeviceProfile(descMap, dp, dp_id, fncmd_orig, dp_len) { def fncmd = fncmd_orig if (state.deviceProfile == null) { return false } //if (!(DEVICE.device?.type == "radar")) { return false } // enabled for all devices - 10/22/2023 !!! // only these models are handled here for now ... if (isSpammyDPsToIgnore(descMap)) { return true } // do not perform any further processing, if this is a spammy report that is not needed for anyhting (such as the LED status) def tuyaDPsMap = deviceProfilesV2[state.deviceProfile].tuyaDPs if (tuyaDPsMap == null || tuyaDPsMap == []) { return false } // no any Tuya DPs defined in the Device Profile def foundItem = null tuyaDPsMap.each { item -> if (item['dp'] == (dp as int)) { foundItem = item return } } if (foundItem == null) { // DP was not found into the tuyaDPs list for this particular deviceProfile updateStateUnknownDPs(descMap, dp, dp_id, fncmd, dp_len) // continue processing the DP report in the old code ... return false } // added 10/31/2023 - preProc the DP value if needed if (foundItem.preProc != null) { fncmd = preProc(foundItem, fncmd_orig) logDebug "preProc changed ${foundItem.name} from ${fncmd_orig} to ${fncmd}" } else { // logDebug "no preProc for ${foundItem.name} : ${foundItem}" } def name = foundItem.name // preference name as in the tuyaDPs map def existingPrefValue = settings[name] // preference name as in Hubitat settings (preferences), if already created. def perfValue = null // preference value boolean preferenceExists = existingPrefValue != null // check if there is an existing preference for this dp boolean isAttribute = device.hasAttribute(foundItem.name) // check if there is such a attribute for this dp boolean isEqual = false boolean wasChanged = false boolean doNotTrace = isSpammyDPsToNotTrace(descMap) // do not log/trace the spammy DP's if (!doNotTrace) { //logDebug "processTuyaDPfromDeviceProfile dp=${dp} ${foundItem.name} (type ${foundItem.type}, rw=${foundItem.rw} isAttribute=${isAttribute}, preferenceExists=${preferenceExists}) value is ${fncmd} - ${foundItem.description}" } // check if the dp has the same value as the last one, or the value has changed // the previous value may be stored in an attribute, as a preference, as both attribute and preference or not stored anywhere ... String unitText = foundItem.unit != null ? "$foundItem.unit" : "" def valueScaled // can be number or decimal or string String descText = descText = "${name} is ${fncmd} ${unitText}" // the default description text for log events // TODO - check if DP is in the list of the received state.tuyaDPs - then we have something to compare ! if (!isAttribute && !preferenceExists) { // if the previous value of this dp is not stored anywhere - just seend an Info log if Debug is enabled if (!doNotTrace) { // only if the DP is not in the spammy list (isEqual, valueScaled) = compareAndConvertTuyaToHubitatEventValue(foundItem, fncmd, doNotTrace) descText = "${name} is ${valueScaled} ${unitText}" if (settings.logEnable) { logInfo "${descText}"} } // no more processing is needed, as this DP is not a preference and not an attribute return true } // first, check if there is a preference defined to be updated if (preferenceExists) { // preference exists and its's value is extracted def oldPerfValue = device.getSetting(name) (isEqual, perfValue) = compareAndConvertTuyaToHubitatPreferenceValue(foundItem, fncmd, existingPrefValue) if (isEqual == true) { // the DP value is the same as the preference value - no need to update the preference logDebug "no change: preference '${name}' existingPrefValue ${existingPrefValue} equals scaled value ${perfValue} (dp raw value ${fncmd})" } else { logDebug "preference '${name}' value ${existingPrefValue} differs from the new scaled value ${perfValue} (dp raw value ${fncmd})" if (debug) log.info "updating par ${name} from ${existingPrefValue} to ${perfValue} type ${foundItem.type}" try { device.updateSetting("${name}",[value:perfValue, type:foundItem.type]) wasChanged = true } catch (e) { logWarn "exception ${e} caught while updating preference ${name} to ${fncmd}, type ${foundItem.type}" } } } else { // no preference exists for this dp // if not in the spammy list - log it! unitText = foundItem.unit != null ? "$foundItem.unit" : "" //logInfo "${name} is ${fncmd} ${unitText}" } // second, send an event if this is declared as an attribute! if (isAttribute) { // this DP has an attribute that must be sent in an Event (isEqual, valueScaled) = compareAndConvertTuyaToHubitatEventValue(foundItem, fncmd, doNotTrace) descText = "${name} is ${valueScaled} ${unitText}" if (settings?.logEnable == true) { descText += " (raw:${fncmd})" } if (isEqual && !wasChanged) { // this DP report has the same value as the last one - just send a debug log and move along! if (!doNotTrace) { if (settings.logEnable) { logInfo "${descText} (no change)"} } // patch for inverted motion sensor 2-in-1 if (name == "motion" && is2in1()) { logDebug "patch for inverted motion sensor 2-in-1" // continue ... } else { return true // we are done (if there was potentially a preference, it should be already set to the same value) } } // DP value (fncmd) is not equal to the attribute last value or was changed- we must send an event! def value = safeToInt(fncmd) def divider = safeToInt(foundItem.scale ?: 1) ?: 1 def valueCorrected = value / divider //if (!doNotTrace) { logDebug "value=${value} foundItem.scale=${foundItem.scale} divider=${divider} valueCorrected=${valueCorrected}" } switch (name) { case "motion" : handleMotion(motionActive = fncmd) break case "temperature" : temperatureEvent(fncmd / getTemperatureDiv()) break case "humidity" : humidityEvent(fncmd / getHumidityDiv()) break case "illuminance" : case "illuminance_lux" : illuminanceEventLux(valueCorrected) break case "pushed" : logDebug "button event received fncmd=${fncmd} valueScaled=${valueScaled} valueCorrected=${valueCorrected}" buttonEvent(valueScaled) break default : sendEvent(name : name, value : valueScaled, unit:unitText, descriptionText: descText, type: "physical", isStateChange: true) // attribute value is changed - send an event ! if (!doNotTrace) { logDebug "event ${name} sent w/ value ${valueScaled}" logInfo "${descText}" // send an Info log also (because value changed ) // TODO - check whether Info log will be sent also for spammy DPs ? } break } //log.trace "attrValue=${attrValue} valueScaled=${valueScaled} equal=${isEqual}" } // all processing was done here! return true } void processTuyaDP(descMap, dp, dp_id, fncmd, dp_len) { if (!isSpammyDPsToNotTrace(descMap)) { logDebug "processTuyaDP: received: dp_id=${dp_id} dp=${dp} fncmd=${fncmd}" } // if (processTuyaDPfromDeviceProfile(descMap, dp, dp_id, fncmd, dp_len) == true) { // sucessfuly processed the new way - we are done. return } switch (dp) { case 0x01 : // motion for 2-in-1 TS0601 (_TZE200_3towulqd) and presence state for almost of the radars logDebug "(DP=0x01) motion event fncmd = ${fncmd}" handleMotion(motionActive = fncmd) break case 0x04 : // battery level for TS0202 and TS0601 2in1 ; battery1 for Fantem 4-in-1 (100% or 0% ) Battery level for _TZE200_3towulqd (2in1) logDebug "Tuya battery status report dp_id=${dp_id} dp=${dp} fncmd=${fncmd}" handleTuyaBatteryLevel( fncmd ) break case 0x05 : // tamper alarm for TS0202 4-in-1 def value = fncmd==0 ? 'clear' : 'detected' logInfo "${device.displayName} tamper alarm is ${value} (dp=05,fncmd=${fncmd})" sendEvent(name : "tamper", value : value, isStateChange : true) break case 0x07 : // temperature for 4-in-1 (no data) logDebug "4-in-1 temperature (dp=07) is ${fncmd / 10.0 } ${fncmd}" temperatureEvent( fncmd / getTemperatureDiv()) break case 0x08 : // humidity for 4-in-1 (no data) logDebug "4-in-1 humidity (dp=08) is ${fncmd} ${fncmd}" humidityEvent( fncmd / getHumidityDiv()) break case 0x09 : // sensitivity for TS0202 4-in-1 and 2in1 _TZE200_3towulqd logInfo "received sensitivity : ${sensitivityOpts.options[fncmd]} (${fncmd})" device.updateSetting("sensitivity", [value:fncmd.toString(), type:"enum"]) break case 0x0A : // (10) keep time for TS0202 4-in-1 and 2in1 _TZE200_3towulqd logInfo "Keep Time (dp=0x0A) is ${keepTimeIASOpts.options[fncmd]} (${fncmd})" device.updateSetting("keepTime", [value:fncmd.toString(), type:"enum"]) break case 0x19 : // (25) logDebug "Motion Switch battery status report dp_id=${dp_id} dp=${dp} fncmd=${fncmd}" handleTuyaBatteryLevel( fncmd ) break case 0x65 : // (101) // Tuya 3 in 1 (101) -> motion (ocupancy) + TUYATEC logDebug "motion event 0x65 fncmd = ${fncmd}" handleMotion(motionActive=fncmd) break case 0x66 : // (102) if (is4in1()) { // // case 102 //reporting time intervl for 4 in 1 logInfo "4-in-1 reporting time interval is ${fncmd} minutes" device.updateSetting("reportingTime4in1", [value:fncmd as int , type:"number"]) } else if (is3in1()) { // battery level for 3 in 1; logDebug "Tuya battery status report dp_id=${dp_id} dp=${dp} fncmd=${fncmd}" handleTuyaBatteryLevel( fncmd ) } else { logDebug "reported unknown parameter dp=${dp} value=${fncmd}" } break case 0x68 : // (104) if (isYXZBRB58radar()) { // [0x68, 'radar_scene', tuya.valueConverterBasic.lookup({ 'default': tuya.enum(0), 'bathroom': tuya.enum(1), 'bedroom': tuya.enum(2), 'sleeping': tuya.enum(3), })], logInfo "YXZBRB58 radar reported radar_scene dp=${dp} value=${fncmd}" } else if (is4in1()) { // case 104: // 0x68 temperature compensation def val = fncmd; // for negative values produce complimentary hex (equivalent to negative values) if (val > 4294967295) val = val - 4294967295; logInfo "4-in-1 temperature calibration is ${val / 10.0}" } else { logDebug "reported unknown parameter dp=${dp} value=${fncmd}" } break case 0x69 : // (105) if (is4in1()) { // case 105:// 0x69 humidity calibration (compensation) def val = fncmd; if (val > 4294967295) val = val - 4294967295; logInfo "4-in-1 humidity calibration is ${val}" } else { logDebug "reported unknown parameter dp=${dp} value=${fncmd}" } break case 0x6A : // (106) if (is4in1()) { // case 106: // 0x6a lux calibration (compensation) def val = fncmd; if (val > 4294967295) val = val - 4294967295; logInfo "4-in-1 lux calibration is ${val}" } else { logDebug "reported unknown parameter dp=${dp} value=${fncmd}" } break case 0x6B : // (107) if (is4in1()) { // Tuya 4 in 1 (107) -> temperature in ?C temperatureEvent( fncmd / getTemperatureDiv()) } else { logDebug "(UNEXPECTED) : ${fncmd} (DP=0x6B)" } break case 0x6C : // (108) Tuya 4 in 1 -> humidity in % if (is4in1()) { humidityEvent (fncmd / getHumidityDiv()) } else { logDebug "(UNEXPECTED) : ${fncmd} (DP=0x6C)" } break case 0x6D : // (109) if (is4in1()) { // case 109: 0x6d PIR enable (PIR power) logInfo "4-in-1 enable is ${fncmd}" } else { logDebug "reported unknown parameter dp=${dp} value=${fncmd}" } break case 0x6E : // (110) Tuya 4 in 1 if (is4in1()) { logDebug "Tuya battery status report dp_id=${dp_id} dp=${dp} fncmd=${fncmd}" handleTuyaBatteryLevel( fncmd ) } else { logDebug "reported unknown parameter dp=${dp} value=${fncmd}" } break case 0x6F : // (111) Tuya 4 in 1: // 0x6f led enable if (is4in1()) { logInfo "4-in-1 LED is: ${fncmd == 1 ? 'enabled' :'disabled'}" device.updateSetting("ledEnable", [value:fncmd as boolean, type:"boolean"]) } else { // 3in1 - temperature alarm switch if (settings?.logEnable) log.info "${device.displayName} Temperature alarm switch is: ${fncmd} (DP=0x6F)" } break case 0x70 : // (112) if (is4in1()) { // case 112: 0x70 reporting enable (Alarm type) if (settings?.txtEnable) log.info "${device.displayName} reporting enable is ${fncmd}" } else { if (settings?.logEnable) log.info "${device.displayName} Humidity alarm switch is: ${fncmd} (DP=0x6F)" } break case 0x71 : // (113) if (is4in1()) { // case 113: 0x71 unknown ( ENUM) if (settings?.logEnable) log.info "${device.displayName} UNKNOWN (0x71 reporting enable?) DP=0x71 fncmd = ${fncmd}" } else { // 3in1 - Alarm Type if (settings?.txtEnable) log.info "${device.displayName} Alarm type is: ${fncmd}" } break default : logDebug "NOT PROCESSED Tuya cmd: dp=${dp} value=${fncmd} descMap.data = ${descMap?.data}" break } } private int getTuyaAttributeValue(ArrayList _data) { int retValue = 0 if (_data.size() >= 6) { int dataLength = _data[5] as Integer int power = 1; for (i in dataLength..1) { retValue = retValue + power * zigbee.convertHexToInt(_data[i+5]) power = power * 256 } } return retValue } def handleTuyaBatteryLevel( fncmd ) { def rawValue = 0 if (fncmd == 0) rawValue = 100 // Battery Full else if (fncmd == 1) rawValue = 75 // Battery High else if (fncmd == 2) rawValue = 50 // Battery Medium else if (fncmd == 3) rawValue = 25 // Battery Low else if (fncmd == 4) rawValue = 100 // Tuya 3 in 1 -> USB powered else rawValue = fncmd getBatteryPercentageResult(rawValue*2) } // not used def parseIasReport(Map descMap) { logDebug "pareseIasReport: descMap=${descMap} value= ${Integer.parseInt(descMap?.value, 16)}" def zs = new ZoneStatus(Integer.parseInt(descMap?.value, 16)) if (settings?.logEnable) { log.debug "zs.alarm1 = $zs.alarm1" log.debug "zs.alarm2 = $zs.alarm2" log.debug "zs.tamper = $zs.tamper" log.debug "zs.battery = $zs.battery" log.debug "zs.supervisionReports = $zs.supervisionReports" log.debug "zs.restoreReports = $zs.restoreReports" log.debug "zs.trouble = $zs.trouble" log.debug "zs.ac = $zs.ac" log.debug "zs.test = $zs.test" log.debug "zs.batteryDefect = $zs.batteryDefect" } handleMotion(zs.alarm1) } def parseIasMessage(String description) { // https://developer.tuya.com/en/docs/iot-device-dev/tuya-zigbee-water-sensor-access-standard?id=K9ik6zvon7orn try { Map zs = zigbee.parseZoneStatusChange(description) if (zs.alarm1Set == true) { handleMotion(motionActive=true) } else { handleMotion(motionActive=false) } } catch (e) { log.error "${device.displayName} This driver requires HE version 2.2.7 (May 2021) or newer!" return null } } private handleMotion( motionActive, isDigital=false ) { if (settings.invertMotion == true) { motionActive = ! motionActive } if (motionActive) { def timeout = motionResetTimer ?: 0 // If the sensor only sends a motion detected message, the reset to motion inactive must be performed in code if (settings.motionReset == true && timeout != 0) { runIn(timeout, resetToMotionInactive, [overwrite: true]) } if (device.currentState('motion')?.value != "active") { state.motionStarted = unix2formattedDate(now().toString()) } } else { if (device.currentState('motion')?.value == "inactive") { logDebug "ignored motion inactive event after ${getSecondsInactive()}s" return [:] // do not process a second motion inactive event! } } sendMotionEvent(motionActive, isDigital) } def sendMotionEvent( motionActive, isDigital=false ) { def descriptionText = "Detected motion" if (!motionActive) { descriptionText = "Motion reset to inactive after ${getSecondsInactive()}s" } else { descriptionText = device.currentValue("motion") == "active" ? "Motion is active ${getSecondsInactive()}s" : "Detected motion" } /* if (isBlackSquareRadar() && device.currentValue("motion", true) == "active" && (motionActive as boolean) == true) { // TODO - obsolete return // the black square radar sends 'motion active' every 4 seconds! } */ if (txtEnable) log.info "${device.displayName} ${descriptionText}" sendEvent ( name : 'motion', value : motionActive ? 'active' : 'inactive', //isStateChange : true, type : isDigital == true ? "digital" : "physical", descriptionText : descriptionText ) runIn( 1, formatAttrib, [overwrite: true]) } def resetToMotionInactive() { if (device.currentState('motion')?.value == "active") { def descText = "Motion reset to inactive after ${getSecondsInactive()}s (software timeout)" sendEvent( name : "motion", value : "inactive", isStateChange : true, type: "digital", descriptionText : descText ) if (txtEnable) log.info "${device.displayName} ${descText}" } else { if (txtEnable) log.debug "${device.displayName} ignored resetToMotionInactive (software timeout) after ${getSecondsInactive()}s" } } def getSecondsInactive() { def unixTime = formattedDate2unix(state.motionStarted) if (unixTime) { return Math.round((now() - unixTime)/1000) } else { return motionResetTimer ?: 0 } } def temperatureEvent( temperature ) { def map = [:] map.name = "temperature" map.unit = "\u00B0"+"${location.temperatureScale}" String tempConverted = convertTemperatureIfNeeded(temperature, "C", precision=1) map.value = tempConverted map.type = "physical" map.descriptionText = "${map.name} is ${map.value} ${map.unit}" map.isStateChange = true logInfo "${map.descriptionText}" sendEvent(map) runIn( 1, formatAttrib, [overwrite: true]) } def humidityEvent( humidity ) { def map = [:] map.name = "humidity" map.value = humidity as int map.unit = "% RH" map.type = "physical" map.isStateChange = true map.descriptionText = "${map.name} is ${Math.round((humidity) * 10) / 10} ${map.unit}" logInfo "${map.descriptionText}" sendEvent(map) runIn( 1, formatAttrib, [overwrite: true]) } def illuminanceEvent( rawLux ) { def lux = rawLux > 0 ? Math.round(Math.pow(10,(rawLux/10000))) : 0 illuminanceEventLux( lux as Integer) } def illuminanceEventLux( lux ) { Integer illumCorrected = Math.round((lux * ((settings?.illuminanceCoeff ?: 1.00) as float))) Integer delta = Math.abs(safeToInt(device.currentValue("illuminance")) - (illumCorrected as int)) if (device.currentValue("illuminance", true) == null || (delta >= safeToInt(settings?.luxThreshold))) { sendEvent("name": "illuminance", "value": illumCorrected, "unit": "lx", "type": "physical", "descriptionText": "Illuminance is ${lux} Lux") logInfo "Illuminance is ${illumCorrected} Lux" } else { logDebug "ignored illuminance event ${illumCorrected} lx : the change of ${delta} lx is less than the ${safeToInt(settings?.luxThreshold)} lux threshold!" } runIn( 1, formatAttrib, [overwrite: true]) } def occupancyEvent( raw ) { logDebug "occupancyEvent: raw=${raw}" def map = [:] map.name = "occupancy" map.value = raw ? "occupied" : "unoccupied" map.unit = "" map.type = "physical" map.isStateChange = true map.descriptionText = "${map.name} state is ${map.value}" logInfo "${map.descriptionText}" sendEvent(map) } def buttonEvent( action, buttonNumber=1 ) { logInfo "button $buttonNumber was $action" sendEvent(name: action, value: '1', data: [buttonNumber: 1], descriptionText: "button 1 was pushed", isStateChange: true, type: 'physical') } def powerSourceEvent( state = null) { String ps = null if (state != null && state == 'unknown' ) { ps = "unknown" } else if (state != null ) { ps = state } else { if (DEVICE.device?.powerSource != null) { ps = DEVICE.device?.powerSource } else if (!("Battery" in DEVICE.capabilities)) { ps = "dc" } else { ps = "unknown" } } sendEvent(name : "powerSource", value : ps, descriptionText: "device is back online", type: "digital") } // called on initial install of device during discovery // also called from initialize() in this driver! def installed() { log.info "${device.displayName} installed()..." initialize( fullInit = true ) //unschedule() } // called when preferences are saved def updated() { logInfo "updated()..." checkDriverVersion() updateTuyaVersion() ArrayList cmds = [] logInfo "Updating ${device.getLabel()} (${device.getName()}) model ${device.getDataValue('model')} manufacturer ${device.getDataValue('manufacturer')} deviceProfile=${state.deviceProfile}" logInfo "Debug logging is ${logEnable}; Description text logging is ${txtEnable}" if (logEnable==true) { runIn(86400, logsOff, [overwrite: true]) // turn off debug logging after 24 hours logInfo "Debug logging will be turned off after 24 hours" } else { unschedule(logsOff) } if (settings.allStatusTextEnable == false) { device.deleteCurrentState("all") } else { formatAttrib() } if (settings?.forcedProfile != null) { 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" } } else { //logDebug "forcedProfile is not set" } // LED enable - TODO ! if (is4in1()) { logDebug "4-in-1: changing ledEnable to : ${settings?.ledEnable }" cmds += sendTuyaCommand("6F", DP_TYPE_BOOL, settings?.ledEnable == true ? "01" : "00") logDebug "4-in-1: changing reportingTime4in1 to : ${settings?.reportingTime4in1} minutes" cmds += sendTuyaCommand("66", DP_TYPE_VALUE, zigbee.convertToHexString(settings?.reportingTime4in1 as int, 8)) } // sensitivity for PIR devices if (settings?.sensitivity != null) { if ((DEVICE.device?.type == "PIR") && (("sensitivity" in DEVICE.preferences) && (DEVICE.preferences.sensitivity != false))) { def val = settings?.sensitivity as int if (isIAS()) { if (val != null) { logDebug "changing IAS sensitivity to : ${sensitivityOpts.options[val]} (${val})" cmds += sendSensitivityIAS(val) } } else { if (settings?.logEnable) { log.warn "${device.displayName} changing TS0601 sensitivity to : ${val}" } setPar( "sensitivity", val as String) } } } else { logDebug "sensitivity is not set" } // keep time for PIR devices if (settings?.keepTime != null) { if ((DEVICE.device?.type == "PIR") && (("keepTime" in DEVICE.preferences) && (DEVICE.preferences.keepTime != false))) { if (isIAS() && (settings?.keepTime != null)) { cmds += sendKeepTimeIAS( settings?.keepTime ) logDebug "changing IAS Keep Time to : ${keepTime4in1Opts.options[settings?.keepTime as int]} (${settings?.keepTime})" } else { if (settings?.logEnable) { log.warn "${device.displayName} changing TS0601 Keep Time to : ${(settings?.keepTime as int )}" } setPar( "keepTime", settings?.keepTime as String) } } } else { logDebug "keepTime is not set" } // new update method for all radars, WITHOUT Linptech - TODO ! if (isLINPTECHradar()) { setPar( "fadingTime", settings?.fadingTime ?: 10 ) setPar( "motionDetectionDistance", settings?.motionDetectionDistance ) setPar( "motionDetectionSensitivity", settings?.motionDetectionSensitivity) setPar( "staticDetectionSensitivity", settings?.staticDetectionSensitivity ) } if (isSONOFF()) { setPar( "fadingTime", settings?.fadingTime ?: 60 ) setPar( "radarSensitivity", settings?.radarSensitivity ?: 2) // read backk the parameters from the device cmds += zigbee.readAttribute(0x0406, 0x0020, [:], delay=201) cmds += zigbee.readAttribute(0x0406, 0x0022, [:], delay=201) } else if (DEVICE.device?.type in ["radar", "PIR"]) { // Itterates through all settings cmds += updateAllPreferences() } // if ("DistanceMeasurement" in DEVICE.capabilities) { if (settings?.ignoreDistance == true ) { device.deleteCurrentState('distance') } } // if (("indicatorLight" in DEVICE.preferences)) { // BlackSquareRadar TODO !! if (indicatorLight != null) { def value = safeToInt(indicatorLight.value) def dpValHex = zigbee.convertToHexString(value as int, 2) cmds += sendTuyaCommand("67", DP_TYPE_BOOL, dpValHex) logDebug "setting indicator light to : ${blackRadarLedOptions[value.toString()]} (${value})" } } // if (settings.allStatusTextEnable == true) { runIn( 1, formatAttrib, [overwrite: true]) } int totalSize = 0 //log.warn "cmds=${cmds} length=${cmds?.size()}" if (cmds != null && cmds != [null]) { for (int i = 0; i < cmds?.size(); i++) { if (cmds[i] != null) totalSize += cmds[i].size() } } if (totalSize >= 7) { logDebug "sending the changed AdvancedOptions (size=${cmds?.size()}, totalSize=${totalSize}) to the device..." sendZigbeeCommands( cmds ) logInfo "preferencies updates are sent to the device..." } else { logDebug "no preferences are changed (totalSize=${totalSize}) cmds=${cmds}" } } def ping() { logInfo "ping() is not implemented" } // TODO - remove SiHAS specific code !!! private getILLUMINANCE_MEASUREMENT_CLUSTER() { 0x0400 } private getRELATIVE_HUMIDITY_CLUSTER() { 0x0405 } private getOCCUPANCY_SENSING_CLUSTER() { 0x0406 } private getATTRIBUTE_IAS_ZONE_STATUS() { 0x0000 } private getPOWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE() { 0x0020 } private getTEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE() { 0x0000 } private getRELATIVE_HUMIDITY_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE() { 0x0000 } private getILLUMINANCE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE() { 0x0000 } private getOCCUPANCY_SENSING_OCCUPANCY_ATTRIBUTE() { 0x0000 } // TODO - move to the standard Device Profile configuration settings !!! def refreshSiHAS() { if (settings?.logEnable) {log.debug "${device.displayName} refreshSiHAS()"} def refreshCmds = [] refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, POWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE, [:], delay=201) refreshCmds += zigbee.readAttribute(RELATIVE_HUMIDITY_CLUSTER, RELATIVE_HUMIDITY_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE, [:], delay=202) refreshCmds += zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, TEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE, [:], delay=203) refreshCmds += zigbee.readAttribute(ILLUMINANCE_MEASUREMENT_CLUSTER, ILLUMINANCE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE, [:], delay=204) refreshCmds += zigbee.readAttribute(OCCUPANCY_SENSING_CLUSTER, OCCUPANCY_SENSING_OCCUPANCY_ATTRIBUTE, [:], delay=205) refreshCmds += zigbee.enrollResponse(300) return refreshCmds } // TODO - move to the standard Device Profile configuration settings !!! def configureSiHAS() { if (settings?.logEnable) {log.debug "${device.displayName} configureSiHAS()"} def configCmds = [] // Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time) // sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default // humidity minReportTime 30 seconds, maxReportTime 60 min // illuminance minReportTime 30 seconds, maxReportTime 60 min // occupancy sensing minReportTime 10 seconds, maxReportTime 60 min // ex) zigbee.configureReporting(0x0001, 0x0020, DataType.UINT8, 600, 21600, 0x01) // This is for cluster 0x0001 (power cluster), attribute 0x0021 (battery level), whose type is UINT8, // the minimum time between reports is 10 minutes (600 seconds) and the maximum time between reports is 6 hours (21600 seconds), // and the amount of change needed to trigger a report is 1 unit (0x01). configCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, POWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE, DataType.UINT8, 30, 21600, 0x01/*100mv*1*/, [:], delay=221) configCmds += zigbee.configureReporting(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, TEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE, DataType.INT16, 15, 300, 10/*10/100=0.1?*/, [:], delay=222) configCmds += zigbee.configureReporting(RELATIVE_HUMIDITY_CLUSTER, RELATIVE_HUMIDITY_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE, DataType.UINT16, 15, 300, 40/*10/100=0.4%*/, [:], delay=223) configCmds += zigbee.configureReporting(ILLUMINANCE_MEASUREMENT_CLUSTER, ILLUMINANCE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE, DataType.UINT16, 15, 3600, 1/*1 lux*/, [:], delay=224) configCmds += zigbee.configureReporting(OCCUPANCY_SENSING_CLUSTER, OCCUPANCY_SENSING_OCCUPANCY_ATTRIBUTE, DataType.BITMAP8, 1, 600, 1, [:], delay=225) //configCmds += zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, 0, 0xffff, null, [:], delay=226) // set : none reporting flag, device sends out notification to the bound devices. configCmds += ["zdo bind 0x${device.deviceNetworkId} 0x${endpoint} 0x01 0x0500 {${device.zigbeeId}} {}", "delay 229", ] configCmds += zigbee.enrollResponse() + zigbee.readAttribute(0x0500, 0x0000) return configCmds + refreshSiHAS() } def refresh() { logInfo "refresh()..." checkDriverVersion() updateTuyaVersion() ArrayList cmds = [] cmds += zigbee.readAttribute(0x0000, 0x0007, [:], delay=191) // Power Source cmds += zigbee.readAttribute(0x0001, 0x0020, [:], delay=192) // batteryVoltage cmds += zigbee.readAttribute(0x0001, 0x0021, [:], delay=193) // batteryPercentageRemaining if (isIAS() || is4in1()) { IAS_ATTRIBUTES.each { key, value -> cmds += zigbee.readAttribute(0x0500, key, [:], delay=199) } } if (is4in1()) { cmds += zigbee.command(0xEF00, 0x07, "00") // Fantem Tuya Magic } if (isTuya()) { cmds += zigbee.command(0xEF00, 0x03) } if (isSiHAS()) { cmds += refreshSiHAS() } if (settings.allStatusTextEnable == true) { runIn( 1, formatAttrib, [overwrite: true]) } sendZigbeeCommands( cmds ) } 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()}" unschedule('pollPresence') // now replaced with deviceHealthCheck scheduleDeviceHealthCheck() updateTuyaVersion() initializeVars( fullInit = false ) state.driverVersion = driverVersionAndTimeStamp() if (state.tuyaDPs == null) state.tuyaDPs = [:] if (state.lastPresenceState != null) state.remove('lastPresenceState') // removed in version 1.0.6 if (state.hashStringPars != null) state.remove('hashStringPars') // removed in version 1.1.0 if (state.lastBattery != null) state.remove('lastBattery') // removed in version 1.3.0 if ("DistanceMeasurement" in DEVICE?.capabilities) { if (settings?.ignoreDistance == true ) { device.deleteCurrentState('distance') } device.deleteCurrentState('battery') device.deleteCurrentState('tamper') device.deleteCurrentState('temperature') } validateAndFixPreferences() // new in version 1.6.3 } } void scheduleDeviceHealthCheck() { Random rnd = new Random() //schedule("1 * * * * ? *", 'deviceHealthCheck') // for quick test schedule("${rnd.nextInt(59)} ${rnd.nextInt(59)} 1/3 * * ? *", 'deviceHealthCheck') } def logInitializeRezults() { if (settings?.txtEnable) log.info "${device.displayName} manufacturer = ${device.getDataValue("manufacturer")}" if (settings?.txtEnable) log.info "${device.displayName} Initialization finished\r version=${version()} (Timestamp: ${timeStamp()})" } // delete all attributes void deleteAllCurrentStates() { def ctr = 0 device.properties.supportedAttributes.each { it-> //logDebug "deleting $it" device.deleteCurrentState("$it") ctr++ } logInfo "All ${ctr} current states (attributes) DELETED" } void resetStats() { logInfo "Stats are reset..." state.tuyaDPs = [:] state.txCounter = 0 state.txCounter = 0 } // called by initialize() button void initializeVars( boolean fullInit = false ) { logInfo "InitializeVars( fullInit = ${fullInit} )..." if (fullInit == true) { deleteAllCurrentStates() state.clear() resetStats() state.driverVersion = driverVersionAndTimeStamp() state.motionStarted = unix2formattedDate(now()) } if (/*fullInit == true || */state.deviceProfile == null) { setDeviceNameAndProfile() } // state.packetID = 0 state.rxCounter = 0 state.txCounter = 0 if (fullInit == true || state.notPresentCounter == null) state.notPresentCounter = 0 // if (settings.logEnable == null) device.updateSetting("logEnable", true) if (settings.txtEnable == null) device.updateSetting("txtEnable", true) if (fullInit == true || settings.motionReset == null) device.updateSetting("motionReset", false) if (fullInit == true || settings.motionResetTimer == null) device.updateSetting("motionResetTimer", 60) if (settings.advancedOptions == null) device.updateSetting("advancedOptions", false) if (fullInit == true || settings.sensitivity == null) device.updateSetting("sensitivity", [value:"2", type:"enum"]) if (fullInit == true || settings.keepTime == null) device.updateSetting("keepTime", [value:"0", type:"enum"]) if (fullInit == true || settings.ignoreDistance == null) device.updateSetting("ignoreDistance", true) if (fullInit == true || settings.ledEnable == null) device.updateSetting("ledEnable", 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.luxOffset == null) device.updateSetting("luxOffset",[value:1.0, type:"decimal"]) if (fullInit == true || settings.luxThreshold == null) device.updateSetting("luxThreshold", [value:5, type:"number"]) if ((DEVICE?.capabilities?.IlluminanceMeasurement == true) && (DEVICE?.preferences.luxThreshold == false)) { logDebug "setting luxThreshold to 0" device.updateSetting("luxThreshold", [value:0, type:"number"]) } else { logDebug "luxThreshold is not set to 0 (luxThreshold=${DEVICE?.preferences.luxThreshold}, IlluminanceMeasurement=${DEVICE?.capabilities?.IlluminanceMeasurement})" } if (fullInit == true || settings.illuminanceCoeff == null) device.updateSetting("illuminanceCoeff", [value:1.0, type:"decimal"]) if (fullInit == true || settings.parEvents == null) device.updateSetting("parEvents", true) if (fullInit == true || settings.invertMotion == null) device.updateSetting("invertMotion", is2in1() ? true : false) if (fullInit == true || settings.reportingTime4in1 == null) device.updateSetting("reportingTime4in1", [value:DEFAULT_REPORTING_4IN1, type:"number"]) if (fullInit == true || settings.allStatusTextEnable == null) device.updateSetting("allStatusTextEnable", false) // version 1.6.0 - load DeviceProfile specific defaults ... if (fullInit == true) { resetPreferencesToDefaults() } // if (fullInit == true) sendEvent(name : "powerSource", value : "?", isStateChange : true) // TODO !!! if (device.currentValue('healthStatus') == null) sendHealthStatusEvent('unknown') // } // TODO - refine ! def isTuya() { return (device.getDataValue("model")?.startsWith("TS") == true) } def tuyaBlackMagic() { List cmds = [] if (isTuya()) { 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) } else { logDebug "tuyaBlackMagic for non-Tuya device ..." cmds += zigbee.readAttribute(0x0000, [0x0004, 0x0005, 0x4000], [:], delay=200) // manufacturer, model, SoftwareBuildID if (isIAS() ) { logDebug "tuyaBlackMagic for IAS device ..." cmds += zigbee.readAttribute(0x0500, 0x0001, [:], delay=201) // IAS Zone Cluster, Zone Type } } return cmds } // called when used with capability "Configuration" is called when the configure button is pressed on the device page. // Runs when driver is installed, after installed() is run. if capability Configuration exists, a Configure command is added to the ui // It is also called on initial install after discovery. def configure() { if (settings?.txtEnable) log.info "${device.displayName} configure().." //runIn( defaultPollingInterval, pollPresence, [overwrite: true]) scheduleDeviceHealthCheck() state.motionStarted = unix2formattedDate(now()) ArrayList cmds = [] cmds += tuyaBlackMagic() // commented out 10/21/2023 - we aleady have the BlackMagic in the initialize() method ! int intMinTime = safeToInt(3600) // TODO: make it configurable int intMaxTime = safeToInt(7200) // TODO: make it configurable if (isIAS() ) { cmds += zigbee.enrollResponse(300) + zigbee.readAttribute(0x0500, 0x0000, [:], delay=224) logDebug "Enrolling IAS device ..." } if (isSiHAS()) { cmds += configureSiHAS() } else { if ("0x0001" in DEVICE.configuration) { // Power Configuration cluster logDebug "configuring the battery reporting... (min=${intMinTime}, max=${intMaxTime}, delta=0x02)" cmds += zigbee.configureReporting(0x0001, 0x20, DataType.UINT8, intMinTime, intMaxTime, 0x02, [:], delay=226) // TEST - seems to be overwritten by the next line configuration? cmds += zigbee.configureReporting(0x0001, 0x21, DataType.UINT8, intMinTime, intMaxTime, 0x02, [:], delay=225) // delta 0x02 = 1% change battery percentage remaining cmds += zigbee.readAttribute(0x0001, 0x0020, [:], delay=228) // try also battery voltage cmds += zigbee.readAttribute(0x0001, 0x0021, [:], delay=227) // battery percentage - SONOFF GW configures and reads only attr 0x0021 ! } if ("0x0500" in DEVICE.configuration) { cmds += zigbee.configureReporting(0x0500, 0x0002, 0x19, 0, 3600, 0x00, [:], delay=227) } if ("0x0400" in DEVICE.configuration) { cmds += ["zdo bind 0x${device.deviceNetworkId} 0x01 0x01 0x0400 {${device.zigbeeId}} {}", "delay 229", ] } if ("0x0402" in DEVICE.configuration) { cmds += ["zdo bind 0x${device.deviceNetworkId} 0x01 0x01 0x0402 {${device.zigbeeId}} {}", "delay 229", ] } if ("0x0405" in DEVICE.configuration) { cmds += ["zdo bind 0x${device.deviceNetworkId} 0x01 0x01 0x0405 {${device.zigbeeId}} {}", "delay 229", ] } if ("0x0406" in DEVICE.configuration) { cmds += ["zdo bind 0x${device.deviceNetworkId} 0x01 0x01 0x0406 {${device.zigbeeId}} {}", "delay 229", ] // OWON and SONOFF motion/occupancy cluster } if ("0xFC11" in DEVICE.configuration) { cmds += zigbee.configureReporting(0xFC11, 0x2001, DataType.UINT16, 0, 1440, 0x01, [:], delay=230) // attribute 2001 - ?? } } sendZigbeeCommands(cmds) } // called when used with capability "Initialize" it will call this method every time the hub boots up. So for things that need refreshing or re-connecting (LAN integrations come to mind here) .. // runs first time driver loads, ie system startup // when capability Initialize exists, a Initialize command is added to the ui. def initialize( boolean fullInit = true ) { log.info "${device.displayName} Initialize( fullInit = ${fullInit} )..." unschedule() initializeVars( fullInit ) configure() runIn( 1, 'updated', [overwrite: true]) runIn( 3, 'logInitializeRezults', [overwrite: true]) runIn( 4, 'refresh', [overwrite: true]) } private sendTuyaCommand(dp, dp_type, fncmd) { ArrayList cmds = [] int tuyaCmd = is4in1() ? 0x04 : SETDATA cmds += zigbee.command(CLUSTER_TUYA, tuyaCmd, PACKET_ID + dp + dp_type + zigbee.convertToHexString((int)(fncmd.length()/2), 4) + fncmd ) logDebug "${device.displayName} sendTuyaCommand = ${cmds}" if (state.txCounter != null) state.txCounter = state.txCounter + 1 return cmds } Integer safeToInt(val, Integer defaultVal=0) { return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal } Double safeToDouble(val, Double defaultVal=0.0) { return "${val}"?.isDouble() ? "${val}".toDouble() : defaultVal } void sendZigbeeCommands(ArrayList cmd) { logDebug "sendZigbeeCommands (cmd=$cmd)" hubitat.device.HubMultiAction allActions = new hubitat.device.HubMultiAction() cmd.each { allActions.add(new hubitat.device.HubAction(it, hubitat.device.Protocol.ZIGBEE)) if (state.txCounter != null) state.txCounter = state.txCounter + 1 } sendHubCommand(allActions) } private getPACKET_ID() { state.packetID = ((state.packetID ?: 0) + 1 ) % 65536 return zigbee.convertToHexString(state.packetID, 4) } private getDescriptionText(msg) { def descriptionText = "${device.displayName} ${msg}" if (settings?.txtEnable) log.info "${descriptionText}" return descriptionText } def logsOff(){ if (settings?.logEnable) log.info "${device.displayName} debug logging disabled..." device.updateSetting("logEnable",[value:"false",type:"bool"]) } def getBatteryPercentageResult(rawValue) { def value = Math.round(rawValue / 2) //logDebug "getBatteryPercentageResult: rawValue = ${rawValue} -> ${value} %" if (value >= 0 && value <= 100) { sendBatteryEvent(value) } else { if (settings?.logEnable) log.warn "${device.displayName} ignoring getBatteryPercentageResult (${rawValue})" } } def sendBatteryVoltageEvent(rawValue) { logDebug "batteryVoltage = ${(double)rawValue / 10.0} V" 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 if (false) { result.value = Math.min(100, roundedPct) result.name = 'battery' result.unit = '%' result.descriptionText = "${device.displayName} battery is ${roundedPct} %" } else { result.value = volts result.name = 'batteryVoltage' result.unit = 'V' result.descriptionText = "${device.displayName} battery is ${volts} Volts" } result.type = 'physical' result.isStateChange = true if (settings?.txtEnable) log.info "${result.descriptionText}" sendEvent(result) } else { if (settings?.logEnable) log.warn "${device.displayName} ignoring BatteryResult(${rawValue})" } } def sendBatteryEvent( batteryPercent, isDigital=false ) { def map = [:] map.name = 'battery' map.timeStamp = now() map.value = batteryPercent < 0 ? 0 : batteryPercent > 100 ? 100 : (batteryPercent as int) map.unit = '%' map.type = isDigital ? 'digital' : 'physical' map.descriptionText = "${map.name} is ${map.value} ${map.unit}" map.isStateChange = true // def latestBatteryEvent = device.latestState('battery', skipCache=true) def latestBatteryEventTime = latestBatteryEvent != null ? latestBatteryEvent.getDate().getTime() : now() def timeDiff = ((now() - latestBatteryEventTime) / 1000) as int if (settings?.batteryDelay == null || (settings?.batteryDelay as int) == 0 || timeDiff > (settings?.batteryDelay as int)) { // send it now! sendDelayedBatteryEvent(map) } else { def delayedTime = (settings?.batteryDelay as int) - timeDiff map.delayed = delayedTime map.descriptionText += " [delayed ${map.delayed} seconds]" logDebug "this battery event (${map.value}%) will be delayed ${delayedTime} seconds" runIn( delayedTime, 'sendDelayedBatteryEvent', [overwrite: true, data: map]) } } private void sendDelayedBatteryEvent(Map map) { logInfo "${map.descriptionText}" //map.each {log.trace "$it"} sendEvent(map) } def setMotion( mode ) { switch (mode) { case "active" : handleMotion(motionActive=true, isDigital=true) break case "inactive" : handleMotion(motionActive=false, isDigital=true) break default : if (settings?.logEnable) log.warn "${device.displayName} please select motion action)" break } } def sendSensitivityIAS( lvl ) { def sensitivityLevel = safeToInt(lvl, -1) if (sensitivityLevel < 0 || sensitivityLevel > 2) { logWarn "IAS sensitivity is not set for ${device.getDataValue('manufacturer')}, invalid value ${sensitivityLevel}" return null } ArrayList cmds = [] String str = sensitivityOpts.options[sensitivityLevel] cmds += zigbee.writeAttribute(0x0500, 0x0013, DataType.UINT8, sensitivityLevel as int, [:], delay=200) logDebug "${device.displayName} sending IAS sensitivity : ${str} (${sensitivityLevel})" // only prepare the cmds here! return cmds } def sendKeepTimeIAS( lvl ) { def keepTimeVal = safeToInt(lvl, -1) if (keepTimeVal < 0 || keepTimeVal > 5) { logWarn "IAS Keep Time is not set for ${device.getDataValue('manufacturer')}, invalid value ${keepTimeVal}" return null } ArrayList cmds = [] String str = keepTime4in1Opts.options[keepTimeVal] cmds += zigbee.writeAttribute(0x0500, 0xF001, DataType.UINT8, keepTimeVal as int, [:], delay=200) logDebug "${device.displayName} sending IAS Keep Time : ${str} (${keepTimeVal})" // only prepare the cmds here! return cmds } // 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 only once now - 2023-01-31 // TODO - check! runIn( 1, formatAttrib, [overwrite: true]) logInfo "is online" } 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 offline!" } if (!(device.currentValue('motion', true) in ['inactive', '?'])) { handleMotion(false, isDigital=true) if (settings?.txtEnable) log.warn "${device.displayName} forced motion to 'inactive" } runIn( 1, formatAttrib, [overwrite: true]) } } else { logDebug "deviceHealthCheck - online (notPresentCounter=${state.notPresentCounter})" } } def sendHealthStatusEvent(value) { sendEvent(name: "healthStatus", value: value, descriptionText: "${device.displayName} healthStatus set to $value") } void formatAttrib() { if (settings.allStatusTextEnable == false) { // do not send empty html or text attributes return } String attrStr = "" attrStr += addToAttr("status", "healthStatus") attrStr += addToAttr("motion", "motion") if (DEVICE.capabilities?.DistanceMeasurement == true && settings?.ignoreDistance == false) { attrStr += addToAttr("distance", "distance") } if (DEVICE.capabilities?.Battery == true) { attrStr += addToAttr("battery", "battery") } if (DEVICE.capabilities?.IlluminanceMeasurement == true) { attrStr += addToAttr("illuminance", "illuminance") } if (DEVICE.capabilities?.TemperatureMeasurement == true) { attrStr += addToAttr("temperature", "temperature") } if (DEVICE.capabilities?.RelativeHumidityMeasurement == true) { attrStr += addToAttr("humidity", "humidity") } attrStr = attrStr.substring(0, attrStr.length() - 3); // remove the ', ' updateAttr("all", attrStr) if (attrStr.length() > 64) { updateAttr("all", "Max Attribute Size Exceeded: ${attrStr.length()}") } } String addToAttr(String name, String key, String convert = "none") { String retResult = '' String attrUnit = getUnitFromState(key) if (attrUnit == null) { attrUnit = "" } def curVal = device.currentValue(key,true) if (curVal != null) { if (convert == "int") { retResult += safeToInt(curVal).toString() + "" + attrUnit } else if (convert == "double") { retResult += safeToDouble(curVal).toString() + "" + attrUnit } else retResult += curVal.toString() + "" + attrUnit } else { retResult += "n/a" } retResult += ', ' return retResult } String getUnitFromState(String attrName){ return device.currentState(attrName)?.unit } void updateAttr(String aKey, aValue, String aUnit = "") { sendEvent(name:aKey, value:aValue, unit:aUnit, type: "digital") } def deleteAllStatesAndJobs() { state.clear() // clear all states unschedule() device.deleteCurrentState('') log.info "${device.displayName} jobs and states cleared. HE hub is ${getHubVersion()}, version is ${location.hub.firmwareVersionString}" } 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 setReportingTime4in1( val ) { if (is4in1()) { def value = safeToInt(val, -1) if (value >= 0) { logDebug "changing reportingTime4in1 to ${value==0 ? 10 : $val} ${value==0 ? 'seconds' : 'minutes'} (raw=${value})" return sendTuyaCommand("66", DP_TYPE_VALUE, zigbee.convertToHexString(value, 8)) } } else { return null } } // Linptech radar exception code below // TODO - refactor it! def setRadarSensitivity( val ) { if (isSONOFF()) { def value = safeToInt(val) logDebug "changing SONOFF radar sensitivity to ${val} " return zigbee.writeAttribute(0x0406, 0x0022, 0x20, val as int, [:], delay=200) } else { return null } } def setFadingTime( val ) { if (isLINPTECHradar()) { def value = safeToInt(val) logDebug "changing LINPTECH radar fadingTime to ${value} seconds" return sendTuyaCommand( "65", DP_TYPE_VALUE, zigbee.convertToHexString(value, 8)) // this is the only Linptech command that used Tuya DPs ... } else if (isSONOFF()) { def value = safeToInt(val) logDebug "changing SONOFF radar fadingTime to ${val} seconds" return zigbee.writeAttribute(0x0406, 0x0020, 0x21, val as int, [:], delay=200) } else { return null } } // Linptech specific - refactor it! def setMotionDetectionDistance( scaledValue ) { if (isLINPTECHradar()) { logDebug "changing LINPTECH radar MotionDetectionDistance to scaledValue=${scaledValue}" return zigbee.writeAttribute(0xE002, 0xE00B, 0x21, scaledValue as int, [:], delay=200) } else { return null } } def setMotionDetectionSensitivity( val ) { if (isLINPTECHradar()) { logDebug "changing LINPTECH radar MotionDetectionSensitivity to ${val}" return zigbee.writeAttribute(0xE002, 0xE004, 0x20, val as int, [:], delay=200) } else { return null } } def setStaticDetectionSensitivity( val ) { if (isLINPTECHradar()) { logDebug "changing LINPTECH radar StaticDetectionSensitivity to ${val}" return zigbee.writeAttribute(0xE002, 0xE005, 0x20, val as int, [:], delay=200) } else { return null } } /** * Returns the scaled value of a preference based on its type and scale. * @param preference The name of the preference to retrieve. * @param dpMap A map containing the type and scale of the preference. * @return The scaled value of the preference, or null if the preference is not found or has an unsupported type. */ def getScaledPreferenceValue(String preference, Map dpMap) { def value = settings."${preference}" def scaledValue if (value == null) { logDebug "getScaledPreferenceValue: preference ${preference} not found!" return null } switch(dpMap.type) { case "number" : scaledValue = safeToInt(value) break case "decimal" : scaledValue = safeToDouble(value) if (dpMap.scale != null && dpMap.scale != 1) { scaledValue = Math.round(scaledValue * dpMap.scale) } break case "bool" : scaledValue = value == "true" ? 1 : 0 break case "enum" : //log.warn "getScaledPreferenceValue: ENUM preference ${preference} type:${dpMap.type} value = ${value} dpMap.scale=${dpMap.scale}" if (dpMap.map == null) { logDebug "getScaledPreferenceValue: preference ${preference} has no map defined!" return null } scaledValue = value if (dpMap.scale != null && safeToInt(dpMap.scale) != 1) { scaledValue = Math.round(safeToDouble(scaledValue ) * safeToInt(dpMap.scale)) } break default : logDebug "getScaledPreferenceValue: preference ${preference} has unsupported type ${dpMap.type}!" return null } logDebug "getScaledPreferenceValue: preference ${preference} value = ${value} scaledValue = ${scaledValue} (scale=${dpMap.scale})" return scaledValue } // called from updated() method // TODO !!!!!!!!!! - refactor it !!! IAS settings do not use Tuya DPs !!! def updateAllPreferences() { logDebug "updateAllPreferences: preferences=${DEVICE.preferences}" ArrayList cmds = [] if (DEVICE.preferences == null || DEVICE.preferences == [:]) { logDebug "updateAllPreferences: no preferences defined for device profile ${getDeviceGroup()}" return null } Integer dpInt = 0 def scaledValue // int or String for enums (DEVICE.preferences).each { name, dp -> dpInt = safeToInt(dp, -1) if (dpInt <= 0) { // this is the IAS and other non-Tuya DPs preferences .... logDebug "updateAllPreferences: preference ${name} has invalid Tuya dp value ${dp}" return null } def dpMaps = DEVICE.tuyaDPs Map foundMap foundMap = getPreferencesMap(name) //logDebug "foundMap = ${foundMap}" if (foundMap != null) { scaledValue = getScaledPreferenceValue(name, foundMap) if (scaledValue != null) { logDebug "updateAllPreferences: preference ${foundMap.name} type:${foundMap.type} scaledValue = ${scaledValue} " if (foundMap.type == "enum") { logDebug "updateAllPreferences: ENUM preference ${foundMap.name} type:${foundMap.type} scaledValue = ${scaledValue} " } String DPType = (foundMap.type in ["number", "decimal"]) ? DP_TYPE_VALUE : foundMap.type == "bool" ? DP_TYPE_BOOL : foundMap.type == "enum" ? DP_TYPE_ENUM : "unknown" if (scaledValue != null) { cmds += setRadarParameterTuya(foundMap.name, zigbee.convertToHexString(dpInt, 2), DPType, scaledValue as int ) } else { logDebug "updateAllPreferences: preference ${foundMap.name} type:${foundMap.type} scaledValue = ${scaledValue} " } } else { logDebug "updateAllPreferences: preference ${foundMap.name} value not found!" return null } } else { logDebug "warning: couldn't find tuyaDPs map for preference ${name} (dp = ${dp})" return null } } logDebug "updateAllPreferences: ${cmds}" return cmds } def divideBy100( val ) { return (val as int) / 100 } def multiplyBy100( val ) { return (val as int) * 100 } def divideBy10( val ) { if (val > 10) { return (val as int) / 10 } else { return (val as int) } } def multiplyBy10( val ) { return (val as int) * 10 } def divideBy1( val ) { return (val as int) / 1 } //tests /** * Sets the radar parameter for the Tuya Multi Sensor 4 In 1 device. * @param parName The name of the parameter to set. * @param DPcommand The Tuya DP command to send. * @param DPType The type of the Tuya DP command. * @param DPval The value to set for the parameter. * @return An ArrayList of commands sent to the device. */ // TODO - replace this device-specific method !!! def setRadarParameterTuya( String parName, String DPcommand, String DPType, Integer DPval) { ArrayList cmds = [] def value switch (DPType) { case DP_TYPE_BOOL : case DP_TYPE_ENUM : value = zigbee.convertToHexString(DPval as int, 2) case DP_TYPE_VALUE : value = zigbee.convertToHexString(DPval as int, 8) break default : logWarn "${command}: unsupported DPType ${DPType} !" return null } cmds = sendTuyaCommand( DPcommand, DPType, value) logDebug "sending radar parameter ${parName} value ${DPval} (raw=${value}) Tuya dp=${DPcommand} (${zigbee.convertHexToInt(DPcommand)})" return cmds } // TODO - commands DP to be extracted from the device profile def resetSettings(val) { return radarCommand("resetSettings", isHL0SS9OAradar() ? "71" : "70", DP_TYPE_BOOL) } def moveSelfTest(val) { return radarCommand("moveSelfTest", isHL0SS9OAradar() ? "72" : "76", DP_TYPE_BOOL) } // check! def smallMoveSelfTest(val) { return radarCommand("smallMoveSelfTest", isHL0SS9OAradar() ? "6E" : "77", DP_TYPE_BOOL) } // check! def breatheSelfTest(val) { return radarCommand("breatheSelfTest", isHL0SS9OAradar() ? "6F" : "78", DP_TYPE_BOOL) } // check! // TODO - refactor !!! def radarCommand( String command, String DPcommand, String DPType) { ArrayList cmds = [] if (!(isHL0SS9OAradar() || is2AAELWXKradar())) { logWarn "${command}: unsupported model ${state.deviceProfile} !" return null } def value switch (DPType) { case DP_TYPE_BOOL : case DP_TYPE_ENUM : case DP_TYPE_VALUE : value = "00" cmds = sendTuyaCommand( DPcommand, DPType, value) break default : logWarn "${command}: unsupported DPType ${DPType} !" return null } logDebug "sending ${state.deviceProfile} radarCommand ${command} dp=${DPcommand} (${zigbee.convertHexToInt(DPcommand)})" return cmds } /** * Sends a command to the device. * @param command The command to send. Must be one of the commands defined in the DEVICE.commands map. * @param val The value to send with the command. * @return void */ def sendCommand( command=null, val=null ) { //logDebug "sending command ${command}(${val}))" ArrayList cmds = [] def supportedCommandsMap = DEVICE.commands if (supportedCommandsMap == null || supportedCommandsMap == []) { logInfo "sendCommand: no commands defined for device profile ${getDeviceGroup()} !" return } // TODO: compare ignoring the upper/lower case of the command. def supportedCommandsList = DEVICE.commands.keySet() as List // check if the command is defined in the DEVICE commands map if (command == null || !(command in supportedCommandsList)) { logInfo "sendCommand: the command ${(command ?: '')} must be one of these : ${supportedCommandsList}" return } def func try { func = DEVICE.commands.find { it.key == command }.value if (val != null) { cmds = "${func}"(val) logInfo "executed $func($val)" } else { cmds = "${func}"() logInfo "executed $func()" } } catch (e) { logWarn "sendCommand: Exception '${e}' caught while processing $func(${val})" return } if (cmds != null && cmds != []) { sendZigbeeCommands( cmds ) } } /** * Returns a list of valid parameters per model based on the device preferences. * * @return List of valid parameters. */ def getValidParsPerModel() { List validPars = [] if (DEVICE?.preferences != null && DEVICE?.preferences != [:]) { // use the preferences to validate the parameters validPars = DEVICE.preferences.keySet().toList() } return validPars } /** * Called from setPar() method only! * Validates the parameter value based on the given dpMap type and scales it if needed. * * @param dpMap The map containing the parameter type, minimum and maximum values. * @param val The value to be validated and scaled. * @return The validated and scaled value if it is within the specified range, null otherwise. */ def validateAndScaleParameterValue(Map dpMap, String val) { def value = null // validated value - integer, floar def scaledValue = null logDebug "validateAndScaleParameterValue dpMap=${dpMap} val=${val}" switch (dpMap.type) { case "number" : value = safeToInt(val, -1) scaledValue = value // scale the value - added 10/26/2023 also for integer values ! if (dpMap.scale != null) { scaledValue = (value * dpMap.scale) as Integer } break case "decimal" : value = safeToDouble(val, -1.0) // scale the value if (dpMap.scale != null) { scaledValue = (value * dpMap.scale) as Integer } break case "bool" : if (val == '0' || val == 'false') { value = scaledValue = 0 } else if (val == '1' || val == 'true') { value = scaledValue = 1 } else { log.warn "${device.displayName} sevalidateAndScaleParameterValue: bool parameter ${val}. value must be one of 0 1 false true" return null } break case "enum" : // val could be both integer or float value ... check if the scaling is different than 1 in dpMap if (dpMap.scale != null && safeToInt(dpMap.scale) != 1) { // we have a float parameter input - convert it to int value = safeToDouble(val, -1.0) scaledValue = (value * safeToInt(dpMap.scale)) as Integer } else { value = scaledValue = safeToInt(val, -1) } if (scaledValue == null || scaledValue < 0) { // get the keys of dpMap.map as a List List keys = dpMap.map.keySet().toList() log.warn "${device.displayName} validateAndScaleParameterValue: enum parameter ${val}. value must be one of ${keys}" return null } break default : logWarn "validateAndScaleParameterValue: unsupported dpMap type ${parType}" return null } //log.warn "validateAndScaleParameterValue before checking scaledValue=${scaledValue}" // check if the value is within the specified range if ((dpMap.min != null && value < dpMap.min) || (dpMap.max != null && value > dpMap.max)) { log.warn "${device.displayName} validateAndScaleParameterValue: invalid ${dpMap.name} parameter value ${value} (scaled ${scaledValue}). Value must be within ${dpMap.min} and ${dpMap.max}" return null } //log.warn "validateAndScaleParameterValue returning scaledValue=${scaledValue}" return scaledValue } /** * Sets the parameter value for the device. * @param par The name of the parameter to set. * @param val The value to set the parameter to. * @return Nothing. */ def setPar( par=null, val=null ) { if (DEVICE?.preferences != null && DEVICE?.preferences != [:]) { // new method logDebug "setPar new method: setting parameter ${par} to ${val}" ArrayList cmds = [] Boolean validated = false if (par == null) { log.warn "${device.displayName} setPar: 'parameter' must be one of these : ${getValidParsPerModel()}" return } if (!(par in getValidParsPerModel())) { log.warn "${device.displayName} setPar: parameter '${par}' must be one of these : ${getValidParsPerModel()}" return } // find the tuayDPs map for the par Map dpMap = getPreferencesMap(par, false) if ( dpMap == null ) { log.warn "${device.displayName} setPar: tuyaDPs map not found for parameter ${par}" return } if (val == null) { log.warn "${device.displayName} setPar: 'value' must be specified for parameter ${par} in the range ${dpMap.min} to ${dpMap.max}" return } // convert the val to the correct type and scales it if needed def tuyaValue = validateAndScaleParameterValue(dpMap, val as String) if (tuyaValue == null) { log.warn "${device.displayName} setPar: invalid parameter value ${val}. Must be in the range ${dpMap.min} to ${dpMap.max}" return } // update the device setting try { device.updateSetting("$par", [value:val, type:dpMap.type]) } catch (e) { logWarn "setPar: Exception '${e}'caught while updateSetting $par($val) type=${dpMap.type}" return } logDebug "parameter ${par} value ${val}, type ${dpMap.type} validated and scaled to ${tuyaValue} type=${dpMap.type}" // search for set function String capitalizedFirstChar = par[0].toUpperCase() + par[1..-1] String setFunction = "set${capitalizedFirstChar}" // check if setFunction method exists if (!this.respondsTo(setFunction)) { //logDebug "setPar: set function ${setFunction} not found" // try sending the parameter using the new universal method cmds = sendTuyaParameter(dpMap, par, tuyaValue) if (cmds == null || cmds == []) { logDebug "setPar: sendTuyaParameter par ${par} tuyaValue ${tuyaValue} returned null or empty list" return } else { logInfo "setPar: successfluly executed setPar $setFunction($val (tuyaValue=${tuyaValue}))" sendZigbeeCommands( cmds ) return } } logDebug "setPar: found setFunction=${setFunction}, tuyaValue=${tuyaValue} (val=${val})" // execute the setFunction try { cmds = "$setFunction"(tuyaValue) } catch (e) { logWarn "setPar: Exception '${e}'caught while processing $setFunction($tuyaValue) (val=${val}))" return } logDebug "setFunction result is ${cmds}" if (cmds == null || cmds == []) { logDebug "setPar: $setFunction($tuyaValue) returned null or empty list" // try sending the parameter using the new universal method cmds = sendTuyaParameter(dpMap, par, tuyaValue) if (cmds == null || cmds == []) { logWarn "setPar: $setFunction($tuyaValue) returned null or empty list" return } else { logInfo "setPar: successfluly executed setPar $setFunction($tuyaValue)" sendZigbeeCommands( cmds ) return } } logInfo "setPar: successfluly executed setPar $setFunction($tuyaValue)" sendZigbeeCommands( cmds ) return } } // function to send a Tuya command to data point taken from dpMap with value tuyaValue and type taken from dpMap // TODO - reuse it !!! def sendTuyaParameter( Map dpMap, String par, tuyaValue) { //logDebug "sendTuyaParameter: trying to send parameter ${par} value ${tuyaValue}" ArrayList cmds = [] if (dpMap == null) { log.warn "${device.displayName} sendTuyaParameter: tuyaDPs map not found for parameter ${par}" return null } String dp = zigbee.convertToHexString(dpMap.dp, 2) if (dpMap.dp <= 0 || dpMap.dp >= 256) { log.warn "${device.displayName} sendTuyaParameter: invalid dp ${dpMap.dp} for parameter ${par}" return null } String dpType = dpMap.type == "bool" ? DP_TYPE_BOOL : dpMap.type == "enum" ? DP_TYPE_ENUM : (dpMap.type in ["value", "number", "decimal"]) ? DP_TYPE_VALUE: null //log.debug "dpType = ${dpType}" if (dpType == null) { log.warn "${device.displayName} sendTuyaParameter: invalid dpType ${dpMap.type} for parameter ${par}" return null } // sendTuyaCommand def dpValHex = dpType == DP_TYPE_VALUE ? zigbee.convertToHexString(tuyaValue as int, 8) : zigbee.convertToHexString(tuyaValue as int, 2) logDebug "sendTuyaParameter: sending parameter ${par} dpValHex ${dpValHex} (raw=${tuyaValue}) Tuya dp=${dp} dpType=${dpType} " cmds = sendTuyaCommand( dp, dpType, dpValHex) return cmds } /** * Updates the Tuya version of the device based on the application version. * If the Tuya version has changed, updates the device data value and logs the change. */ void updateTuyaVersion() { if (!isTuya()) { return } def application = device.getDataValue("application") if (application != null) { Integer ver try { ver = zigbee.convertHexToInt(application) } catch (e) { logWarn "exception caught while converting application version ${application} to tuyaVersion" return } def str = ((ver&0xC0)>>6).toString() + "." + ((ver&0x30)>>4).toString() + "." + (ver&0x0F).toString() if (device.getDataValue("tuyaVersion") != str) { device.updateDataValue("tuyaVersion", str) logInfo "tuyaVersion set to $str" } } else { logDebug "application version is NULL" } } /** * Returns the device name and profile based on the device model and manufacturer. * @param model The device model (optional). If not provided, it will be retrieved from the device data value. * @param manufacturer The device manufacturer (optional). If not provided, it will be retrieved from the device data value. * @return A list containing the device name and profile. */ def getDeviceNameAndProfile( model=null, manufacturer=null) { def deviceName = UNKNOWN def deviceProfile = UNKNOWN String deviceModel = model != null ? model : device.getDataValue('model') ?: UNKNOWN String deviceManufacturer = manufacturer != null ? manufacturer : device.getDataValue('manufacturer') ?: UNKNOWN deviceProfilesV2.each { profileName, profileMap -> profileMap.fingerprints.each { fingerprint -> if (fingerprint.model == deviceModel && fingerprint.manufacturer == deviceManufacturer) { deviceProfile = profileName deviceName = fingerprint.deviceJoinName ?: deviceProfilesV2[deviceProfile].deviceJoinName ?: UNKNOWN logDebug "found exact match for model ${deviceModel} manufacturer ${deviceManufacturer} : profileName=${deviceProfile} deviceName =${deviceName}" return [deviceName, deviceProfile] } } } if (deviceProfile == UNKNOWN) { logInfo "NOT FOUND! deviceName =${deviceName} profileName=${deviceProfile} for model ${deviceModel} manufacturer ${deviceManufacturer}" } return [deviceName, deviceProfile] } // called from initializeVars( fullInit = true) def setDeviceNameAndProfile( model=null, manufacturer=null) { def (String deviceName, String deviceProfile) = getDeviceNameAndProfile(model, manufacturer) if (deviceProfile == null || deviceProfile == UNKNOWN) { logInfo "unknown model ${deviceModel} manufacturer ${deviceManufacturer}" // don't change the device name when unknown state.deviceProfile = UNKNOWN } def dataValueModel = model != null ? model : device.getDataValue('model') ?: UNKNOWN def dataValueManufacturer = manufacturer != null ? manufacturer : device.getDataValue('manufacturer') ?: UNKNOWN if (deviceName != NULL && deviceName != UNKNOWN ) { device.setName(deviceName) state.deviceProfile = deviceProfile device.updateSetting("forcedProfile", [value:deviceProfilesV2[deviceProfile].description, type:"enum"]) //logDebug "after : forcedProfile = ${settings.forcedProfile}" logInfo "device model ${dataValueModel} manufacturer ${dataValueManufacturer} was set to : deviceProfile=${deviceProfile} : deviceName=${deviceName}" } else { logInfo "device model ${dataValueModel} manufacturer ${dataValueManufacturer} was not found!" } } private String clusterLookup(Object cluster) { if (cluster) { int clusterInt = cluster in String ? hexStrToUnsignedInt(cluster) : cluster.toInteger() String label = zigbee.clusterLookup(clusterInt)?.clusterLabel String hex = "0x${intToHexStr(clusterInt, 2)}" return label ? "${label} (${hex}) cluster" : "cluster ${hex}" } return 'unknown cluster' } def testTuyaCmd( dpCommand, dpValue, dpTypeString ) { ArrayList cmds = [] def dpType = dpTypeString=="DP_TYPE_VALUE" ? DP_TYPE_VALUE : dpTypeString=="DP_TYPE_BOOL" ? DP_TYPE_BOOL : dpTypeString=="DP_TYPE_ENUM" ? DP_TYPE_ENUM : null def dpValHex = dpTypeString=="DP_TYPE_VALUE" ? zigbee.convertToHexString(dpValue as int, 8) : dpValue log.warn " sending TEST command=${dpCommand} value=${dpValue} ($dpValHex) type=${dpType}" sendZigbeeCommands( sendTuyaCommand(dpCommand, dpType, dpValHex) ) } def inputIt( String param, boolean debug=false ) { Map input = [:] Map foundMap = [:] if (!(param in DEVICE.preferences)) { if (debug) log.warn "inputIt: preference ${param} not defined for this device!" return null } def preference boolean isTuyaDP try { preference = DEVICE.preferences["$param"] } catch (e) { if (debug) log.warn "inputIt: exception ${e} caught while parsing preference ${param} value ${preference}" return null } // check for boolean values try { if (preference in [true, false]) { if (debug) log.warn "inputIt: preference ${param} is boolean value ${preference} - skipping it for now!" return null } } catch (e) { if (debug) log.warn "inputIt: exception ${e} caught while checking for boolean values preference ${param} value ${preference}" return null } try { isTuyaDP = preference.isNumber() } catch (e) { if (debug) log.warn "inputIt: exception ${e} caught while checking isNumber() preference ${param} value ${preference}" return null } //if (debug) log.debug "inputIt: preference ${param} found. value is ${preference} isTuyaDP=${isTuyaDP}" foundMap = getPreferencesMap(param) //if (debug) log.debug "foundMap = ${foundMap}" if (foundMap == null) { if (debug) log.warn "inputIt: map not found for param '${param}'!" return null } if (foundMap.rw != "rw") { if (debug) log.warn "inputIt: param '${param}' is read only!" return null } input.name = foundMap.name input.type = foundMap.type // bool, enum, number, decimal input.title = foundMap.title input.description = foundMap.description if (input.type in ["number", "decimal"]) { if (foundMap.min != null && foundMap.max != null) { input.range = "${foundMap.min}..${foundMap.max}" } if (input.range != null && input.description !=null) { input.description += "
Range: ${input.range}" if (foundMap.unit != null && foundMap.unit != "") { input.description += " (${foundMap.unit})" } } } else if (input.type == "enum") { input.options = foundMap.map }/* else if (input.type == "bool") { input.options = ["true", "false"] }*/ else { if (debug) log.warn "inputIt: unsupported type ${input.type} for param '${param}'!" return null } if (input.defaultValue != null) { input.defaultValue = foundMap.defaultValue } return input } def testParse(description) { logWarn "testParse: ${description}" parse(description) log.trace "---end of testParse---" } String unix2formattedDate( unixTime ) { try { if (unixTime == null) return null def date = new Date(unixTime.toLong()) return date.format("yyyy-MM-dd HH:mm:ss.SSS", location.timeZone) } catch (Exception e) { logDebug "Error formatting date: ${e.message}. Returning current time instead." return new Date().format("yyyy-MM-dd HH:mm:ss.SSS", location.timeZone) } } def formattedDate2unix( formattedDate ) { try { if (formattedDate == null) return null def date = Date.parse("yyyy-MM-dd HH:mm:ss.SSS", formattedDate) return date.getTime() } catch (Exception e) { logDebug "Error parsing formatted date: ${formattedDate}. Returning current time instead." return now() } } @Field static final Map SettableParsFieldMap = new ConcurrentHashMap<>().withDefault { new ConcurrentHashMap() } def getSettableParsList() { if (device?.id == null) { return ["SEE LOGS"] } if (SettableParsFieldMap.get(device?.id)) { return SettableParsFieldMap.get(device?.id).pars.keySet().toList() } // put a map in the SettableParsFieldMap for the device.id if it doesn't exist, containing the settable parameters Map settableParsMap = [:] settableParsMap['pars'] = DEVICE.preferences SettableParsFieldMap.put(device?.id, settableParsMap) def result = SettableParsFieldMap.get(device?.id).pars.keySet().toList() //log.trace "${result}" log.warn "stored ${SettableParsFieldMap.get(device?.id)}" return result } def validateAndFixPreferences() { //logDebug "validateAndFixPreferences: preferences=${DEVICE.preferences}" if (DEVICE.preferences == null || DEVICE.preferences == [:]) { logDebug "validateAndFixPreferences: no preferences defined for device profile ${getDeviceGroup()}" return null } def validationFailures = 0 def validationFixes = 0 def oldSettingValue def newValue String settingType DEVICE.preferences.each { Map foundMap = getPreferencesMap(it.key) if (foundMap == null) { logDebug "validateAndFixPreferences: map not found for preference ${it.key}" // 10/21/2023 - sevirity lowered to debug return null } settingType = device.getSettingType(it.key) oldSettingValue = device.getSetting(it.key) if (settingType == null) { logDebug "validateAndFixPreferences: settingType not found for preference ${it.key}" return null } //logDebug "validateAndFixPreferences: preference ${it.key} (dp=${it.value}) oldSettingValue = ${oldSettingValue} mapType = ${foundMap.type} settingType=${settingType}" if (foundMap.type != settingType) { logDebug "validateAndFixPreferences: preference ${it.key} (dp=${it.value}) new mapType = ${foundMap.type} differs from the old settingType=${settingType} (oldSettingValue = ${oldSettingValue}) " validationFailures ++ // remove the setting and create a new one using the foundMap.type try { device.removeSetting(it.key) logDebug "validateAndFixPreferences: removing setting ${it.key}" } catch (e) { logWarn "validateAndFixPreferences: exception ${e} caught while removing setting ${it.key}" return null } // first, try to use the old setting value try { // correct the oldSettingValue type if (foundMap.type == "decimal") { newValue = oldSettingValue.toDouble() } else if (foundMap.type == "number") { newValue = oldSettingValue.toInteger() } else if (foundMap.type == "bool") { newValue = oldSettingValue == "true" ? 1 : 0 } else if (foundMap.type == "enum") { // check if the old settingValue was 'true' or 'false' and convert it to 1 or 0 if (oldSettingValue == "true" || oldSettingValue == "false" || oldSettingValue == true || oldSettingValue == false) { newValue = (oldSettingValue == "true" || oldSettingValue == true) ? "1" : "0" } // check if there are any period chars in the foundMap.map string keys as String and format the settingValue as string with 2 decimals else if (foundMap.map.keySet().toString().any { it.contains(".") }) { newValue = String.format("%.2f", oldSettingValue) } else { // format the settingValue as a string of the integer value newValue = String.format("%d", oldSettingValue) } } // device.updateSetting(it.key, [value:newValue, type:foundMap.type]) logDebug "validateAndFixPreferences: removed and updated setting ${it.key} from old type ${settingType} to new type ${foundMap.type} with the old value ${oldSettingValue} to new value ${newValue}" validationFixes ++ } catch (e) { logWarn "validateAndFixPreferences: exception '${e}' caught while creating setting ${it.key} with type ${foundMap.type} to new type ${foundMap.type} with the old value ${oldSettingValue} to new value ${newValue}" // change the settingValue to the foundMap default value try { settingValue = foundMap.defaultValue device.updateSetting(it.key, [value:settingValue, type:foundMap.type]) logDebug "validateAndFixPreferences: updated setting ${it.key} from old type ${settingType} to new type ${foundMap.type} with default value ${newValue} " validationFixes ++ } catch (e2) { logWarn "validateAndFixPreferences: exception '${e2}' caught while setting default value ... Giving up!" return null } } } } logDebug "validateAndFixPreferences: validationFailures = ${validationFailures} validationFixes = ${validationFixes}" } def test( val ) { /* def result = inputIt( val, debug=true ) logWarn "test inputIt(${val}) = ${result}" */ log.trace settings resetPreferencesToDefaults(true) log.trace settings /* settings.each { k, v -> String settingName = k // remove [ and ] from the setting name settingName = settingName.replaceAll("\\[|\\]", "") logWarn "settings ${k} = ${v} settingName = ${settingName}" logWarn "settings ${k} = ${v} type = ${getSettingType('advancedOptions')}" } */ /* settings.each { k, v -> def x = device.getSettingType(k) log.info ("settings ${k} = ${v} (${x})") } */ //validateAndFixPreferences() //resetPreferencesToDefaults(true) //getPreferencesMap( "motionReset", true) }