[Release] Sonoff Orb Button (SNZB-01M) Driver (first attempt with Chat GPT)

I got the Sonoff Orb Button to try today. It's a 4 button zigbee button that looks very similar to the Hue Tap Dial but does not have the rotating ring. These are about half the price of the Hue Tap Dial so I was interested to try them. They're available on Amazon - link to UK listing:

First impressions:

  • Feels solid/looks good - very similar in appearance to Hue Tap Dial
  • Has a magnetic base as the Hue device
  • The backplate is perfect in size to replace a UK single gang light switch whereas the Hue is a tiny bit undersized.

I've got a driver working with Chat GPT which is pasted below for anyone who's interested. It supports push, double-push, triple-push and held.

This is my first attempt at a driver so it's likely there may be issues but I've done some tests and it seems to be working correctly.

Known issues:

  • Triple tap is not available in Rule Machine as a trigger. The work around is to set the trigger as Custom Attribute, pick the device, select the 'lastAction' attribute, use 'contains' for the comparison with value of 'triple-tapped'

  • 'Release' is not supported by the device

Here is the code:

/*
 * Sonoff SNZB-01M Zigbee Button
 * 4 endpoints = 4 buttons
 */

import groovy.transform.Field

@Field static final String DRIVER_VERSION = "1.0.3"
@Field static final String DRIVER_DATE    = "2026-01-12"

metadata {
    definition(
        name: "Sonoff Orb (SNZB-01M) Button",
        namespace: "John Williamson",
        author: "John Williamson with ChatGPT"
    ) {
        capability "PushableButton"
        capability "DoubleTapableButton"
        capability "HoldableButton"
        capability "Battery"
        capability "Configuration"
        capability "Initialize"

        attribute "lastAction", "string"
        attribute "driverVersion", "string"
        attribute "driverDate", "string"

        fingerprint profileId: "0104",
                    inClusters: "0000,0001,0003,0020,FC12",
                    outClusters: "0003,0004,0005,0006,0008,0019,1000",
                    manufacturer: "SONOFF",
                    model: "SNZB-01M",
                    deviceJoinName: "Sonoff SNZB-01M Button"
    }

    preferences {
        input name: "enableDebug", type: "bool",
              title: "Enable debug logging", defaultValue: false

        input name: "enableDescriptionText", type: "bool",
              title: "Enable description text & Live Log logging",
              defaultValue: true
    }
}

/* ------------------------------------------------------------ */

void installed() {
    log.info "${device.displayName} installed (v${DRIVER_VERSION}, ${DRIVER_DATE})"
    sendEvent(name: "driverVersion", value: DRIVER_VERSION)
    sendEvent(name: "driverDate", value: DRIVER_DATE)
    initialize()
}

void updated() {
    log.info "${device.displayName} updated (v${DRIVER_VERSION}, ${DRIVER_DATE})"
    sendEvent(name: "driverVersion", value: DRIVER_VERSION)
    sendEvent(name: "driverDate", value: DRIVER_DATE)
}

void initialize() {
    sendEvent(name: "numberOfButtons", value: 4)
}

/* ------------------------------------------------------------ */
/* Capability command handlers (Device page / apps)             */
/* ------------------------------------------------------------ */

void push(BigDecimal button) {
    handleAction(button.intValue(), 0x01)
}

void doubleTap(BigDecimal button) {
    handleAction(button.intValue(), 0x02)
}

void hold(BigDecimal button) {
    handleAction(button.intValue(), 0x03)
}

/* ------------------------------------------------------------ */

void parse(String description) {
    Map descMap = zigbee.parseDescriptionAsMap(description)
    if (!descMap) return

    if (enableDebug) log.debug "Parsed: ${descMap}"

    // Battery cluster
    if (descMap.clusterInt == 0x0001) {
        handleBattery(descMap)
        return
    }

    // Manufacturer-specific FC12 cluster
    if (descMap.clusterInt == 0xFC12 && descMap.command == "0A") {
        handleFC12(descMap)
    }
}

/* ------------------------------------------------------------ */
/* Battery handling                                             */
/* ------------------------------------------------------------ */

private void handleBattery(Map descMap) {
    if (!descMap.attrInt || !descMap.value) return

    Integer rawValue = Integer.parseInt(descMap.value, 16)

    // Battery percentage (0x0021) – half-percent units
    if (descMap.attrInt == 0x0021) {
        Integer pct = Math.round(rawValue / 2)
        pct = Math.max(0, Math.min(100, pct))

        sendEvent(name: "battery", value: pct, unit: "%")

        if (enableDescriptionText) {
            log.info "${device.displayName} battery ${pct}%"
        }
        return
    }

    // Battery voltage (0x0020) – 0.1V units
    if (descMap.attrInt == 0x0020) {
        BigDecimal volts = rawValue / 10.0
        Integer pct = voltageToPercent(volts)

        sendEvent(name: "battery", value: pct, unit: "%")

        if (enableDescriptionText) {
            log.info "${device.displayName} battery ${pct}% (${volts} V)"
        }
    }
}

private Integer voltageToPercent(BigDecimal volts) {
    BigDecimal minV = 2.6
    BigDecimal maxV = 3.0

    if (volts <= minV) return 0
    if (volts >= maxV) return 100

    return ((volts - minV) / (maxV - minV) * 100)
        .setScale(0, BigDecimal.ROUND_HALF_UP)
        .intValue()
}

/* ------------------------------------------------------------ */
/* Button handling (FC12 cluster)                                */
/* ------------------------------------------------------------ */

private void handleFC12(Map descMap) {
    Integer button = descMap.endpoint ?
        Integer.parseInt(descMap.endpoint, 16) : 1

    Integer action = descMap.value ?
        Integer.parseInt(descMap.value, 16) : null

    if (action == null) return

    if (enableDebug)
        log.debug "FC12 action=${action} button=${button}"

    handleAction(button, action)
}

private void handleAction(Integer button, Integer action) {
    String dn = device.displayName
    Map evt = [value: button, isStateChange: true]

    switch (action) {
        case 0x01:
            evt.name = "pushed"
            evt.data = [presses: 1]
            evt.descriptionText = "${dn} button ${button} pushed"
            break

        case 0x02:
            evt.name = "doubleTapped"
            evt.descriptionText = "${dn} button ${button} double-tapped"
            break

        case 0x03:
            evt.name = "held"
            evt.descriptionText = "${dn} button ${button} held"
            break

        case 0x04:
            evt.name = "pushed"
            evt.data = [presses: 3]
            evt.descriptionText = "${dn} button ${button} triple-pushed"
            break

        default:
            if (enableDebug)
                log.warn "Unknown FC12 action ${action} for button ${button}"
            return
    }

    sendEvent(name: "lastAction", value: evt.descriptionText)
    sendEvent(evt)

    // Explicit Live Log logging (optional, v1.0.2 behavior)
    if (enableDescriptionText) {
        log.info evt.descriptionText
    }
}

/* ------------------------------------------------------------ */

void configure() {
    log.info "Configuring ${device.displayName}"
}
3 Likes

Congrats on your first published Hubitat device driver, John! :partying_face:

1 Like

lol - thanks. My previous attempts at using ChatGPT to code something failed miserably. It's usually produced something near what was needed and then I've broken it asking it to correct things. On this occasion providing ChatGPT with debug output from the logs was especially helpful.

I've updated the code with versioning and will continue to amend it in the first post as changes are made.

1 Like

Sinnce it uses a weird battery CR2477($$$), I'd be curious about battery life

Yeah it's the first time I've seen one of those. I just had a look on Amazon UK. Panasonic are expensive but there are some such as EEMB that are around £1

I have 2 older ST motion sensors that use those batteries, and that's the reason I stopped using them. The generic ones burned out too fast