Hubitat with Homemade Temperature, Humidity, Pressure and Light sensor

Yes. I have old driver for earlier generation of the sensor module without the "EX" postifx.

Thanks for the update that the "configure" fix the issue. It is good to share if others in the community experience the rare weirdness.

@iharyadi can you help me with a child driver for the analog port. I would like to use it a binary input if its possible. Thanks

He has several in his too including this contact sensor that I use with one of mine. Power source also uses analog input.

thanks for the advice, but contacts drivers references back to zigbee clusters for binary input. not analog.

Hum ok yes looked at that sensor and I am using binary, corrected above. Look at power detector, it uses analog on my other sensor:

1 Like

thanks @ritchierich, what is your use case for "power detector" driver? In my context, I want to use the analog input as binary input for a switch device. I know is possible, but eludes my current skills.

Alerts me if there is power loss. I have an environmental sensor plugged into the same outlet as my sump pump so if power goes out I get alerted. @iharyadi discusses it here:

Here is an example how I think it would look like if you want to use analog input as digital input.

metadata {
       definition (name: "Motion Sensor", namespace: "iharyadi", author: "iharyadi") {
	   capability "Configuration"
       capability "Motion Sensor"
       capability "Sensor" }
        
       preferences {}
}

def parse(String description) { 

def event;

if(!description?.startsWith("read attr - raw:")){
  return event;
}

def descMap = zigbee.parseDescriptionAsMap(description)

def adc;
def vdd;

if(descMap.attrInt?.equals(0x0103)){
   adc = descMap.value
}
else if (descMap.attrInt?.equals(0x0104)){
   vdd = descMap.value
}
else{   
   adc = descMap.additionalAttrs?.find { item -> 
      item.attrInt?.equals(0x0103)}?.value
}

if(!adc){
   return event   
}

if(adc < 0x0100){
   event = createEvent(name:"motion",value:"inactive")
}
else if( adc > 0x0FFF) {
   event = createEvent(name:"motion",value:"active")
}	

return event;
}

def configure_child() {
    def cmds = []
    cmds = cmds + parent.writeAttribute(0x000C, 0x00105, DataType.UINT32, 250)
    cmds = cmds + parent.configureReporting(0x000C, 0x0103, DataType.UINT16, 5, 306,25)
    return cmds
}

def installed() {
}

I have not test it. It is just for example. What it does is reading the adc value. In this case, we do not need it to read the adc in voltage. We just use raw value. The past few lines inside the parse statement compare the value of adc and translate it to event.

I choose any arbitrary threshold. Please choose the rights number for your sensor. The if statement is basically form hysteresis. If the adc value less than 0x0100, I consider this as low. If above 0x0FFF, I consider it high. This is how a digital signal work. We just have to emulate the adc reading with this logic. The reason digital signal does this is that the analog value usually fluctuating. You can print/log your reading to determine the value that you want to use. You just have to make your sensor go low and then read the value of the adc. You can do the same with high value. Then, you can give them some threshold. I am guessing that the low value for your adc will be 0.

Thanks
Iman

1 Like

Thanks good sir. Will give it a try and come back with my notes.

So this is what I get with the ADC Motion Driver.

While high:

image

While low:
image

Was not able to to follow the groovy code flow (coding impared, shame on me) .
But in a nutshell, the logic I would like to able to attain the binary is:
If Analog input value is > 3.0 then update DHT as true/active/on/detected
If Analog input value is < 3.0 then update DHT as false/inactive/off/clear

or

If Analog input value is > 3.0 then update DHT as true/active/on, else false/inactive/off/clear.

So your code performs the comparation in this line:

if(adc < 0x0100){
event = createEvent(name:"motion",value:"inactive")
}
else if( adc > 0x0FFF) {
event = createEvent(name:"motion",value:"active")
}

But then I get these parsing errors:image

Sorry if this is basic, but as stated earlier I have a coding deficit.

Lets try convert the adc to integer. Replace the code like below.

int temp = zigbee.convertHexToInt(adc)
if(temp< 0x0100){
event = createEvent(name:"motion",value:"inactive")
}
else if( temp> 0x0FFF) {
event = createEvent(name:"motion",value:"active")
}

1 Like

Thanks for the lift @iharyadi, made it work by inverting logic and adding log debug to be able to guide myself:

Working code for me:

int temp = zigbee.convertHexToInt(adc)
if(temp> 7000){
event = createEvent(name:"motion",value:"inactive")
log.debug("State Variables (inactive) ADC $temp")
}
else if( temp< 2300) {
event = createEvent(name:"motion",value:"active")
log.debug("State Variables (active) ADC $temp")
}

image

Thankiu good Sir.

1 Like

cool!!! That is what I would do. You do not need to convert it to volt for this purpose.

@iharyadi Sorry to be a nuance again. But I've been trying an idea I have with your board, I require a momentary relay, I know how to manipulate it on the child devices, but for my use case I need the parent to be the switch to make it work. I ve tried a few things, but the parent device seems to have some kind parsing safeguard aka catchall (IDK if thats a thing :wink:

On my child device this is how I manage my momentary relay with "runInMillis" function:

def off() {
parent.sendCommandP(parent.binaryoutputOff())
}

def on() {
parent.sendCommandP(parent.binaryoutputOn())
runInMillis(1500, off)

But then, on the parent device I tried the same, but for some reason, if I add the "runInMillis", will not even action the relay on the "def on()", without the runinmillis would work as design, but if I add the delay would not:

def on() {
binaryoutputOn()
runInMillis(1500, off)

}

def off() {
binaryoutputOff()

}

Is there any additional consideration I am missing here, would appreciate your dev wisdom, and thank you.

Lets try to switch the command arround.

def on() {
  runInMillis(1500, off)
  binaryoutputOn()
}

This could be just the grrovy implicit return object.

You made it look so simple, solution was so easy I feel embarrassed and here I spent over 4 hours pounding my head lol. It worked. Much Obliged!!!

Wohoo!!! It's alive... sort off. I did some script kiddie and code monkeying, it's not pretty, but hey it works :wink: . So this is what I have been up to. Thanks @iharyadi for your support, maybe this will inspire you to take your board into other markets, IDK if it has been made before. But from my perspective is the perfect garage opener that has everything, without the need for rulemachine or virtual devices. I already have a tasmota ESP8266 running inmy garage that has been rock solid and had zero problems with it. But a zigbee solution is much better in my scenario, as not only I don't have to rely on WIFI, but will also help my ST zigbee presence beacons (in each car) to be picked up faster an more reliably. I add to all my garage openers a MQ7 with the intention of instantly opening the garage door in case of extreme concentration of CO2, MQ7 (give me at least a 10% on the patent lol) works perfectly and already had proven itself once, with my wife leaving car on during winter with the garage door closed.

Tasmota dht's looks like this:

This is the current prototype for the garage opener with Sensor EX board, simple 5v relay, a reed sensor for the door and MQ7 connected to analog, but using it as digital out instead:

This is the concept use case as seen on HomKit:

Here is the ugly copy pasta code I made if you guys are into it:

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 "GarageDoorControl"

        capability "Refresh"

        //capability "Battery"

        capability "Temperature Measurement"

        capability "RelativeHumidityMeasurement"

        capability "Illuminance Measurement"

        capability "PressureMeasurement"

        //capability "Switch"

        //capability "SmokeDetector"

        //capability "PowerSource"

        //capability "CarbonMonoxideDetector"

        capability "Sensor"

        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

        }

    }

}

def open()

{

  runInMillis(1500, binaryoutputOn)

  binaryoutputOff()

}

def close()

{

  runInMillis(1500, binaryoutputOn)

  binaryoutputOff()

}

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 ? "false" : "true"

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

    log.debug ("$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)

}

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")?"open":"closed")

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

            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 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 binaryoutputOn()

{

    zigbee.writeAttribute(0x0010, 0x0055, DataType.BOOLEAN, 1)

  

}

def binaryoutputOff()

{

    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.")

    }

}
2 Likes

@manuelangelrivera, Thank you for sharing your garage door project. It is well done. It show case the expansion capability of the module.

Hey It was a lot of fun, appreciate your lift as well. I think you should consider taking it up a notch and making a dedicated "Garage" only parent, (one better than my sloppy code lol).
I feel you could broaden your market audience and be pioneer on on this garage segment.
Thank you for bringing us such cool board.

image

1 Like

Thank you for the suggestion about the garage door packaging. I see that it can help other members. Using MQ7 as one of the sensor is quite interesting one. It is really a good idea.