/* * Tuya Multi Sensor * * Description: * This Hubitat driver is designed for use with the Tuya Multi Sensor TS0201 * * Features List: * Ability to receive ZigBee temperature reports -20C ~ 80C * Ability to receive ZigBee humidity reports 0% ~ 100% * Ability to receive ZigBee luminance reports 0 Lux ~ 157000 Lux * Ability to check a website (mine) if there is a newer version of the driver available * * Incomplete Features: * Device does not appear to react to setting reporting interval, so that is removed * Driver CAN receive ZigBee temperature and humidity reports but does not appear device sends them * * 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.4 - Correction to ProcessEvent function and removal of old driver-specific attributes when Preferences are saved * 0.1.3 - Changes for battery and other reporting as well as correcting this as a Tuya-based device * 0.1.2 - Removing reporting interval preference and adding Refresh & ReadDevice commands * 0.1.1 - Correction for illuminance * 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 import hubitat.device.HubAction import hubitat.device.Protocol // Returns the driver name def DriverName(){ return "Tuya Multi Sensor" } // Returns the driver version def DriverVersion(){ return "0.1.4" } metadata{ definition( name: "Tuya Multi Sensor", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/Tuya Multi Sensor.groovy" ){ capability "Configuration" capability "Refresh" capability "Battery" capability "Sensor" capability "TemperatureMeasurement" capability "RelativeHumidityMeasurement" capability "IlluminanceMeasurement" // Commands that have been implemented //command "DoSomething" // Test command 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 fingerprint profileId: "0104", endpointId: "01", inClusters: "0000, 0001, 0400, E002", outClusters: "0019, 000A", manufacturer: "_TZ3000_qaayslip", model: "TS0201", deviceJoinName: "Multi Sensor with Display" fingerprint profileId: "0104", endpointId: "02", inClusters: "0402, 0405", outClusters: "0019, 000A", manufacturer: "_TZ3000_qaayslip", model: "TS0201", deviceJoinName: "Multi Sensor with Display" fingerprint profileId: "0104", endpointId: "01", inClusters: "0000, 0001, 0400, 0402, 0405, E002", outClusters: "0019, 000A", manufacturer: "_TZ3210_ncw88jfq", model: "TS0201", deviceJoinName: "Multi Sensor with Display" } preferences{ section{ input( type: "enum", name: "ReportInterval", title: "Reporting interval?", description: "", required: false, multiple: false, options: [ "1 minute" ,"5 minutes", "10 minutes", "1 hour", "6 hours" ], defaultValue: "5 minutes" ) input( type: "enum", name: "LogType", title: "Enable Logging?", required: true, multiple: false, options: [ "None", "Info", "Debug", "Trace" ], defaultValue: "Info" ) input( type: "enum", name: "DeviceType", title: "Device Type?", required: true, multiple: false, options: [ "MultiSensor", "TS0201" ], defaultValue: "MultiSensor" ) } } } // def DoSomething(){ unschedule() def cmds = zigbee.readAttribute( 0x0000, 0x0005, [ destEndpoint: 0xff ] ) cmds += zigbee.readAttribute( 0x0001, 0x0020, [ destEndpoint: 0xff ] ) cmds += zigbee.readAttribute( 0x0001, 0x0021, [ destEndpoint: 0xff ] ) cmds += zigbee.readAttribute( 0x0402, 0x0000, [ destEndpoint: 0x02 ] ) cmds += zigbee.readAttribute( 0x0405, 0x0000, [ destEndpoint: 0x02 ] ) cmds += zigbee.readAttribute( 0xe002, 0x00, [ destEndpoint: 0xff ] ) return cmds } // 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 ) 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" LuminanceReport( descMap ) Handled = true 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" HumidityReport( descMap ) Handled = true 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 "E002": // Tuya Alternative Cluster ClusterIdentified = "Tuya Alternative Cluster" HandleTuyaCluster( descMap ) Handled = true break case "EF00": // Tuya Cluster ClusterIdentified = "Tuya Cluster" //Logging( "Tuya Data : ${ descMap }", 4 ) //HandleTuyaCluster( descMap ) //Handled = true 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 } ${ ClusterIdentified }, descMap = ${ descMap }", 3 ) } } } // Processes temperature related reports private TemperatureReport( descMap ){ if( descMap.value == null) return def valueRaw = hexStrToSignedInt( descMap.value ) valueRaw = valueRaw / 100 def value = convertTemperatureIfNeeded( valueRaw.toFloat(),"c", 1 ) def unit = "°${ location.temperatureScale }" Logging( "Temperature DescMap = ${ descMap }", 4 ) Logging( "Temperature is ${ value }${ unit }", 3 ) sendEvent( name: "temperature", value: value, unit: unit ) } // Processes luminance related reports private LuminanceReport( descMap ){ if( descMap.value == null) return def valueRaw = hexStrToSignedInt( descMap.value ) def value = valueRaw //Logging( "Luminance descmap = ${ descMap }", 4 ) //Logging( "Luminance Hex = ${ descMap.value }, processed value = ${ value } lx", 4 ) sendEvent( name: "illuminance", value: value, unit: "lx" ) } // Processes humidity related reports private HumidityReport( descMap ){ if( descMap.value == null) return Logging( "Humidity DescMap = ${ descMap }", 4 ) def RawValue = hexStrToSignedInt( descMap.value ) def Value = RawValue Logging( "Humidity RawValue = ${ RawValue }", 4 ) if( DeviceType == "TS0201" ){ if( Value < 2000 ){ Value = ( ( Value as float ) / 10 ) } else { Value = ( ( Value as float ) / 100 ) } } Logging( "Humidity is ${ Value }%", 3 ) sendEvent( name: "humidity", value: Value, unit: "%" ) } // Processes battery related reports private BatteryReport( descMap ){ if( descMap.data != null ){ def BatteryPercent if( DeviceType == "TS0201" ){ BatteryPercent = hexStrToSignedInt( descMap.value ) if( BatteryPercent == 200 ){ BatteryPercent = 100 } } else { BatteryPercent = descMap.data[ 1 ] as int } Logging( "Battery DescMap = ${ descMap }", 4 ) Logging( "Battery at ${ BatteryPercent }%", 3 ) sendEvent( name: "battery", value: BatteryPercent, unit: "%" ) } } // Attempts to process the Tuya Specific cluster private HandleTuyaCluster( descMap ){ 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 - Humidity: ${ Value }", 4 ) ProcessEvent( "humidity", Math.round( Value / 10 ), "%" ) break case "12": // temperature Logging( "Tuya Field 12 - Temperature: ${ Value }", 4 ) ProcessEvent( "temperature", Math.round( ConvertTemperature( "C", ( Value / 10 ) ) ), "°${ location.temperatureScale }" ) break case "13": // carbon dioxide Logging( "Tuya Field 13 - CO2: ${ Value }", 4 ) ProcessEvent( "carbonDioxide", Value, "ppm" ) break case "15": // formaldehyde Logging( "Tuya Field 15 - Formaldehyde: ${ Value }", 4 ) ProcessEvent( "formaldehyde", ( Value / 10 ), "ppm" ) break case "16": // VOC Logging( "Tuya Field 16 - VOC: ${ Value }", 4 ) ProcessEvent( "VOC", ( Value / 10 ), "mg/m3" ) break default: Logging( "Tuya Cluster Data: ${ descMap.data }", 4 ) break } } else if( descMap.data.size() == 2 ){ switch( descMap.data[ 0 ] ){ case "06": // Maybe battery? Logging( "Tuya Field 6 descMap = ${ descMap }", 4 ) Logging( "Tuya Field 6 - Battery? = ${ descMap.data[ 1 ] }&", 4 ) //ProcessEvent( "humidity", Math.round( Value / 10 ), "%" ) break default: Logging( "Tuya Cluster Data: ${ descMap }", 4 ) break } } else { Logging( "Unknown Tuya Cluster Data: ${ descMap }", 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 = 03 cmds = cmds + zigbee.readAttribute( 0x0000, 0x0001 ) // ApplicationVersion = Reported = 44 cmds = cmds + zigbee.readAttribute( 0x0000, 0x0002 ) // StackVersion = Reported = 00 cmds = cmds + zigbee.readAttribute( 0x0000, 0x0003 ) // HardwareVersion = Reported = 01 cmds = cmds + zigbee.readAttribute( 0x0000, 0x0004 ) // ManufacturerName = Reported = _TZ3000_qaaysllp cmds = cmds + zigbee.readAttribute( 0x0000, 0x0005 ) // ModelIdentifier = Reported = TS0201 cmds = cmds + zigbee.readAttribute( 0x0000, 0x0007 ) // PowerSource = Reported = Battery */ def cmds = zigbee.readAttribute( 0x0000, [0x0004, 0x000, 0x0001, 0x0005, 0x0007, 0xfffe ], [:], delay=200 ) Logging( "Sending request for Basic cluster data", 4 ) return cmds } // Sets the list of read commands def ZigBeeReadCommands(){ def cmds = zigbee.readAttribute( 0x0001, 0x0020, [:] ) // battery cmds = cmds + zigbee.readAttribute( 0x0001, 0x0021, [:] ) // battery if( ( DeviceType == "MultiSensor" ) || ( DeviceType == null ) ){ cmds = cmds + zigbee.readAttribute( 0x0400, 0x0000, [:] ) // luminance } cmds = cmds + zigbee.readAttribute( 0x0402, 0x0000, [:] ) // temperature cmds = cmds + zigbee.readAttribute( 0x0405, 0x0000, [:] ) // humidity Logging( "Sending request to read device attributes", 4 ) return cmds } // Gets the list of commands def ZigBeeReportingCommands(){ def cmds = ZigBeeReadCommands() //cmds += zigbee.configureReporting( 0xE002, 0x00, DataType.INT16, 30, 60, null ) // manufacturer specific cmds += zigbee.configureReporting( 0x0001, 0x0021, DataType.UINT8, 150, 3660, 0x01, [:], 200 ) // battery switch( ReportInterval ){ case "1 minute": if( ( DeviceType == "MultiSensor" ) || ( DeviceType == null ) ){ cmds += zigbee.configureReporting( 0x0400, 0x0000, DataType.UINT16, 30, 60, 50, [:], 200 ) // luminance } cmds += zigbee.configureReporting( 0x0402, 0x0000, DataType.INT16, 30, 60, 50, [:], 200 ) // temperature cmds += zigbee.configureReporting( 0x0405, 0x0000, DataType.UINT16, 30, 60, 50, [:], 200 ) // humidity break case "5 minutes": if( ( DeviceType == "MultiSensor" ) || ( DeviceType == null ) ){ cmds += zigbee.configureReporting( 0x0400, 0x0000, DataType.UINT16, 150, 300, 50, [:], 200 ) } cmds += zigbee.configureReporting( 0x0402, 0x0000, DataType.INT16, 150, 300, 50, [:], 200 ) cmds += zigbee.configureReporting( 0x0405, 0x0000, DataType.UINT16, 150, 300, 50, [:], 200 ) break case "10 minutes": if( ( DeviceType == "MultiSensor" ) || ( DeviceType == null ) ){ cmds += zigbee.configureReporting( 0x0400, 0x0000, DataType.UINT16, 300, 600, 50, [:], 200 ) } cmds += zigbee.configureReporting( 0x0402, 0x0000, DataType.INT16, 300, 600, 50, [:], 200 ) cmds += zigbee.configureReporting( 0x0405, 0x0000, DataType.UINT16, 300, 600, 50, [:], 200 ) break case "1 hour": if( ( DeviceType == "MultiSensor" ) || ( DeviceType == null ) ){ cmds += zigbee.configureReporting( 0x0400, 0x0000, DataType.UINT16, 1800, 3600, 50, [:], 200 ) } cmds += zigbee.configureReporting( 0x0402, 0x0000, DataType.INT16, 1800, 3600, 50, [:], 200 ) cmds += zigbee.configureReporting( 0x0405, 0x0000, DataType.UINT16, 1800, 3600, 50, [:], 200 ) break case "6 hours": if( ( DeviceType == "MultiSensor" ) || ( DeviceType == null ) ){ cmds += zigbee.configureReporting( 0x0400, 0x0000, DataType.UINT16, 10800, 21600, 50, [:], 200 ) } cmds += zigbee.configureReporting( 0x0402, 0x0000, DataType.INT16, 10800, 21600, 50, [:], 200 ) cmds += zigbee.configureReporting( 0x0405, 0x0000, DataType.UINT16, 10800, 21600, 50, [:], 200 ) break } cmds += zigbee.reportingConfiguration(0x0402, 0x0000, [:], 250) cmds += zigbee.reportingConfiguration(0x0405, 0x0000, [:], 250) cmds += zigbee.reportingConfiguration(0x0001, 0x0021, [:], 250) 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 ){ if( state."${ Variable }" != Value ){ 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 } } }