Physical Events Not Logging Consistently

Yeah I may create a ticket, or @mike.maxwell might have some input. I would like to avoid having to re-pair all those devices, because then I have to add the device back to all of my automation.

This was the custom driver that I used that does show the hail commands. I mainly used this driver in the past to configure the dimmer's settings, since it made it much easier then doing it at the switch itself.

/**
 *  Copyright 2017 Jason Xia
 *
 *  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 {
    definition (name: "Leviton Decora Z-Wave Plus Dimmer", namespace: "jasonxh", author: "Jason Xia", ocfDeviceType: "oic.d.light") {
        capability "Actuator"
        capability "Configuration"
        capability "Health Check"
        capability "Indicator"
        capability "Light"
        capability "Polling"
        capability "Refresh"
        capability "Sensor"
        capability "Switch"
        capability "Switch Level"

        attribute "loadType", "enum", ["incandescent", "led", "cfl"]
        attribute "presetLevel", "number"
        attribute "minLevel", "number"
        attribute "maxLevel", "number"
        attribute "fadeOnTime", "number"
        attribute "fadeOffTime", "number"
        attribute "levelIndicatorTimeout", "number"
		attribute "firmwareVersion", "string"

        command "low"
        command "medium"
        command "high"
        command "levelUp"
        command "levelDown"

        fingerprint mfr:"001D", prod:"3201", model:"0001", deviceJoinName: "Leviton Decora Z-Wave Plus 600W Dimmer"
        fingerprint mfr:"001D", prod:"3301", model:"0001", deviceJoinName: "Leviton Decora Z-Wave Plus 1000W Dimmer"
    }

    preferences {
        input name: "levelIncrement", type: "number", title: "In-App Level Increment",
                description: "1 - 100 (default $defaultLevelIncrement)", range: "1..100", defaultValue: defaultLevelIncrement,
                displayDuringSetup: false, required: false

        input type: "paragraph", element: "paragraph", title: "Device Preferences",
                description: "The following preferences are configuring the device behaviors. " +
                        "All of them are optional. Leave a preference empty to skip configuring it."

        input name: "loadType", type: "enum", title: "Load type",
                options: ["Incandescent (default)", "LED", "CFL"],
                displayDuringSetup: false, required: false
        input name: "indicatorStatus", type: "enum", title: "Indicator LED is lit",
                options: ["When switch is off (default)", "When switch is on", "Never"],
                displayDuringSetup: false, required: false
        input name: "presetLevel", type: "number", title: "Light turns on to level",
                description: "0 to 100 (default 0)", range: "0..100",
                displayDuringSetup: false, required: false
        input type: "paragraph", element: "paragraph", title: "",
                description: "0 = last dim level (default)\n1 - 100 = fixed level"
        input name: "minLevel", type: "number", title: "Minimum light level",
                description: "0 to 100 (default 10)", range: "0..100",
                displayDuringSetup: false, required: false
        input name: "maxLevel", type: "number", title: "Maximum light level",
                description: "0 to 100 (default 100)", range: "0..100",
                displayDuringSetup: false, required: false
        input name: "fadeOnTime", type: "number", title: "Fade-on time",
                description: "0 to 253 (default 2)", range: "0..253",
                displayDuringSetup: false, required: false
        input type: "paragraph", element: "paragraph", title: "",
                description: "0 = instant on\n1 - 127 = 1 - 127 seconds (default 2)\n128 - 253 = 1 - 126 minutes"
        input name: "fadeOffTime", type: "number", title: "Fade-off time",
                description: "0 to 253 (default 2)", range: "0..253",
                displayDuringSetup: false, required: false
        input type: "paragraph", element: "paragraph", title: "",
                description: "0 = instant off\n1 - 127 = 1 - 127 seconds (default 2)\n128 - 253 = 1 - 126 minutes"
        input name: "levelIndicatorTimeout", type: "number", title: "Dim level indicator timeout",
                description: "0 to 255 (default 3)", range: "0..255",
                displayDuringSetup: false, required: false
        input type: "paragraph", element: "paragraph", title: "",
                description: "0 = dim level indicator off\n1 - 254 = timeout in seconds (default 3)\n255 = dim level indicator always on"
    }
}

def installed() {
    log.debug "installed..."
    initialize()
    response(refresh())
}

def updated() {
    if (state.lastUpdatedAt != null && state.lastUpdatedAt >= now() - 1000) {
        log.debug "ignoring double updated"
        return
    }
    log.debug "updated..."
    state.lastUpdatedAt = now()

    initialize()
    response(configure())
}

def configure() {
    def commands = []
    if (loadType != null) {
        commands.addAll(setLoadType(loadType))
    }
    if (indicatorStatus != null) {
        commands.addAll(setIndicatorStatus(indicatorStatus))
    }
    if (presetLevel != null) {
        commands.addAll(setPresetLevel(presetLevel as short))
    }
    if (minLevel != null) {
        commands.addAll(setMinLevel(minLevel as short))
    }
    if (maxLevel != null) {
        commands.addAll(setMaxLevel(maxLevel as short))
    }
    if (fadeOnTime != null) {
        commands.addAll(setFadeOnTime(fadeOnTime as short))
    }
    if (fadeOffTime != null) {
        commands.addAll(setFadeOffTime(fadeOffTime as short))
    }
    if (levelIndicatorTimeout != null) {
        commands.addAll(setLevelIndicatorTimeout(levelIndicatorTimeout as short))
    }
    log.debug "Configuring with commands $commands"
    commands
}

def parse(String description) {
    def result = null
    def cmd = zwave.parse(description, [0x20: 1, 0x25:1, 0x26: 1, 0x70: 1, 0x72: 2])
    if (cmd) {
        result = zwaveEvent(cmd)
        log.debug "Parsed $cmd to $result"
    } else {
        log.debug "Non-parsed event: $description"
    }
    result
}

def on() {
    def fadeOnTime = device.currentValue("fadeOnTime")
    def presetLevel = device.currentValue("presetLevel")

    short duration = fadeOnTime == null ? 255 : fadeOnTime
    short level = presetLevel == null || presetLevel == 0 ? 0xFF : toZwaveLevel(presetLevel as short)
    delayBetween([
            zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: duration).format(),
            zwave.switchMultilevelV1.switchMultilevelGet().format()
    ], durationToSeconds(duration) * 1000 + commandDelayMs)
}

def off() {
    def fadeOffTime = device.currentValue("fadeOffTime")

    short duration = fadeOffTime == null ? 255 : fadeOffTime
    delayBetween([
            zwave.switchMultilevelV2.switchMultilevelSet(value: 0x00, dimmingDuration: duration).format(),
            zwave.switchMultilevelV1.switchMultilevelGet().format()
    ], durationToSeconds(duration) * 1000 + commandDelayMs)
}

def setLevel(value, durationSeconds = null) {
    log.debug "setLevel >> value: $value, durationSeconds: $durationSeconds"
    short level = toDisplayLevel(value as short)
    short dimmingDuration = durationSeconds == null ? 255 : secondsToDuration(durationSeconds as int)

    sendEvent(name: "level", value: level, unit: "%")
    sendEvent(name: "switch", value: level > 0 ? "on" : "off")
    delayBetween([
            zwave.switchMultilevelV2.switchMultilevelSet(value: toZwaveLevel(level), dimmingDuration: dimmingDuration).format(),
            zwave.switchMultilevelV1.switchMultilevelGet().format()
    ], durationToSeconds(dimmingDuration) * 1000 + commandDelayMs)
}

def poll() {
    delayBetween(statusCommands, commandDelayMs)
}

def ping() {
    poll()
}

def refresh() {
    def commands = statusCommands
    commands << zwave.versionV1.versionGet().format()

    if (getDataValue("MSR") == null) {
        commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
    }
    for (i in 1..8) {
        commands << zwave.configurationV1.configurationGet(parameterNumber: i).format()
    }
    log.debug "Refreshing with commands $commands"
    delayBetween(commands, commandDelayMs)
}

def indicatorNever() {
    sendEvent(name: "indicatorStatus", value: "never")
    configurationCommand(7, 0)
}

def indicatorWhenOff() {
    sendEvent(name: "indicatorStatus", value: "when off")
    configurationCommand(7, 255)
}

def indicatorWhenOn() {
    sendEvent(name: "indicatorStatus", value: "when on")
    configurationCommand(7, 254)
}

def low() {
    setLevel(10)
}

def medium() {
    setLevel(50)
}

def high() {
    setLevel(100)
}

def levelUp() {
    setLevel(device.currentValue("level") + (levelIncrement ?: defaultLevelIncrement))
}

def levelDown() {
    setLevel(device.currentValue("level") - (levelIncrement ?: defaultLevelIncrement))
}


private static int getCommandDelayMs() { 1000 }
private static int getDefaultLevelIncrement() { 10 }

private initialize() {
    // Device-Watch simply pings if no device events received for 32min(checkInterval)
    sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])
}

private zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) {
    dimmerEvent(cmd.value)
}

private zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) {
    dimmerEvent(cmd.value)
}

private zwaveEvent(hubitat.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) {
    response(zwave.switchMultilevelV1.switchMultilevelGet().format())
}

private zwaveEvent(hubitat.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
    if (cmd.value == 0) {
        switchEvent(false)
    } else if (cmd.value == 255) {
        switchEvent(true)
    } else {
        log.debug "Bad switch value $cmd.value"
    }
}

private zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd) {
    def result = null
    switch (cmd.parameterNumber) {
        case 1:
            result = createEvent(name: "fadeOnTime", value: cmd.configurationValue[0])
            break
        case 2:
            result = createEvent(name: "fadeOffTime", value: cmd.configurationValue[0])
            break
        case 3:
            result = createEvent(name: "minLevel", value: cmd.configurationValue[0])
            break
        case 4:
            result = createEvent(name: "maxLevel", value: cmd.configurationValue[0])
            break
        case 5:
            result = createEvent(name: "presetLevel", value: cmd.configurationValue[0])
            break
        case 6:
            result = createEvent(name: "levelIndicatorTimeout", value: cmd.configurationValue[0])
            break
        case 7:
            def value = null
            switch (cmd.configurationValue[0]) {
                case 0: value = "never"; break
                case 254: value = "when on"; break
                case 255: value = "when off"; break
            }
            result = createEvent(name: "indicatorStatus", value: value)
            break
        case 8:
            def value = null
            switch (cmd.configurationValue[0]) {
                case 0: value = "incandescent"; break
                case 1: value = "led"; break
                case 2: value = "cfl"; break
            }
            result = createEvent(name: "loadType", value: value)
            break
    }
    result
}

private zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
    log.debug "manufacturerId:   $cmd.manufacturerId"
    log.debug "manufacturerName: $cmd.manufacturerName"
    log.debug "productId:        $cmd.productId"
    log.debug "productTypeId:    $cmd.productTypeId"
    def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
    updateDataValue("MSR", msr)
    updateDataValue("manufacturer", cmd.manufacturerName)
    createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
}

private zwaveEvent(hubitat.zwave.commands.hailv1.Hail cmd) {
    createEvent(name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false)
}

private zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) {
    createEvent(name: "firmwareVersion", value: "${cmd.applicationVersion}.${cmd.applicationSubVersion}", displayed: false)
}

private zwaveEvent(hubitat.zwave.Command cmd) {
    log.warn "Unhandled zwave command $cmd"
}

private dimmerEvent(short level) {
    def result = null
    if (level == 0) {
        result = [createEvent(name: "level", value: 0, unit: "%"), switchEvent(false)]
    } else if (level >= 1 && level <= 100) {
        result = [createEvent(name: "level", value: toDisplayLevel(level), unit: "%")]
        if (!isNewFirmware && device.currentValue("switch") != "on") {
            // Don't blindly trust level. Explicitly request on/off status.
            result << response(zwave.switchBinaryV1.switchBinaryGet().format())
        } else {
            result << switchEvent(true)
        }
    } else {
        log.debug "Bad dimming level $level"
    }
    result
}

private switchEvent(boolean on) {
    createEvent(name: "switch", value: on ? "on" : "off")
}

private getStatusCommands() {
    def cmds = []
    if (!isNewFirmware) {
        // Even though SwitchBinary is not advertised by this device, it seems to be the only way to assess its true
        // on/off status.
        cmds << zwave.switchBinaryV1.switchBinaryGet().format()
    }
    cmds << zwave.switchMultilevelV1.switchMultilevelGet().format()
    cmds
}

private short toDisplayLevel(short level) {
    level = Math.max(0, Math.min(100, level))
    (level == (short) 99) ? 100 : level
}

private short toZwaveLevel(short level) {
    Math.max(0, Math.min(99, level))
}

private int durationToSeconds(short duration) {
    if (duration >= 0 && duration <= 127) {
        duration
    } else if (duration >= 128 && duration <= 254) {
        (duration - 127) * 60
    } else if (duration == 255) {
        2   // factory default
    } else {
        log.error "Bad duration $duration"
        0
    }
}

private short secondsToDuration(int seconds) {
    if (seconds >= 0 && seconds <= 127) {
        seconds
    } else if (seconds >= 128 && seconds <= 127 * 60) {
        127 + Math.round(seconds / 60)
    } else {
        log.error "Bad seconds $seconds"
        255
    }
}

private configurationCommand(param, value) {
    param = param as short
    value = value as short
    delayBetween([
            zwave.configurationV1.configurationSet(parameterNumber: param, configurationValue: [value]).format(),
            zwave.configurationV1.configurationGet(parameterNumber: param).format()
    ], commandDelayMs)
}

private setFadeOnTime(short time) {
    sendEvent(name: "fadeOnTime", value: time)
    configurationCommand(1, time)
}

private setFadeOffTime(short time) {
    sendEvent(name: "fadeOffTime", value: time)
    configurationCommand(2, time)
}

private setMinLevel(short level) {
    sendEvent(name: "minLevel", value: level)
    configurationCommand(3, level)
}

private setMaxLevel(short level) {
    sendEvent(name: "maxLevel", value: level)
    configurationCommand(4, level)
}

private setPresetLevel(short level) {
    sendEvent(name: "presetLevel", value: level)
    configurationCommand(5, level)
}

private setLevelIndicatorTimeout(short timeout) {
    sendEvent(name: "levelIndicatorTimeout", value: timeout)
    configurationCommand(6, timeout)
}

private setLoadType(String loadType) {
    switch (loadType) {
        case "Incandescent (default)":
            sendEvent(name: "loadType", value: "incandescent")
            return configurationCommand(8, 0)
        case "LED":
            sendEvent(name: "loadType", value: "led")
            return configurationCommand(8, 1)
        case "CFL":
            sendEvent(name: "loadType", value: "cfl")
            return configurationCommand(8, 2)
    }
}

private setIndicatorStatus(String status) {
    switch (indicatorStatus) {
        case "When switch is off (default)":    return indicatorWhenOff()
        case "When switch is on":               return indicatorWhenOn()
        case "Never":                           return indicatorNever()
    }
}

private boolean getIsNewFirmware() { device.currentValue("firmwareVersion") ==~ /1\.2\d/ }

Yes, I use that custom driver for the same reason (ease of configuration). It works well enough except for physical / digital reporting.

It's disconcerting that your switches which used to work correctly now don't.

If you parse the MSR on the "wrong" fingerprint, it is actually correct per the Z-wave product catalog entry:
Manufacturer ID = 0x001D is Leviton (decimal 29)
Product Type ID: 0x3201
Product ID: 0x0001

So I'm not sure why the fingerprint is being interpreted incorrectly / differently.

So I think I found the issue. If I pair the device initially it chooses the "Generic Z-Wave Smart Dimmer" driver which is correct. If I then switch to the custom driver to apply any settings changes to the switch, after I "configure" and "refresh" the device the Manufacturer ID changes from "29" to "Leviton". Even after I switch back to the stock driver and use the "configure" and "refresh" options, the manufacture stays on "Leviton" and sure enough the physical events are not logged anymore.

This would explain why I really did not notice a problem because over time I would have tweaked some of the dimmer settings with the custom driver.

There was bug fixes in the 2.0.4 update concerning Z-Wave but those may be unrelated, or possibly a previous update. I know in the past I have used the custom driver and even after that physical events were still working.

It sounds like the "Generic Z-Wave Smart Dimmer" driver may need to be updated, at least according to what you have found regarding the "wrong" fingerprint actually being the correct one.

I can confirm the issue.

I switched to the custom driver, made a change, configured, refreshed, switched back to the generic driver. And sure enough, the fingerprint changed per your observations.

This is a dimmer I added just 2 days ago, while the hub was still on 2.03 firmware. At that time I was able to switch to the custom driver, configure the settings, and switch back to the generic driver with no ill effects. So this issue is definitely related to the 2.04 update.

Thanks for confirming the problem also. I will reach out to @mike.maxwell or create a support ticket if necessary.

This seems similar to a problem I created a ticket for last night. @bobbyD has seen the phenomenon and we'd like @chuck.schwer to look at it but as luck would have it, he's got the flu. :frowning:

1 Like

Glad I am not the only one. I hope he has a speedy recovery, flu is not fun.

1 Like

Hi There

I havent considered the physical events yet as to get my fibaro dual switch 2 (FGS-223) working I have resorted the Dual Relay Driver approach.

I am currently moving over from STT myself and want to consider the best approach to capture physical switch presses. Over on STT with webCoRE I created a virtual switch to encapsulate the physical switch to use for programmatic changes, a boolean flag to stop cyclic calls and the physical switch for physical activity. I could then ad behaviour on the physical switch to switch states (SleepMode) on/off depending on time of day. This worked well


Switch 38 being the physical switch and Switch 17 being the virtual (Programmatic)

But I am now considering how to code this in HE.
Has anyone got any suggestions for this code/rule based approach to capturing physical interactions?

The upper data is from our pairing, the lower is from an st ported driver which includes it's own mfr report which overwrites the manufacturer attribute.
Several of our drivers use our version of the manufacturer attribute to make parsing decisions, if this gets overwritten some drivers will not work correctly.

Is it possible to have the stock driver overwrite the msr so this is not an issue? Only reason I used the custom driver is because the stock one did not have any options for configuring the switch settings. Trying to avoid re-paring over 30 dimmers not to mention all of the automations.

Could you edit the Custom to not write it's msr?

lines 322-324 would be commented out:

> //    updateDataValue("MSR", msr)
> //    updateDataValue("manufacturer", cmd.manufacturerName)
> //    createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])

That would be worth an experiment I imagine. :slight_smile:

That works. With this change, the custom driver no longer overwrites the manufacturer variable.

@raidflex:

To get the stock driver (as it stands) to recognize the device, I think it is sufficient to change the manufacturer back from Leviton to 29. You will need to modify the custom driver as follows:

Comment out the two lines in the refresh() function:

// if (getDataValue("MSR") == null) {
    commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
// }

Then change lines 322-324 as follows:

updateDataValue("MSR", "")
updateDataValue("manufacturer", "29")
createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])

This sets msr to an empty string, and the manufacturer to 29. Now the fingerprint area looks like this:

I don't know how to completely remove the MSR field from the fingerprint, but it does not seem to hurt. The custom driver now reports physical events once again.

Let me know if this works for you.

Edit: forgot to mention - once you have modified the custom driver, change the device to use it, save it, and do a "configure". Now you can switch the device back to the stock driver.

I'm going to guess it's removeDataValue("MSR")

OR

state.remove("MSR")

Leaving MSR alone does no harm.

So I modified the driver. but nothing changes after "configure" and "refresh" the device. The manufacture is still Leviton and MSR is not removed.

You want to make sure the manufacturerSpecificGet in the refresh() function is being executed when you hit "refresh". Since it is gated by MSR being null, you need to comment out the lines above and below it.

// if (getDataValue("MSR") == null) {
    commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
// }

This will show up in the log as something similar to this:

dev:1932019-01-19 11:44:39.527 pm debugParsed ManufacturerSpecificReport(manufacturerId:29, productId:1, productTypeId:12801) to null
dev:1932019-01-19 11:44:39.526 pm debugproductTypeId: 12801
dev:1932019-01-19 11:44:39.524 pm debugproductId: 1
dev:1932019-01-19 11:44:39.523 pm debugmanufacturerName: Leviton
dev:1932019-01-19 11:44:39.521 pm debugmanufacturerId: 29

Yeah that is it I commented out all 3 lines, instead of the two, once I fixed that it updated properly.

Great! Glad it worked.

@mike.maxwell,

Seconding @raidflex's suggestion: It would be good if the stock drivers would refresh the necessary attributes whenever we switched to them, and not just upon initial pairing. Granted, the ported drivers from ST are not supported, but folks do use them for one reason or another. It would lead to a more robust user experience and probably reduce support overhead.

Thanks for all the help, this saved me a lot of time from having to re-pair all the device.

In general this is the intent of the configure command...
I can't predict every custom attribute that's been jacked into current states, state or data by a custom driver, so protecting against that is a bit onerous.