App and driver porting to Hubitat

Hubitat does not use the "Health Check" capability that ST uses. Here is your driver with the unnecessary bits commented out. After changing your driver to this version, be sure to view the Parent Device and click SAVE under the user settings section as I added an "unschedule()" call in the "updated()" routine to make sure the HealthCheck schedule is disabled.

/*
 *  Zemi  ZigBee Switch - touch
 * 
 *  Copyright 2020 SmartThings
 *
 *  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.
 *
 *  author : fuls@naver.com
 */
public static String version() { return "v0.0.2.20200426" }
/*
 *   2020/04/26 >>> v0.0.2.20200426 - Support to old Jemi swtich version
 *   2020/04/25 >>> v0.0.1.20200425 - Initialize
 */

import java.lang.Math

metadata {
    definition(name: "Zemi ZigBee Switch (Touch)", namespace: "zemismart", author: "Onaldo", ocfDeviceType: "oic.d.switch", vid: "generic-switch", genericHandler: "ZLL") {
        capability "Actuator"
        capability "Configuration"
        capability "Refresh"
//        capability "Health Check"
        capability "Switch"

        command "childOn", ["string"]
        command "childOff", ["string"]

        // Zemi ZigBee Multi Switch
        fingerprint profileId: "0104", deviceId: "0002", inClusters: "0000, 0003, 0004, 0005, 0006", manufacturer: "Feibit Inc co.", model: "FB56+ZSW1GKJ2.5", deviceJoinName: "Zemi Zigbee Switch"
        fingerprint profileId: "0104", deviceId: "0002", inClusters: "0000, 0004, 0005, 0006", outClusters: "0000", manufacturer: "Feibit Inc co.", model: "FB56+ZSW1HKJ2.5", deviceJoinName: "Zemi Zigbee Switch 1"
        fingerprint profileId: "0104", deviceId: "0002", inClusters: "0000, 0003, 0004, 0005, 0006", manufacturer: "Feibit Inc co.", model: "FB56+ZSW1IKJ2.5", deviceJoinName: "Zemi Zigbee Switch 1"

        fingerprint profileId: "0104", deviceId: "0002", inClusters: "0000, 0003, 0004, 0005, 0006", manufacturer: "Feibit Inc co.", model: "FB56+ZSW1GKJ2.7", deviceJoinName: "Zemi Zigbee Switch"
        fingerprint profileId: "0104", deviceId: "0002", inClusters: "0000, 0004, 0005, 0006", outClusters: "0000", manufacturer: "Feibit Inc co.", model: "FB56+ZSW1HKJ2.7", deviceJoinName: "Zemi Zigbee Switch 1"
        fingerprint profileId: "0104", deviceId: "0002", inClusters: "0000, 0003, 0004, 0005, 0006", manufacturer: "Feibit Inc co.", model: "FB56+ZSW1IKJ2.7", deviceJoinName: "Zemi Zigbee Switch 1"

        fingerprint profileId: "C05E", deviceId: "0000", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "FeiBit", model: "FNB56-ZSW01LX2.0", deviceJoinName: "Zemi Zigbee Switch"
        fingerprint profileId: "C05E", deviceId: "0000", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "FeiBit", model: "FNB56-ZSW02LX2.0", deviceJoinName: "Zemi Zigbee Switch 1"
        fingerprint profileId: "C05E", deviceId: "0000", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "FeiBit", model: "FNB56-ZSW03LX2.0", deviceJoinName: "Zemi Zigbee Switch 1"

        fingerprint profileId: "C05E", deviceId: "0000", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "3A Smart Home DE", model: "LXN-1S27LX1.0", deviceJoinName: "Zemi Zigbee Switch"
        fingerprint profileId: "C05E", deviceId: "0000", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "3A Smart Home DE", model: "LXN-2S27LX1.0", deviceJoinName: "Zemi Zigbee Switch 1"
        fingerprint profileId: "C05E", deviceId: "0000", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "3A Smart Home DE", model: "LXN-3S27LX1.0", deviceJoinName: "Zemi Zigbee Switch 1"
    }

    preferences {
        input type: "paragraph", element: "paragraph", title: "Version", description: version(), displayDuringSetup: false
    }

    /*
    // simulator metadata
    simulator {
        // status messages
        status "on": "on/off: 1"
        status "off": "on/off: 0"

        // reply messages
        reply "zcl on-off on": "on/off: 1"
        reply "zcl on-off off": "on/off: 0"
    }

    tiles(scale: 2) {
        multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) {
            tileAttribute("device.switch", key: "PRIMARY_CONTROL") {
                attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#00A0DC", nextState: "turningOff"
                attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "turningOn"
                attributeState "turningOn", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#00A0DC", nextState: "turningOff"
                attributeState "turningOff", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "turningOn"
            }
        }
        standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
            state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh"
        }
        main "switch"
        details(["switch", "refresh"])
    }
*/
}

def isUnexpectedEndpoint(String model) {
    switch (model) {
        case 'FB56+ZSW1HKJ2.5':
        case 'FB56+ZSW1IKJ2.5':
        case 'FB56+ZSW1HKJ2.7':
        case 'FB56+ZSW1IKJ2.7': return true
        default               : return false
    }
}

def installed() {
    log.debug "installed()"
    def endpointCount = getEndpointCount()
    if (endpointCount == 1) {
        // for 1 gang switch - ST Official local dth
        setDeviceType("ZigBee Switch")
    } else if (endpointCount > 1) {
        def model = device.getDataValue("model")
        if (isUnexpectedEndpoint(model)) {
            device.updateDataValue("endpointId", "10")
        }
        // for multi switch, cloud device
        createChildDevices()
    }
    updateDataValue("onOff", "catchall")
    refresh()
}

def updated() {
    log.debug "updated()"
    unschedule()
    updateDataValue("onOff", "catchall")
    refresh()
}

def parse(String description) {
    Map eventMap = zigbee.getEvent(description)
    Map eventDescMap = zigbee.parseDescriptionAsMap(description)

    if (!eventMap && eventDescMap) {
        eventMap = [:]
        if (eventDescMap?.clusterId == zigbee.ONOFF_CLUSTER) {
            eventMap[name] = "switch"
            eventMap[value] = eventDescMap?.value
        }
    }

    if (eventMap) {
        def endpointId = device.getDataValue("endpointId")
        log.debug "eventMap $eventMap | eventDescMap $eventDescMap"

        if (eventDescMap?.sourceEndpoint == endpointId) {
            log.debug "parse - sendEvent parent $eventDescMap.sourceEndpoint"
            sendEvent(eventMap)
        } else {
            log.debug "parse - sendEvent child  $eventDescMap.sourceEndpoint"
            def childDevice = childDevices.find {
                it.deviceNetworkId == "$device.deviceNetworkId:${eventDescMap.sourceEndpoint}"
            }
            if (childDevice) {
                childDevice.sendEvent(eventMap)
            } else {
                log.debug "Child device: $device.deviceNetworkId:${eventDescMap.sourceEndpoint} was not found"
                def parentEndpointInt = zigbee.convertHexToInt(endpointId)
                def childEndpointInt = zigbee.convertHexToInt(eventDescMap?.sourceEndpoint)
                def childEndpointHexString = zigbee.convertToHexString(childEndpointInt, 2).toUpperCase()
                def deviceLabel = "${device.displayName[0..-2]}"
                def deviceIndex = Math.abs(childEndpointInt - parentEndpointInt) + 1
                createChildDevice("$deviceLabel$deviceIndex", childEndpointHexString)
            }
        }
    }
}

private getEndpointCount() {
    def model = device.getDataValue("model")

    switch (model) {
        case 'FNB56-ZSW01LX2.0' : return 1
        case 'FNB56-ZSW02LX2.0' : return 2
        case 'FNB56-ZSW03LX2.0' : return 3
        case 'FB56+ZSW1GKJ2.5'  : return 1
        case 'FB56+ZSW1HKJ2.5'  : return 2
        case 'FB56+ZSW1IKJ2.5'  : return 3
        case 'FB56+ZSW1GKJ2.7'  : return 1
        case 'FB56+ZSW1HKJ2.7'  : return 2
        case 'FB56+ZSW1IKJ2.7'  : return 3
        case 'LXN-1S27LX1.0'    : return 1
        case 'LXN-2S27LX1.0'    : return 2
        case 'LXN-3S27LX1.0'    : return 3
        default                 : return 0
    }
}

private void createChildDevices() {
    log.debug("createChildDevices of $device.deviceNetworkId")
    def endpointCount = getEndpointCount()
    def endpointId = device.getDataValue("endpointId")
    def endpointInt = zigbee.convertHexToInt(endpointId)
    def deviceLabel = "${device.displayName[0..-2]}"

    for (i in 1..endpointCount - 1) {
        def endpointHexString = zigbee.convertToHexString(endpointInt + i, 2).toUpperCase()
        createChildDevice("$deviceLabel${i + 1}", endpointHexString)
    }
}

private void createChildDevice(String deviceLabel, String endpointHexString) {
    def childDevice = childDevices.find {
        it.deviceNetworkId == "$device.deviceNetworkId:$endpointHexString"
    }
    if (!childDevice) {
        log.debug("Need to createChildDevice: $device.deviceNetworkId:$endpointHexString")
        addChildDevice("smartthings", "Child Switch Health", "$device.deviceNetworkId:$endpointHexString", device.hubId,
                       [completedSetup: true, label: deviceLabel, isComponent: false])
    } else {
        log.debug("createChildDevice: SKIP - $device.deviceNetworkId:${endpointHexString}")
    }
}

private getChildEndpoint(String dni) {
    dni.split(":")[-1] as String
}

def on() {
    log.debug("on")
    zigbee.on()
}

def off() {
    log.debug("off")
    zigbee.off()
}

def childOn(String dni) {
    log.debug("child on ${dni}")
    def childEndpoint = getChildEndpoint(dni)
    def endpointInt = zigbee.convertHexToInt(childEndpoint)
    zigbee.command(zigbee.ONOFF_CLUSTER, 0x01, "", [destEndpoint: endpointInt])
}

def childOff(String dni) {
    log.debug("child off ${dni}")
    def childEndpoint = getChildEndpoint(dni)
    def endpointInt = zigbee.convertHexToInt(childEndpoint)
    zigbee.command(zigbee.ONOFF_CLUSTER, 0x00, "", [destEndpoint: endpointInt])
}

/**
 * PING is used by Device-Watch in attempt to reach the Device
 * */
def ping() {
    return refresh()
}

def refresh() {
    def cmds = zigbee.onOffRefresh()
    def endpointCount = getEndpointCount()

    if (endpointCount > 1) {
        def endpointId = device.getDataValue("endpointId")
        def endpointInt = zigbee.convertHexToInt(endpointId)

        for (i in 1..endpointCount - 1) {
            cmds += zigbee.readAttribute(zigbee.ONOFF_CLUSTER, 0x0000, [destEndpoint: endpointInt + i])
        }
    } else {
        cmds += zigbee.readAttribute(zigbee.ONOFF_CLUSTER, 0x0000, [destEndpoint: 0xFF])
    }

    return cmds
}

def poll() {
    refresh()
}

/*
def healthPoll() {
    log.debug "healthPoll()"
    def cmds = refresh()
    cmds.each { sendHubCommand(new hubitat.device.HubAction(it)) }
}

def configureHealthCheck() {
    Integer hcIntervalMinutes = 12
    if (!state.hasConfiguredHealthCheck) {
        log.debug "Configuring Health Check, Reporting"
        unschedule("healthPoll")
        runEvery5Minutes("healthPoll")
        def healthEvent = [name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]]
        // Device-Watch allows 2 check-in misses from device
        sendEvent(healthEvent)
        childDevices.each {
            it.sendEvent(healthEvent)
        }
        state.hasConfiguredHealthCheck = true
    }
}
*/

def configure() {
    log.debug "configure()"
//    configureHealthCheck()

    //other devices supported by this DTH in the future
    def cmds = zigbee.onOffConfig(0, 120)
    def endpointCount = getEndpointCount()

    if (endpointCount > 1) {
        def endpointId = device.getDataValue("endpointId")
        def endpointInt = zigbee.convertHexToInt(endpointId)

        for (i in 1..endpointCount - 1) {
            cmds += zigbee.configureReporting(zigbee.ONOFF_CLUSTER, 0x0000, 0x10, 0, 120, null, [destEndpoint: endpointInt + i])
        }
    } else {
        cmds += zigbee.configureReporting(zigbee.ONOFF_CLUSTER, 0x0000, 0x10, 0, 120, null, [destEndpoint: 0xFF])
    }
    cmds += refresh()
    return cmds
}
4 Likes

Thank you, solved my problem.
I have the driver for the version with physical buttons

A friend of mine made this script in Tamper Monkey that can generate a map of the ZigBee network on Smartthings, I know it is a very laborious thing to do but I didn't find anything like that in Hubitat. I'll leave it here so that if anyone wants to get inspired and do something similar or adapt the code it would be very interesting.

1 Like

Hi all (and with a particular shout-out to @ogiewon, @mike.maxwell, @bcopeland, @bravenel, and @bobbyD),

After living for way too long on a hybrid Smartthings/Hubitat network, I've finally made a start on porting my Device-Type Handler and SmartApp for the Leviton VRCZ4-M04 Zone Controller. I have it half-working now, and wondered if I might run a couple of remaining issues by you all (with apologies if I've missed any relevant bits that are already posted to this thread and/or in docs.hubitat.com, though I haven't found any as yet)?

These are:

  1. Dimming is only working in one direction (down), because the zwave command 133.4 Switch Multilevel Start Level Change is parsing differently in HE vs ST. Specifically,

     zwave.parse('zw device: 03, command: 2604, payload: 40 00 ')
    

parses into this cmd on Smartthings:

SwitchMultilevelStartLevelChange(ignoreStartLevel: false, reserved00: 0, startLevel: 0, dimmingDuration: null, stepSize: null, upDown: 1, incDec: 0)

but parses on Hubitat as:

SwitchMultilevelStartLevelChange(dimmingDuration:null, ignoreStartLevel:null, incDec:null, startLevel:null, stepSize:null, upDown:null)

It does seem odd that I get some valid field values on ST, while the all-null field list on HE at least matches both docs.hubitat.com:

Switch Multilevel Start Level Change
Command: 0x04
.
    class hubitat.zwave.commands.switchmultilevelv3.SwitchMultilevelStartLevelChange {
    Short dimmingDuration
    Boolean ignoreStartLevel
    Short incDec
    Short startLevel
    Short stepSize
    Boolean upDown
. 
    List<Short> getPayload()
    String format()
}

...and https://graph.api.smartthings.com/ide/doc/zwave-utils.html#switchMultilevelV3:

class physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStartLevelChange
{
    Short	dimmingDuration
    Boolean	ignoreStartLevel
    Short	incDec
    Short	startLevel
    Short	stepSize
    Short	upDown
    List<Short>	payload
.
    Short	INC_DEC_INCREMENT	= 0
    Short	UP_DOWN_UP	= 0
    Short	INC_DEC_DECREMENT	= 1
    Short	UP_DOWN_DOWN	= 1
    Short	UP_DOWN_RESERVED2	= 2
    Short	INC_DEC_RESERVED2	= 2
    Short	UP_DOWN_NONE	= 3
    Short	INC_DEC_NONE	= 3
 .
    String format()
}

All I really need here is the upDown value, which is unfortunately null (like all the other values) on HE. Happy to provide the HE versions of my code (so far) if that helps?

  1. Controlling the LEDs in the switches on the VRCZ4 is not working on HE -- this works on ST using some "black box" code inherited from ygelfand (see [OBSOLETE] Leviton VRCS4 - #90 by ygelfand - Community Created Device Types - SmartThings Community), to wit:

    def setLightStatus(one,two,three,four)
    {
        def hidden = ["0F", "00", "13", device.deviceNetworkId ]
        def start = ["91", "00", "1D", "0D", "01", "FF"]
        def end = ["00", "00", "0A"]
        def light = integer(one) + (integer(two) << 1) + (integer(three) << 2) + (integer(four) << 3)
        log.debug "Setting button-light statuses for ${device.displayName}..."
    	   log.debug "one,two,three,four = $one,$two,$three,$four"
        log.debug "HIDDEN: $hidden"
        log.debug "START: $start"
        log.debug "END: $end"
        log.debug "LIGHT: $light"
        def checksum = 255
        hidden.each { checksum^=(integerhex(it)) }
        log.debug "checksum, post-hidden: $checksum"
        start.each { checksum^=(integerhex(it)) }
        log.debug "checksum, post-start: $checksum"
        end.each { checksum^=(integerhex(it)) }
        log.debug "checksum, post-end: $checksum"    
        checksum^=light
        log.debug "checksum, post-light: $checksum"    
        log.debug "${start.join()}${String.format("%02X",light)}${end.join()}${String.format("%02X",checksum)}"
        sendHubCommand(new hubitat.device.HubAction("${start.join()}${String.format("%02X",light)}${end.join()}${String.format("%02X",checksum)}"))
    }  
    

I know there was some early discussion of sendHubCommand() not working for devices, but I think that's since been remedied? I did also try just "returning" the new hubitat.device.HubAction() at the end instead (both with and without an explicit "return" keyword), to no effect.

IIRC, even ygelfand had inherited this code, which relied on some hackery to arrive at the construction of that command (since Leviton remains maddeningly proprietary and non-cooperative). But I can't see why the same HubAction() value that works when coming from ST
wouldn't work when coming from HE?

In any case, I'd greatly appreciate any insights, suggestions, and/or pointers to relevant HE threads/docs. Thanks in advance...

1 Like

Regarding my second issue, I just noticed that the hub is generating log entries like so:

[sys:1] warn Unable to execute hubAction:91001D0D01FFFF00000A6A sent from Entrance 4-Button South, invalid or unspecified protocol

So it would seem that the HE hub is missing something here that the ST hub is understanding?

1 Like

For the first issue, parse the switch multilevel class as v1, zwave.parse(description,[0x26])
For the second specify the protocol for the hub action by adding this to the hubAction call:
',hubitat.device.Protocol.ZWAVE'

Wow, amazing...done and done. Many thanks, Mike!

I'll likely make further tweaks to the dimming response, but my app/driver are fully functional now - probably time to post the code.

I found @bobbyD's thread Custom Device Drivers [Wiki], which led me to @csteele's thread Inventory of User Device Drivers? - #8 by csteele, which took me to HubitatCommunity ยท GitHub. I see in GitHub - HubitatCommunity/HubitatPublic that:

...you need to be a Member first. Therefore, to add a repository, contact csteele-PD (c steele) ยท GitHub. Repositories can be anything from a Readme pointing to your personal site to a multi-contributor, versioned driver or app! For more information, check out the forums at: https://community.hubitat.com/

I'll drop a direct message to @csteele straight away...

4 Likes

For the future... if any developer wants access, I really just need to know your Github ID to send an invite to the right person :slight_smile: PM me your GitHub ID and I'll send an Invite.

3 Likes

Thanks, @csteele -- I've put the Leviton VRCZ4-M04 Zone Controller App and Driver up on my personal github site, with an intermediate Readme pointer at GitHub - HubitatCommunity/mpk: Hubitat Elevation Apps and Drivers.

Let me know (@bobbyD?) if/how I should update Community Apps, Community Device Drivers (AKA Compatible Devices Wiki), or any other listings? Looks like it's de rigueur to point from these lists to a new [RELEASE] topic in category Apps and Drivers > Community Apps (or Community Drivers), or Developers > Code Share (with tag community_app or community_driver)? Also unsure which category would be most appropriate under the Apps list?

2 Likes

I'm porting over a ST app and having trouble, what's the hubitat equivalent of this ST URL?

def redirectUrl = "https://graph.api.smartthings.com/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${getApiServerUrl()}"

Here's the app I'm porting:

Answer is probably here: App OAuth - Hubitat Documentation

Take a look at this package for the Honeywell Lyric and T-series thermostats. It's using the same Honeywell API as the leak sensors. It should answer any questions you might have:

GitHub - thecloudtaylor/hubitat-honeywell

Not a lot of luck here. I was able to get the new app built over at the development section of Honeywell. Copied the codes from there into the HUbitat app. Logged in to y account, picked the water sensors. Clicked done, hit the discovery button on the app...nothing. Tried the discovery button on the regular devices...Nothing

How does the "capability geolocation" from dth's translate in Hubitat?

capability "Switch"
capability "Lock"
capability "Refresh"
capability "Power Meter"
capability "Sensor"
capability "Actuator"
capability "Geolocation"

It doesn't?

https://docs.hubitat.com/index.php?title=Driver_Capability_List

Not that I've been able to find.

1 Like

I didn't use my words very well.

I meant that I don't think that capability, or any equivalent, exists in hubitat.

Ok, gotcha. Guess I'll have to make something up then.

A post was split to a new topic: Need help porting app