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}"
}