@Field static Map is empty inside List.each() {}

I'm new to Groovy and working on my first driver (zigbee). The full driver is included at the bottom of this topic.

My problem is in the configureReporting method. Here is the snippet with the issue. The first log entry is as I expect. The second entry is just [].

List attrs =
    [electricalMeasurementCluster.attrs.rmsVoltage, 
     electricalMeasurementCluster.attrs.rmsCurrent,
     electricalMeasurementCluster.attrs.activePower]
        
    log.debug "commands: ${commands}"  // correct- logs shows [configureReporting:06, readReportingConfiguration:08, reportAttributes:0A, defaultResponse:0B, discoverAttributesExtended:15]
    
    attrs.each() {
        
       log.debug "commands: ${commands}"  // bad- logs show []

commands is defined as:

@Field static Map<String,String> commands = [
    configureReporting: "06",
    readReportingConfiguration:"08",
    reportAttributes:"0A", 
    defaultResponse:"0B",
    discoverAttributesExtended: "15"]

Here is the complete driver.

/*
	Switch Driver for Third Reality 3RSP02028BZ

    Comments reference sections of Zigbee Alliance Cluster Library Specification, 
    Document 07-5123 Revision 8, 6 Chapter Document 14-0125
    https://zigbeealliance.org/wp-content/uploads/2021/10/07-5123-08-Zigbee-Cluster-Library.pdf
    References to the spec will be abbreviated at ZS + a section number.
	
    2023 1.0
*/

/* Libraries from https://github.com/jvmahon/Hubitat-Zigbee.  
   See also https://community.hubitat.com/t/zigbee-full-format-for-sending-commands-using-he-raw/101926 */
#include zigbeeTools.sendZigbeeAdvancedCommands   
#include zigbeeTools.endpointAndChildDeviceTools  

import hubitat.helper.HexUtils

/* See ZS 4.9 */
@Field static Map electricalMeasurementCluster = [id: "0B04", 
                                         attrs: [activePower: "050B", rmsCurrent: "0508", rmsVoltage: "0505"]]

/* See ZS 3.8 */
@Field static Map onOffCluster = [id: "0006",
                          attrs: [onOff: "0000"]]

/* See ZS 2.6.2.2 */
@Field static Map<String,String> dataTypes = [
    int16: "29",
    uint16: "21",
    bool: "10"
]

/* See ZS 2.5.9.1.2 */
@Field static Map<String,String> direction = [
    reported: "00",
    received: "01"
]

/* See ZB 2.5 */
@Field static Map<String,String> commands = [
    configureReporting: "06",
    readReportingConfiguration:"08",
    reportAttributes:"0A", 
    defaultResponse:"0B",
    discoverAttributesExtended: "15"]

/* See https://docs2.hubitat.com/developer/driver/definition and 
   https://stdavedemo.readthedocs.io/en/latest/index.html  */
metadata {
    definition(name: "Paige Power Switch", namespace: "hubitat", author: "Paige Vinall", component: true) {
        capability "Switch"
        capability "Refresh"
        capability "PowerMeter"
        capability "Actuator"
        capability "Configuration"
        
        attribute "LastUpdate", "number"  // Time stamp of last message from device
        attribute "RMSCurrent", "number"
        attribute "RMSVoltage", "number"
        command "logDeviceInfo"
        fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0003,0004,0005,0006,1000,0B04", outClusters:"0019", model:"3RSP02028BZ", manufacturer:"Third Reality, Inc"
    }
    preferences {
        input name: "logEnable", type: "bool", title: "Enable logging", defaultValue: true
    }
}

Long convertSignedHexToInt(String hex) {
  Long.parseLong(hex, 16);
}

Long decodeNumericValue(String value, String encoding) {
    if (encoding==dataTypes.int16) return convertSignedHexToInt(value)
    else if (encoding==dataTypes.uint16) return HexUtils.hexStringToInt(value)
    else {
        log.debug "Unknown encoding dataType ${encoding} in decodeNumericValue"
        return 0
    }
}


void updated() {
    if (logEnable) log.info "Updated..."
    log.warn "description logging is: ${txtEnable == true}"
}

void installed() {
    if (logEnable) log.info "Installed..."
    device.updateSetting("txtEnable",[type:"bool",value:true])
    refresh()
}

void setLastUpdate() {
    def name = "lastUpdate"
    Date date = new Date();
    def valueText = date.toString()
    def descriptionText = "${name} is ${valueText}"
    def value = date.getTime()
    
    if (logEnable) log.warn descriptionText
    sendEvent(name:name,value:value,descriptionText:descriptionText,unit:"ms")
}

/* Logs information about device.  Not currently used. */
void logDeviceInfo() {
    log.warn "device.getData(): "+device.getData()
    log.warn "capabilities: "+device.capabilities
    log.warn "attributes: "+device.attributes
    log.warn "supportedAttributes: "+device.supportedAttributes
}

/* See ZS 2.5.2 */
void processReportAttributeReadRecord(cluster,attrId,encoding,value) {
   
    int numericValue = decodeNumericValue(value,encoding)
    boolean boolValue = (value=="01")
    if (logEnable) log.warn "cluster ${cluster}, attrId ${attrId}, encoding: ${encoding}"
 
    if (cluster==electricalMeasurementCluster.id && attrId==electricalMeasurementCluster.attrs.activePower) {
        sendEvent(name:"power",value:numericValue,descriptionText:"power is ${numericValue}", unit:"W")     
    }
    else if (cluster==electricalMeasurementCluster.id && attrId==electricalMeasurementCluster.attrs.rmsCurrent) {
        sendEvent(name:"RMSCurrent",value:numericValue,descriptionText:"RMScurrent is ${numericValue}", unit:"A")    
    }
    else if (cluster==electricalMeasurementCluster.id && attrId==electricalMeasurementCluster.attrs.rmsVoltage) {
        sendEvent(name:"RMSVoltage",value:numericValue,descriptionText:"${numericValue}", unit:"V")    
    }
    else if (cluster==onOffCluster.id && attrId==onOffCluster.attrs.onOff) {
        if (boolValue)
          sendEvent(name:"switch",value:"on",descriptionText:"outlet is on")    
        else
          sendEvent(name:"switch",value:"off",descriptionText:"outlet is off")    
    }
    else {
        log.debug "Unexpected report attribute: cluster ${cluster}, attrId ${attrId}, encoding: ${encoding}, value: ${value}"
    }
}

/* See ZS 2.5.2 */
/* Makes a list of all attribute read records and then iterates through them.  
   Note that my devices only return a single record so some of this code is untested. */
void processReportAttributes(Map msgMap) {
    List<Map> attrList = []
    
    attrList.add(["attrId":msgMap.attrId, "value":msgMap.value, "encoding":msgMap.encoding])
    if (msgMap.additionalAttrs) attrList.addAll(msgMap.additionalAttrs)  // Untested
    
    if (logEnable) log.warn "additionalAttr: ${attrList.inspect()}"
    
    attrList.each{
        processReportAttributeReadRecord(msgMap.cluster,it.attrId,it.encoding,it.value)
    }
}

void parse(String description) { 
    setLastUpdate()
    
    def msgMap = zigbee.parseDescriptionAsMap(description)
    
    if (logEnable) log.warn "description: ${description}" 
    if (logEnable) log.warn "msgMap: ${msgMap.inspect()}"
    
     switch (msgMap.command){
        case commands.reportAttributes: //Report attributes
          processReportAttributes(msgMap)
          break
        case commands.defaultResponse: //Default Response
        break
         
        default:
            log.debug "Unexpected response in parse: ${msgMap.inspect()}"
     }
}

/* Never gets called  */
void parse(List description) {
    log.debug "Parse(List) called unexpectedly with ${List.inspect()}"
    description.each {
        log.warn "It=${it}"
        if (it.name in ["switch"]) {
            if (txtEnable) log.info it.descriptionText
            sendEvent(it)
        }
    }
}

/* Called when hubitat wants switch turned on.  See https://docs2.hubitat.com/developer/zigbee-object */
def on() {
    if (logEnable) log.warn "on cmd: "+zigbee.on()
    return zigbee.on()
}

/* Called when hubitat wants switch turned off.  See https://docs2.hubitat.com/developer/zigbee-object */
def off() {
    if (logEnable) log.warn "off cmd: "+zigbee.off()
    return zigbee.off()
}

/* See ZS 2.5.22 */
/* Not called by driver but used in creating driver. */
void discoverAttributesExtended() {
   String startAttributeIdentifier="0000"
   String maximumAttributeIdentifiers="FF"
   String payload = startAttributeIdentifier+maximumAttributeIdentifiers
    
  sendZCLAdvanced(
     destinationNetworkId: device.deviceNetworkId,
     destinationEndpoint: 1 , 
     sourceEndpoint: 1, 
     clusterId: electricalMeasurementCluster.id,
     profileId: 0x0000,
     commandId: commands.discoverAttributesExtended,
     commandPayload: payload,
     commandPayloadAutoreverse: false
     )     
}

/* See ZS 2.5.7 */
void configureReporting() {
    String minimumReportingInterval="0100"
    String maximumReportingInterval="FEFF"
    String reportableChangeField="0001"
    String payload

    List attrs =
    [electricalMeasurementCluster.attrs.rmsVoltage, 
     electricalMeasurementCluster.attrs.rmsCurrent,
     electricalMeasurementCluster.attrs.activePower]
        
    log.debug "commands: ${commands}"  // correct- logs [configureReporting:06, readReportingConfiguration:08, reportAttributes:0A, defaultResponse:0B, discoverAttributesExtended:15]
    
    attrs.each() {
        
       log.debug "commands: ${commands}"  // bad- logs []
        
       payload = direction.reported+
       it+
       dataTypes.uint16+
       minimumReportingInterval+
       maximumReportingInterval+
       reportableChangeField
        
        if (logEnable) log.warn "payload: ${payload}"
                     
       sendZCLAdvanced(
         destinationNetworkId: device.deviceNetworkId,
         destinationEndpoint: 1 , 
         sourceEndpoint: 1, 
         clusterId: electricalMeasurementCluster.id,
         profileId: 0x0000,
         commandId: commands.configureReporting,
         commandPayload: payload,
         commandPayloadAutoreverse: false
       )     
    }

}

/* See ZS 2.5.9.1 */
/* Not currently used by driver.  Used during development. */
void readReportingConfiguration() {
    
    
  String payload = direction.reported+electricalMeasurementCluster.attrs.activePower
                   //+direction.reported+electricalMeasurementCluster.attrs.rmsCurrent
    
  if (logEnable) log.warn "payload: ${payload}"
  sendZCLAdvanced(
     destinationNetworkId: device.deviceNetworkId,
     destinationEndpoint: 1 , 
     sourceEndpoint: 1, 
     clusterId: electricalMeasurementCluster.id,
     profileId: 0x0000,
     commandId: commands.readReportingConfiguration,
     commandPayload: payload,
     commandPayloadAutoreverse: false,
  )     
 
  payload = direction.reported+onOffCluster.attrs.onOff
  if (logEnable) log.warn "payload: ${payload}"
    
  sendZCLAdvanced(
     destinationNetworkId: device.deviceNetworkId,
     destinationEndpoint: 1 , 
     sourceEndpoint: 1, 
     clusterId: onOffCluster.id,
     profileId: 0x0000,
     commandId: commands.readReportingConfiguration,
     commandPayload: payload,
     commandPayloadAutoreverse: false,

   )     
}

void refresh() {
    readReportingConfiguration()
}

void configure() {
    configureReporting()
}

For your second debug log it should be using the "it" variable since you did not specify a variable name in your each statement.

log.debug "commands: ${it}"

You can also do something like this

attrs.each { myVar ->
   log.debug "commands: ${myVar}"
}

This syntax still confuses me all the time and I have to go back and look at my other drivers to remember how to do it!

Thank you. In the debug statement, I'm trying to reference the static Map because I need to use that to tell zigbee the right command to run. I do reference "it" to build the payload and that works fine. Very weirdly, the same structure worked for

@Field static Map electricalMeasurementCluster = [id: "0B04", 
                                         attrs: [activePower: "050B", rmsCurrent: "0508", rmsVoltage: "0505"]]

A couple more thoughts:

I made a local shallow copy of commands as a workaround and that took care of it.

I'm wondering if the name "commands" has some special meaning to Hubitat.

Oh yeah sorry I totally looked at that wrong. It is odd that the commands list cannot be read once inside the each loop. Maybe when you run 'each' it defines its own commands variable for some purpose which would take precedence over the global one.

I guess it must, because I changed the Map name to zigbeeCommands and the problem went away. I'm not going to worry about it any more.

I appreciate the help.

1 Like