Hubitat with Homemade Temperature, Humidity, Pressure and Light sensor

Added a decisional catch for the open/close command to deter garage door from redundant commands. As such, if reed sensor is closed (door) and you issue a close command, it will catch it and not engage, also will send info.warn.

Summary

import groovy.json.JsonSlurper
import hubitat.zigbee.clusters.iaszone.ZoneStatus

metadata {
definition (name: "Garage Sensor EX", namespace: "iharyadi", author: "iharyadi", ocfDeviceType: "oic.r.temperature") {
capability "Configuration"
capability "Refresh"
//capability "Battery"
capability "Temperature Measurement"
capability "RelativeHumidityMeasurement"
capability "Illuminance Measurement"
capability "PressureMeasurement"
//capability "Switch"
//capability "SmokeDetector"
//capability "PowerSource"
//capability "CarbonMonoxideDetector"
capability "Sensor"
capability "GarageDoorControl"
command "binaryoutputOff"
command "binaryoutputOn"

    MapDiagAttributes().each{ k, v -> attribute "$v", "number" }
    
    attribute "BinaryOutput", "BOOLEAN"
    attribute "BinaryInput", "BOOLEAN"
    attribute "AnalogInput", "number"
    attribute "relativePressure", "number"
    
    fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0006, 0402, 0403, 0405, 0400, 0B05, 000F, 000C, 0010", manufacturer: "KMPCIL", model: "RES001", deviceJoinName: "Environment Sensor"
    fingerprint profileId: "0104", inClusters: "0000, 0003, 0006, 0402, 0403, 0405, 0400, 0B05, 000F, 000C, 0010,1001", manufacturer: "KMPCIL", model: "RES001", deviceJoinName: "Environment Sensor"
    fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0006, 0402, 0403, 0405, 0B05, 000F, 000C, 0010", manufacturer: "KMPCIL", model: "RES002", deviceJoinName: "Environment Sensor"
    fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0006, 0400, 0B05, 000F, 000C, 0010", manufacturer: "KMPCIL", model: "RES003", deviceJoinName: "Environment Sensor"
    fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0006, 0B05, 000F, 000C, 0010", manufacturer: "KMPCIL", model: "RES004", deviceJoinName: "Environment Sensor"
    fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0006, 0402, 0403, 0405, 0400, 0B05, 000F, 000C, 0010, 1001", manufacturer: "KMPCIL", model: "RES005", deviceJoinName: "Environment Sensor"
    fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0006, 0402, 0403, 0405, 0400, 0B05, 000F, 000C, 0010, 0500", manufacturer: "KMPCIL", model: "RES006", deviceJoinName: "Environment Sensor"

}

preferences {

    section("Environment Sensor")
    {
        input name:"tempOffset", type:"decimal", title: "Degrees", description: "Adjust temperature by this many degrees in Celcius",
              range: "*..*", displayDuringSetup: false
        input name:"tempFilter", type:"decimal", title: "Coeficient", description: "Temperature filter between 0.0 and 1.0",
              range: "0..1", displayDuringSetup: false
        input name:"humOffset", type:"decimal", title: "Percent", description: "Adjust humidity by this many percent",
              range: "*..*", displayDuringSetup: false
        input name:"illumAdj", type:"decimal", title: "Factor", description: "Adjust illuminace base on formula illum / Factor", 
            range: "1..*", displayDuringSetup: false
        input name:"relativePressOffset", type:"decimal", title: "Relative Pressure", description: "Relative pressure offset in kPA", 
            range: "0..*", displayDuringSetup: false
        input name: "pressureInHg", defaultValue: "false", type: "bool", title: "Report pressure in inhg", description: "",
            displayDuringSetup: false
    }
    
    section("Expansion Sensor")
    {
        input name:"enableAnalogInput", type: "bool", title: "Analog Input", description: "Enable Analog Input",
            defaultValue: "false", displayDuringSetup: false 
        
        input name:"childAnalogInput", type:"text", title: "Analog Input Handler", description: "Analog Input Child Handler",
               displayDuringSetup: false
          
        input name:"enableBinaryInput", type: "bool", title: "Binary Input", description: "Enable Binary Input",
               defaultValue: "false", displayDuringSetup: false
        
        input name:"childBinaryInput", type:"string", title: "Binary Input Handler", description: "Binary Input Device Handler",
               displayDuringSetup: false
          
        input name:"enableBinaryOutput", type: "bool", title: "Binary Output", description: "Enable Binary Output",
               defaultValue: "false", displayDuringSetup: false  
        
        input name:"childBinaryOutput", type:"text", title: "Binary Output Handler", description: "Binary Output Child Handler",
               displayDuringSetup: false
    }
    
    section("Serial Device Children")
    {
        input name:"childSerialDevices", type:"text", title: "Children[JSON]", description: "Serial Children Handler",
               displayDuringSetup: false
    }
    
    section("Debug Messages")
    {
        input name: "logEnabled", defaultValue: "true", type: "bool", title: "Enable info message logging", description: "",
            displayDuringSetup: false
    }
}

}

private def Log(message) {
if (logEnabled)
log.info "${message}"
}

private def NUMBER_OF_RESETS_ID()
{
return 0x0000;
}

private def MAC_TX_UCAST_RETRY_ID()
{
return 0x0104;
}

private def MAC_TX_UCAST_FAIL_ID()
{
return 0x0105;
}

private def NWK_DECRYPT_FAILURES_ID()
{
return 0x0115;
}

private def PACKET_VALIDATE_DROP_COUNT_ID()
{
return 0x011A;
}

private def PARENT_COUNT_ID()
{
return 0x011D+1;
}

private def CHILD_COUNT_ID()
{
return 0x011D+2;
}

private def NEIGHBOR_COUNT_ID()
{
return 0x011D+3;
}

private def LAST_RSSI_ID()
{
return 0x011D;
}

private def BATT_REMINING_ID()
{
return 0x0021;
}

private def DIAG_CLUSTER_ID()
{
return 0x0B05;
}

private def TEMPERATURE_CLUSTER_ID()
{
return 0x0402;
}

private def PRESSURE_CLUSTER_ID()
{
return 0x0403;
}

private def HUMIDITY_CLUSTER_ID()
{
return 0x0405;
}

private def ILLUMINANCE_CLUSTER_ID()
{
return 0x0400;
}

private def POWER_CLUSTER_ID()
{
return 0x0001;
}

private def BINARY_INPUT_CLUSTER_ID()
{
return 0x000F;
}

private def BINARY_OUTPUT_CLUSTER_ID()
{
return 0x0010;
}

private def ANALOG_INPUT_CLUSTER_ID()
{
return 0x000C;
}

private def SENSOR_VALUE_ATTRIBUTE()
{
return 0x0000;
}

private def SERIAL_TUNNEL_CLUSTER_ID()
{
return 0x1001;
}

private def MapDiagAttributes()
{
def result = [(CHILD_COUNT_ID()):'Children',
(NEIGHBOR_COUNT_ID()):'Neighbor',
(NUMBER_OF_RESETS_ID()):'ResetCount',
(MAC_TX_UCAST_RETRY_ID()):'TXRetry',
(MAC_TX_UCAST_FAIL_ID()):'TXFail',
(LAST_RSSI_ID()):'RSSI',
(NWK_DECRYPT_FAILURES_ID()):'DecryptFailure',
(PACKET_VALIDATE_DROP_COUNT_ID()):'PacketDrop']

return result;

}

private def createDiagnosticEvent( String attr_name, type, value )
{
def result = [:]
result.name = attr_name
result.translatable = true

def converter = [(DataType.INT8):{int val -> return (byte) val},
(DataType.INT16):{int val -> return val},
(DataType.UINT16):{int val -> return (long)val}] 

result.value = converter[zigbee.convertHexToInt(type)]( zigbee.convertHexToInt(value));

result.descriptionText = "${device.displayName} ${result.name} is ${result.value}"

return createEvent(result)

}

private def parseDiagnosticEvent(def descMap)
{
def attr_name = MapDiagAttributes()[zigbee.convertHexToInt(descMap.attrId)];
if(!attr_name)
{
return null;
}

return createDiagnosticEvent(attr_name, descMap.encoding, descMap.value)

}

private def createPressureEvent(float pressure)
{
String unit = pressureInHg ? "inhg": "kPa"
def result = [:]
result.name = "pressure"
result.translatable = true
result.unit = unit
result.value = (pressureInHg ? (pressure/3.386):pressure).round(2)
result.descriptionText = "${device.displayName} ${result.name} is ${result.value} ${result.unit}"

if (relativePressOffset && relativePressOffset != 0)
{
    pressure = pressure+relativePressOffset
    def relPEvent = [:]
    relPEvent.name = "relativePressure"
    relPEvent.translatable = true
    relPEvent.unit = unit
    relPEvent.value = (pressureInHg ? (pressure/3.386):pressure).round(2)
    relPEvent.descriptionText = "${device.displayName} ${result.name} is ${result.value} ${result.unit}"
    sendEvent(relPEvent)
}

return result

}

private def parsePressureEvent(def descMap)
{
if(zigbee.convertHexToInt(descMap.attrId) != SENSOR_VALUE_ATTRIBUTE())
{
return null
}
float pressure = (float)zigbee.convertHexToInt(descMap.value) / 10.0
return createPressureEvent(pressure)
}

private def createHumidityEvent(float humidity)
{
def result = [:]
result.name = "humidity"
result.translatable = true
result.value = humidity
result.unit = "%"

if (humOffset) {
    result.value = result.value + humOffset
}

result.value = result.value.round(2) 
result.descriptionText = "${device.displayName} ${result.name} is ${result.value} ${result.unit}"
return result

}

private def parseHumidityEvent(def descMap)
{
if(zigbee.convertHexToInt(descMap.attrId) != SENSOR_VALUE_ATTRIBUTE())
{
return null
}

float humidity = (float)zigbee.convertHexToInt(descMap.value)/100.0
return createHumidityEvent(humidity)

}

private def createIlluminanceEvent(int illum)
{
def result = [:]
result.name = "illuminance"
result.translatable = true
result.unit = "Lux"

if(!illumAdj ||  illumAdj < 1.0)
{
    double val = 0.0
    if(illum > 0)
    {
        val = 10.0 ** (((double) illum -1.0)/10000.0)
    }
    
    result.value = val.round(2)  
}
else
{
    result.value = ((double)illum / illumAdj).toInteger()
}

result.descriptionText = "${device.displayName} ${result.name} is ${result.value} ${result.unit}"
return result

}

def parseIlluminanceEvent(def descMap)
{
if(zigbee.convertHexToInt(descMap.attrId) != SENSOR_VALUE_ATTRIBUTE())
{
return null
}

int res =  zigbee.convertHexToInt(descMap.value)

return createIlluminanceEvent(res)

}

private def createTempertureEvent(float temp)
{
def result = [:]
result.name = "temperature"
result.value = temperature
result.unit = "°${location.temperatureScale}"

result.value = convertTemperatureIfNeeded(temp,"c",1) 
result.descriptionText = "${device.displayName} ${result.name} is ${result.value} ${result.unit}"

return result

}

private float adjustTemp(float val)
{
if (tempOffset) {
val = val + tempOffset
}

if(tempFilter)
{
    if(state.tempCelcius)
    {
        val = tempFilter*val + (1.0-tempFilter)*state.tempCelcius
    }
    state.tempCelcius = val
}


return val

}

private def parseTemperatureEvent(def descMap)
{
float temp = adjustTemp((hexStrToSignedInt(descMap.value) / 100.00).toFloat())

return createTempertureEvent(temp)   

}

private def createBinaryOutputEvent(boolean val)
{
def result = [:]
result.name = "BinaryOutput"
result.translatable = true
result.value = val ? "true" : "false"
result.descriptionText = "${device.displayName} ${result.name} is ${result.value}"
return result
}

def parseBinaryOutputEvent(def descMap)
{
def present_value = descMap.attrId?.equals("0055")?
descMap.value:
descMap.additionalAttrs?.find { item -> item.attrId?.equals("0055")}?.value

if(!present_value)
{
    return null
}
    
return createBinaryOutputEvent(zigbee.convertHexToInt(present_value) > 0)

}

private def createAnalogInputEvent(float value)
{
def result = [:]
result.name = "AnalogInput"
result.translatable = true
result.value = value
result.unit = "Volt"
result.descriptionText = "${device.displayName} ${result.name} is ${result.value}"
return result
}

private Float ConvertStringIntBitsToFloat(String val)
{
Long i = Long.parseLong(val, 16);
Float f = Float.intBitsToFloat(i.intValue());
return f;
}

def parseAnalogInputEvent(def descMap)
{
def adc;
def vdd;

if(descMap.attrId?.equals("0103"))
{
    adc = descMap.value
}
else if (descMap.attrId?.equals("0104"))
{
    vdd  = descMap.value
}
else
{   
    adc = descMap.additionalAttrs?.find { item -> item.attrId?.equals("0103")}.value
    vdd = descMap.additionalAttrs?.find { item -> item.attrId?.equals("0104")}.value        
}

if(vdd)
{
    state.lastVdd = (((float)zigbee.convertHexToInt(vdd)*3.45)/0x1FFF)
}   

if(!adc)
{
    return null   
}

float volt = 0;
if(state.lastVdd)
{
       volt = (zigbee.convertHexToInt(adc) * state.lastVdd)/0x1FFF
}

return createAnalogInputEvent(volt)

}

///////////////////
def open()
{
//log.info device.currentValue("door")
if ((device.currentValue("door"))== "closed")
{
log.info "${device} is being opened"
runInMillis(1500, binaryoutputOff)
binaryoutputOn()
}
else
{
log.warn "Garage door was already open, did not proceed with open command"
}

}
def close()
{
//log.info device.currentValue("door")
if ((device.currentValue("door"))== "open")
{
log.info "${device} is being closed"
runInMillis(1500, binaryoutputOff)
binaryoutputOn()
}
else
{
log.warn "Garage door was already closed, did not proceed with close command"
}

}

///////////////////

private def createBinaryInputEvent(boolean val)
{
def result = [:]
result.name = "BinaryInput"
result.translatable = true
result.value = val
result.descriptionText = "${device.displayName} ${result.name} is ${result.value}"
return result
}

def parseBinaryInputEvent(def descMap)
{
def value = descMap.attrId?.equals("0055") ?
descMap.value :
descMap.additionalAttrs?.find { item -> item.attrId?.equals("0055")}.value

if(!value)
{
    return null
}
       
return createBinaryInputEvent(zigbee.convertHexToInt(value)==0)

}

private def reflectToChild(String childtype, String description)
{
if(!childtype)
{
return
}

def childDevice = getChildDevice("${device.deviceNetworkId}-$childtype")

if(!childDevice)
{
    return    
}
    
def childEvent = childDevice.parse(description)
if(!childEvent)
{
    return
}

childDevice.sendEvent(childEvent)    

}

private def reflectToSerialChild(def data)
{
def zigbeeAddress = device.getZigbeeId()

Integer page = zigbee.convertHexToInt(data[1])
    
def childDevice = getChildDevice("$zigbeeAddress-SerialDevice-$page")

if(!childDevice)
{
    return    
}
    
def childEvent = childDevice.parse(data)
if(!childEvent)
{
    return
}

childDevice.sendEvent(childEvent)  

}

private def createBattEvent(int val)
{
def result = [:]
result.name = "battery"
result.translatable = true
result.value = val/2
result.unit = "%"
result.descriptionText = "${device.displayName} ${result.name} is ${result.value}"
return result
}

def parseBattEvent(def descMap)
{
def value = descMap?.attrId?.equals(zigbee.convertToHexString(BATT_REMINING_ID(),4)) ?
descMap.value :
null

if(!value)
{
    return null
}
       
return createBattEvent(zigbee.convertHexToInt(value))

}

def parseCustomEvent(String description)
{
def event = null

if(description?.startsWith("read attr - raw:"))
{
    def descMap = zigbee.parseDescriptionAsMap(description)
    
    if(descMap?.cluster?.equals(zigbee.convertToHexString(TEMPERATURE_CLUSTER_ID(),4)))
    {
        event = parseTemperatureEvent(descMap)
    }
    else if(descMap?.cluster?.equals(zigbee.convertToHexString(DIAG_CLUSTER_ID(),4)))
    {
        event = parseDiagnosticEvent(descMap);
    }
    else if(descMap?.cluster?.equals(zigbee.convertToHexString(PRESSURE_CLUSTER_ID(),4)))
    {
        event = parsePressureEvent(descMap);
    }
    else if(descMap?.cluster?.equals(zigbee.convertToHexString(HUMIDITY_CLUSTER_ID(),4)))
    {
         event = parseHumidityEvent(descMap); 
    }
    else if(descMap?.cluster?.equals(zigbee.convertToHexString(ILLUMINANCE_CLUSTER_ID(),4)))
    {
         event = parseIlluminanceEvent(descMap); 
    }
    else if(descMap?.cluster?.equals(zigbee.convertToHexString(BINARY_INPUT_CLUSTER_ID(),4)))
    {
        event = parseBinaryInputEvent(descMap);
                    //log.debug ("$event")
        ///////////////////////////////////////////////////////////
        sendEvent (name:"door",value:(("$event.value")== "true")?"closed":"open")
       
        //log.info ("$event.value")
        
        
        //////////////////////////////////////////////////////////
        reflectToChild(childBinaryInput,description)
    }
    else if(descMap?.cluster?.equals(zigbee.convertToHexString(ANALOG_INPUT_CLUSTER_ID(),4)))
    {
        event = parseAnalogInputEvent(descMap)
        
        reflectToChild(childAnalogInput,description)
    }
    else if(descMap?.cluster?.equals(zigbee.convertToHexString(BINARY_OUTPUT_CLUSTER_ID(),4)))
    {
        event = parseBinaryOutputEvent(descMap)

        reflectToChild(childBinaryOutput,description)
    }
    else if(descMap?.cluster?.equals(zigbee.convertToHexString(POWER_CLUSTER_ID(),4)))
    {
        event = parseBattEvent(descMap)
    }

}
return event
}

boolean parseSerial(String description)
{
if(!description?.startsWith("catchall:"))
{
return false
}

def descMap = zigbee.parseDescriptionAsMap(description)

if( !(descMap.profileId?.equals("0104") ) )
{
    return false
}    

if( !(descMap.clusterInt?.equals(SERIAL_TUNNEL_CLUSTER_ID()) ) )
{
    return false
}

if( !(descMap.command?.equals("00") ) )
{
    return false
}

if(!descMap.data)
{
    return false
}

reflectToSerialChild(descMap.data)
  
return true

}

private boolean parseIasMessage(String description) {

if (description?.startsWith('enroll request')) 
{
	Log ("Sending IAS enroll response...")
    
	def cmds = zigbee.enrollResponse()
    
    cmds?.collect{ sendHubCommand(new hubitat.device.HubAction(it,hubitat.device.Protocol.ZIGBEE) ) };
    return true
}
else if (description?.startsWith('zone status ')) 
{        
    ZoneStatus zs = zigbee.parseZoneStatus(description)
    String[] iasinfo = description.split(" ")
    int x =  hexStrToSignedInt(iasinfo[6])
    
    def resultMap;
    
    if(zs.alarm1)
    {
        if(x == 0)
        {
            resultMap = createEvent(name: "smoke", value: "detected")
        }
        else
        {
            resultMap = createEvent(name: "carbonMonoxide", value: "detected")
        }
        
        sendEvent(resultMap)
        
    }
    else
    {
        
       resultMap = createEvent(name: "smoke", value: "clear")
       sendEvent(resultMap)
        
       resultMap = createEvent(name: "carbonMonoxide", value: "clear")
       sendEvent(resultMap)
    }
 
    if(zs.ac)
    {
        resultMap = createEvent(name: "powerSource", value: "battery")
    }
    else
    {
        resultMap = createEvent(name: "powerSource", value: "main")
    }   
    sendEvent(resultMap)
    
    return true
}

return false

}

// Parse incoming device messages to generate events
def parse(String description) {
Log("parse: $description")

if(parseIasMessage(description))
{
    return
}

event = parseCustomEvent(description)
if(event)
{
    sendEvent(event)
    return
}

def event = zigbee.getEvent(description)
if(event)
{
    sendEvent(event)
    return
}

if(parseSerial(description))
{
    return   
}

Log("DID NOT PARSE MESSAGE : $description")

}

def off() {
}

def on() {
}

def sendCommandPDelay(data)
{
return data
}

def sendCommandP(def cmd)
{
runIn(0, sendCommandPDelay, [overwrite: false,data: cmd])
}

def sendToSerialdevice(byte[] serialCmd)
{
String serial = serialCmd.encodeHex().toString()

return zigbee.command(SERIAL_TUNNEL_CLUSTER_ID(), 0x00,[:],0,serial)

}

def command(Integer Cluster, Integer Command, String payload)
{
return zigbee.command(Cluster,Command,payload)
}

def command(Integer Cluster, Integer Command)
{
return zigbee.command(Cluster,Command)
}

def readAttribute(Integer Cluster, Integer attributeId, Map additionalParams)
{
return zigbee.readAttribute(Cluster, attributeId, additionalParams)
}

def readAttribute(Integer Cluster, Integer attributeId)
{
return zigbee.readAttribute(Cluster, attributeId)
}

def writeAttribute(Integer Cluster, Integer attributeId,
Integer dataType, Integer value,
Map additionalParams)
{
return zigbee.writeAttribute(Cluster, attributeId,
dataType, value,
additionalParams)
}

def writeAttribute(Integer Cluster, Integer attributeId,
Integer dataType, Integer value)
{
return zigbee.writeAttribute(Cluster, attributeId,
dataType, value)
}

def configureReporting(Integer Cluster,
Integer attributeId, Integer dataType,
Integer minReportTime, Integer MaxReportTime,
Integer reportableChange,
Map additionalParams)
{
return zigbee.configureReporting( Cluster,
attributeId, dataType,
minReportTime, MaxReportTime,
reportableChange,
aditionalParams)
}

def configureReporting(Integer Cluster,
Integer attributeId, Integer dataType,
Integer minReportTime, Integer MaxReportTime,
Integer reportableChange)
{
return zigbee.configureReporting( Cluster,
attributeId, dataType,
minReportTime, MaxReportTime,
reportableChange)
}

def configureReporting(Integer Cluster,
Integer attributeId, Integer dataType,
Integer minReportTime, Integer MaxReportTime)
{
return zigbee.configureReporting( Cluster,
attributeId, dataType,
minReportTime, MaxReportTime)
}

def binaryoutputOff()
{
zigbee.writeAttribute(0x0010, 0x0055, DataType.BOOLEAN, 1)
}

def binaryoutputOn()
{
zigbee.writeAttribute(0x0010, 0x0055, DataType.BOOLEAN, 0)
}

private def refreshExpansionSensor()
{
def cmds = []

def mapExpansionRefresh = [[0x0010,enableBinaryOutput,0x0055],
    [0x000F,enableBinaryInput,0x0055],
    [0x000C, enableAnalogInput,0x00104],
    [0x000C, enableAnalogInput,0x00103]]
    
mapExpansionRefresh.findAll { return it[1] }.each{
    cmds = cmds + zigbee.readAttribute(it[0],it[2])
    }
    
return cmds

}

private def refreshOnBoardSensor()
{
def model = device.getDataValue("model")

def cmds = [];

 def mapRefresh = ["RES001":[TEMPERATURE_CLUSTER_ID(), HUMIDITY_CLUSTER_ID(), PRESSURE_CLUSTER_ID(),ILLUMINANCE_CLUSTER_ID()],
     "RES002":[TEMPERATURE_CLUSTER_ID(), HUMIDITY_CLUSTER_ID(), PRESSURE_CLUSTER_ID()],
     "RES003":[ILLUMINANCE_CLUSTER_ID()],
     "RES005":[TEMPERATURE_CLUSTER_ID(), HUMIDITY_CLUSTER_ID(), PRESSURE_CLUSTER_ID(),ILLUMINANCE_CLUSTER_ID()],
     "RES006":[TEMPERATURE_CLUSTER_ID(), HUMIDITY_CLUSTER_ID(), PRESSURE_CLUSTER_ID(),ILLUMINANCE_CLUSTER_ID()]]
 
mapRefresh[model]?.each{
    cmds = cmds + zigbee.readAttribute(it,SENSOR_VALUE_ATTRIBUTE());
}

return cmds

}

private def refreshDiagnostic()
{
def cmds = [];
MapDiagAttributes().each{ k, v -> cmds += zigbee.readAttribute(DIAG_CLUSTER_ID(), k) }
return cmds
}

private def refreshBatt()
{
return zigbee.readAttribute(POWER_CLUSTER_ID(), BATT_REMINING_ID())
}

def refresh() {
Log ("Refresh")
state.lastRefreshAt = new Date(now()).format("yyyy-MM-dd HH:mm:ss", location.timeZone)

return refreshOnBoardSensor() + 
    refreshExpansionSensor() + 
    refreshDiagnostic() +
    refreshBatt()

}

private def reportBME280Parameters()
{
return [[TEMPERATURE_CLUSTER_ID(),DataType.INT16, 5, 300, 10],
[HUMIDITY_CLUSTER_ID(),DataType.UINT16, 5, 301, 100],
[PRESSURE_CLUSTER_ID(),DataType.UINT16, 5, 302, 2]]
}

private def reportTEMT6000Parameters()
{
return [[ILLUMINANCE_CLUSTER_ID(),DataType.UINT16, 5, 303, 500]]
}

private String swapEndianHex(String hex) {
reverseArray(hex.decodeHex()).encodeHex()
}

private byte[] reverseArray(byte[] array) {
int i = 0;
int j = array.length - 1;
byte tmp;
while (j > i) {
tmp = array[j];
array[j] = array[i];
array[i] = tmp;
j--;
i++;
}
return array
}

private def configureIASZone()
{
def cmds = []

cmds += "zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 0x01 0x0500 {${device.zigbeeId}} {}"
cmds += "delay 1500"
        
cmds += "he wattr 0x${device.deviceNetworkId} 0x${device.endpointId} 0x0500 0x0010 0xf0 {${swapEndianHex(device.hub.zigbeeId)}}"
cmds += "delay 2000"
      
return cmds

}

def configure() {

Log("Configuring Reporting and Bindings.")
state.remove("tempCelcius")

def mapConfigure = ["RES001":reportBME280Parameters()+reportTEMT6000Parameters(),
    "RES002":reportBME280Parameters(),
    "RES003":reportTEMT6000Parameters(),
    "RES005":reportBME280Parameters()+reportTEMT6000Parameters(),
    "RES006":reportBME280Parameters()+reportTEMT6000Parameters()]

def model = device.getDataValue("model")

def cmds = [];
mapConfigure[model]?.each{
    cmds = cmds + zigbee.configureReporting(it[0], SENSOR_VALUE_ATTRIBUTE(), it[1],it[2],it[3],it[4])
}

cmds += zigbee.configureReporting(POWER_CLUSTER_ID(), BATT_REMINING_ID(), DataType.UINT8,60,307,4)

if(model == "RES006")
{
   cmds = cmds + configureIASZone()
}

cmds = cmds + refresh();

return cmds

}

private def createChild(String childDH, String component)
{
if(!childDH)
{
return null
}

def childDevice = getChildDevice("${device.deviceNetworkId}-$childDH")
if(!childDevice)
{
    childDevice = addChildDevice("iharyadi", 
                   "$childDH", 
                   "${device.deviceNetworkId}-$childDH",
                   [label: "${device.displayName} $childDH",
                    isComponent: false, 
                    componentName: component, 
                    componentLabel: "${device.displayName} $childDH"])
}

return childDevice?.configure_child()

}

private def updateExpansionSensorSetting()
{
def cmds = []

def mapExpansionEnable = [[0x0010,enableBinaryOutput,DataType.BOOLEAN,0x0055],
    [0x000F,enableBinaryInput,DataType.BOOLEAN,0x0055],
    [0x000C, enableAnalogInput,DataType.UINT16,0x0103]]
    
mapExpansionEnable.each{ 
    cmds = cmds + zigbee.writeAttribute(it[0], 0x0051, DataType.BOOLEAN, it[1]?1:0)
    if(!it[1])
    {
        cmds = cmds + zigbee.configureReporting(it[0], it[3], it[2], 0xFFFF, 0xFFFF,1)
    }
}

def mapExpansionChildrenCreate = [[enableBinaryOutput,childBinaryOutput,"BinaryOutput"],
    [enableBinaryInput,childBinaryInput,"BinaryInput"],
    [enableAnalogInput,childAnalogInput,"AnalogInput"]]

mapExpansionChildrenCreate.findAll{return (it[0] && it[1])}.each{
    cmds = cmds + createChild(it[1],it[2])
}

return cmds

}

private def createSerialDeviceChild(String childDH, Integer page)
{
createSerialDeviceChildWithLabel(childDH,page, "${device.displayName} SerialDevice-$page")
}

def createSerialDeviceChildWithLabel(String childDH, Integer page, String label)
{
if(!childDH)
{
return null
}

def zigbeeAddress = device.getZigbeeId()
def childDevice = getChildDevice("$zigbeeAddress-SerialDevice-$page")
if(!childDevice)
{
    childDevice = addChildDevice("iharyadi", 
                   "$childDH", 
                   "$zigbeeAddress-SerialDevice-$page",
                   [label: "${label}",
                    isComponent: false, 
                    componentName: "SerialDevice-$page", 
                    componentLabel: "${device.displayName} SerialDevice-$page",
                    pageNumber: page])
}

return childDevice?.configure_child()

}

private def updateSerialDevicesSetting()
{
def cmds = []
if(!childSerialDevices)
{
return cmds;
}

def jsonSlurper = new JsonSlurper()
def serialchild = jsonSlurper.parseText(childSerialDevices)

serialchild.each{
    createSerialDeviceChild(it.DH, it.Page)
} 

cmds += "zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 0x01 0x${Integer.toHexString(SERIAL_TUNNEL_CLUSTER_ID())} {${device.zigbeeId}} {}"
cmds += "delay 1500"       

return cmds

}

def updated() {
Log("updated():")

if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) {
    state.updatedLastRanAt = now()
    state.remove("tempCelcius")
    
    def cmds = updateExpansionSensorSetting()
    
    if(device.getDataValue("model") == "RES005")
    {
        cmds += updateSerialDevicesSetting()
    }
    
    cmds += refresh()
    return cmds
}
else {
    Log("updated(): Ran within last 2 seconds so aborting.")
}

}

1 Like

It would be helpful if you would "collapse" long sections of posts. To do this:

  1. Select the section you wish to collapse by dragging over it
  2. Click on the "gear" icon in the message editor toolbar and select "Hide Details"

I'm sure I'm not the only one who will appreciate it. :slightly_smiling_face:

6 Likes

totally my fault, thanks for the heads up

2 Likes

I want to thank @manuelangelrivera for sharing what you have done with your project using Environment Sensor. This show that the Environment Sensor is not just one trick pony.

This is a very good example of what you can do to extend the Environment sensor. With a little bit of code on the Hubitat side, one can mix and match additional sensor/devices customized for your own DIY project.

2 Likes

Thanks to you for sharing and bringing us such a great board. I would like to see your expert coding skills take the Garage EX code and make it even cooler :wink:
So far my monkey code with copy pasta has been working flawlessly!!!


I could not be happier, this is the best garage opener I've ever come across in all my IOT iterations, with added super benefit of nearly instant presence status using ST fob's with zero false positives!!!

3 Likes

Hi, was interested in using this with a pressure strip sensor such as https://www.superdroidrobots.com/shop/item.aspx/force-sensing-resistor-24-x-0-25-strip/1302 which has resistance ranging from 1MO (no pressure) to a little over 100 Ohms at 10kg.

I only have a basic understanding of circuits, but am I right in assuming that if I got the expandable board, I should connect that pressure sensor to the 5V and the AI connections, then the driver would report the voltage through the pressure pad?

@amosyuen, you will need to form a voltage divider to measure the resistance of the pressure strip.

Take a look at the following for voltage divider.

https://learn.sparkfun.com/tutorials/voltage-dividers/all

For the power supply, you can use the 5V pin to get the best range. With the right combination of the resistor value, you may get the full range of the load that you will need to measure.

Now, there is one complication with the sensor while the sensor is not loaded. 1M Ohm resistance is too high for a lot of ADC. You will have issue with this with most MCU (not just mine). Based on the page your linked and the datasheet, once the sensor has some pressure, the resistance is about 100K to 100 ohm. At this range, there should be no issue.

With a simple voltage divider, you may experience very strange reading when the sensor is not loaded. Once loaded, you may get to the point where it is in the range where the MCU can work properly to measure its resistance. If your use case is where the sensor is always loaded with some pressure, the voltage divider above is all you need.

If you need full range of the sensor resistance, you will need a voltage follower. It is a simple circuit that will decouple the impedance. Let me know if you need this information. I can google a link for you. This will require to work with op-amp. It is not a complicated circuit but it has more pins to deal with versus working with simple resistors.

Reading the AI pin is the easy part. You are correct that it would just a matter of reading the AI pin.

BTW, if you can share more detail of what you like to do, perhaps, it can be solve with different strategy. Would you need to know specifically the value of the resistance? Or do you just need it for presence detection?

Thanks
Iman

Thanks for all the info iharyadi, the use case is to put it underneath the sofa cushions and use it for presence detection. It doesn't need to be analog. I thought analog would be nice as I could easily fine tune the threshold on software side, but having a circuit with a threshold near the max weight (around 10 kg / 22 lbs) probably should be fine too.

This is correct. However, you are working on sensor which the resistance is quite high. This can cause issue for most ADC (not limited to what I am offering).

Since you are going to convert them to present or not present, you can move the decision and the threshold to hardware. A simple comparator will do. I do not have out of the box comparator in mind that ready to sell. However, I play around with the following.

https://www.ebay.com/itm/383967048661?_trkparms=ispr%3D1&hash=item59663833d5:g:yf0AAOSwqSlgMCjA&amdata=enc%3AAQAFAAACkBaobrjLl8XobRIiIML1V4Imu%252Fn%252BzU5L90Z278x5ickkxFtV7J5P58ubuVigtBH%252FewgR5cnoaeYaE878Hx8SVCYjRUnNh58KItVBPWgGOIPHbtlurgLxzGoygO0P1o3pjJAQ969zuYLjm7rsWFT5it%252F2bCyU4tPJC1Rh2s7yW0ZcjnC%252BbPBNxJU8cUTdFdqYcpzcbQ61CosX%252F%252F7p%252ByVjkvV5WYfwrk4CmkxHEP9lKeTRFpq8AFPvmAubDzPnURW%252FxTK3sILcpt7YTTfn%252FuugKaXW%252FJojQBIx9mzAO5my7qInjtf1L7dABr1D%252FB2QYwBQAF1AF9NhueD0HubXNE8eI239drhGzKdfOjvyfGPuxkBxv1FSMdzUSUHV1JifiMeRVsTUCg0xePRaUracom8sodmU2KJXszdW1EjfOocptPqCS9%252BIYZfSyfaiTxZ5W4nEBkD60n2Wt6HlFtRdYk%252BHWCx8zQIkFmfCvcg3%252FpmvH49gAUsiGJ02mkI4u1noCb5Cq%252BRxvHUnbztiQ5VOuQvX6TUkvtzEf8GJQdWttQLTj94%252BTsNJM%252B9%252Beb4CioXng2dfto8%252FfllmSbGEfgiR%252BkKbYVXItmmMa%252BrSGKraP3sJgCQ5u7GZtcsPvE9IY%252FIG6BCcuw1moGqnsp8zkfNZbitN0R2VioLuhgtOwPFVQ%252BxEqrtH2AGNOFN9fipdaIWTHWjG33D5nq0NefrKLmRq45fKGYVx1f5Nx0vubaoXbcz%252BCkrRQm6REutPMtVCtxjnDexJsQrsj%252FNqJoG4pdx60Kb7tcd%252BOkgkgB9QPE%252Bcww0fW37GtHMHAe95yC4IezvAfYOu1KGaov%252FFaH5nJJLpeNanozn6SR8G7NjTimJX6Reytiqz|cksum%3A383967048661b467c2ebbf544fa4bf724a4c97d9acc0|ampid%3APL_CLK|clp%3A2334524

Forget about the moisture probe, you can throw that away. The module (the one with pcb) is the one that you may reuse in this case. It is the comparator that I am talking about. You can connect the strip sensor instead of the moisture sensor. There is a potentiometer that you can adjust as the threshold. The only thing that you may have to adjust is the value of the resistance on that board. It may be designed to compare higher resistance value. But, you can be lucky that they may actually match.

You also mention the following.

If you want to play around with your sensor (perhaps you have already have one), that cushion may put the strip sensor in the range that we can measure. My impression from the datasheet is that the high impedance only when there is no pressure at all. You may have a chance that we can use voltage divider. I would keep that in mind.

1 Like

You might find the following helpful, although I will admit using these pressure sensors does take some tweaking to get the electrical circuit to behave reliably. It really does not matter whether you use @iharyadi awesome Zigbee solution, or an ESP8266 running my HubDuino software... Electrically, both systems need a reliable 'on/off' voltage signal to detect.

3 Likes

Hello Everyone,

I apologize for an extremely long hold on the BME680 project. I had the code and board ready. Then, the semiconductor shortage strike as we speak. I did not have a couple major components sourced (MCU and BME680 sensor) at that time.

I have managed as of a week ago sourced the MCU and BME680. Component availability is start to trickling in. However, the cost of the components are currently inflated in the factor of at least 2X especially with the quantity that I am making. Regardless the current challenges, I believe pushing forward the project is a must. Therefore, I did purchased the needed components and built 10 complete module for us.

The BME680 is an addon that plugged into the expansion ports of the Environment Sensor.

Where is what you will see in the hub.

BME680 comes with a gas sensor. It also comes with a proprietary software component to calculate some attribute such as the Indoor Air Quality (IAQ) index, CO2, VOC. With these additional information, we can use it to control our ventilation system better. I uses it with the nest integration and rule machine to start air circulation at some IAQ values.

I also learn that the sensor reading can help as secondary data to determine whether a room is occupied. It is not a good idea to use it as the primary sensor for this purpose. As a human, we produce CO2. As the room is occupied, the sensor reading will increase while a room is occupied. The reading is slow compared to other sensor such as PIR. At this point, it is nice that I can glace over my dashboard to be able to confirm whether a room is occupied. The reading becomes important in the case where you would like to determine whether there are too many people in the room since the reading will be extremely elevated at that point.

A note: BME680 has one gas sensor sensitive to variety of gas. The BOSCH software interpolate the reading and give us an estimate such as the CO2 and VOC level in the air. I do not think that it is able to differentiate the type of the gas. While reading the sensor and making a decision, I would read it the following way for the example above. The BME680 is reading 547.2 ppm if it is a CO2 "OR" 0.59 ppm if it is VOC. It does not mean that my current room contain 547.2 ppm of CO2 and .59 ppm of VOC.

The BME680 modules plugged in to existing Environment Sensor model RES005. The RES005 has BME280 which make the temperature, humidity and pressure measurement redundant just FYI if you are looing to use it RES005. I also make Environment Sensor model RES006. RES006 is the same as RES005 with BME280 removed.

The module also come with power outage detection. The Environment Sensor comes with recharge able battery backup feature. the BME680 add on module has a small circuit that will detect power outage when it lost the DC power.

While on DC power, it will report DC as below. It will turn to battery the event of power loss.

Some of you have reach out to get the BME680. I will try to look back on my email and reach back to you for the update on the pricing for the modules. If I miss you, please send me a PM. Currently, I only make the BME680 in very limited quantity. The cost is high for the components. However, if there are huge demand, I am would love to make them at much higher quantity so that we can lower the cost by buying in bulk.

Thanks
Iman

7 Likes

Hey @iharyadi was thinking about the other day. Now that Hubitat allows zigbee firmware updates, are you considering posting your updates on the platform?
Keep it coming!!!

1 Like

I am new to hubitat. could i use esp32 as a bluetooth receiver over lan? If not what device to i need to get for hubitat to receive bluetooth data. Can we use a bridge like this one

What I want to build is a ZigBee weather station. No station exist that's ZigBee.
I can build on with a PI but looking for something simpler.

Hello Everyone,

For those requested the BME680 modules, they should start to be delivered. I just want to write small instruction here. The basic work started the same as paring Environment Sensor. Please get the latest DTH Environment Sensor from my github.

Please add BME680.groovy to your DTH. There is also Power Detector.groovy that you may want to install. Obviously, you will need to use battery if you want to take advantage the power detector. I can notify the hub when it detect power outage.

Once the Environment Sensor is paired, you can go to its detail page and add the following information.

Please pay attention to the double quote on the Serial Children Handler. It should be as follow.

[{"Page":2,"DH":"BME680"}]

I have seen somehow the double quote got upside down. It will cause the DTH not to get installed.

Step 2 and 3 is optional if you want to take advantage the power outage detection.

Please click the Save Preference on step 4. A new BME680 will be automatically created. You can go to its detail page and rename it.

Within 10 to 15 minutes, the BME680 should start sending data. I personally wait at least 24 hours before I use the gas related data. The gas data is not absolute. Bosch have custom software to learn the environment and use it to give estimate reading. In my experience, the reading will get better as the time goes. You will see something like below.

image

Finally, thank you for trying out the BME680 modules. I hope we can put to a good use. The BME 680 modules is based on STM32 Arduino code. I will make it open source for us to improve the BME680 addon module. I do have less than a handful BME680 modules left. Let me know if you are interested.

Thanks
Iman

1 Like

I received my devices a few days ago and paired one yesterday, I've tried to get the BME to show up and am not having any luck. I've copied your serial child handler no luck, I used a child handler you did for my serial temp device correcting the device name. No luck. Any suggestions where I should look? BTW if you have any more I will I'm interested in another.

Thanks!

I am assuming the Environment Sensor has paired up as you mentioned.

Did the BME680 child device show up? If it is not showing up, would you mind open the log window and watch it as you save preference.

I remember that I helped you with another child device that had issue. Please make sure that the double quote " is correct. At the time, It was upside down.

I still have some BME680 if you still need it. Just let me know when you would like to get it.

Thanks
Iman

BTW, The EnvironmentSensorEX.groovy has recently been updated. Please update yours.

The child device does not get created.

Thanks

Please double check that this is the setting that you use.

[{"Page":2,"DH":"BME680"}]

Here is the comparison what is good (yours) vs bad (mine).