Zigbee Makegood MG-AUZG01 Double GPO Driver

Driver for Makegood MG-AUZG01 Double GPO with power reporting.

Based on the work of Richard Laxton: Zigbee reporting on multi-endpoint devices

Bought from eBay: Zigbee Double Power Point | eBay

Looks like:
image

Identifying manufacturer string: _TZ3000_dd8wwzcy

/*
 *  Copyright 2021 Richard Laxton, based on work by Steve White
 *  Modifications copyright 2023 Dan Murphy
 *
 *	Licensed under the Apache License, Version 2.0 (the "License"); you may not
 *	use this file except in compliance with the License. You may obtain a copy
 *	of the License at:
 *
 *		http://www.apache.org/licenses/LICENSE-2.0
 *
 *	Unless required by applicable law or agreed to in writing, software
 *	distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 *	WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 *	License for the specific language governing permissions and limitations
 *	under the License.
 */
metadata 
{
	// Automatically generated. Make future change here.
	definition(name: "Makegood MG-AUZG01 Double GPO", namespace: "community", author: "Dan Murphy")
	{
		capability "Actuator"
		capability "Switch"
		capability "PowerMeter"
		capability "Voltage Measurement"
        	capability "CurrentMeter"
		capability "Configuration"
		capability "Refresh"
		capability "Sensor"
		
		attribute "ACFrequency", "number"
		attribute "RMSCurrent", "number"
		
		command "toggle"
		command "identify"
		command "resetToFactoryDefault"
        
		fingerprint profileId: "0104", inClusters: "0004,0005,0006,0702,0B04,E001,0000", outClusters: "0004,0005,0006", manufacturer: "_TZ3000_dd8wwzcy", model: "Double Power Point", deviceJoinName: "Smart GPO"
	}


	preferences
	{
		section()
		{
        		input "intervalMin", "number", title: "Minimum interval (seconds) between power, current and voltage reports:", defaultValue: 5, range: "1..600", required: false
        		input "intervalMax", "number", title: "Maximum interval (seconds) between power, current and voltage reports:", defaultValue: 600, range: "1..600", required: false
       			input "minDeltaW", "enum", title: "Amount of power (W) change required to trigger a report:", defaultValue: "5", options: ["1", "5", "10", "20", "50", "100", "500", "1000"], required: false
       			input "minDeltaV", "enum", title: "Amount of voltage (V) change required to trigger a report:", defaultValue: "1", options: ["1", "2", "5", "10"], required: false
       			input "minDeltaA", "enum", title: "Amount of current (A) change required to trigger a report:", defaultValue: "0.1", options: ["0.1", "0.5", "1", "2", "3", "5"], required: false
		}
		section
		{
			input "enableTrace", "bool", title: "Enable trace logging?", description: "Show high-level activities during the operation of the device?", defaultValue: false, required: false, multiple: false
			input "enableDebug", "bool", title: "Enable debug logging?", description: "Show detailed responses to device commands?", defaultValue: false, required: false, multiple: false
		}
	}
}

/*
	installed
*/
def installed()
{
    logInfo "Installed"
	initialize()
}


/*
	Called when the preferences are saved
    
	Doesn't do much other than call configure().
*/
def updated()
{
    logInfo "Configuration saved"
	initialize()
}

/*
	initialize
    
	Doesn't do much other than call configure().
*/
def initialize()
{
    logInfo "Initialize"    
	state.lastSwitch = 0

	if (enableTrace || enableDebug)
	{
		logInfo "Verbose logging has been enabled for the next 30 minutes."
		runIn(1800, logsOff)
	}
    
    configure()
}


/*
	parse
    
	Processes incoming zigbee messages
*/
def parse(String description)
{
    logTrace "Msg: Description is ${description}"

	def event = zigbee.getEvent(description)
	def msg = zigbee.parseDescriptionAsMap(description)

	logTrace "Parsed data...  Evt: ${event.name}, msg: ${msg}"

	// Hubitat does not seem to support power events
	if (event)
	{
		if (event.name == "power")
		{
			def value = (event.value.parseInt()) / 10
			event = createEvent(name: event.name, value: value, descriptionText: "${device.displayName} power is ${value} watts")
			logTrace "${device.displayName} power is ${value} watts"
		}
		else if (event.name == "switch")
		{
			def descriptionText = event.value == "on" ? "${device.displayName} is On" : "${device.displayName} is Off"
			event = createEvent(name: event.name, value: event.value, descriptionText: descriptionText)
			
			// Since the switch has reported that it is off it can't be using any power.  Set to zero in case the power report does not arrive, but do not report in event logs.
			if (event.value == "off") sendEvent(name: "power", value: "0", descriptionText: "${device.displayName} power is 0 watts")
            
            // Pass the switch event to the appropriate child device. Endpoint is identified by different properties depending on whether switching is initiated from the child device 
            // or from a physical button press
            logDebug "Looking for device ${getChildNetworkId(msg.sourceEndpoint ?: msg.endpoint)}"
            def cd = getChildDevice(getChildNetworkId(msg.sourceEndpoint ?: msg.endpoint)) 
            if (cd)
            {
                logDebug("Passing event to child ${cd.displayName}")
                
                cd.parse([event]);
            }

			// DEVICE HEALTH MONITOR: Switch state (on/off) should report every 10 mins or so, regardless of any state changes.
			// Capture the time of this message
			state.lastSwitch = now()
		}
	}
	
	// Handle interval-based power reporting
	else if (msg?.cluster == "0B04")
	{
		
		// Volts
		if (msg.attrId == "0505")
		{
			def value = Integer.parseInt(msg.value, 16)
			event = createEvent(name: "voltage", value: value, descriptionText: "${device.displayName} voltage is ${value} volts")
			logTrace "${device.displayName} voltage is ${value} volts"
		}

		
		// Current and power are in additional attributes
		
		log.debug "msg = $msg"
		log.debug " in additionalAttrs"
		def additionalAttrs = msg?.additionalAttrs
		additionalAttrs.each
		{
		    log.debug "it=${it}"
		    log.debug "itVal=${it.value}"
		    log.debug "itAttrId=${it.attrId}"
			
			// Watts
		    if (it.attrId == "050B")
		    {
		    	def value = Integer.parseInt(it.value, 16)
		    	sendEvent(name: "power", value: value, descriptionText: "${device.displayName} power is ${value} watts")
				//event = createEvent(name: "power", value: value, descriptionText: "${device.displayName} power is ${value} watts")
				logTrace "${device.displayName} power is ${value} watts"
		    } 
			
			// RMS Current
			else if (it.attrId== "0508")
			{
				def value = Integer.parseInt(it.value, 16) / 1000        // Current is measured in mA for the outlet
				sendEvent(name: "amperage", value: value, descriptionText: "${device.displayName} RMS current is ${value} A")
				//event = createEvent(name: "amperage", value: value, descriptionText: "${device.displayName} RMS current is ${value} A")
				logTrace "${device.displayName} RMS current is ${value} A"
			}
		}
	}
    
	// Handle everything else
	else
	{
		def cluster = zigbee.parse(description)

		if (cluster?.clusterId == 0x0006 && (cluster.command == 0x07 || cluster.command == 0x0B))
		{
			if (cluster.data[0] == 0x00 || cluster.data[0] == 0x02)
			{
				// DEVICE HEALTH MONITOR: Switch state (on/off) should report every 10mins or so, regardless of any state changes.
				// Capture the time of this message
				state.lastSwitch = now()

				if (cluster.data[0] == 0x00) logDebug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
				if (cluster.data[0] == 0x02) logDebug "ON/OFF TOGGLE RESPONSE: " + cluster
			}
			else
			{
				logError "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
				event = null
			}
		}
		else if (cluster?.clusterId == 0x0B04 && cluster.command == 0x07)
		{
			if (cluster.data[0] == 0x00)
			{
				logDebug "POWER REPORTING CONFIG RESPONSE: " + cluster
			}
			else
			{
				logError "POWER REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
				event = null
			}
		}
		else if (cluster?.clusterId == 0x0003 && cluster.command == 0x04)
		{
			logInfo "LOCATING DEVICE FOR 30 SECONDS"
		}
        
        // Handle bind responses
        else if (cluster?.clusterId == 0x8021)
        {
            logInfo "Bind response"
        }

        // Handle general cluster events
        else if (cluster?.clusterId == 0x8001)
        {
            logInfo "General cluster event"
        }

        else
		{
			logWarn "DID NOT PARSE MESSAGE for description : $description"
			logDebug "${cluster}"
		}
	}
	return event ? createEvent(event) : event
}


private onOffClusterCommand(int destEndpoint, int command)
{
    logInfo("onOffCLusterCommand(${destEndpoint}, ${command})")
    if (destEndpoint < 0x01 || destEndpoint > 0x02)
    {
        logError("Unknown target endpoint ${destEndpoint}")
        return;
    }
    
    if (command < 0x00 || command > 0x02)
    {
        logError("Unknown command for onOffCluster")
        return;
    }

    // Send command to on/off cluster (0x0006) on the appropriate target
    return "he cmd 0x${device.deviceNetworkId} 0x${destEndpoint} 0x${zigbee.ON_OFF_CLUSTER} 0x${command} {}, delay 200"
}

/*
	on
    
	Turns the specific targetEndpoint of the device on .

	Uses standard Zigbee on/off cluster.
*/
def on(int destEndpoint)
{
    onOffClusterCommand(destEndpoint, 0x01);
}

/*
	off
    
	Turns the specific targetEndpoint of the device on .

	Uses standard Zigbee on/off cluster.
*/
def off(int destEndpoint)
{
    onOffClusterCommand(destEndpoint, 0x00);
}

/*
	toggle
    
	Turns the specific targetEndpoint of the device on .

	Uses standard Zigbee on/off cluster.
*/
def toggle(int destEndpoint)
{
    onOffClusterCommand(destEndpoint, 0x02);
}

/*
 * Turn on all endpoints
 */
def on()
{
    [on(0x01), on(0x02)]
}


/*
 * Turn off all endpoints
*/
def off()
{
    [off(0x01), off(0x02)]
}


/*
	Toggle all endpoints
*/
def toggle()
{
    [toggle(0x01), toggle(0x02)]
}

/*
	identify
    
	Flashes the blue LED on the plug to identify it.

*/
def identify()
{
	zigbee.writeAttribute(0x0003, 0x0000, DataType.UINT16, 0x00A, [:], 200)
}


/*
	resetToFactoryDefault
    
	Resets the plug to factory defaults but does not unpair the device.

*/
def resetToFactoryDefault()
{
	logWarn "Resetting device to factory defaults..."
	zigbee.command(0x0000, 0x00, [:], 200)
	
	runIn(1, configure)
}


/*
	refresh
    
	Refreshes the device by requesting manufacturer-specific information.

	Note: This is called from the refresh capbility
*/
def refresh()
{
    def cmd = []

    // Switch states
    for (endpoint = 1; endpoint <= 2; endpoint++)
    {
        cmd += ["he rattr 0xB77D ${endpoint} 6 0 {}", "delay 1000"]
    }

    cmd += zigbee.electricMeasurementPowerRefresh(1000)         // Watts
    cmd += zigbee.readAttribute(0x0B04, 0x0505, [:], 1000)      // Volts
    cmd += zigbee.readAttribute(0x0B04, 0x0508, [:], 1000)      // Amps
        
    return cmd
}

def getDestinationEndpoint(childDevice)
{
    def epId = childDevice.deviceNetworkId.split('-')[1]
    return Integer.parseInt(epId, 16)
}

def getChildNetworkId(destinationEndpoint)
{
    def destEndpoint = (destinationEndpoint instanceof Integer) ? 
            destinationEndpoint :
            Integer.parseInt(destinationEndpoint, 16)
    
    return "${device.deviceNetworkId}-${destEndpoint}"
}

def getChildLabel(destinationEndpoint)
{
    switch (destinationEndpoint)
    {
        case 2: 
            return "Left socket"
            break;
         case 1:
            return "Right socket"
    }
}

def componentOn(child)
{
    logDebug("componentOn(${child.deviceNetworkId}")
    sendHubCommand(new hubitat.device.HubAction(on(getDestinationEndpoint(child)), hubitat.device.Protocol.ZIGBEE))
}

def componentOff(child)
{
    logDebug("componentOff(${child.deviceNetworkId})")
    sendHubCommand(new hubitat.device.HubAction(off(getDestinationEndpoint(child)), hubitat.device.Protocol.ZIGBEE))
}

def componentRefresh(child)
{
    logDebug("componentRefresh(${child.deviceNetworkId})")
    refresh()
}


def deleteChildDevices()
{
    for (child in getChildDevices())
    {
        deleteChildDevice(child.deviceNetworkId);
    }
}

def createChildDevices()
{
    for (endpoint = 1; endpoint <= 2; endpoint++)
    {
        def childDeviceNetworkId = getChildNetworkId(endpoint)
        def childLabel = getChildLabel(endpoint)
        def cd = getChildDevice(childDeviceNetworkId)
        if (!cd)
        {
            cd = addChildDevice("hubitat", "Generic Component Switch", childDeviceNetworkId, [label: childLabel, isComponent: true])
        }
        else
        {
            // Update the names of the child device in case the device labelling rules changed
            cd.name = "Generic Component Switch"
            cd.label = childLabel
        }
    }
}
                                               
                                               
/*
	configure
    
	Configures the device, creates children and sets up any reporting
*/
def configure()
{
	logDebug "Configure called..."
    
    createChildDevices()
    
    def cmd = []
    
	// On/Off reporting of 0 seconds, maximum of 15 minutes if the device does not report any on/off activity
    for (endpoint = 1; endpoint <= 2; endpoint++)
    {
        cmd += ["zdo bind ${device.deviceNetworkId} ${endpoint} 0x01 0x0006 {${device.zigbeeId}} {}", 
                "delay 1000", 
                "he cr 0x${device.deviceNetworkId} ${endpoint} 6 0 16 0 900 {}", 
                "delay 1000"]
    }
    cmd += powerConfig()
    cmd += refresh()
    
    return cmd
}


/*
	powerConfig
    
	Set power reporting configuration for devices with min reporting interval as 5 seconds and reporting interval if no activity as 10min (600s),
	if not otherwise specified.
*/
def powerConfig()
{
	// Calculate threshold
	int powerDelta = (int) (Float.parseFloat(minDeltaW ?: "1") * 10)           // Power is in tenths of a watt
	int currentDelta = (int) (Float.parseFloat(minDeltaA ?: "0.1") * 1000)     // Current is in mA
    int voltageDelta = (int) (Float.parseFloat(minDeltaV ?: "10")  * 10)       // Voltage is in tenths of a volt
    
    def cfg = []

    // All reporting shares the same interval between 5 seconds and 3 minutes by default, override in settings
	cfg +=	zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x050B, DataType.INT16, (int) (intervalMin ?: 5), (int) (intervalMax ?: 600), powerDelta, [:], 1000)	    // Wattage report
    
    // Voltage report trigger level does not seem to work so set minimum interval to 120 seconds
	cfg +=	zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x0505, DataType.UINT16, 120, (int) (intervalMax ?: 600), voltageDelta, [:], 1000)    // Voltage report
    cfg +=  zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x0508,  DataType.UINT16, (int) (intervalMin ?: 5), (int) (intervalMax ?: 600), currentDelta, [:], 1000);  // RMS Current

	return cfg
}


/*
	getEndpointId
    
	Helper function to get device endpoint (hex) as a String.
*/
private getEndpointId()
{
	new BigInteger(device.endpointId, 16).toString()
}


/*
	logError
    
	Displays dewarningbug output to IDE logs based on user preference.
*/
private logError(msgOut)
{
	log.error msgOut
}


/*
	logWarn
    
	Displays dewarningbug output to IDE logs based on user preference.
*/
private logWarn(msgOut)
{
		log.warn msgOut
}


/*
	logDebug
    
	Displays debug output to IDE logs based on user preference.
*/
private logDebug(msgOut)
{
	if (settings.enableDebug)
	{
		log.debug msgOut
	}
}


/*
	logTrace
    
	Displays trace output to IDE logs based on user preference.
*/
private logTrace(msgOut)
{
	if (settings.enableTrace)
	{
		log.trace msgOut
	}
}


/*
	logInfo
    
	Displays informational output to IDE logs.
*/
private logInfo(msgOut)
{
	log.info msgOut
}


/*
	logsOff
    
	Disables debug logging.
*/
def logsOff()
{
    logWarn "debug logging disabled..."
    device.updateSetting("enableTrace", [value:"false",type:"bool"])
	device.updateSetting("enableDebug", [value:"false",type:"bool"])
}
2 Likes

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.