I just bought one of these on Amazon, and I did not find a driver that utilized all of it's capabilities, so I ask AI (Grok 4.5) to build one for me. This is what I got. It works for me.
I am not a programmer, so you will only find this here. It is not on GitHub, or Hubitat Package Manager.
Code follows ![]()
/**
- Sonoff SNZB-02M – Hubitat driver
- Temperature / Humidity / Pressure / Battery / Dew point / VPD / FC11
- Author: Grok 4.5, built by xAI.
- Based on community work for Sonoff SNZB-02M on Hubitat
- Endian handling: Hubitat parseDescriptionAsMap returns logical (BE) multi-byte values
- Capabilities:
-
- TemperatureMeasurement
-
- RelativeHumidityMeasurement
-
- PressureMeasurement
-
- Battery
-
- Sensor
-
- Refresh
-
- Configuration
-
- HealthCheck
- Hubitat parseDescriptionAsMap already returns multi-byte values in
- logical (big-endian) hex — do NOT little-endian swap.
*/
metadata {
definition(
name: "Sonoff SNZB-02M Temp Humidity Pressure",
namespace: "custom.sonoff",
author: "Custom",
importUrl: ""
) {
capability "Sensor"
capability "TemperatureMeasurement"
capability "RelativeHumidityMeasurement"
capability "PressureMeasurement"
capability "Battery"
capability "Refresh"
capability "Configuration"
capability "HealthCheck"
attribute "lastCheckin", "string"
attribute "healthStatus", "enum", ["online", "offline"]
attribute "dewPoint", "number"
attribute "vpd", "number"
attribute "temperatureCalibration", "number"
attribute "humidityCalibration", "number"
attribute "pressureCalibration", "number"
attribute "comfortTempMin", "number"
attribute "comfortTempMax", "number"
attribute "comfortHumidityMin", "number"
attribute "comfortHumidityMax", "number"
fingerprint profileId: "0104",
inClusters: "0000,0001,0003,0020,0402,0403,0405,FC57,FC11",
outClusters: "0019",
manufacturer: "SONOFF",
model: "SNZB-02M",
deviceJoinName: "Sonoff SNZB-02M"
}
preferences {
input name: "tempUnit", type: "enum", title: "Temperature unit",
options: ["C": "Celsius (°C)", "F": "Fahrenheit (°F)"],
defaultValue: "C", required: true
input name: "tempOffset", type: "decimal", title: "Temperature offset (°C)",
range: "-10..10", defaultValue: 0.0
input name: "humOffset", type: "decimal", title: "Humidity offset (%)",
range: "-20..20", defaultValue: 0.0
input name: "pressOffset", type: "decimal", title: "Pressure offset (hPa)",
range: "-50..50", defaultValue: 0.0
input name: "devTempCal", type: "decimal", title: "Device temp calibration (°C)",
range: "-20..60", defaultValue: 0.0
input name: "devHumCal", type: "decimal", title: "Device humidity calibration (%)",
range: "-20..20", defaultValue: 0.0
input name: "devPressCal", type: "decimal", title: "Device pressure calibration (hPa)",
range: "-400..400", defaultValue: 0.0
input name: "comfortTMin", type: "decimal", title: "Comfort temp min (°C)",
range: "-10..60", defaultValue: 19.0
input name: "comfortTMax", type: "decimal", title: "Comfort temp max (°C)",
range: "-10..60", defaultValue: 27.0
input name: "comfortHMin", type: "decimal", title: "Comfort humidity min (%)",
range: "0..100", defaultValue: 40.0
input name: "comfortHMax", type: "decimal", title: "Comfort humidity max (%)",
range: "0..100", defaultValue: 60.0
input name: "writeDeviceParams", type: "bool",
title: "Write FC11 settings on Configure",
description: "Leave OFF unless you intend to push values to the device",
defaultValue: false
input name: "logEnable", type: "bool", title: "Debug logging", defaultValue: false
input name: "txtEnable", type: "bool", title: "Description text logging", defaultValue: true
}
}
def installed() { initialize() }
def updated() {
unschedule()
initialize()
if (logEnable) runIn(1800, "logsOff")
if (state.tempC != null && saneTemp(state.tempC as double)) {
publishTemperature(state.tempC as double)
if (state.humidity != null) publishDewPointAndVpd(state.tempC as double, state.humidity as double)
}
}
def initialize() {
try {
sendEvent(name: "checkInterval", value: 12 * 60 * 60 + 2 * 60, displayed: false,
data: [protocol: "zigbee", hubHardwareId: device?.hub?.hardwareID])
} catch (Exception ignored) { }
sendEvent(name: "healthStatus", value: "online")
}
def configure() {
logInfo "configure() – wake sensor first"
def cmds =
cmds += zigbee.configureReporting(0x0402, 0x0000, DataType.INT16, 10, 3600, 20)
cmds += zigbee.configureReporting(0x0405, 0x0000, DataType.UINT16, 10, 3600, 100)
cmds += zigbee.configureReporting(0x0001, 0x0021, DataType.UINT8, 30, 21600, 1)
cmds += ["zdo bind 0x${device.deviceNetworkId} 0x01 0x01 0x0403 {${device.zigbeeId}} {}", "delay 200"]
cmds += zigbee.configureReporting(0x0403, 0x0000, DataType.INT16, 10, 3600, 10)
if (writeDeviceParams) cmds += writeFc11Preferences()
cmds += readMain()
return cmds
}
def refresh() {
logInfo "refresh()"
return readMain()
}
def ping() { return zigbee.readAttribute(0x0000, 0x0000) }
private List readMain() {
def cmds =
cmds += zigbee.readAttribute(0x0402, 0x0000)
cmds += zigbee.readAttribute(0x0405, 0x0000)
cmds += zigbee.readAttribute(0x0001, 0x0021)
cmds += zigbee.readAttribute(0x0403, 0x0000)
cmds += zigbee.readAttribute(0xFC11, 0x2003)
cmds += zigbee.readAttribute(0xFC11, 0x2004)
cmds += zigbee.readAttribute(0xFC11, 0x2007)
cmds += zigbee.readAttribute(0xFC11, 0x0003)
cmds += zigbee.readAttribute(0xFC11, 0x0004)
cmds += zigbee.readAttribute(0xFC11, 0x0005)
cmds += zigbee.readAttribute(0xFC11, 0x0006)
return cmds
}
private List writeFc11Preferences() {
logInfo "Writing FC11 settings"
def cmds =
// Hubitat wattr payload is little-endian on the wire
cmds += wFc11Int16(0x2003, (int) Math.round(prefD(devTempCal) * 100))
cmds += wFc11Int16(0x2004, (int) Math.round(prefD(devHumCal) * 100))
cmds += wFc11Int16(0x2007, (int) Math.round(prefD(devPressCal) * 100))
cmds += wFc11Int16(0x0003, (int) Math.round(prefD(comfortTMax, 27) * 100))
cmds += wFc11Int16(0x0004, (int) Math.round(prefD(comfortTMin, 19) * 100))
cmds += wFc11UInt16(0x0005, (int) Math.round(prefD(comfortHMin, 40) * 100))
cmds += wFc11UInt16(0x0006, (int) Math.round(prefD(comfortHMax, 60) * 100))
return cmds
}
private List wFc11Int16(int attr, int v) {
v = Math.max(-32768, Math.min(32767, v))
if (v < 0) v += 65536
String le = String.format("%02X%02X", v & 0xFF, (v >> 8) & 0xFF)
return ["he wattr 0x${device.deviceNetworkId} 0x01 0xFC11 0x${String.format('%04X', attr)} 0x29 {${le}}", "delay 300"]
}
private List wFc11UInt16(int attr, int v) {
v = Math.max(0, Math.min(65535, v))
String le = String.format("%02X%02X", v & 0xFF, (v >> 8) & 0xFF)
return ["he wattr 0x${device.deviceNetworkId} 0x01 0xFC11 0x${String.format('%04X', attr)} 0x21 {${le}}", "delay 300"]
}
def parse(String description) {
if (logEnable) log.debug "parse: ${description}"
if (description?.startsWith("catchall:")) return
def descMap
try {
descMap = zigbee.parseDescriptionAsMap(description)
} catch (Exception e) {
return
}
if (!descMap?.value || descMap.clusterInt == null || descMap.attrInt == null) return
sendEvent(name: "lastCheckin", value: new Date().format("yyyy-MM-dd HH:mm:ss"), displayed: false)
sendEvent(name: "healthStatus", value: "online")
int cluster = descMap.clusterInt as int
int attr = descMap.attrInt as int
// Logical hex from Hubitat (already endian-corrected). Multi-attr blobs: take leading bytes only.
String hex = (descMap.value as String).replaceAll("[^0-9A-Fa-f]", "")
// Temperature 0x0402 / 0x0000 — INT16, °C × 100
if (cluster == 0x0402 && attr == 0x0000) {
int raw = parseInt16(hex)
double c = raw / 100.0d + prefD(tempOffset)
if (!saneTemp(c)) {
if (logEnable) log.warn "Temp out of range value=${hex} -> ${c}°C (ignored)"
return
}
state.tempC = c
publishTemperature(c)
if (state.humidity != null) publishDewPointAndVpd(c, state.humidity as double)
return
}
// Humidity 0x0405 / 0x0000 — UINT16, % × 100
if (cluster == 0x0405 && attr == 0x0000) {
int raw = parseUInt16(hex)
double h = raw / 100.0d + prefD(humOffset)
h = Math.round(h * 10.0d) / 10.0d
if (h < 0.0d || h > 100.0d) {
if (logEnable) log.warn "Humidity out of range value=${hex} -> ${h}% (ignored)"
return
}
state.humidity = h
sendEvent(name: "humidity", value: h, unit: "%")
if (txtEnable) log.info "Humidity: ${h}%"
if (state.tempC != null) publishDewPointAndVpd(state.tempC as double, h)
return
}
// Pressure 0x0403 / 0x0000 — INT16, 0.1 kPa (numeric hPa)
if (cluster == 0x0403 && attr == 0x0000) {
int raw = parseInt16(hex)
double p = raw + prefD(pressOffset)
p = Math.round(p * 10.0d) / 10.0d
if (p < 300.0d || p > 1100.0d) {
if (logEnable) log.warn "Pressure out of range value=${hex} -> ${p} hPa (ignored)"
return
}
sendEvent(name: "pressure", value: p, unit: "hPa")
if (txtEnable) log.info "Pressure: ${p} hPa"
return
}
// Pressure manufacturer 0x0403 / 0x0004 — INT32 × 100
if (cluster == 0x0403 && attr == 0x0004) {
long raw = parseInt32(hex)
double p = raw / 100.0d + prefD(pressOffset)
p = Math.round(p * 10.0d) / 10.0d
if (p < 300.0d || p > 1100.0d) {
if (logEnable) log.warn "Pressure(mfg) out of range value=${hex} -> ${p} hPa (ignored)"
return
}
sendEvent(name: "pressure", value: p, unit: "hPa")
if (txtEnable) log.info "Pressure: ${p} hPa"
return
}
// Battery 0x0001 / 0x0021
if (cluster == 0x0001 && attr == 0x0021) {
int raw = Integer.parseInt(hex.length() >= 2 ? hex.substring(0, 2) : hex, 16)
int bat = (raw > 100) ? (int) Math.round(raw / 2.0d) : raw
bat = Math.max(0, Math.min(100, bat))
sendEvent(name: "battery", value: bat, unit: "%")
if (txtEnable) log.info "Battery: ${bat}%"
return
}
// FC11
if (cluster == 0xFC11) {
switch (attr) {
case 0x2003:
double v = parseInt16(hex) / 100.0d
sendEvent(name: "temperatureCalibration", value: v, unit: "°C")
if (txtEnable) log.info "Device temp calibration: ${v}°C"
break
case 0x2004:
double v = parseInt16(hex) / 100.0d
sendEvent(name: "humidityCalibration", value: v, unit: "%")
if (txtEnable) log.info "Device humidity calibration: ${v}%"
break
case 0x2007:
double v = parseInt16(hex) / 100.0d
sendEvent(name: "pressureCalibration", value: v, unit: "hPa")
if (txtEnable) log.info "Device pressure calibration: ${v} hPa"
break
case 0x0003:
double v = parseInt16(hex) / 100.0d
if (v >= -20 && v <= 80) {
sendEvent(name: "comfortTempMax", value: v, unit: "°C")
if (txtEnable) log.info "Comfort temp max: ${v}°C"
}
break
case 0x0004:
double v = parseInt16(hex) / 100.0d
if (v >= -20 && v <= 80) {
sendEvent(name: "comfortTempMin", value: v, unit: "°C")
if (txtEnable) log.info "Comfort temp min: ${v}°C"
}
break
case 0x0005:
double v = parseUInt16(hex) / 100.0d
if (v >= 0 && v <= 100) {
sendEvent(name: "comfortHumidityMin", value: v, unit: "%")
if (txtEnable) log.info "Comfort humidity min: ${v}%"
}
break
case 0x0006:
double v = parseUInt16(hex) / 100.0d
if (v >= 0 && v <= 100) {
sendEvent(name: "comfortHumidityMax", value: v, unit: "%")
if (txtEnable) log.info "Comfort humidity max: ${v}%"
}
break
}
return
}
}
// ---- Logical (Hubitat) multi-byte parse — no endian swap ----
private int parseUInt16(String hex) {
String h = hex.padLeft(4, "0").substring(0, 4)
return Integer.parseInt(h, 16) & 0xFFFF
}
private int parseInt16(String hex) {
int v = parseUInt16(hex)
return (v > 32767) ? (v - 65536) : v
}
private long parseInt32(String hex) {
String h = hex.padLeft(8, "0").substring(0, 8)
long v = Long.parseLong(h, 16)
if (v > 0x7FFFFFFFL) v -= 0x100000000L
return v
}
private boolean saneTemp(double c) { return c >= -40.0d && c <= 85.0d }
private void publishTemperature(double tempC) {
String u = (settings?.tempUnit ?: "C")
double t = (u == "F") ? (tempC * 1.8d + 32.0d) : tempC
t = Math.round(t * 10.0d) / 10.0d
String unit = (u == "F") ? "°F" : "°C"
sendEvent(name: "temperature", value: t, unit: unit)
if (txtEnable) log.info "Temperature: ${t}${unit}"
}
private void publishDewPointAndVpd(double tempC, double humidity) {
if (!saneTemp(tempC)) return
if (humidity < 0.0d || humidity > 100.0d) return
double svp = 0.61078d * Math.exp((17.27d * tempC) / (tempC + 237.3d))
double avp = (humidity / 100.0d) * svp
double vpd = Math.round((svp - avp) * 100.0d) / 100.0d
sendEvent(name: "vpd", value: vpd, unit: "kPa")
if (humidity <= 0.0d) return
double alpha = (17.27d * tempC) / (tempC + 237.7d) + Math.log(humidity / 100.0d)
double denom = 17.27d - alpha
if (Math.abs(denom) < 1e-6d) return
double dewC = (237.7d * alpha) / denom
if (!saneTemp(dewC)) return
dewC = Math.round(dewC * 10.0d) / 10.0d
String u = (settings?.tempUnit ?: "C")
double dew = (u == "F") ? (dewC * 1.8d + 32.0d) : dewC
dew = Math.round(dew * 10.0d) / 10.0d
String unit = (u == "F") ? "°F" : "°C"
sendEvent(name: "dewPoint", value: dew, unit: unit)
if (txtEnable) log.info "Dew point: ${dew}${unit}, VPD: ${vpd} kPa"
}
private double prefD(def p, double fb = 0.0d) {
if (p == null) return fb
try { return p as double } catch (Exception e) { return fb }
}
def logsOff() {
log.warn "Debug logging disabled"
device.updateSetting("logEnable", [value: "false", type: "bool"])
}
private void logInfo(String msg) {
if (txtEnable) log.info "${device.displayName} ${msg}"
}