[GUIDE] Writing Z-Wave Drivers for S2

Security Encapsulation

Built-in method zwaveSecureEncap handles proper security encapsulation based on what the device is granted during inclusion and if the command is expected to be encapsulated by the receiving device.

String zwaveSecureEncap(String cmd) 

Supervision

Supervision is the mechanism used to ensure reception of a command by the hub or device as ACK packets alone aren't sufficient when included with S2. The nonce in S0 is requested at the beginning of each command sent, in S2 communication is requested once unless it gets out of sync. If the S2 nonce gets out of sync the original packet is probably lost.

Supervision Get handling

S2 included devices will send many commands encapsulated in a supervision get message. In the case of multi-channel devices, you will see multi-channel encapsulated -> supervision encapsulated commands. It is critical that you send a supervision response in reply to this or the device will assume the communication was a failure.

Example non-multi-channel supervisionGet handling:

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

Example multi-channel supervisionGet handling:

void zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd, ep = 0) {
    if (logEnable) log.debug "Supervision Get - SessionID: ${cmd.sessionID}, CC: ${cmd.commandClassIdentifier}, Command: ${cmd.commandIdentifier}"
    hubitat.zwave.Command encapsulatedCommand = cmd.encapsulatedCommand(CMD_CLASS_VERS)
    if (encapsulatedCommand) {
        zwaveEvent(encapsulatedCommand, ep)
    }
    if (ep > 0) {
        sendHubCommand(new hubitat.device.HubAction(zwaveSecureEncap(zwave.multiChannelV4.multiChannelCmdEncap(sourceEndPoint: 0, bitAddress: 0, res01: 0, destinationEndPoint: ep).encapsulate(zwave.supervisionV1.supervisionReport(sessionID: cmd.sessionID, reserved: 0, moreStatusUpdates: false, status: 0xFF, duration: 0)).format()), hubitat.device.Protocol.ZWAVE))
    } else {
        sendHubCommand(new hubitat.device.HubAction(zwaveSecureEncap(zwave.supervisionV1.supervisionReport(sessionID: cmd.sessionID, reserved: 0, moreStatusUpdates: false, status: 0xFF, duration: 0).format()), hubitat.device.Protocol.ZWAVE))
    }
}

Encapsulating outgoing commands in Supervision Get

This step is optional and not expected to be used on all commands, should only be used on actuator type commands. Ex: switch binary set, switch multilevel set, etc. Using this will ensure the packet is properly received by the destination. To use this, you must track the sessionIDs and resend if no supervision report has been received. If the sessionID is not incremented the receiving device will likely drop the packet as it is seen as a duplicate.

Example non-multi-channel code:

@Field static Map<String, Map<Short, String>> supervisedPackets = [:]
@Field static Map<String, Short> sessionIDs = [:]

void zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionReport cmd) {
    if (logEnable) log.debug "supervision report for session: ${cmd.sessionID}"
    if (!supervisedPackets."${device.id}") { supervisedPackets."${device.id}" = [:] }
    if (supervisedPackets["${device.id}"][cmd.sessionID] != null) { supervisedPackets["${device.id}"].remove(cmd.sessionID) }
    unschedule(supervisionCheck)
}

void supervisionCheck() {
    // re-attempt once
    if (!supervisedPackets."${device.id}") { supervisedPackets."${device.id}" = [:] }
    supervisedPackets["${device.id}"].each { k, v ->
        if (logEnable) log.debug "re-sending supervised session: ${k}"
        sendHubCommand(new hubitat.device.HubAction(zwaveSecureEncap(v), hubitat.device.Protocol.ZWAVE))
        supervisedPackets["${device.id}"].remove(k)
    }
}

Short getSessionId() {
    Short sessId = 1
    if (!sessionIDs["${device.id}"]) {
        sessionIDs["${device.id}"] = sessId
        return sessId
    } else {
        sessId = sessId + sessionIDs["${device.id}"]
        if (sessId > 63) sessId = 1
        sessionIDs["${device.id}"] = sessId
        return sessId
    }
}

hubitat.zwave.Command supervisedEncap(hubitat.zwave.Command cmd) {
    if (getDataValue("S2")?.toInteger() != null) {
        hubitat.zwave.commands.supervisionv1.SupervisionGet supervised = new hubitat.zwave.commands.supervisionv1.SupervisionGet()
        supervised.sessionID = getSessionId()
        if (logEnable) log.debug "new supervised packet for session: ${supervised.sessionID}"
        supervised.encapsulate(cmd)
        if (!supervisedPackets."${device.id}") { supervisedPackets."${device.id}" = [:] }
        supervisedPackets["${device.id}"][supervised.sessionID] = supervised.format()
        runIn(5, supervisionCheck)
        return supervised
    } else {
        return cmd
    }
}

Example non-multi-channel usage:

zwaveSecureEncap(supervisedEncap(cmd).format())

Example multi-channel code:

@Field static Map<String, Map<Short, String>> supervisedPackets = [:]
@Field static Map<String, Short> sessionIDs = [:]

void zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionReport cmd, ep=0) {
    if (logEnable) log.debug "supervision report for session: ${cmd.sessionID}"
    if (!supervisedPackets."${device.id}") { supervisedPackets."${device.id}" = [:] }
    if (supervisedPackets["${device.id}"][cmd.sessionID] != null) { supervisedPackets["${device.id}"].remove(cmd.sessionID) }
    unschedule(supervisionCheck)
}

void supervisionCheck() {
    // re-attempt once
    if (!supervisedPackets."${device.id}") { supervisedPackets."${device.id}" = [:] }
    supervisedPackets["${device.id}"].each { k, v ->
        if (logEnable) log.debug "re-sending supervised session: ${k}"
        sendHubCommand(new hubitat.device.HubAction(zwaveSecureEncap(v), hubitat.device.Protocol.ZWAVE))
        supervisedPackets["${device.id}"].remove(k)
    }
}

Short getSessionId() {
    Short sessId = 1
    if (!sessionIDs["${device.id}"]) {
        sessionIDs["${device.id}"] = sessId
        return sessId
    } else {
        sessId = sessId + sessionIDs["${device.id}"]
        if (sessId > 63) sessId = 1
        sessionIDs["${device.id}"] = sessId
        return sessId
    }
}

hubitat.zwave.Command supervisedMCEncap(hubitat.zwave.Command cmd, ep = 0) {
    if (ep == 0) { return supervisedEncap(cmd) }
    if (getDataValue("S2")?.toInteger() != null) {
        hubitat.zwave.commands.supervisionv1.SupervisionGet supervised = new hubitat.zwave.commands.supervisionv1.SupervisionGet()
        supervised.sessionID = getSessionId()
        if (logEnable) log.debug "new supervised packet for session: ${supervised.sessionID}"
        supervised.encapsulate(cmd)
        hubitat.zwave.Command epEncapped = zwave.multiChannelV4.multiChannelCmdEncap(sourceEndPoint: 0, bitAddress: 0, res01: 0, destinationEndPoint: ep).encapsulate(supervised)
        if (!supervisedPackets."${device.id}") { supervisedPackets."${device.id}" = [:] }
        supervisedPackets["${device.id}"][supervised.sessionID] = epEncapped.format()
        runIn(5, supervisionCheck)
        return epEncapped
    } else {
        return zwave.multiChannelV4.multiChannelCmdEncap(sourceEndPoint: 0, bitAddress: 0, res01: 0, destinationEndPoint: ep).encapsulate(cmd)
    }
}

Example multi-channel usage:

zwaveSecureEncap(supervisedEncap(cmd, ep).format())

This will also handle the multi-channel encapsulation.

12 Likes

I find these code snippet postings to be very valuable.

5 Likes

Although not explicitly listed, it is implied that there is a field static map in the driver that defines "commandClassVersions".

Something a la:

@Field static Map commandClassVersions = 
[
         0x26: 3    //switchMultiLevel
        ,0x70: 2    //configuration
        ,0x72: 2    //Manufacturer Specific
        ,0x85: 2    //association
     	,0x86: 3    //Version
]
2 Likes

It does .. Thanks... And you should always specify command class versions..

2 Likes

I plan to release a few of these “guides” on various Z-Wave Driver topics.. This one obviously on the quirks of dealing with S2..

7 Likes

Is Supervision only available if the device has S2 security?

I agree with @dennypage that the code examples is very helpful for developers.

Would you consider publishing one or two of the new generic z-wave plus drivers as functional examples?

2 Likes

Typically yes only on S2 capable devices, but S2 inclusion is not a requirement for use. However it is not very useful without S2 as the normal ACK is sufficient without S2.

Yes, I would consider.. I'll talk it over with the rest of the engineering team..

3 Likes

Does failure to receive the normal "Ack" result in a retransmit if the command/packet is lost? I thought it didn't (and put a lot of effort adding supervision to my drivers under that assumption, even for the non-S2 case). If it does, maybe I had just wasted a bit of time and don't need the complexity (but why then is it needed at all - if an ack failure causes retransmission, shouldn't it do the same in S2.). I thought I understood this - maybe I'm more confused than I thought. Thanks.

Yes.. In fact if you watch zniffer failure to receive an ack causes a flurry of activity in an attempt to get the packet to the destination.. Including re-routing...

It does but only for the original packet.. The problem comes in where S2 nonce gets out of sync, there will be an ACK but the receiving device will not be able to decrypt the packet and then a nonce re-sync happens.. The original packet must be re-transmitted/re-encrypted after this re-sync..

1 Like

You can see from this diagram that ACK is sent on receipt of the packet even if the packet could not be decrypted..
nonce-resync

Thank you. That was helpful and easy to follow.

One more question . . .

  • If you use Supervise without S2 -- i.e., for a non-secure node, you have to be careful about what is supervised (commands can be supervised, certain "gets" can't be, etc.).

  • If you supervise only for S2 added nodes, do you need to use the same caution? Or, since you are supervising the "secure" packet rather than the underlining payload, can you just supervise everything without being concerned about what is in the secure payload?

Correct.. not everything should be supervised..

Yes

1 Like

3.7.2 Compatibility considerations
This command class is used as an integrated part of the Security 2 Command Class but may also be used for non-secure applications.

The Supervision Command Class MAY be used for solitary commands such as Set, Remove and unsolicited Report commands.

The Supervision Command Class MUST NOT be used for session-like command flows such as Get<->Report command exchanges or firmware update.

The Supervision Get Command MAY carry multiple commands grouped with the Multi Command encapsulation command.

So it looks like my GE Enbrighten drivers do this correctly - that's good.

I did notice, though, that my zwave.supervisionV1.supervisionReport command does not have a ".format()" on it. Mine's working, so I guess I'll leave it alone.

I admit I never know when to/not to use .format() for sure anyway...

1 Like

It will work like that.. But you are passing the class on to zwaveSecureEncap.. .format() before passes a string (slightly less resource intensive) ..

1 Like

Do nodes have a "timeout" allowing the same sessionID to be used twice in a row for different commands?

Specific issue - on Hub restart, you don't know the last sessionID seen by a node (unless you save it to state which seems like extra work), so there's a 1 in 64 chance you'll use a sessionID last used by the node. Will this result in a failure if you happen to do that, or does the z-wave standard / sdk cause the node to "reset" after a while so that re-using the last sessionID after a reboot will still work. Yes, I realize its a relatively rare failure scenario, but just wondering if its already accounted for.

I would say this would be a pretty unlikely scenario .. But you could initialize with a random to reduce this possibility