/* * Tuya Air Sensor * * Description: * This Hubitat driver is designed for use with a variety of Tuya Sensor * * NOTE: If you update the driver using the import or other method, you should save your Preferences again. * * Features List: * Ability to check temperature 0-60 (℃) * Ability to check humidity 0-95 (%) * Ability to check CO2 0-1000 (ppm) * Ability to check a website (mine) if there is a newer version of the driver available * Ability to check formaldehyde 0-10.0 (mg/m3) * Ability to check VOC gas 0-99.9 (ppm) * Ability to check PM2.5 * * Incomplete Features: * Not yet able to set regular reporting interval * * Licensing: * Copyright 2024 David Snell * 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. * * Version Control: * 0.1.7 - Correction to ProcessEvent function and removal of old driver-specific attributes when Preferences are saved * 0.1.6 - Changed clusters for the 6-in-1 sensor and added a ModelType preference * 0.1.5 - Changing PM2.5 to an integer value not a float * 0.1.4 - Added support for a 6-in-1 sensor that includes PM2.5 * 0.1.3 - Added new fingerprint per request from @kkossev and updates to version handling * 0.1.2 - Added preferences to offset reported values and changed all values to float with 0.1 rounding * 0.1.1 - Update to correct which is which between Formaldehyde and VOC values (they were reversed) * 0.1.0 - Initial start of driver * * Thank you(s): * I would like to thank @Cobra his contributions to the community. Parts based on Cobra's driver update code * have been included at the bottom of the driver and are noted as such. */ import hubitat.zigbee.zcl.DataType // Returns the driver name def DriverName(){ return "Tuya Air Sensor" } // Returns the driver version def DriverVersion(){ return "0.1.7" } metadata{ definition( name: "Tuya Air Sensor", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/Tuya Air Sensor.groovy" ){ //capability "Configuration" //capability "Refresh" capability "Sensor" capability "TemperatureMeasurement" capability "CarbonDioxideMeasurement" capability "RelativeHumidityMeasurement" // Commands that have been implemented //command "ReadDevice" // Returns a few basic attributes to trace logging if enabled // Attributes being built into the device attribute "DriverName", "string" // Driver identifies the driver being used for update purposes attribute "DriverVersion", "string" // Version number of the driver attribute "DriverStatus", "string" // Status of the driver version compared to what is currently published attribute "Status", "string" // Used to provide general status of the device attribute "VOC", "number" // Volatile Organic Compounds (VOC) value reported attribute "formaldehyde", "number" // Formaldehyde (HCHO) value reported attribute "PM2.5", "number" // PM2.5 value reported fingerprint profileId: "0104", inClusters: "0000, 0004, 0005, EF00", outClusters: "0019, 000A", manufacturer: "_TZE200_8ygsuhe1", model: "TS0601", deviceJoinName: "Tuya ZigBee Air Sensor" fingerprint profileId: "0104", inClusters: "0000, 0004, 0005, EF00", outClusters: "0019, 000A", manufacturer: "_TZE200_yvx5lh6k", model: "TS0601", deviceJoinName: "Tuya ZigBee 5-in-1 Sensor" fingerprint profileId: "0104", inClusters: "0000, 0004, 0005, EF00", outClusters: "0019, 000A", manufacturer: "_TZE200_dwcarsat", model: "TS0601", deviceJoinName: "Tuya ZigBee 6-in-1 Sensor" } preferences{ section{ if( ShowAllPreferences || ShowAllPreferences == null ){ input( type: "decimal", name: "TemperatureOffset", title: "Temperature Offset", required: false, multiple: false, defaultValue: 0 ) input( type: "decimal", name: "HumidityOffset", title: "Humidity Offset", required: false, multiple: false, defaultValue: 0 ) input( type: "decimal", name: "CO2Offset", title: "CO2 Offset", required: false, multiple: false, defaultValue: 0 ) input( type: "decimal", name: "FormaldehydeOffset", title: "Formaldehyde Offset", required: false, multiple: false, defaultValue: 0 ) input( type: "decimal", name: "VOCOffset", title: "VOC Offset", required: false, multiple: false, defaultValue: 0 ) if( ModelType == "Tuya ZigBee 6-in-1 Sensor" ){ input( type: "decimal", name: "PMOffset", title: "PM2.5 Offset", required: false, multiple: false, defaultValue: 0 ) } //input( type: "enum", name: "ReportInterval", title: "Reporting interval?", description: "", required: false, multiple: false, options: [ "1 minute" ,"5 minutes", "10 minutes", "1 hour", "1 day" ], defaultValue: "5 minutes" ) input( type: "enum", name: "ModelType", title: "Model Type", required: true, multiple: false, options: [ "Tuya ZigBee Air Sensor", "Tuya ZigBee 5-in-1 Sensor", "Tuya ZigBee 6-in-1 Sensor" ], defaultValue: "Tuya ZigBee Air Sensor" ) input( type: "enum", name: "LogType", title: "Enable Logging?", required: true, multiple: false, options: [ "None", "Info", "Debug", "Trace" ], defaultValue: "Info" ) input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", required: false, defaultValue: true ) } else { input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", required: false, defaultValue: true ) } } } } // Called when preferences are saved // This clears state variables then sets some basic information as well as does a reconfig of the device def updated(){ Logging( "Saved preferences", 2 ) // Set the driver name and version before update checking is scheduled if( state."Driver Name" != null ){ state.remove( "Driver Name" ) state.remove( "Driver Version" ) device.deleteCurrentState( "Driver Name" ) device.deleteCurrentState( "Driver Version" ) } ProcessState( "DriverName", "${ DriverName() }" ) ProcessState( "DriverVersion", "${ DriverVersion() }" ) // Set up the regular checks for driver version schedule( new Date(), CheckForUpdate ) // Configure the regular ZigBee reporting //configure() } // Parse incoming device messages to generate events def parse( String description ){ parseReport( description ) } /* * This parses ZigBee reports. I am including as many clusters as possible in here, even ones this * driver will never handle, so that it can be a useful reference for future work or others. */ private parseReport( String description ){ def descMap = zigbee.parseDescriptionAsMap( description ) def cluster = descMap.cluster ?: descMap.clusterId def attrId = descMap.attrId def ClusterIdentified = "Unknown" as String // Meant for logging with class data came in for def Handled = false as boolean // Meant to help identify if a report handler exists for this data switch( cluster ){ // General Clusters case "0000": // Basic ClusterIdentified = "Basic" BasicReport( descMap ) Handled = true break case "0001": // Power configuration ClusterIdentified = "Power config" //BatteryReport( descMap ) //Handled = true break case "0002": // Device temperature config ClusterIdentified = "Device temperature config" break case "0003": // Identify ClusterIdentified = "Identify" break case "0004": // Groups ClusterIdentified = "Groups" break case "0005": // Scenes ClusterIdentified = "Scenes" break case "0006": // On/Off ClusterIdentified = "On/Off" break case "0007": // On/Off switch configuration ClusterIdentified = "On/Off switch configuration" break case "0008": // Level control ClusterIdentified = "Level control" break case "0009": // Alarms ClusterIdentified = "Alarms" break case "000A": // Time ClusterIdentified = "Time" break case "000B": // RSSI Location ClusterIdentified = "RSSI Location" break case "000C": // Analog Input ClusterIdentified = "Analog Input" break case "000D": // Analog Output ClusterIdentified = "Analog Output" break case "000E": // Analog Value ClusterIdentified = "Analog Value" break case "000F": // Binary Input ClusterIdentified = "Binary Input" break case "0010": // Binary Output ClusterIdentified = "Binary Output" break case "0011": // Binary Value ClusterIdentified = "Binary Value" break case "0012": // Multistate Input ClusterIdentified = "Multistate Input" break case "0013": // Multistate Output & ZDO Device Announce ClusterIdentified = "Multistate Output & ZDO Device Announce" AnnounceReport( descMap ) Handled = true break case "0014": // Multistate Value ClusterIdentified = "Multistate Value" break case "0015": // Commissioning ClusterIdentified = "Commissioning" break case "0016": // Partition Cluster ClusterIdentified = "Partition Cluster" break case "0017": // Node_Desc_Store_req ClusterIdentified = "Node_Desc_Store_req" break case "0018": // Power_Desc_Store_req ClusterIdentified = "Power_Desc_Store_req" break case "0019": // OTA Upgrades ClusterIdentified = "OTA Upgrades" break case "001A": // Power Profile ClusterIdentified = "Power Profile" break case "001B": // Appliance Control ClusterIdentified = "Appliance Control" break case "001C": // Find_Node_Cache_req ClusterIdentified = "Find_Node_Cache_req" break case "0020": // Poll Control ClusterIdentified = "Poll Control" break case "0021": // Bind_req ClusterIdentified = "Bind_req" break case "0022": // Unbind_req ClusterIdentified = "Unbind_req" break case "0023": // Bind_Register_req ClusterIdentified = "Bind_Register_req" break case "0024": // Replace_Device_req ClusterIdentified = "Replace_Device_req" break case "0025": // Store_Bkup_Bind_Entry_req ClusterIdentified = "Store_Bkup_Bind_Entry_req" break case "0026": // Rm_Bkup_Bind_Entry_req ClusterIdentified = "Rm_Bkup_Bind_Entry_req" break case "0027": // Backup_Bind_Table_req ClusterIdentified = "Backup_Bind_Table_req" break case "0028": // Recover_Bind_Table_req ClusterIdentified = "Recover_Bind_Table_req" break case "0029": // Backup_Source_Bind_req ClusterIdentified = "Backup_Source_Bind_req" break case "002A": // Recover_Source_Bind_req ClusterIdentified = "Recover_Source_Bind_req" break // Network Management case "0030": // Mgmt_NWK_Disc_req ClusterIdentified = "Mgmt_NWK_Disc_req" break case "0031": // Mgmt_LQI_req ClusterIdentified = "Mgmt_LQI_req" break case "0032": // Mgmt_Rtg_req ClusterIdentified = "Mgmt_Rtg_req" break case "0033": // Mgmt_Bind_req ClusterIdentified = "Mgmt_Bind_req" break case "0034": // Mgmt_Leave_req ClusterIdentified = "Mgmt_Leave_req" break case "0035": // Mgmt_Direct_Join_req ClusterIdentified = "Mgmt_Direct_Join_req" break case "0036": // Mgmt_Permit_Join_req ClusterIdentified = "Mgmt_Permit_Join_req" break case "0037": // Mgmt_Cache_req ClusterIdentified = "Mgmt_Cache_req" break case "0038": // Management Network Update Request ClusterIdentified = "Management Network Update Request" break // Closures Clusters case "0100": // Shade configuration ClusterIdentified = "Shade configuration" break case "0101": // Door Lock ClusterIdentified = "Door Lock" break case "0102": // Window Covering ClusterIdentified = "Window Covering" break case "0103": // Barrier Control ClusterIdentified = "Barrier Control" break // HVAC Clusters case "0200": // Pump config and control ClusterIdentified = "Pump config and control" break case "0201": // Thermostat ClusterIdentified = "Thermostat" break case "0202": // Fan Control ClusterIdentified = "Fan control" break case "0203": // Dehumidifier control ClusterIdentified = "Dehumidifier control" break case "0204": // Thermostat UI config ClusterIdentified = "Thermostat UI configur" break // Lighting Clusters case "0300": // Color control ClusterIdentified = "Color control" break case "0301": // Ballast configuration ClusterIdentified = "Ballast configuration" break // Measurement and Sensing Clusters case "0400": // Luminance measurement ClusterIdentified = "Luminance measurement" break case "0401": // Luminance level sensing ClusterIdentified = "Luminance level sensing" break case "0402": // Temperature measurement ClusterIdentified = "Temperature measurement" //TemperatureReport( descMap ) //Handled = true break case "0403": // Pressure measurement ClusterIdentified = "Pressure measurement" break case "0404": // Flow measurement ClusterIdentified = "Flow measurement" break case "0405": // Relative humidity measurement ClusterIdentified = "Relative humidity measurement" break case "0406": // Occupancy sensing ClusterIdentified = "Occupancy sensing" break case "040C": // CARBON_MONOXIDE_CONCENTRATION_MEASUREMENT ClusterIdentified = "CARBON_MONOXIDE_CONCENTRATION_MEASUREMENT" break case "040D": // CARBON_DIOXIDE_CONCENTRATION_MEASUREMENT ClusterIdentified = "CARBON_DIOXIDE_CONCENTRATION_MEASUREMENT" break case "040E": // ETHYLENE_CONCENTRATION_MEASUREMENT ClusterIdentified = "ETHYLENE_CONCENTRATION_MEASUREMENT" break case "040F": // ETHYLENE_OXIDE_CONCENTRATION_MEASUREMENT ClusterIdentified = "ETHYLENE_OXIDE_CONCENTRATION_MEASUREMENT" break case "0410": // HYDROGEN_CONCENTRATION_MEASUREMENT ClusterIdentified = "HYDROGEN_CONCENTRATION_MEASUREMENT" break case "0411": // HYDROGEN_SULPHIDE_CONCENTRATION_MEASUREMENT ClusterIdentified = "HYDROGEN_SULPHIDE_CONCENTRATION_MEASUREMENT" break case "0412": // NITRIC_OXIDE_CONCENTRATION_MEASUREMENT ClusterIdentified = "NITRIC_OXIDE_CONCENTRATION_MEASUREMENT" break case "0413": // NITROGEN_DIOXIDE_CONCENTRATION_MEASUREMENT ClusterIdentified = "NITROGEN_DIOXIDE_CONCENTRATION_MEASUREMENT" break case "0414": // OXYGEN_CONCENTRATION_MEASUREMENT ClusterIdentified = "OXYGEN_CONCENTRATION_MEASUREMENT" break case "0415": // OZONE_CONCENTRATION_MEASUREMENT ClusterIdentified = "OZONE_CONCENTRATION_MEASUREMENT" break case "0416": // SULFUR_DIOXIDE_CONCENTRATION_MEASUREMENT ClusterIdentified = "SULFUR_DIOXIDE_CONCENTRATION_MEASUREMENT" break case "0417": // DISSOLVED_OXYGEN_CONCENTRATION_MEASUREMENT ClusterIdentified = "DISSOLVED_OXYGEN_CONCENTRATION_MEASUREMENT" break case "0418": // BROMATE_CONCENTRATION_MEASUREMENT ClusterIdentified = "BROMATE_CONCENTRATION_MEASUREMENT" break case "0419": // CHLORAMINES_CONCENTRATION_MEASUREMENT ClusterIdentified = "CHLORAMINES_CONCENTRATION_MEASUREMENT" break case "041A": // CHLORINE_CONCENTRATION_MEASUREMENT ClusterIdentified = "CHLORINE_CONCENTRATION_MEASUREMENT" break case "041B": // FECAL_COLIFORM_AND_E_COLI_CONCENTRATION_MEASUREMENT ClusterIdentified = "FECAL_COLIFORM_AND_E_COLI_CONCENTRATION_MEASUREMENT" break case "041C": // FLUORIDE_CONCENTRATION_MEASUREMENT ClusterIdentified = "FLUORIDE_CONCENTRATION_MEASUREMENT" break case "041D": // HALOACETIC_ACIDS_CONCENTRATION_MEASUREMENT ClusterIdentified = "HALOACETIC_ACIDS_CONCENTRATION_MEASUREMENT" break case "041E": // TOTAL_TRIHALOMETHANES_CONCENTRATION_MEASUREMENT ClusterIdentified = "TOTAL_TRIHALOMETHANES_CONCENTRATION_MEASUREMENT" break case "041F": // TOTAL_COLIFORM_BACTERIA_CONCENTRATION_MEASUREMENT ClusterIdentified = "TOTAL_COLIFORM_BACTERIA_CONCENTRATION_MEASUREMENT" break case "0420": // TURBIDITY_CONCENTRATION_MEASUREMENT ClusterIdentified = "TURBIDITY_CONCENTRATION_MEASUREMENT" break case "0421": // COPPER_CONCENTRATION_MEASUREMENT ClusterIdentified = "COPPER_CONCENTRATION_MEASUREMENT" break case "0422": // LEAD_CONCENTRATION_MEASUREMENT ClusterIdentified = "LEAD_CONCENTRATION_MEASUREMENT" break case "0423": // MANGANESE_CONCENTRATION_MEASUREMENT ClusterIdentified = "MANGANESE_CONCENTRATION_MEASUREMENT" break case "0424": // SULFATE_CONCENTRATION_MEASUREMENT ClusterIdentified = "SULFATE_CONCENTRATION_MEASUREMENT" break case "0425": // BROMODICHLOROMETHANE_CONCENTRATION_MEASUREMENT ClusterIdentified = "BROMODICHLOROMETHANE_CONCENTRATION_MEASUREMENT" break case "0426": // BROMOFORM_CONCENTRATION_MEASUREMENT ClusterIdentified = "BROMOFORM_CONCENTRATION_MEASUREMENT" break case "0427": // CHLORODIBROMOMETHANE_CONCENTRATION_MEASUREMENT ClusterIdentified = "CHLORODIBROMOMETHANE_CONCENTRATION_MEASUREMENT" break case "0428": // CHLOROFORM_CONCENTRATION_MEASUREMENT ClusterIdentified = "CHLOROFORM_CONCENTRATION_MEASUREMENT" break case "0429": // SODIUM_CONCENTRATION_MEASUREMENT ClusterIdentified = "SODIUM_CONCENTRATION_MEASUREMENT" break // Security & Safety Clusters case "0500": // IAS Zone ClusterIdentified = "IAS Zone" //IASZoneReport( descMap ) //Handled = true break case "0501": // IAS ACE "Ancillary Control" ClusterIdentified = "IAS ACE Ancillary Control" break case "0502": // IAS WD "Warning Devices" ClusterIdentified = "IAS WD Warning Devices" break // Protocol Interface Clusters case "0600": // Generic Tunnel ClusterIdentified = "Interface Clusters" break case "0601": // BACnet Protocol Tunnel ClusterIdentified = "BACnet Protocol Tunnel" break case "0602": // Analog Input (BACnet Regular) ClusterIdentified = "Analog Input (BACnet Regular)" break case "0603": // Analog Input (BACnet Extended) ClusterIdentified = "Analog Input (BACnet Extended)" break case "0604": // Analog Output (BACnet Regular) ClusterIdentified = "Analog Output (BACnet Regular)" break case "0605": // Analog Output (BACnet Extended) ClusterIdentified = "Analog Output (BACnet Extended)" break case "0606": // Analog Value (BACnet Regular) ClusterIdentified = "Analog Value (BACnet Regular)" break case "0607": // Analog Value (BACnet Extended) ClusterIdentified = "Analog Value (BACnet Extended)" break case "0608": // Binary Input (BACnet Regular) ClusterIdentified = "Binary Input (BACnet Regular)" break case "0609": // Binary Input (BACnet Extended) ClusterIdentified = "Binary Input (BACnet Extended)" break case "060A": // Binary Output (BACnet Regular) ClusterIdentified = "Binary Output (BACnet Regular)" break case "060B": // Binary Output (BACnet Extended) ClusterIdentified = "Binary Output (BACnet Extended)" break case "060C": // Binary Value (BACnet Regular) ClusterIdentified = "Binary Value (BACnet Regular)" break case "060D": // Binary Value (BACnet Extended) ClusterIdentified = "Binary Value (BACnet Extended)" break case "060E": // Multistate Input (BACnet Regular) ClusterIdentified = "Multistate Input (BACnet Regular)" break case "060F": // Multistate Input (BACnet Extended) ClusterIdentified = "Multistate Input (BACnet Extended)" break case "0610": // Multistate Output (BACnet Regular) ClusterIdentified = "Multistate Output (BACnet Regular)" break case "0611": // Multistate Output (BACnet Extended) ClusterIdentified = "Multistate Output (BACnet Extended)" break case "0612": // Multistate Value (BACnet Regular) ClusterIdentified = "Multistate Value (BACnet Regular)" break case "0613": // Multistate Value (BACnet Extended) ClusterIdentified = "Multistate Value (BACnet Extended)" break case "0614": // Protocol Tunnel Cluster ClusterIdentified = "Protocol Tunnel Cluster" break case "0615": // Protocol Tunnel ClusterIdentified = "Protocol Tunnel" break case "0700": // Price ClusterIdentified = "Price" break case "0701": // Demand Response and Load Control ClusterIdentified = "Demand Response and Load Control" break case "0702": // Metering ClusterIdentified = "Metering" break case "0703": // Messaging ClusterIdentified = "Messaging" break case "0704": // Tunneling Cluster ClusterIdentified = "Tunneling Cluster" break case "0705": // PREPAYMENT ClusterIdentified = "PREPAYMENT" break case "0706": // ENERGY_MANAGEMENT ClusterIdentified = "ENERGY_MANAGEMENT" break case "0707": // CALENDAR ClusterIdentified = "CALENDAR" break case "0708": // DEVICE_MANAGEMENT ClusterIdentified = "DEVICE_MANAGEMENT" break case "0709": // EVENTS ClusterIdentified = "EVENTS" break case "070A": // MDU_PAIRING ClusterIdentified = "MDU_PAIRING" break case "070B": // SUB_GHZ ClusterIdentified = "SUB_GHZ" break case "0800": // Key Establishment ClusterIdentified = "Key Establishment" break case "0900": // INFORMATION ClusterIdentified = "INFORMATION" break case "0901": // DATA_SHARING ClusterIdentified = "DATA_SHARING" break case "0902": // GAMING ClusterIdentified = "GAMING" break case "0903": // DATA_RATE_CONTROL ClusterIdentified = "DATA_RATE_CONTROL" break case "0904": // VOICE_OVER_ZIGBEE ClusterIdentified = "VOICE_OVER_ZIGBEE" break case "0905": // CHATTING ClusterIdentified = "CHATTING" break case "0A00": // PAYMENT ClusterIdentified = "PAYMENT" break case "0A01": // BILLING ClusterIdentified = "BILLING" break case "0B00": // APPLIANCE_IDENTIFICATION ClusterIdentified = "APPLIANCE_IDENTIFICATION" break case "0B01": // Meter Identification ClusterIdentified = "Meter Identification" break case "0B02": // Appliance Events and Alerts ClusterIdentified = "Appliance Events and Alerts" break case "0B03": // Appliance Statistics ClusterIdentified = "Appliance Statistics" break case "0B04": // Electrical Measurement ClusterIdentified = "Electrical Measurement" break case "0B05": // Diagnostic ClusterIdentified = "Diagnostic" break case "1000": // ZLL_COMMISSIONING ClusterIdentified = "ZLL_COMMISSIONING" break case "8000": // NWK_ADDR_RSP ClusterIdentified = "NWK_ADDR_RSP" break case "8001": // IEEE_ADDR_RSP ClusterIdentified = "IEEE_ADDR_RSP" break case "8002": // NODE_DESC_RSP ClusterIdentified = "NODE_DESC_RSP" break case "8003": // POWER_DESC_RSP ClusterIdentified = "POWER_DESC_RSP" break case "8004": // SIMPLE_DESC_RSP ClusterIdentified = "SIMPLE_DESC_RSP" break case "8005": // ACTIVE_EP_RSP ClusterIdentified = "ACTIVE_EP_RSP" break case "8006": // MATCH_DESC_RSP ClusterIdentified = "MATCH_DESC_RSP" break case "8010": // Complex_Desc_rsp ClusterIdentified = "Complex_Desc_rsp" break case "8011": // User_Desc_rsp ClusterIdentified = "User_Desc_rsp" break case "8012": // Discovery_Cache_rsp ClusterIdentified = "Discovery_Cache_rsp" break case "8014": // User_Desc_Conf ClusterIdentified = "User_Desc_Conf" break case "8015": // System_Server_Discover_rsp ClusterIdentified = "System_Server_Discover_rsp" break case "8016": // Discovery_Store_rsp ClusterIdentified = "Discovery_Store_rsp" break case "8017": // Node_Desc_Store_rso ClusterIdentified = "Node_Desc_Store_rso" break case "8018": // Power_Desc_Store_rsp ClusterIdentified = "Power_Desc_Store_rsp" break case "8019": // Active_EP_Store_rsp ClusterIdentified = "Active_EP_Store_rsp" break case "801A": // Simple_Desc_Store_rsp ClusterIdentified = "Simple_Desc_Store_rsp" break case "801B": // Remove_Node_Cache_rsp ClusterIdentified = "Remove_Node_Cache_rsp" break case "801C": // Find_Node_Cache_rsp ClusterIdentified = "Find_Node_Cache_rsp" break // End Device Bind, Unbind and Bind Management case "8020": // End_Dev_Bind_rsp ClusterIdentified = "End_Dev_Bind_rsp" break case "8021": // Bind_rsp ClusterIdentified = "Bind_rsp" HandleBindResponse( descMap ) Handled = true break case "8022": // Unbind_rsp ClusterIdentified = "Unbind_rsp" break case "8023": // Bind_Register_rsp ClusterIdentified = "Bind_Register_rsp" break case "8024": // Replace_Device_rsp ClusterIdentified = "Replace_Device_rsp" break case "8025": // Store_Bkup_Bind_Entry_rsp ClusterIdentified = "Store_Bkup_Bind_Entry_rsp" break case "8026": // Rm_Bkup_Bind_Entry_rsp ClusterIdentified = "Rm_Bkup_Bind_Entry_rsp" break case "8027": // Backup_Bind_Table_rsp ClusterIdentified = "Backup_Bind_Table_rsp" break case "8028": // Recover_Bind_Table_rsp ClusterIdentified = "Recover_Bind_Table_rsp" break case "8029": // Backup_Source_Bind_rsp ClusterIdentified = "Backup_Source_Bind_rsp" break case "802A": // Recover_Source_Bind_rsp ClusterIdentified = "Recover_Source_Bind_rsp" break // Network Management case "8030": // Mgmt_NWK_Disc_rsp ClusterIdentified = "Mgmt_NWK_Disc_rsp" break case "8031": // Mgmt_LQI_rsp ClusterIdentified = "Mgmt_LQI_rsp" break case "8032": // Mgmt_Rtg_rsp ClusterIdentified = "Mgmt_Rtg_rsp" break case "8033": // Mgmt_Bind_rsp ClusterIdentified = "Mgmt_Bind_rsp" break case "8034": // Mgmt_Leave_rsp ClusterIdentified = "Mgmt_Leave_rsp" HandleLeaveResponse( descMap ) Handled = true break case "8035": // Mgmt_Direct_Join_rsp ClusterIdentified = "Mgmt_Direct_Join_rsp" break case "8036": // Mgmt_Permit_Join_rsp ClusterIdentified = "Mgmt_Permit_Join_rsp" break case "8037": // Mgmt_Cache_rsp ClusterIdentified = "Mgmt_Cache_rsp" break case "8038": // Management Network Update Notify ClusterIdentified = "Management Network Update Notify" break case "EF00": // Tuya Specific Cluster ClusterIdentified = "Tuya Specific Cluster" //Logging( "Tuya Data : ${ descMap }", 4 ) switch( ModelType ){ case "Tuya ZigBee 6-in-1 Sensor": HandleTuyaCluster6( descMap ) Handled = true break default: HandleTuyaCluster( descMap ) Handled = true break } break case "FC00": // SAMPLE_MFG_SPECIFIC ClusterIdentified = "SAMPLE_MFG_SPECIFIC" break case "FC01": // OTA_CONFIGURATION ClusterIdentified = "OTA_CONFIGURATION" break case "FC02": // MFGLIB ClusterIdentified = "MFGLIB" break case "FC57": // SL_WWAH ClusterIdentified = "SL_WWAH" break case "FD00": // Halo Smoke Detector Only ??? ClusterIdentified = "Halo Smoke Detector Only ???" break case "FD01": // Halo Smoke Detector Only ??? ClusterIdentified = "Halo Smoke Detector Only ???" break case "FD02": // Halo Smoke Detector Only ??? ClusterIdentified = "Halo Smoke Detector Only ???" break default: ClusterIdentified = "Unknown" break } if( ClusterIdentified == "Unknown" ){ Logging( "Unknown cluster. descMap = ${ descMap }", 3 ) } else { if( !Handled ){ Logging( "Unhandled cluster. ${ cluster } is ${ ClusterIdentified }, descMap = ${ descMap }", 3 ) } } } // Attempts to process the Tuya Specific cluster private HandleTuyaCluster( descMap ){ Logging( "Raw Tuya Cluster Data = ${ descMap.data }", 4 ) if( descMap == null ) return if( descMap.data == null ) return if( descMap.data.size() == 10 ){ def Value = hexStrToSignedInt( "${ descMap.data[ 8 ] }${ descMap.data[ 9 ] }" ) switch( descMap.data[ 2 ] ){ case "02": // humidity Logging( "Tuya Field 2 - Raw Humidity: ${ Value }", 4 ) if( HumidityOffset == null ){ HumidityOffset = 0 } ProcessEvent( "humidity", ( Math.round( ( ( Value / 10 ) + ( HumidityOffset as float ) ) * 10 ) / 10 ) as float, "%" ) break case "12": // temperature Logging( "Tuya Field 12 - Raw Temperature: ${ Value }", 4 ) if( TemperatureOffset == null ){ TemperatureOffset = 0 } ProcessEvent( "temperature", ( Math.round( ( ConvertTemperature( "C", ( Value / 10 ) ) + ( TemperatureOffset as float ) ) * 10 ) / 10 ) as float, "°${ location.temperatureScale }" ) break case "13": // carbon dioxide Logging( "Tuya Field 13 - Raw CO2: ${ Value }", 4 ) if( CO2Offset == null ){ CO2Offset = 0 } ProcessEvent( "carbonDioxide", ( Math.round( ( Value + ( CO2Offset as float ) ) * 10 ) / 10 ) as float, "ppm" ) break case "15": // formaldehyde Logging( "Tuya Field 15 - Raw Formaldehyde: ${ Value }", 4 ) if( FormaldehydeOffset == null ){ FormaldehydeOffset = 0 } ProcessEvent( "formaldehyde", ( Math.round( ( ( Value / 10 ) + ( FormaldehydeOffset as float ) ) * 10 ) / 10 ) as float, "ppm" ) break case "16": // VOC Logging( "Tuya Field 16 - Raw VOC: ${ Value }", 4 ) if( VOCOffset == null ){ VOCOffset = 0 } ProcessEvent( "VOC", ( Math.round( ( ( Value / 10 ) + ( VOCOffset as float ) ) * 10 ) / 10 ) as float, "mg/m3" ) break default: Logging( "Tuya Cluster Data: ${ descMap.data }", 4 ) break } } else { Logging( "Unknown Tuya Cluster Data: ${ descMap.data }", 4 ) } } // Attempts to process the Tuya Specific cluster for the 6-in-1 private HandleTuyaCluster6( descMap ){ Logging( "Raw Tuya Cluster Data = ${ descMap.data }", 4 ) if( descMap == null ) return if( descMap.data == null ) return if( descMap.data.size() == 10 ){ def Value = hexStrToSignedInt( "${ descMap.data[ 8 ] }${ descMap.data[ 9 ] }" ) switch( descMap.data[ 2 ] ){ case "02": // PM2.5??? Logging( "Tuya Field 02 - Raw PM2.5???: ${ Value }", 4 ) if( PMOffset == null ){ PMOffset = 0 } ProcessEvent( "PM2.5", ( ( Value as int ) + ( PMOffset as int ) ) ) break case "12": // temperature Logging( "Tuya Field 12 - Raw Temperature: ${ Value }", 4 ) if( TemperatureOffset == null ){ TemperatureOffset = 0 } ProcessEvent( "temperature", ( Math.round( ( ConvertTemperature( "C", ( Value / 10 ) ) + ( TemperatureOffset as float ) ) * 10 ) / 10 ) as float, "°${ location.temperatureScale }" ) break case "13": // carbon dioxide Logging( "Tuya Field 13 - Raw CO2: ${ Value }", 4 ) if( CO2Offset == null ){ CO2Offset = 0 } ProcessEvent( "carbonDioxide", ( Math.round( ( Value + ( CO2Offset as float ) ) * 10 ) / 10 ) as float, "ppm" ) break case "14": // formaldehyde Logging( "Tuya Field 14 - Raw Formaldehyde: ${ Value }", 4 ) if( FormaldehydeOffset == null ){ FormaldehydeOffset = 0 } ProcessEvent( "formaldehyde", ( Math.round( ( ( Value / 10 ) + ( FormaldehydeOffset as float ) ) * 10 ) / 10 ) as float, "ppm" ) break case "15": // VOC Logging( "Tuya Field 15 - Raw VOC: ${ Value }", 4 ) if( VOCOffset == null ){ VOCOffset = 0 } ProcessEvent( "VOC", ( Math.round( ( ( Value / 10 ) + ( VOCOffset as float ) ) * 10 ) / 10 ) as float, "mg/m3" ) break case "16": // humidity Logging( "Tuya Field 16 - Raw Humidity: ${ Value }", 4 ) if( HumidityOffset == null ){ HumidityOffset = 0 } ProcessEvent( "humidity", ( Math.round( ( ( Value / 10 ) + ( HumidityOffset as float ) ) * 10 ) / 10 ) as float, "%" ) break default: Logging( "Tuya Cluster Data: ${ descMap.data }", 4 ) break } } else { Logging( "Unknown Tuya Cluster Data: ${ descMap.data }", 4 ) } } // Processes bind responses def HandleBindResponse( descMap ){ //Logging( "Bind Response: ${ descMap }", 4 ) } // Processes bind responses def HandleLeaveResponse( descMap ){ Logging( "Received the command to leave the hub.", 2 ) } // Processes reporting for the Basic class def BasicReport( descMap ){ switch( descMap.attrId ){ case "0000": Logging( "ZCLVersion = ${ descMap.value }", 4 ) break case "0001": Logging( "ApplicationVersion = ${ descMap.value }", 4 ) break case "0002": Logging( "StackVersion = ${ descMap.value }", 4 ) break case "0003": Logging( "HardwareVersion = ${ descMap.value }", 4 ) break case "0004": Logging( "ManufacturerName = ${ descMap.value }", 4 ) break case "0005": Logging( "ModelIdentifier = ${ descMap.value }", 4 ) break case "0006": Logging( "DateCode = ${ descMap.value }", 4 ) break case "0007": switch( descMap.value ){ case "00": Logging( "PowerSource = Unknown", 3 ) break case "01": Logging( "PowerSource = Mains (single phase)", 4 ) break case "02": Logging( "PowerSource = Mains (3 phase)", 4 ) break case "03": Logging( "PowerSource = Battery", 4 ) break case "04": Logging( "PowerSource = DC source", 4 ) break case "05": Logging( "PowerSource = Emergency mains constantly powered", 4 ) break case "06": Logging( "PowerSource = Emergency mains and transfer switch", 4 ) break default: Logging( "PowerSource is unlisted", 4 ) break } break case "0010": Logging( "LocationDescription = ${ descMap.value }", 4 ) break case "0011": Logging( "PhysicalEnvironment = ${ descMap.value }", 4 ) break case "0012": Logging( "DeviceEnabled = ${ descMap.value }", 4 ) break case "0013": Logging( "AlarmMask = ${ descMap.value }", 4 ) break case "0014": Logging( "DisableLocalConfig = ${ descMap.value }", 4 ) break case "4000": Logging( "SWBuildID = ${ descMap.value }", 4 ) break default: Logging( "Basic descMap = ${ descMap }", 3 ) break } } // Handles ZDO Announce reports def AnnounceReport( descMap ){ switch( descMap.command ){ case "00": Logging( "Device alive/awake", 4 ) break default: Logging( "Announce Report ${ descMap }", 3 ) break } } // ReadDevice is meant to check all the device basic information def ReadDevice(){ def cmds = zigbee.readAttribute( 0x0000, 0x0000 ) // ZCLVersion = REPORTED cmds = cmds + zigbee.readAttribute( 0x0000, 0x0001 ) // ApplicationVersion = REPORTED cmds = cmds + zigbee.readAttribute( 0x0000, 0x0002 ) // StackVersion = REPORTED cmds = cmds + zigbee.readAttribute( 0x0000, 0x0003 ) // HardwareVersion = REPORTED cmds = cmds + zigbee.readAttribute( 0x0000, 0x0004 ) // ManufacturerName = REPORTED cmds = cmds + zigbee.readAttribute( 0x0000, 0x0005 ) // ModelIdentifier = REPORTED //cmds = cmds + zigbee.readAttribute( 0x0000, 0x0006 ) // DateCode = NULL REPORTED cmds = cmds + zigbee.readAttribute( 0x0000, 0x0007 ) // PowerSource = REPORTED //cmds = cmds + zigbee.readAttribute( 0x0000, 0x0010 ) // LocationDescription = NOT REPORTED //cmds = cmds + zigbee.readAttribute( 0x0000, 0x0011 ) // PhysicalEnvironment = NOT REPORTED //cmds = cmds + zigbee.readAttribute( 0x0000, 0x0012 ) // DeviceEnabled = NOT REPORTED //cmds = cmds + zigbee.readAttribute( 0x0000, 0x0013 ) // AlarmMask = NOT REPORTED //cmds = cmds + zigbee.readAttribute( 0x0000, 0x0014 ) // DisableLocalConfig = NOT REPORTED //cmds = cmds + zigbee.readAttribute( 0x0000, 0x4000 ) // SWBuildID = NOT REPORTED //cmds = cmds + zigbee.readAttribute( 0x0500, 0x0001 ) // Type of sensor = NOT REPORTED Logging( "Sending request for Basic cluster data", 4 ) return cmds } // Sets the list of read commands def ZigBeeReadCommands(){ def cmds = zigbee.readAttribute( 0xEF00, 0x00 ) Logging( "Sending request to read device attributes", 4 ) return cmds } // Gets the list of commands def ZigBeeReportingCommands(){ /* Tuya Field 2 - Humidity = Value / 10 Tuya Field 12 - Temperature = Value / 10 Tuya Field 13 - CO2 = Value Tuya Field 15 - Formaldehyde = Value / 10 Tuya Field 16 - VOC = Value / 10 */ def cmds = ZigBeeReadCommands() switch( ReportInterval ){ case "1 minute": cmds += zigbee.configureReporting( 0xEF00, 0x00, DataType.INT16, 30, 60, null ) //cmds += zigbee.configureReporting( 0xEF00, 0x0002, DataType.INT16, 1, 60, 20 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0012, DataType.INT16, 1, 60, 20 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0013, DataType.INT16, 1, 60, 10 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0015, DataType.INT16, 1, 60, 1 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0016, DataType.INT16, 1, 60, 1 ) break case "5 minutes": cmds += zigbee.configureReporting( 0xEF00, 0x00, DataType.INT16, 150, 300, null ) //cmds += zigbee.configureReporting( 0xEF00, 0x0002, DataType.INT16, 1, 300, 20 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0012, DataType.INT16, 1, 300, 20 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0013, DataType.INT16, 1, 300, 10 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0015, DataType.INT16, 1, 300, 1 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0016, DataType.INT16, 1, 300, 1 ) break case "10 minutes": cmds += zigbee.configureReporting( 0xEF00, 0x00, DataType.INT16, 300, 600, null ) //cmds += zigbee.configureReporting( 0xEF00, 0x0002, DataType.INT16, 1, 600, 20 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0012, DataType.INT16, 1, 600, 20 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0013, DataType.INT16, 1, 600, 10 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0015, DataType.INT16, 1, 600, 1 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0016, DataType.INT16, 1, 600, 1 ) break case "1 hour": cmds += zigbee.configureReporting( 0xEF00, 0x00, DataType.INT16, 1800, 3600, null ) //cmds += zigbee.configureReporting( 0xEF00, 0x0002, DataType.INT16, 1, 3600, 20 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0012, DataType.INT16, 1, 3600, 20 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0013, DataType.INT16, 1, 3600, 10 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0015, DataType.INT16, 1, 3600, 1 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0016, DataType.INT16, 1, 3600, 1 ) break case "1 day": cmds += zigbee.configureReporting( 0xEF00, 0x00, DataType.INT16, 43200, 86400, null ) //cmds += zigbee.configureReporting( 0xEF00, 0x0002, DataType.INT16, 1, 86400, 20 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0012, DataType.INT16, 1, 86400, 20 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0013, DataType.INT16, 1, 86400, 10 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0015, DataType.INT16, 1, 86400, 1 ) //cmds += zigbee.configureReporting( 0xEF00, 0x0016, DataType.INT16, 1, 86400, 1 ) break } Logging( "Sending request to set ZigBee reporting", 4 ) return cmds } // refresh command def refresh(){ Logging( "Refreshing device...", 2 ) return ZigBeeReadCommands() } // Configures the device, typically at install or when preferences are saved def configure(){ Logging( "Configuring device...", 2 ) return ZigBeeReportingCommands() } // installed is called when the device is installed, all it really does is run updated def installed(){ Logging( "Installed", 2 ) updated() } // initialize is called when the device is initialized, all it really does is run updated def initialize(){ Logging( "Initialized", 2 ) updated() } // Checks the location.getTemperatureScale() to convert temperature values def ConvertTemperature( String Scale, Number Value ){ if( Value != null ){ def ReturnValue = Value as double if( location.getTemperatureScale() == "C" && Scale.toUpperCase() == "F" ){ ReturnValue = ( ( ( Value - 32 ) * 5 ) / 9 ) Logging( "Temperature Conversion ${ Value }°${ Scale.toUpperCase() } to ${ ReturnValue }°${ location.getTemperatureScale() }", 4 ) } else if( location.getTemperatureScale() == "F" && Scale.toUpperCase() == "C" ) { ReturnValue = ( ( ( Value * 9 ) / 5 ) + 32 ) Logging( "Temperature Conversion ${ Value }°${ Scale.toUpperCase() } to ${ ReturnValue }°${ location.getTemperatureScale() }", 4 ) } else if( location.getTemperatureScale() == Scale.toUpperCase() ){ ReturnValue = Value } def TempInt = ( ReturnValue * 100 ) as int ReturnValue = ( TempInt / 100 ) return ReturnValue } } // Process data to check against current state value and then send an event if it has changed def ProcessEvent( Variable, Value, Unit = null, ForceEvent = false ){ if( ( state."${ Variable }" != Value ) || ( ForceEvent == true ) ){ state."${ Variable }" = Value if( Unit != null ){ Logging( "Event: ${ Variable } = ${ Value }${ Unit }", 4 ) sendEvent( name: "${ Variable }", value: Value, unit: Unit, isStateChange: true ) } else { Logging( "Event: ${ Variable } = ${ Value }", 4 ) sendEvent( name: "${ Variable }", value: Value, isStateChange: true ) } } } // Process data to check against current state value and then send an event if it has changed def ProcessState( Variable, Value ){ if( state."${ Variable }" != Value ){ Logging( "State: ${ Variable } = ${ Value }", 4 ) state."${ Variable }" = Value } } // Handles whether logging is enabled and thus what to put there. def Logging( LogMessage, LogLevel ){ // Add all messages as info logging if( ( LogLevel == 2 ) && ( LogType != "None" ) ){ log.info( "${ device.displayName } - ${ LogMessage }" ) } else if( ( LogLevel == 3 ) && ( ( LogType == "Debug" ) || ( LogType == "Trace" ) ) ){ log.debug( "${ device.displayName } - ${ LogMessage }" ) } else if( ( LogLevel == 4 ) && ( LogType == "Trace" ) ){ log.trace( "${ device.displayName } - ${ LogMessage }" ) } else if( LogLevel == 5 ){ log.error( "${ device.displayName } - ${ LogMessage }" ) } } // Checks drdsnell.com for the latest version of the driver // Original inspiration from @cobra's version checking def CheckForUpdate(){ ProcessEvent( "DriverName", DriverName() ) ProcessEvent( "DriverVersion", DriverVersion() ) httpGet( uri: "https://www.drdsnell.com/projects/hubitat/drivers/versions.json", contentType: "application/json" ){ resp -> switch( resp.status ){ case 200: if( resp.data."${ DriverName() }" ){ CurrentVersion = DriverVersion().split( /\./ ) if( resp.data."${ DriverName() }".version == "REPLACED" ){ ProcessEvent( "DriverStatus", "Driver replaced, please use ${ resp.data."${ state.DriverName }".file }" ) } else if( resp.data."${ DriverName() }".version == "REMOVED" ){ ProcessEvent( "DriverStatus", "Driver removed and no longer supported." ) } else { SiteVersion = resp.data."${ DriverName() }".version.split( /\./ ) if( CurrentVersion == SiteVersion ){ Logging( "Driver version up to date", 3 ) ProcessEvent( "DriverStatus", "Up to date" ) } else if( ( CurrentVersion[ 0 ] as int ) > ( SiteVersion [ 0 ] as int ) ){ Logging( "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 ) ProcessEvent( "DriverStatus", "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" ) } else if( ( CurrentVersion[ 1 ] as int ) > ( SiteVersion [ 1 ] as int ) ){ Logging( "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 ) ProcessEvent( "DriverStatus", "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" ) } else if( ( CurrentVersion[ 2 ] as int ) > ( SiteVersion [ 2 ] as int ) ){ Logging( "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 ) ProcessEvent( "DriverStatus", "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" ) } else if( ( SiteVersion[ 0 ] as int ) > ( CurrentVersion[ 0 ] as int ) ){ Logging( "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 ) ProcessEvent( "DriverStatus", "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" ) } else if( ( SiteVersion[ 1 ] as int ) > ( CurrentVersion[ 1 ] as int ) ){ Logging( "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 ) ProcessEvent( "DriverStatus", "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" ) } else if( ( SiteVersion[ 2 ] as int ) > ( CurrentVersion[ 2 ] as int ) ){ Logging( "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 ) ProcessEvent( "DriverStatus", "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" ) } } } else { Logging( "${ DriverName() } is not published on drdsnell.com", 2 ) ProcessEvent( "DriverStatus", "${ DriverName() } is not published on drdsnell.com" ) } break default: Logging( "Unable to check drdsnell.com for ${ DriverName() } driver updates.", 2 ) break } } }