GE Enbrighten / JASCO / Universal Devices - toggle switch driver

I used Claude to update the Enbrighten switch driver. It found a few bugs, patched them, and updated the driver. It also now allows you to set or fetch any parameter if you need to tinker. Install it, push CONFIGURE to update and enjoy. Feel free to copy, update, etc. Hope this helps.

The real reason I did this was that I installed a new switch and the generic driver wouldn't allow me to turn off the LED. Tried tweaking the parameter, failed, so searched, found the driver and updated it with Claude.

/**
 *  GE Enbrighten Z-Wave Plus Switch
 *
 *  Original driver by Jason Bottjen (Botched1)
 *  https://raw.githubusercontent.com/Botched1/Hubitat/master/Drivers/GE%20Enbrighten%20Switch/GE%20Enbrighten%20Switch.groovy
 *
 *  NOTE: The importUrl has intentionally been removed so that "Import" in Hubitat
 *  does not overwrite these fixes with the original upstream code.
 *
 *  1.0.0 (07/16/2019) - Initial Version
 *  1.1.0 (07/17/2019) - Removed DoubleTap from BasicSet, added DoubleTap UP/DOWN and TripleTap UP/DOWN as standard buttons 1-4
 *  1.2.0 (02/07/2020) - Added pushed, held, and released capability. Required renumbering the buttons. Now 1/2=Up/Down, 3/4=Double Up/Down, 5/6=Triple Up/Down
 *  1.2.1 (02/07/2020) - Added doubleTapped events and added doubleTap capability. Now users can use button 3/4 for double tap or the system "doubleTapped" events.
 *  1.3.0 (05/17/2020) - Added associations and inverted paddle options
 *  2.0.0f (08/13/2020) - Added S2 capability for Hubitat 2.2.3 and newer
 *  2.1.0  (08/20/2020) - Fixed some command version issues
 *  2.2.0 (08/29/2020) - Added number of button config to configure
 *  2.3.0 (02/13/2021) - Added Alternate Exclusion mode to preferences.
 *  2.3.1 (03/08/2022) - Added setIndicatorBehavior command. Now users can control LED indicator behavior through custom actions.
 *  2.4.0 (10/13/2026) - Patched version:
 *                       - Fixed group 3 association self-repair (commands were built but never sent)
 *                       - Fixed description-logging status message (was reading nonexistent txtEnable setting)
 *                       - Added push(), hold(), release() and doubleTap() commands required by the button capabilities
 *                       - Fixed "Held Down" event text (said "Up held")
 *                       - Lifecycle methods (installed/updated) now send their commands explicitly via sendHubCommand
 *                       - updated() no longer assigns to preference variables; uses local defaults instead
 *                       - configure() now also applies LED / inverted / alternate exclusion parameters and requests firmware version
 *                       - refresh() now queries the parameters this driver actually uses (3, 4, 19)
 *                       - ConfigurationReport now logs readable values and keeps preferences in sync with the device
 *                       - Digital/physical flag now auto-clears after 5 seconds so a missed report can't mislabel the next physical press
 *                       - Central Scene descriptionText no longer claims the switch turned on/off (that comes from the switch report)
 *                       - Removed SmartThings-only "displayed" event option and pointless result list in Central Scene handler
 *                       - Added fingerprints for Jasco in-wall switch product IDs 0x3135, 0x3137 and 0x3238 (device type 0x4952)
 *  2.4.1 (10/13/2026) - Corrected fingerprint comments to the actual models (per the Z-Wave JS device database):
 *                         0x3135 = GE/Enbrighten 46201 paddle, 0x3137 = GE/Enbrighten 46202 toggle, 0x3238 = UltraPro 39354/54912 toggle
 *                       - "Inverted" preference now notes it is only documented for the 46202 (parameter 4)
 *                       - Added setParameter command (parameter number, size in bytes, value) and getParameter command
 *                       - Configuration reports for parameters other than 3/4/19 are now logged at info level so results are visible
*/

import groovy.transform.Field

@Field static Map commandClassVersions =
    [
         0x20: 1    // Basic
        ,0x25: 1    // Switch Binary
        ,0x5B: 1    // Central Scene
        ,0x6C: 1    // Supervision
        ,0x70: 2    // Configuration
        ,0x72: 2    // Manufacturer Specific
        ,0x85: 2    // Association
        ,0x86: 3    // Version
    ]

@Field static Map ledOptions = [
    "0": "LED ON When Switch OFF",
    "1": "LED ON When Switch ON",
    "2": "LED Always OFF",
    "3": "LED Always ON"
]

metadata {
    definition (name: "GE Enbrighten Z-Wave Plus Switch", namespace: "Botched1", author: "Jason Bottjen") {
        capability "Actuator"
        capability "DoubleTapableButton"
        capability "HoldableButton"
        capability "PushableButton"
        capability "ReleasableButton"
        capability "Configuration"
        capability "Refresh"
        capability "Sensor"
        capability "Switch"
        capability "Light"

        command "setIndicatorBehavior", [[name: "LED Indicator Behavior", type: "ENUM", description: "", constraints: ["LED ON When Switch OFF", "LED ON When Switch ON", "LED Always OFF", "LED Always ON", "0", "1", "2", "3"]]]
        command "setParameter", [
            [name: "Parameter Number*", type: "NUMBER", description: "Configuration parameter number (1-255)"],
            [name: "Size (bytes)*", type: "ENUM", description: "Parameter length in bytes", constraints: ["1", "2", "4"]],
            [name: "Value*", type: "NUMBER", description: "Value to set (fits within the chosen size)"]
        ]
        command "getParameter", [
            [name: "Parameter Number*", type: "NUMBER", description: "Configuration parameter number (1-255); the result appears in the logs"]
        ]

        // Jasco in-wall on/off switches (manufacturer 0x0063, product type 0x4952), per the Z-Wave JS device database
        fingerprint mfr: "0063", prod: "4952", deviceId: "3135", deviceJoinName: "GE Enbrighten Z-Wave Plus Switch"   // GE/Enbrighten 46201 / ZW4008 paddle, 500 series
        fingerprint mfr: "0063", prod: "4952", deviceId: "3137", deviceJoinName: "GE Enbrighten Z-Wave Plus Switch"   // GE/Enbrighten 46202 / ZW4009 toggle, 500 series
        fingerprint mfr: "0063", prod: "4952", deviceId: "3238", deviceJoinName: "UltraPro Z-Wave Plus Switch"        // UltraPro 39354 / 54912 / ZW4009 toggle, 500 series
    }

    preferences {
        input name: "paramLED", type: "enum", title: "LED Indicator Behavior", multiple: false, options: ["0": "LED ON When Switch OFF (default)", "1": "LED ON When Switch ON", "2": "LED Always OFF", "3": "LED Always ON"], required: false, displayDuringSetup: true
        input name: "paramInverted", type: "enum", title: "Switch Buttons Direction (parameter 4; documented for GE 46202 only, likely ignored on UltraPro 39354)", multiple: false, options: ["0": "Normal (default)", "1": "Inverted"], required: false, displayDuringSetup: true
        input name: "paramAlternateExclusion", type: "enum", title: "Enable alternate exclusion (2-ON, 2-OFF). NOTE: Turning this ON can make device report status faster (firmware bug?)!", multiple: false, options: ["0": "OFF (default)", "1": "ON"], required: false, displayDuringSetup: true
        input name: "requestedGroup2", type: "text", title: "Association Group 2 Members (hex node IDs, comma separated, max of 5):", required: false
        input name: "requestedGroup3", type: "text", title: "Association Group 3 Members (hex node IDs, comma separated, max of 4):", required: false
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: false
        input name: "logDesc", type: "bool", title: "Enable descriptionText logging", defaultValue: true
    }
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Parse
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void parse(String description) {
    if (logEnable) log.debug "parse description: ${description}"
    hubitat.zwave.Command cmd = zwave.parse(description, commandClassVersions)
    if (cmd) {
        zwaveEvent(cmd)
    }
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
String secure(String cmd) {
    return zwaveSecureEncap(cmd)
}

String secure(hubitat.zwave.Command cmd) {
    return zwaveSecureEncap(cmd)
}

// Sends a list of formatted Z-Wave commands from places where a returned list is NOT
// automatically sent by the hub (event handlers, installed(), updated()).
void sendCommands(List<String> cmds, Integer delay = 250) {
    if (!cmds) return
    sendHubCommand(new hubitat.device.HubMultiAction(delayBetween(cmds, delay), hubitat.device.Protocol.ZWAVE))
}

private String settingOrDefault(String name, String defaultValue) {
    def value = settings[name]
    return (value == null || value.toString() == "") ? defaultValue : value.toString()
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Z-Wave Messages
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd) {
    hubitat.zwave.Command encapCmd = cmd.encapsulatedCommand(commandClassVersions)
    if (encapCmd) {
        zwaveEvent(encapCmd)
    }
    sendHubCommand(new hubitat.device.HubAction(secure(zwave.supervisionV1.supervisionReport(sessionID: cmd.sessionID, reserved: 0, moreStatusUpdates: false, status: 0xFF, duration: 0)), hubitat.device.Protocol.ZWAVE))
}

void zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) {
    if (logEnable) log.debug "---BASIC REPORT V1--- ${device.displayName} sent ${cmd} (ignored; switch state comes from Switch Binary reports)"
}

void zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) {
    if (logEnable) log.debug "---BASIC SET V1--- ${device.displayName} sent ${cmd} (ignored; taps come from Central Scene notifications)"
}

void zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) {
    if (logEnable) log.debug "---ASSOCIATION REPORT V2--- ${device.displayName} sent groupingIdentifier: ${cmd.groupingIdentifier} maxNodesSupported: ${cmd.maxNodesSupported} nodeId: ${cmd.nodeId} reportsToFollow: ${cmd.reportsToFollow}"
    if (cmd.groupingIdentifier == 3) {
        if (cmd.nodeId.contains(zwaveHubNodeId)) {
            sendEvent(name: "numberOfButtons", value: 6)
        } else {
            log.warn "${device.displayName}: hub is not in association group 3, re-adding it so button events work"
            sendEvent(name: "numberOfButtons", value: 0)
            sendCommands([
                secure(zwave.associationV2.associationSet(groupingIdentifier: 3, nodeId: zwaveHubNodeId).format()),
                secure(zwave.associationV2.associationGet(groupingIdentifier: 3).format())
            ])
        }
    }
}

void zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) {
    if (logEnable) log.debug "---CONFIGURATION REPORT V2--- ${device.displayName} sent ${cmd}"
    Integer value = cmd.scaledConfigurationValue
    switch (cmd.parameterNumber) {
        case 3:
            if (logDesc) log.info "${device.displayName} LED indicator: ${ledOptions[value.toString()] ?: value}"
            device.updateSetting("paramLED", [type: "enum", value: value.toString()])
            break
        case 4:
            if (logDesc) log.info "${device.displayName} paddle direction: ${value == 1 ? 'Inverted' : 'Normal'}"
            device.updateSetting("paramInverted", [type: "enum", value: value.toString()])
            break
        case 19:
            if (logDesc) log.info "${device.displayName} alternate exclusion: ${value == 1 ? 'ON' : 'OFF'}"
            device.updateSetting("paramAlternateExclusion", [type: "enum", value: value.toString()])
            break
        default:
            log.info "${device.displayName} parameter ${cmd.parameterNumber} (size ${cmd.size}) = ${value}"
    }
}

void zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
    if (logEnable) log.debug "---BINARY SWITCH REPORT V1--- ${device.displayName} sent ${cmd}"

    // If a command was sent from the hub in the last few seconds, treat this report as digital
    String newType = state.bin ? "digital" : "physical"
    state.bin = false
    unschedule("clearDigitalFlag")

    String newValue = cmd.value ? "on" : "off"
    String curValue = device.currentValue("switch")

    if (curValue != newValue) {
        String desc = "${device.displayName} was turned ${newValue} [${newType}]"
        if (logDesc) log.info desc
        sendEvent(name: "switch", value: newValue, descriptionText: desc, type: newType, isStateChange: true)
    }
}

void zwaveEvent(hubitat.zwave.commands.versionv3.VersionReport cmd) {
    String fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}"
    device.updateDataValue("firmwareVersion", fw)
    if (logEnable) log.debug "---VERSION REPORT V3--- ${device.displayName} is running firmware version: ${fw}, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}"
}

void zwaveEvent(hubitat.zwave.commands.hailv1.Hail cmd) {
    if (logEnable) log.debug "Hail command received (ignored)"
}

void zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) {
    if (logEnable) log.debug "Device Specific Report: ${cmd}"
    if (cmd.deviceIdType == 1) {
        String serialNumber = ""
        if (cmd.deviceIdDataFormat == 1) {
            cmd.deviceIdData.each { serialNumber += hubitat.helper.HexUtils.integerToHexString(it & 0xff, 1).padLeft(2, '0') }
        } else {
            cmd.deviceIdData.each { serialNumber += (char) it }
        }
        device.updateDataValue("serialNumber", serialNumber)
    }
}

void zwaveEvent(hubitat.zwave.commands.centralscenev1.CentralSceneNotification cmd) {
    if (logEnable) log.debug "CentralSceneNotification: sceneNumber ${cmd.sceneNumber}, keyAttributes ${cmd.keyAttributes}"

    Integer scene = cmd.sceneNumber as Integer   // 1 = Up/top, 2 = Down/bottom
    if (scene != 1 && scene != 2) return
    String dir = scene == 1 ? "Up" : "Down"

    switch (cmd.keyAttributes as Integer) {
        case 0: // single press
            buttonEvent("pushed", scene, "${dir} Pushed (button ${scene})", "physical")
            break
        case 1: // released after hold
            buttonEvent("released", scene, "${dir} Released (button ${scene})", "physical")
            break
        case 2: // held
            buttonEvent("held", scene, "${dir} Held (button ${scene})", "physical")
            break
        case 3: // double tap -> buttons 3 (up) / 4 (down)
            buttonEvent("pushed", scene + 2, "Doubletap ${dir.toLowerCase()} (button ${scene + 2})", "physical")
            buttonEvent("doubleTapped", scene, "Doubletap ${dir.toLowerCase()} (doubleTapped ${scene})", "physical", false)
            break
        case 4: // triple tap -> buttons 5 (up) / 6 (down)
            buttonEvent("pushed", scene + 4, "Tripletap ${dir.toLowerCase()} (button ${scene + 4})", "physical")
            break
        default:
            if (logEnable) log.debug "Unhandled keyAttributes ${cmd.keyAttributes} (e.g. 4x/5x tap)"
    }
}

private void buttonEvent(String name, Integer button, String what, String type, Boolean logIt = true) {
    String desc = "${device.displayName} had ${what} [${type}]"
    if (logIt && logDesc) log.info desc
    sendEvent(name: name, value: button, descriptionText: desc, type: type, isStateChange: true)
}

void zwaveEvent(hubitat.zwave.Command cmd) {
    log.warn "${device.displayName} received unhandled command: ${cmd}"
}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver Commands
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
def on() {
    if (logEnable) log.debug "Turn device ON"
    markDigital()
    return [secure(zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF).format())]
}

def off() {
    if (logEnable) log.debug "Turn device OFF"
    markDigital()
    return [secure(zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00).format())]
}

private void markDigital() {
    state.bin = true
    runIn(5, "clearDigitalFlag")
}

void clearDigitalFlag() {
    state.bin = false
}

// Button capability commands (virtual presses from the device page, dashboards or rules)
void push(button) {
    buttonEvent("pushed", button as Integer, "button ${button} pushed", "digital")
}

void hold(button) {
    buttonEvent("held", button as Integer, "button ${button} held", "digital")
}

void release(button) {
    buttonEvent("released", button as Integer, "button ${button} released", "digital")
}

void doubleTap(button) {
    buttonEvent("doubleTapped", button as Integer, "button ${button} double-tapped", "digital")
}

def refresh() {
    log.info "refresh() called"
    return delayBetween([
        secure(zwave.switchBinaryV1.switchBinaryGet().format()),
        secure(zwave.configurationV2.configurationGet(parameterNumber: 3).format()),
        secure(zwave.configurationV2.configurationGet(parameterNumber: 4).format()),
        secure(zwave.configurationV2.configurationGet(parameterNumber: 19).format()),
        secure(zwave.associationV2.associationGet(groupingIdentifier: 3).format())
    ], 250)
}

void installed() {
    log.info "installed..."
    sendCommands(configureCmds())
}

void updated() {
    log.info "updated..."
    log.warn "debug logging is: ${logEnable == true}"
    log.warn "description logging is: ${logDesc == true}"
    unschedule("logsOff")
    if (logEnable) runIn(1800, "logsOff")

    if (state.lastUpdated && now() <= state.lastUpdated + 3000) return
    state.lastUpdated = now()

    sendCommands(associationCmds() + parameterCmds())
}

def configure() {
    log.info "configure triggered"
    return delayBetween(configureCmds(), 250)
}

private List<String> configureCmds() {
    state.bin = false
    sendEvent(name: "numberOfButtons", value: 6)
    List<String> cmds = associationCmds() + parameterCmds()
    cmds << secure(zwave.versionV3.versionGet().format())
    cmds << secure(zwave.switchBinaryV1.switchBinaryGet().format())
    return cmds
}

private List<String> associationCmds() {
    List<String> cmds = []

    // Group 1: lifeline
    cmds << secure(zwave.associationV2.associationSet(groupingIdentifier: 1, nodeId: zwaveHubNodeId).format())

    // Group 2: local load (user-specified devices only)
    List nodes = parseAssocGroupList(settings.requestedGroup2, 2)
    cmds << secure(zwave.associationV2.associationRemove(groupingIdentifier: 2, nodeId: []).format())
    if (nodes) cmds << secure(zwave.associationV2.associationSet(groupingIdentifier: 2, nodeId: nodes).format())
    cmds << secure(zwave.associationV2.associationGet(groupingIdentifier: 2).format())
    state.currentGroup2 = settings.requestedGroup2

    // Group 3: double tap / scenes (always includes the hub)
    nodes = parseAssocGroupList(settings.requestedGroup3, 3)
    cmds << secure(zwave.associationV2.associationRemove(groupingIdentifier: 3, nodeId: []).format())
    cmds << secure(zwave.associationV2.associationSet(groupingIdentifier: 3, nodeId: nodes).format())
    cmds << secure(zwave.associationV2.associationGet(groupingIdentifier: 3).format())
    state.currentGroup3 = settings.requestedGroup3

    return cmds
}

private List<String> parameterCmds() {
    Integer led      = settingOrDefault("paramLED", "0").toInteger()
    Integer inverted = settingOrDefault("paramInverted", "0").toInteger()
    Integer altExcl  = settingOrDefault("paramAlternateExclusion", "0").toInteger()

    return [
        secure(zwave.configurationV2.configurationSet(scaledConfigurationValue: led, parameterNumber: 3, size: 1).format()),
        secure(zwave.configurationV2.configurationGet(parameterNumber: 3).format()),
        secure(zwave.configurationV2.configurationSet(scaledConfigurationValue: inverted, parameterNumber: 4, size: 1).format()),
        secure(zwave.configurationV2.configurationGet(parameterNumber: 4).format()),
        secure(zwave.configurationV2.configurationSet(scaledConfigurationValue: altExcl, parameterNumber: 19, size: 1).format()),
        secure(zwave.configurationV2.configurationGet(parameterNumber: 19).format())
    ]
}

def setIndicatorBehavior(param) {
    log.info "setting indicator behavior to ${param}"
    Integer paramValue
    switch (param) {
        case "LED ON When Switch OFF": paramValue = 0; break
        case "LED ON When Switch ON":  paramValue = 1; break
        case "LED Always OFF":         paramValue = 2; break
        case "LED Always ON":          paramValue = 3; break
        case "0": case "1": case "2": case "3":
            paramValue = param.toInteger()
            break
        default:
            log.warn "setIndicatorBehavior: unknown value '${param}', ignoring"
            return
    }
    device.updateSetting("paramLED", [type: "enum", value: paramValue.toString()])
    return delayBetween([
        secure(zwave.configurationV2.configurationSet(scaledConfigurationValue: paramValue, parameterNumber: 3, size: 1).format()),
        secure(zwave.configurationV2.configurationGet(parameterNumber: 3).format())
    ], 250)
}

// Set any configuration parameter by number, size (1, 2 or 4 bytes) and value.
// Values above the signed range for the size (e.g. 200 for 1 byte) are sent as the equivalent
// unsigned bit pattern, so both signed and unsigned values can be entered.
def setParameter(paramNum, size, value) {
    Integer p
    Integer s
    Long v
    try {
        p = new BigDecimal(paramNum.toString()).intValueExact()
        s = size.toString().toInteger()
        v = new BigDecimal(value.toString()).longValueExact()
    } catch (Exception e) {
        log.warn "setParameter: invalid input (parameter: ${paramNum}, size: ${size}, value: ${value}); use whole numbers"
        return
    }

    if (p < 1 || p > 255) {
        log.warn "setParameter: parameter number ${p} is out of range (1-255)"
        return
    }
    if (!(s in [1, 2, 4])) {
        log.warn "setParameter: size ${s} is invalid; must be 1, 2 or 4 bytes"
        return
    }

    Long signedMin   = -(1L << (8 * s - 1))
    Long signedMax   = (1L << (8 * s - 1)) - 1
    Long unsignedMax = (1L << (8 * s)) - 1
    if (v < signedMin || v > unsignedMax) {
        log.warn "setParameter: value ${v} does not fit in ${s} byte(s) (allowed ${signedMin} to ${unsignedMax})"
        return
    }
    // Convert unsigned values above the signed max into their two's-complement equivalent
    Integer scaled = (v > signedMax ? v - (1L << (8 * s)) : v) as Integer

    log.info "${device.displayName}: setting parameter ${p} (size ${s}) to ${v}"
    return delayBetween([
        secure(zwave.configurationV2.configurationSet(scaledConfigurationValue: scaled, parameterNumber: p, size: s).format()),
        secure(zwave.configurationV2.configurationGet(parameterNumber: p).format())
    ], 500)
}

def getParameter(paramNum) {
    Integer p
    try {
        p = new BigDecimal(paramNum.toString()).intValueExact()
    } catch (Exception e) {
        log.warn "getParameter: invalid parameter number '${paramNum}'"
        return
    }
    if (p < 1 || p > 255) {
        log.warn "getParameter: parameter number ${p} is out of range (1-255)"
        return
    }
    log.info "${device.displayName}: requesting parameter ${p}"
    return [secure(zwave.configurationV2.configurationGet(parameterNumber: p).format())]
}

private List parseAssocGroupList(list, group) {
    List nodes = group == 2 ? [] : [zwaveHubNodeId]
    if (list) {
        def nodeList = list.split(',')
        Integer max = group == 2 ? 5 : 4
        Integer count = 0

        nodeList.each { node ->
            node = node.trim()
            if (count >= max) {
                log.warn "Association Group ${group}: Too many members. Greater than ${max}! This one was discarded: ${node}"
            } else if (node.matches("\\p{XDigit}+")) {
                Integer nodeId = Integer.parseInt(node, 16)
                if (nodeId == zwaveHubNodeId) {
                    log.warn "Association Group ${group}: Adding the hub ID as an association is not allowed."
                } else if (nodeId > 0 && nodeId < 256) {
                    nodes << nodeId
                    count++
                } else {
                    log.warn "Association Group ${group}: Invalid member: ${node}"
                }
            } else {
                log.warn "Association Group ${group}: Invalid member: ${node}"
            }
        }
    }
    if (logEnable) log.debug "Group ${group} nodes: ${nodes}"
    return nodes
}

void logsOff() {
    log.warn "debug logging disabled..."
    device.updateSetting("logEnable", [value: "false", type: "bool"])
}