[RELEASE] Inovelli Device Drivers

I need to put comments around the whole "section" or it throws errors and will not save.

I tried //log.debug

If there aren't braces around anything (in which case you could be destroying structure), I'm not sure why. But another thing to consider is that if you haven't updated from GitHub recently, the LZW31-SN driver was recently updated to limit verbosity, including an option to turn these off. I'm not sure if the LZW30-SN or the non-SN (Black series) models got similar treatment.

@rjterry21:
The NZW37 version that can disable debugging is posted a bit up in this thread already... Post #85 if I am reading the numbers correctly. I do not have one made for the NZW97. Give me a little bit. :slight_smile:

Here is a NZW97 driver with the ability to disable the debug logging (and it is disabled by default). Like with the NZW37 I provided before, no support on this and no claims about how it works overall since all I did was add a preference for debugging and put every log.debug in an if statement for that preference. With that disclaimer... Here you go:

/**
 *
 * NOTE from David Snell: I am in no way associated with Inovelli. This was provided ONLY so that people could turn off debug logging.
 *  This was accomplished by adding a DebugOn preference that is used by an if( DebugOn){ ... } at every referenced log.debug
 *  No other code was modified by me, although I had to change namespace and author obviously so there would be no confusion with the
 *  "real" driver from Inovelli.
 *
 *  Inovelli 2-Channel Outdoor Smart Plug NZW97
 *  Author: Eric Maycock (erocm123)
 *  Original Publish Date: 2018-05-02
 *
 *  Copyright 2018 Eric Maycock
 *
 *  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.
 *
 *  2019-11-26: Adding ability to disable debug statements - David Snell
 * Inovelli USA Provided Revisions:
 *  2018-05-02: Added support for Z-Wave Association Tool SmartApp. Associations require firmware 1.02+.
 *              https://github.com/erocm123/SmartThingsPublic/tree/master/smartapps/erocm123/z-waveat
 */
 
metadata {
    definition(
        name: "Inovelli 2-Channel Outdoor Smart Plug NZW97", 
        namespace: "InovelliUSA", 
        author: "David Snell",
        importUrl: "https://raw.githubusercontent.com/InovelliUSA/Hubitat/master/Drivers/inovelli-2-channel-outdoor-smart-plug-nzw97.src/inovelli-2-channel-outdoor-smart-plug-nzw97.groovy"
    ) {
        capability "Actuator"
        capability "Sensor"
        capability "Switch"
        capability "Polling"
        capability "Refresh"
        capability "Health Check"
        capability "PushableButton"
        capability "Configuration"
        
        attribute "lastActivity", "String"
        attribute "lastEvent", "String"
        
        command "setAssociationGroup", ["number", "enum", "number", "number"] // group number, nodes, action (0 - remove, 1 - add), multi-channel endpoint (optional)
        command "childOn"
        command "childOff"
        command "childRefresh"

        fingerprint manufacturer: "015D", prod: "6100", model: "6100", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug"
        fingerprint manufacturer: "0312", prod: "6100", model: "6100", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug"
        fingerprint manufacturer: "015D", prod: "0221", model: "611C", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug"
        fingerprint manufacturer: "0312", prod: "0221", model: "611C", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug"
        fingerprint deviceId: "0x1101", inClusters: "0x5E,0x25,0x27,0x85,0x8E,0x59,0x55,0x86,0x72,0x5A,0x73,0x70,0x71,0x60,0x6C,0x7A"
    }
    
    simulator {}
    
    preferences {
        input "autoOff1", "number", title: "Auto Off Channel 1\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767"
        input "autoOff2", "number", title: "Auto Off Channel 2\n\nAutomatically turn switch off after this number of seconds\nRange: 0 to 32767", description: "Tap to set", required: false, range: "0..32767"
        input "ledIndicator", "enum", title: "LED Indicator\n\nTurn LED indicator on when switch is:\n", description: "Tap to set", required: false, options:[["0": "On"], ["1": "Off"], ["2": "Disable"]], defaultValue: "0"
        input description: "Use the \"Z-Wave Association Tool\" SmartApp to set device associations. (Firmware 1.02+)\n\nGroup 2: Sends on/off commands to associated devices when switch is pressed (BASIC_SET).", title: "Associations", displayDuringSetup: false, type: "paragraph", element: "paragraph"
        input "DebugOn", "bool", title: "Enable debug logging", required: false, default: false
    }
    
    tiles {
        multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) {
            tileAttribute("device.switch", key: "PRIMARY_CONTROL") {
                attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn"
                attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc", nextState: "turningOff"
                attributeState "turningOff", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn"
                attributeState "turningOn", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc", nextState: "turningOff"
            }
        }
        
        standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
            state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh"
        }
        
        valueTile("lastActivity", "device.lastActivity", inactiveLabel: false, decoration: "flat", width: 4, height: 1) {
            state "default", label: 'Last Activity: ${currentValue}',icon: "st.Health & Wellness.health9"
        }
        
        valueTile("icon", "device.icon", inactiveLabel: false, decoration: "flat", width: 4, height: 1) {
            state "default", label: '', icon: "https://inovelli.com/wp-content/uploads/Device-Handler/Inovelli-Device-Handler-Logo.png"
        }
    }
}
def parse(String description) {
    def result = []
    def cmd = zwave.parse(description)
    if (cmd) {
        result += zwaveEvent(cmd)
        if( DebugOn ){ log.debug "Parsed ${cmd} to ${result.inspect()}" }
    } else {
        if( DebugOn ){ log.debug "Non-parsed event: ${description}" }
    }
    
    def now
    if(location.timeZone)
    now = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
    else
    now = new Date().format("yyyy MMM dd EEE h:mm:ss a")
    sendEvent(name: "lastActivity", value: now, displayed:false)
    
    return result
}

def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd, ep = null) {
    if( DebugOn ){ log.debug "BasicReport ${cmd} - ep ${ep}" }
    if (ep) {
        def event
        childDevices.each {
            childDevice ->
                if (childDevice.deviceNetworkId == "$device.deviceNetworkId-ep$ep") {
                    childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off")
                }
        }
        if (cmd.value) {
            event = [createEvent([name: "switch", value: "on"])]
        } else {
            def allOff = true
            childDevices.each {
                n ->
                    if (n.currentState("switch").value != "off") allOff = false
            }
            if (allOff) {
                event = [createEvent([name: "switch", value: "off"])]
            } else {
                event = [createEvent([name: "switch", value: "on"])]
            }
        }
        return event
    }
}

def zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep = null) {
    if( DebugOn ){ log.debug "SwitchBinaryReport ${cmd} - ep ${ep}" }
    if (ep) {
        def event
        def childDevice = childDevices.find {
            it.deviceNetworkId == "$device.deviceNetworkId-ep$ep"
        }
        if (childDevice) childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off")
        if (cmd.value) {
            event = [createEvent([name: "switch", value: "on"])]
        } else {
            def allOff = true
            childDevices.each {
                n->
                    if (n.deviceNetworkId != "$device.deviceNetworkId-ep$ep" && n.currentState("switch").value != "off") allOff = false
            }
            if (allOff) {
                event = [createEvent([name: "switch", value: "off"])]
            } else {
                event = [createEvent([name: "switch", value: "on"])]
            }
        }
        return event
    } else {
        def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital")
        def cmds = []
        cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1)
        cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2)
        return [result, response(commands(cmds))] // returns the result of reponse()
    }
}

def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
    if( DebugOn ){ log.debug "MultiChannelCmdEncap ${cmd}" }
    def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1])
    if (encapsulatedCommand) {
        zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer)
    }
}

def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
    if( DebugOn ){ log.debug "ManufacturerSpecificReport ${cmd}" }
    def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
    if( DebugOn ){ log.debug "msr: $msr" }
    updateDataValue("MSR", msr)
}

def zwaveEvent(hubitat.zwave.Command cmd) {
    // This will capture any commands not handled by other instances of zwaveEvent
    // and is recommended for development so you can see every command the device sends
    if( DebugOn ){ log.debug "Unhandled Event: ${cmd}" }
}

def on() {
    if( DebugOn ){ log.debug "on()" }
    commands([
            zwave.switchAllV1.switchAllOn(),
            encap(zwave.switchBinaryV1.switchBinaryGet(), 1),
            encap(zwave.switchBinaryV1.switchBinaryGet(), 2)
    ])
}

def off() {
    if( DebugOn ){ log.debug "off()" }
    commands([
            zwave.switchAllV1.switchAllOff(),
            encap(zwave.switchBinaryV1.switchBinaryGet(), 1),
            encap(zwave.switchBinaryV1.switchBinaryGet(), 2)
    ])
}

def childOn(String dni) {
    if( DebugOn ){ log.debug "childOn($dni)" }
    def cmds = []
    cmds << new hubitat.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0xFF), channelNumber(dni))), hubitat.device.Protocol.ZWAVE)
    cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni))), hubitat.device.Protocol.ZWAVE)
    cmds
}

def childOff(String dni) {
    if( DebugOn ){ log.debug "childOff($dni)" }
    def cmds = []
    cmds << new hubitat.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0x00), channelNumber(dni))), hubitat.device.Protocol.ZWAVE)
    cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni))), hubitat.device.Protocol.ZWAVE)
    cmds
}

def childRefresh(String dni) {
    if( DebugOn ){ log.debug "childRefresh($dni)" }
    def cmds = []
    cmds << new hubitat.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni))), hubitat.device.Protocol.ZWAVE)
    cmds
}

def poll() {
    if( DebugOn ){ log.debug "poll()" }
    commands([
            encap(zwave.switchBinaryV1.switchBinaryGet(), 1),
            encap(zwave.switchBinaryV1.switchBinaryGet(), 2),
    ])
}

def refresh() {
    if( DebugOn ){ log.debug "refresh()" }
    commands([
            encap(zwave.switchBinaryV1.switchBinaryGet(), 1),
            encap(zwave.switchBinaryV1.switchBinaryGet(), 2),
    ])
}

def ping() {
    if( DebugOn ){ log.debug "ping()" }
    refresh()
}

def installed() {
    refresh()
}

def configure() {
    if( DebugOn ){ log.debug "configure()" }
    def cmds = initialize()
    commands(cmds)
}

def integer2Cmd(value, size) {
    try{
	switch(size) {
	case 1:
		[value]
    break
	case 2:
    	def short value1   = value & 0xFF
        def short value2 = (value >> 8) & 0xFF
        [value2, value1]
    break
    case 3:
    	def short value1   = value & 0xFF
        def short value2 = (value >> 8) & 0xFF
        def short value3 = (value >> 16) & 0xFF
        [value3, value2, value1]
    break
	case 4:
    	def short value1 = value & 0xFF
        def short value2 = (value >> 8) & 0xFF
        def short value3 = (value >> 16) & 0xFF
        def short value4 = (value >> 24) & 0xFF
		[value4, value3, value2, value1]
	break
	}
    } catch (e) {
        if( DebugOn ){ log.debug "Error: integer2Cmd $e Value: $value" }
    }
}

def updated() {
    if (!state.lastRan || now() >= state.lastRan + 2000) {
        if( DebugOn ){ log.debug "updated()" }
        state.lastRan = now()
        def cmds = initialize()
        commands(cmds)
    } else {
        if( DebugOn ){ log.debug "updated() ran within the last 2 seconds. Skipping execution." }
    }
}

def initialize() {
    if( DebugOn ){ log.debug "initialize()" }
    if (!childDevices) {
        createChildDevices()
    } else if (device.label != state.oldLabel) {
        childDevices.each {
            if (it.label == "${state.oldLabel} (CH${channelNumber(it.deviceNetworkId)})") {
                def newLabel = "${device.displayName} (CH${channelNumber(it.deviceNetworkId)})"
                it.setLabel(newLabel)
            }
        }
        state.oldLabel = device.label
    }
    sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])
    sendEvent(name: "numberOfButtons", value: 1, displayed: true)
    def cmds = processAssociations()
    cmds << zwave.configurationV1.configurationSet(scaledConfigurationValue: ledIndicator!=null? ledIndicator.toInteger() : 0, parameterNumber: 1, size: 1)
    cmds << zwave.configurationV1.configurationGet(parameterNumber: 1)
    cmds << zwave.configurationV1.configurationSet(configurationValue: autoOff1!=null? integer2Cmd(autoOff1.toInteger(), 2) : integer2Cmd(0,2), parameterNumber: 2, size: 2)
    cmds << zwave.configurationV1.configurationGet(parameterNumber: 2)
    cmds << zwave.configurationV1.configurationSet(configurationValue: autoOff2!=null? integer2Cmd(autoOff2.toInteger(), 2) : integer2Cmd(0,2), parameterNumber: 3, size: 2)
    cmds << zwave.configurationV1.configurationGet(parameterNumber: 3)
    return cmds
}

def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) {
    if( DebugOn ){ log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" }
}

private encap(cmd, endpoint) {
    if (endpoint) {
        zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd)
    } else {
        cmd
    }
}

private command(hubitat.zwave.Command cmd) {
    if (state.sec) {
        zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
    } else {
        cmd.format()
    }
}

private commands(commands, delay = 1000) {
    delayBetween(commands.collect {
        command(it)
    }, delay)
}

private channelNumber(String dni) {
    dni.split("-ep")[-1] as Integer
}
private void createChildDevices() {
    state.oldLabel = device.label
    for (i in 1..2) {
        addChildDevice("Switch Child Device", "${device.deviceNetworkId}-ep${i}", [completedSetup: true, label: "${device.displayName} (CH${i})",
            isComponent: false, componentName: "ep$i", componentLabel: "Channel $i"
        ])
    }
}

def setDefaultAssociations() {
    def smartThingsHubID = zwaveHubNodeId.toString().format( '%02x', zwaveHubNodeId )
    state.defaultG1 = [smartThingsHubID]
    state.defaultG2 = [smartThingsHubID]
    state.defaultG3 = []
}

def setAssociationGroup(group, nodes, action, endpoint = null){
    if (!state."desiredAssociation${group}") {
        state."desiredAssociation${group}" = nodes
    } else {
        switch (action) {
            case 0:
                state."desiredAssociation${group}" = state."desiredAssociation${group}" - nodes
            break
            case 1:
                state."desiredAssociation${group}" = state."desiredAssociation${group}" + nodes
            break
        }
    }
}

def processAssociations(){
   def cmds = []
   setDefaultAssociations()
   def associationGroups = 5
   if (state.associationGroups) {
       associationGroups = state.associationGroups
   } else {
       if( DebugOn ){ log.debug "Getting supported association groups from device" }
       cmds <<  zwave.associationV2.associationGroupingsGet()
   }
   for (int i = 1; i <= associationGroups; i++){
      if(state."actualAssociation${i}" != null){
         if(state."desiredAssociation${i}" != null || state."defaultG${i}") {
            def refreshGroup = false
            ((state."desiredAssociation${i}"? state."desiredAssociation${i}" : [] + state."defaultG${i}") - state."actualAssociation${i}").each {
                if( DebugOn ){ log.debug "Adding node $it to group $i" }
                cmds << zwave.associationV2.associationSet(groupingIdentifier:i, nodeId:Integer.parseInt(it,16))
                refreshGroup = true
            }
            ((state."actualAssociation${i}" - state."defaultG${i}") - state."desiredAssociation${i}").each {
                if( DebugOn ){ log.debug "Removing node $it from group $i" }
                cmds << zwave.associationV2.associationRemove(groupingIdentifier:i, nodeId:Integer.parseInt(it,16))
                refreshGroup = true
            }
            if (refreshGroup == true) cmds << zwave.associationV2.associationGet(groupingIdentifier:i)
            else if( DebugOn ){ log.debug "There are no association actions to complete for group $i" }
         }
      } else {
         if( DebugOn ){ log.debug "Association info not known for group $i. Requesting info from device." }
         cmds << zwave.associationV2.associationGet(groupingIdentifier:i)
      }
   }
   return cmds
}

void zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) {
    def temp = []
    if (cmd.nodeId != []) {
       cmd.nodeId.each {
          temp += it.toString().format( '%02x', it.toInteger() ).toUpperCase()
       }
    } 
    state."actualAssociation${cmd.groupingIdentifier}" = temp
    if( DebugOn ){ log.debug "Associations for Group ${cmd.groupingIdentifier}: ${temp}" }
    updateDataValue("associationGroup${cmd.groupingIdentifier}", "$temp")
}

def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationGroupingsReport cmd) {
    if( DebugOn ){ log.debug "Supported association groups: ${cmd.supportedGroupings}" }
    state.associationGroups = cmd.supportedGroupings
    createEvent(name: "groups", value: cmd.supportedGroupings)
}

void zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) {
    if( DebugOn ){ log.debug cmd }
    if(cmd.applicationVersion && cmd.applicationSubVersion) {
	    def firmware = "${cmd.applicationVersion}.${cmd.applicationSubVersion.toString().padLeft(2,'0')}"
        state.needfwUpdate = "false"
        sendEvent(name: "status", value: "fw: ${firmware}")
        updateDataValue("firmware", firmware)
    }
}
1 Like

Thanks!

:+1:

I couldn't find the dancing banana emoji.

1 Like

Glad to help. I am a fan of the Inovelli devices I have so a little bit to help is worth it.

Silly question, maybe, but I'm new to this stuff (switching from X-10!) I installed the driver for my Black dimmer switch and I see a number of items on my Hubitat device listing now with some explanations and the default value. But I see no way to set them. There are a few buttons that I can set like switch direction, auxiliary switch, etc. But for example, there is an explanation of maximum dim level (or minimum) but no way to change it (except via the hardware config button on the device). Am I missing something?

Can you post a picture of what your seeing?

Good idea. You see here the listings I was talking about with the default values. On the bottom left, you see a piece of the "Invert switch" which actually has a pulldown where you can choose true or false. None of the others things you see here, for example "Ramp Rate" seems to have any way to actually set that value. There are some other values that you CAN set at the bottom, for example "LED Strip Intensity" and "AC Type". I hope this is clear, and thanks for looking...

What browser are you using? I am able to select any of those options and type in a value.

Should I call myself dumb, or Inovelli support brilliant? How about both?

I am using FireFox, but after your reply I looked again and discovered that what I thought was a separator line was actually the bottom shadow of a text box. So I found it and it works. I wanted to set the maximum brightness to 90--much easier than tapping the paddle 90 times :slight_smile: Thank you.

I am not a web designer (I am a software guy though), but let me make a suggestion: If you have a choice, getting the explanatory text NOT to turn the cursor into a text cursor so the cursor would change only over the box where we should write stuff in, that would make it more clear, especially to newbies like me.

Thanks again for solving my mystery.

1 Like

Any ideas when these might get baked into the official drivers?

This is in regards to the older style Inovelli dimmers.

2.1.8, they are complete, just didn't make 2.1.7

6 Likes

Fantastic! Thanks

1 Like

Yea!!!

Will all the Inovelli drivers be baked in, including the older plug modules, NZW97/96, NZW37, etc?

Forgive me EXTREME ignorance. I installed the code for the "Switch Child Device", what is all the SRC files and stuff that seems to be in this thread? anything I need to do. I understand that updates have to be done by hand, is there anything other then putting the new code in , in the future that I will need to do?

I have a question. I understand Inovelli switch can:

  1. Work with a Smart Bulb disabling the relay, so it will be always delivering power to the bulb.
  2. Work with a Dumb Switch on a 3-way setup

But I can't do both right? I mean, if someone turn off the dumb switch it will cut power to the smart bulb, right?

And if I use this with a Hue Smart Bulb, would it be useful to get the dimmer switch or just the on/off switch?

If you're not dimming, you might as well get the switch and not the dimmer. Then you won't have to worry about the smart bulbs accidentally getting "dimmed" (or them not liking a dimmer even at 100%). The only downside is that I wish the switch also had a large LED notification bar.

For the bulbs, you could make that work if they're on different circuits. Obviously you can't cut power to the dumb bulbs but not the smart bulbs if they are. If they aren't, it would be easy to create an automation on the hub that turned on/off one set of lights with the other.

The other down side, in my understanding, is that the switches require a neutral wire, while the dimmers do not.

just fyi, there's a lot of issues with the inovelli and 3-way dumb switches. if you check their support community, you'll see a lot of people who can't get the dimmers to work with a 3-way dumb switch, and the Inovelli team doesn't have a solution yet.

Inovelli is working very closely with them (note -- I'm one of them) in trying to find a solution, but so far, there's a lot of trying and not a lot of definitive answers.