Advertisement
matt123454321

Hubitat driver for Eurotronic Air Quality Sensor (draft!)

Jan 12th, 2021 (edited)
985
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Groovy 15.33 KB | None | 0 0
  1. /*
  2.    V0.0.1 Initial draft of driver, based on MCOHome MH9 carbon dioxide sensor
  3.    - Tested with feedback from Mr. Olsen
  4.    - Credits to csteele-PD for the AeotecMultiSensor6 driver which is the basis for this driver.
  5.         https://github.com/HubitatCommunity/AeotecMultiSensor6
  6. */
  7.  
  8.  public static String version()      {  return "v2.0.0"  }
  9.  
  10. metadata {
  11.     definition (name: "Eurotronic Air Quality Sensor", namespace: "community", author: "mattie_k") {
  12.         capability "Temperature Measurement"
  13.         capability "Relative Humidity Measurement"
  14.         capability "CarbonDioxide Measurement"
  15.         capability "Configuration"
  16.  
  17.         command    "refresh"
  18.  
  19.         attribute  "firmware", "decimal"
  20.         fingerprint  mfr:"015F", prod:"0902", deviceId:"5102", inClusters:"0x5E,0x86,0x72,0x5A,0x85,0x59,0x73,0x70,0x31,0x71,0x9E,0x7A"
  21.     }
  22.    
  23.  
  24.     preferences {
  25.         input name: "debugOutput",   type: "bool", title: "<b>Enable debug logging?</b>",   description: "<br>", defaultValue: true
  26.         input name: "descTextEnable", type: "bool", title: "<b>Enable descriptionText logging?</b>", defaultValue: true
  27.         input name: "co2Threshold", type: "number", title: "<b>CO2 notification threshold 1-2000ppm</b>", range: "1..5000", description: "<br><i>Set the CO2 level (in ppm) that will trigger a notification.</i><br>", defaultValue: 1200
  28.         input name: "co2ReportInterval", type: "number", title: "<b>CO2 report interval</b>", range: "-127...127", description: "<br><i>1 to 127: Report when change > n*5ppm.<br>-1 to -127: Report when change > (n+256) * 5ppm.<br>Set to 0 to disable reporting. </i><br>", defaultValue: 10
  29.         input name: "tempReportInterval", type: "number", title: "<b>Temperature report interval</b>", range: "-127...127", description: "<br><i>1 to 127: Report when change > n*0.5 Celsius.<br>-1 to -127: Report when change > (n+256) * 0.5 Celsius.<br>Set to 0 to disable reporting. </i><br>", defaultValue: 1
  30.         input name: "humidReportInterval", type: "number", title: "<b>Humidity report interval</b>", range: "0...100", description: "<br><i>1 to 99: Report when change > n %.<br>Set to 0 to disable reporting. </i><br>", defaultValue: 2
  31.         input name: "factoryReset", type: "number", title: "<b>Restore factory setting (set to 85)</b>", range: "0...85", description: "Only use to reset to factory defaults.<br>In that case set parameter value to 85", defaultValue: 0                      
  32.     }
  33. }
  34.  
  35.  
  36. /*
  37.     updated
  38.    
  39.     When "Save Preferences" gets clicked...
  40. */
  41. def updated() {
  42.     if (debugOutput) log.debug "In Updated with settings: ${settings}"
  43.     if (debugOutput) log.debug "${device.displayName} is now on ${device.latestValue("powerSource")} power"
  44.     unschedule()
  45.     initialize()
  46.     dbCleanUp()     // remove antique db entries created in older versions and no longer used.
  47.     if (debugOutput) runIn(1800,logsOff)
  48.     return(configure(1))
  49. }
  50.  
  51.  
  52. /*
  53.     parse
  54.    
  55.     Respond to received zwave commands.
  56. */
  57. def parse(String description) {
  58.     // log.debug "In parse() for description: $description"
  59.     def result = null
  60.     if (description.startsWith("Err 106")) {
  61.         log.warn "parse() >> Err 106"
  62.         result = createEvent( name: "secureInclusion", value: "failed",
  63.                              descriptionText: "This sensor (${device.displayName}) failed to complete the network security key exchange. If you are unable to control it via Hubitat, you must remove it from your network and add it again.")
  64.     } else if (description != "updated") {
  65.         // log.debug "About to zwave.parse($description)"    
  66.         def cmd = zwave.parse(description, [0x31: 5, 0x71: 1, 0x72: 1, 0x86: 1])
  67.         if (cmd) {
  68.             // log.debug "About to call handler for ${cmd.toString()}"
  69.             result = zwaveEvent(cmd)
  70.         }
  71.     }
  72.     //log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}"
  73.     return result
  74. }
  75.  
  76.  
  77. /*
  78.     installed
  79.    
  80.     Doesn't do much other than call initialize().
  81. */
  82. void installed()
  83. {
  84.     initialize()
  85. }
  86.  
  87.  
  88.  
  89. /*
  90.     initialize
  91.    
  92.     Doesn't do much.
  93. */
  94. def initialize() {
  95.     state.firmware = state.firmware ?: 0.0d
  96. }
  97.  
  98.  
  99. /*
  100.     Beginning of Z-Wave Commands
  101. */
  102.  
  103. //this notification will be sent only when device is battery powered
  104. def zwaveEvent(hubitat.zwave.commands.wakeupv1.WakeUpNotification cmd) {
  105.     def result = [createEvent(descriptionText: "${device.displayName} woke up")]
  106.     def cmds = []
  107.     if (!isConfigured()) {
  108.         if (debugOutput) log.debug("late configure")
  109.         result << response(configure(3))
  110.     } else {
  111.         //if (debugOutput) log.debug("Device has been configured sending >> wakeUpNoMoreInformation()")
  112.         cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
  113.         result << response(cmds)
  114.     }
  115.     result
  116. }
  117.  
  118.  
  119. def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
  120.     def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 1, 0x70: 1, 0x72: 1, 0x84: 1])
  121.     state.sec = 1
  122.     if (debugOutput) log.debug "encapsulated: ${encapsulatedCommand}"
  123.     if (encapsulatedCommand) {
  124.         zwaveEvent(encapsulatedCommand)
  125.     } else {
  126.         log.warn "Unable to extract encapsulated cmd from $cmd"
  127.         createEvent(descriptionText: cmd.toString())
  128.     }
  129. }
  130.  
  131.  
  132. def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv1.ManufacturerSpecificReport cmd) {
  133.     if (debugOutput) log.debug "ManufacturerSpecificReport cmd = $cmd"
  134.     if (debugOutput) log.debug "manufacturerId:   ${cmd.manufacturerId}"
  135.     if (debugOutput) log.debug "manufacturerName: ${cmd.manufacturerName}"
  136.     if (debugOutput) log.debug "productId:        ${cmd.productId}"
  137.     def model = ""   // We'll decode the specific model for the log, but we don't currently use this info
  138.     if (debugOutput) log.debug "model:            ${model}"
  139.     if (debugOutput) log.debug "productTypeId:    ${cmd.productTypeId}"
  140.     def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
  141.     updateDataValue("MSR", msr)
  142. }
  143.  
  144.  
  145. def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){
  146.     if (debugOutput) log.debug "In multi level report cmd = $cmd"
  147.  
  148.     if (cmd.scaledSensorValue == null) return
  149.     def map = [:]
  150.     switch (cmd.sensorType) {
  151.         case 1:
  152.             if (debugOutput) log.debug "raw temp = $cmd.scaledSensorValue"
  153.             // Convert temperature (if needed) to the system's configured temperature scale
  154.             def finalval = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision)
  155.  
  156.             if (debugOutput) log.debug "finalval = $finalval"
  157.  
  158.             map.value = finalval
  159.             map.unit = "\u00b0" + getTemperatureScale()
  160.             map.name = "temperature"
  161.             map.descriptionText = "${device.displayName} temperature is ${map.value}${map.unit}"
  162.             if (descTextEnable) log.info "Temperature is ${map.value}${map.unit}"
  163.             break
  164.         case 5:
  165.             if (debugOutput) log.debug "raw humidity = $cmd.scaledSensorValue"
  166.             map.value = roundIt(cmd.scaledSensorValue, 0) as Integer     // .toInteger()
  167.             map.unit = "%"
  168.             map.name = "humidity"
  169.             map.descriptionText = "${device.displayName} humidity is ${map.value}%"
  170.             if (descTextEnable) log.info "Humidity is ${map.value}%"
  171.             break
  172.         case 17:
  173.             if (debugOutput) log.debug "raw carbonDioxide = $cmd.scaledSensorValue"
  174.             map.name = "carbonDioxide"
  175.             map.value = roundIt(cmd.scaledSensorValue, 0) as Integer    // .toInteger()
  176.             map.descriptionText = "${device.displayName} CO2 level is ${map.value}"
  177.         if (descTextEnable) log.info "CO2 level is ${map.value}"
  178.             break
  179.         case 39:
  180.             if (debugOutput) log.debug "raw VOC = $cmd.scaledSensorValue"
  181.             map.name = "VOC"
  182.             map.value = roundIt(cmd.scaledSensorValue, 0) as Integer    // .toInteger()
  183.             map.descriptionText = "${device.displayName} VOC level is ${map.value}"
  184.         if (descTextEnable) log.info "VOC level is ${map.value}"
  185.             break
  186.         default:
  187.             map.descriptionText = cmd.toString()
  188.     }
  189.     createEvent(map)
  190. }
  191.  
  192. def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) {
  193.     if (debugOutput) log.debug "NotificationReport: ${cmd}"
  194.     def result = []
  195.     if (cmd.notificationType == 7) {
  196. // Check debug output to determine specs
  197.     } else {
  198.         log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}"
  199.         result << createEvent(descriptionText: cmd.toString())
  200.     }
  201.     result
  202. }
  203.  
  204.  
  205. def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) {
  206.     if (debugOutput) log.debug "---CONFIGURATION REPORT V1--- ${device.displayName} parameter ${cmd.parameterNumber} with a byte size of ${cmd.size} is set to ${cmd.configurationValue}"
  207.     def result = []
  208.     result << response(configure(4))
  209.     result
  210. }
  211.  
  212.  
  213. def zwaveEvent(hubitat.zwave.Command cmd) {
  214.     if (debugOutput) log.debug "General zwaveEvent cmd: ${cmd}"
  215.     createEvent(descriptionText: cmd.toString(), isStateChange: false)
  216. }
  217.  
  218.  
  219. def zwaveEvent(hubitat.zwave.commands.versionv1.VersionCommandClassReport cmd) {
  220.     if (debugOutput) log.debug "in version command class report"
  221.     if (debugOutput) log.debug "---VERSION COMMAND CLASS REPORT V1--- ${device.displayName} has version: ${cmd.commandClassVersion} for command class ${cmd.requestedCommandClass} - payload: ${cmd.payload}"
  222. }
  223.  
  224.  
  225. def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) {
  226.     if (debugOutput) log.debug "in version report"
  227.     // SubVersion is in 1/100ths so that 1.01 < 1.08 < 1.10, etc.//    state.firmware = 0.0d
  228.     if (cmd.firmwareVersion) {
  229.         BigDecimal fw = cmd.firmwareVersion + (cmd.firmwareSubVersion/100)
  230.         state.firmware = fw
  231.     }
  232.     if (debugOutput) log.debug "---VERSION REPORT V1--- ${device.displayName} is running firmware version: ${String.format("%1.2f",state.firmware)}, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}"
  233.     if(state.firmware < 1.10)
  234.         log.warn "--- WARNING: Device handler expects devices to have firmware 1.10 or later"
  235. }
  236.  
  237.  
  238. private command(hubitat.zwave.Command cmd) {
  239.         if (debugOutput) log.debug "Sending Z-wave command: ${cmd.toString()}"
  240.         return zwaveSecureEncap(cmd.format())
  241. }
  242.  
  243.  
  244. private commands(commands, delay=1000) {
  245.     //if (descTextEnable) log.info "sending commands: ${commands}"
  246.     return delayBetween(commands.collect{ command(it) }, delay)
  247. }
  248.  
  249.  
  250. /*
  251.     End of Z-Wave Commands
  252.  
  253.     Beginning of Driver Commands
  254.  
  255. */
  256. def configure(ccc) {
  257.     if (debugOutput) log.debug "ccc: $ccc"
  258.     if (descTextEnable) log.info "${device.displayName} is configuring its settings"
  259.  
  260.  
  261.     if (debugOutput) {
  262.             log.debug "Test debug output"
  263.     }
  264.  
  265.     def now = new Date()
  266.     def tf = new java.text.SimpleDateFormat("dd-MMM-yyyy h:mm a")
  267.     tf.setTimeZone(location.getTimeZone())
  268.     def newtime = "${tf.format(now)}" as String
  269.     sendEvent(name: "lastUpdate", value: newtime, descriptionText: "${device.displayName} configured at ${newtime}")
  270.  
  271.     setConfigured("true")
  272.     def waketime
  273.  
  274.     if (timeOptionValueMap[settings.reportInterval] < 300)
  275.         waketime = timeOptionValueMap[settings.reportInterval]
  276.     else waketime = 300
  277.  
  278.     if (debugOutput) log.debug "wake time reset to $waketime"
  279.     if (debugOutput) log.debug "Current firmware: ${sprintf ("%1.2f", state.firmware)}"
  280.  
  281.     // Retrieve local temperature scale: "C" = Celsius, "F" = Fahrenheit
  282.     // Convert to a value of 1 or 2 as used by the device to select the scale
  283.     if (debugOutput) log.debug "Location temperature scale: ${location.getTemperatureScale()}"
  284.     byte tempScaleByte = (location.getTemperatureScale() == "C" ? 1 : 2)
  285.     selectiveReport = selectiveReporting ? 1 : 0
  286.  
  287.     def request = [
  288.             zwave.versionV1.versionGet(),
  289.             zwave.manufacturerSpecificV1.manufacturerSpecificGet(),
  290.  
  291.             //1. set association groups for hub
  292.             zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId),
  293.  
  294.             // Set CO2 level notification threshold
  295.             zwave.configurationV1.configurationSet(parameterNumber: 1, size: 2, scaledConfigurationValue: co2Threshold as int),
  296.             // Automatically generate a report when CO2 changes by specified amount
  297.             zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: co2ReportInterval as int),
  298.             // Automatically generate a report when temp changes by specified amount
  299.             zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: tempReportInterval as int),
  300.             // Automatically generate a report when humidity changes by specified amount
  301.             zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: humidReportInterval as int),
  302.             // Factory reset device when parameter is set to value 85
  303.             zwave.configurationV1.configurationSet(parameterNumber: 255, size: 1, scaledConfigurationValue: factoryReset as int),
  304.             //7. query sensor data
  305.             zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1), //temperature
  306.             zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 5), //humidity
  307.             zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 17), //carbonDioxide (CO2)
  308.             zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 39) //VOC (CO2)
  309.     ]
  310.     return commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
  311. }
  312.  
  313.  
  314. def refresh() {
  315.     if (debugOutput) log.debug "in refresh"
  316.  
  317.     return commands([
  318.             zwave.versionV1.versionGet(),                                // Retrieve version info (includes firmware version)
  319.             zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1), //temperature
  320.             zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 5), //humidity
  321.             zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 17), //carbonDioxide (CO2)
  322.             zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 39) //VOC
  323.     ])
  324. }
  325.  
  326. /*
  327.     Begin support methods
  328. */
  329.  
  330.  
  331. def getTimeOptionValueMap() { [
  332.     "20 seconds" : 20,
  333.     "30 seconds" : 30,
  334.     "1 minute"   : 60,
  335.     "2 minutes"  : 2*60,
  336.     "3 minutes"  : 3*60,
  337.     "4 minutes"  : 4*60,
  338.     "5 minutes"  : 5*60,
  339.     "10 minutes" : 10*60,
  340.     "15 minutes" : 15*60,
  341.     "30 minutes" : 30*60,
  342.     "1 hour"     : 1*60*60,
  343.     "6 hours"    : 6*60*60,
  344.     "12 hours"   : 12*60*60,
  345.     "18 hours"   : 18*60*60,
  346.     "24 hours"   : 24*60*60,
  347. ]}
  348.  
  349.  
  350. private setConfigured(configure) {
  351.     updateDataValue("configured", configure)
  352. }
  353.  
  354.  
  355. private isConfigured() {
  356.     getDataValue("configured") == "true"
  357. }
  358.  
  359.  
  360. def roundIt( value, decimals=0 ) {
  361.     return (value == null) ? null : value.toBigDecimal().setScale(decimals, BigDecimal.ROUND_HALF_UP)
  362. }
  363.  
  364.  
  365. def roundIt( BigDecimal value, decimals=0) {
  366.     return (value == null) ? null : value.setScale(decimals, BigDecimal.ROUND_HALF_UP)
  367. }
  368.  
  369.  
  370. def logsOff(){
  371.     log.warn "debug logging disabled..."
  372.     device.updateSetting("debugOutput",[value:"false",type:"bool"])
  373. }
  374.  
  375.  
  376. private dbCleanUp() {
  377.     // clean up state variables that are obsolete
  378. //  state.remove("tempOffset")
  379. //  state.remove("version")
  380. //  state.remove("Version")
  381. //  state.remove("sensorTemp")
  382. //  state.remove("author")
  383. //  state.remove("Copyright")
  384.     state.remove("verUpdate")
  385.     state.remove("verStatus")
  386.     state.remove("Type")
  387. }
  388.  
  389. /*
  390.     padVer
  391.  
  392.     Version progression of 1.4.9 to 1.4.10 would mis-compare unless each duple is padded first.
  393.  
  394. */
  395. String padVer(ver) {
  396.     def pad = ""
  397.     ver.replaceAll( "[vV]", "" ).split( /\./ ).each { pad += it.padLeft( 2, '0' ) }
  398.     return pad
  399. }
  400.  
  401. String getThisCopyright(){"&copy; 2021 Mattie_k "}
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement