Not that Iβm aware of it, but I had Gemini and Claude AI (and maybe ChatGPT was helping too) write the driver below for an air pressure sensor that also provides temperature readings. Since temp is a standard Hubitat capability, the Illuminance capability (which was spoofed to get LIDAR distance data into Hubitat for another project of mine) could be dropped. You would also be able to drop the pressure capability, just retaining the temp capability and simplifying the driver user interface.
/*
Zigbee Air Pressure Sensor Driver
Author: Designed by John Land, adapted by Gemini & Claude AI
Version: 2.8a
Date: 2026-05-20
Description: Driver for ESP32-C6 Zigbee connected to a QMP6988 barometric pressure sensor.
Updated to include standard PressureMeasurement capability for 3rd party app compatibility.
v2.8a: Cleanup pass on v2.8.
- SLP attributes now actively reset to their disabled placeholder when
elevationFeet is set back to 0, instead of silently retaining stale values.
- Zigbee reporting config now clamps maxReportInterval and tempMaxReportInterval
to be at least the effective minimum interval, preventing an invalid
min > max configuration from reaching the coordinator.
- Health timeout preference range changed to 30..1440 to match the 30-minute
scheduler granularity; sub-30-minute values were accepted but never honoured.
- logsOff() is now also scheduled in installed(), not only in updated().
- Removed unused state.lastCheckinEventMs (initialised but never read).
- Removed redundant state.lastRawPressure (always identical to state.lastPressure).
- Added explanatory comment on the EP12 / 0x0006 read in configure().
v2.8: Adds Station Elevation preference (feet) and sea-level pressure (SLP / QNH)
computation using the standard hypsometric formula:
SLP = P_station Γ (T_K / (T_K β 0.0065 Γ h)) ^ 5.2559
where T_K is the measured temperature in Kelvin and h is elevation in metres.
Uses the live sensor temperature when available; falls back to ISA standard
15 Β°C if a pressure report arrives before any temperature report has been received.
SLP is recomputed whenever either pressure or temperature is received.
New attributes: slpPressure (in selected display unit) and SLP Summary
(all-units breakdown + station pressure + elevation + temperature used).
No sketch changes required β elevation is a driver-side preference only.
Also fixes a pre-existing gap where Pressure Summary was not refreshed when
preferences (unit or offset) were changed without a new reading arriving.
v2.6a: Adds Pressure Offset (hPa) preference. The raw EP11 pressure report
remains the stored source value; the offset is applied before unit
conversion for pressure, Selected Display, and Pressure Summary.
*/
metadata {
definition (name: "Zigbee Air Pressure Sensor", namespace: "John Land v. 2.8", author: "John Land") {
capability "Sensor"
capability "IlluminanceMeasurement" // For Zigbee compatibility / pressure transport
capability "TemperatureMeasurement" // Standard capability
capability "PressureMeasurement" // Standard pressure capability
// Standard pressure attribute
attribute "pressure", "number"
// Sea-level pressure (QNH) β computed from station pressure + elevation preference
attribute "slpPressure", "number" // Sea-level equivalent pressure in selected unit
attribute "SLP Summary", "string" // All-units SLP + station + elevation + temperature used
// Summary / display attributes
attribute "Selected Display", "number"
attribute "Pressure Summary", "string"
attribute "Temperature Summary", "string"
// Device info
attribute "firmwareVersion", "string"
// Health monitoring
attribute "healthStatus", "enum", ["online", "offline"]
attribute "lastCheckin", "string"
// WiFi mode control
command "enableWiFiMode"
command "refresh"
fingerprint profileId: "0104", inClusters: "0000,0003,0400,0402,0006", outClusters: "", manufacturer: "Espressif", deviceJoinName: "Zigbee Barometric Pressure Sensor"
fingerprint profileId: "0104", inClusters: "0000,0003,0400,0402,0006", outClusters: "", manufacturer: "Arduino", deviceJoinName: "Zigbee Barometric Pressure Sensor"
}
preferences {
input name: "unitPreference", type: "enum", title: "Pressure Display Units",
options: ["Pa": "Pascals", "hPa": "Hectopascals / mbar", "inHg": "in Hg", "mmHg": "mm Hg", "psi": "PSI"],
defaultValue: "hPa", required: true
input name: "pressureOffsetHpa", type: "decimal",
title: "Pressure Offset (hPa) β positive raises reported pressure; negative lowers it",
defaultValue: 0.0
input name: "elevationFeet", type: "decimal",
title: "Station Elevation (feet) β used to compute sea-level pressure (QNH/SLP) via the hypsometric formula; 0 = disabled",
defaultValue: 0.0
input name: "minReportInterval", type: "number",
title: "Pressure Min Report Interval (seconds) β minimum 60",
defaultValue: 60, range: "60..3600"
input name: "maxReportInterval", type: "number", title: "Pressure Max Report Interval (seconds)",
defaultValue: 300, range: "1..3600"
input name: "rawReportDelta", type: "number", title: "Pressure Report Delta (0.1 hPa units)",
defaultValue: 1, range: "1..100"
input name: "tempMinReportInterval", type: "number",
title: "Temperature Min Report Interval (seconds) β minimum 60",
defaultValue: 60, range: "60..3600"
input name: "tempMaxReportInterval", type: "number", title: "Temperature Max Report Interval (seconds)",
defaultValue: 300, range: "1..3600"
input name: "tempReportDeltaDeciC", type: "number",
title: "Temperature Report Delta (0.1 Β°C steps)",
defaultValue: 5, range: "1..100"
input name: "healthTimeoutMinutes", type: "number", title: "Health Timeout (minutes) β minimum 30 (health check runs every 30 minutes)",
defaultValue: 30, range: "30..1440"
input name: "tempUnitPreference", type: "enum", title: "Temperature Display Units",
options: ["C": "Celsius (Β°C)", "F": "Fahrenheit (Β°F)"],
defaultValue: "F", required: true
input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
}
}
def installed() {
log.info "Zigbee Barometric Pressure Sensor installed"
if (logEnable) runIn(1800, "logsOff")
sendEvent(name: "Selected Display", value: 0, unit: getPressureUnitLabel())
sendEvent(name: "slpPressure", value: 0, unit: getPressureUnitLabel())
sendEvent(name: "SLP Summary", value: "Set Station Elevation in preferences to enable sea-level pressure")
sendEvent(name: "healthStatus", value: "offline")
sendEvent(name: "firmwareVersion", value: "unknown")
state.lastObservation = 0L
clearObsoleteCurrentStates()
scheduleHealthCheck()
return configure()
}
def updated() {
log.info "Zigbee Barometric Pressure Sensor updated"
if (logEnable) runIn(1800, "logsOff")
if (device.currentValue("healthStatus") == null) sendEvent(name: "healthStatus", value: "offline")
if (device.currentValue("firmwareVersion") == null) sendEvent(name: "firmwareVersion", value: "unknown")
if (device.currentValue("slpPressure") == null) sendEvent(name: "slpPressure", value: 0, unit: getPressureUnitLabel())
if (device.currentValue("SLP Summary") == null) sendEvent(name: "SLP Summary", value: "Set Station Elevation in preferences to enable sea-level pressure")
clearObsoleteCurrentStates()
scheduleHealthCheck()
updateSelectedPressureDisplay()
updateSelectedPressureAttribute()
updatePressureSummary()
updateSLPPressure()
updateSelectedTemperatureAttribute()
updateTemperatureSummary()
republishLastCheckin()
return configure()
}
def configure() {
log.info "Configuring Zigbee Barometric Pressure Sensor..."
int effPressureMin = Math.max((minReportInterval ?: 60).toInteger(), 60)
int effTempMin = Math.max((tempMinReportInterval ?: 60).toInteger(), 60)
int effPressureMax = Math.max((maxReportInterval ?: 300).toInteger(), effPressureMin)
int effTempMax = Math.max((tempMaxReportInterval ?: 300).toInteger(), effTempMin)
def cmds = []
cmds += zigbee.readAttribute(0x0000, 0x0005, [destEndpoint: 10])
cmds += zigbee.configureReporting(0x0400, 0x0000, 0x21,
effPressureMin,
effPressureMax,
(rawReportDelta ?: 1).toInteger(),
[destEndpoint: 11])
cmds += zigbee.readAttribute(0x0400, 0x0000, [destEndpoint: 11])
cmds += zigbee.configureReporting(0x0402, 0x0000, 0x29,
effTempMin,
effTempMax,
((tempReportDeltaDeciC ?: 5) * 10).toInteger(),
[destEndpoint: 13])
cmds += zigbee.readAttribute(0x0402, 0x0000, [destEndpoint: 13])
// Read EP12 On/Off state at configure time to confirm the device is reachable.
// The result is filtered out in parse() and does not update any attribute;
// it serves only as a handshake ping on initial setup / re-pair.
cmds += zigbee.readAttribute(0x0006, 0x0000, [destEndpoint: 12])
return cmds
}
def refresh() {
if (logEnable) log.debug "Refreshing pressure and temperature readings..."
def cmds = []
cmds += zigbee.readAttribute(0x0000, 0x0005, [destEndpoint: 10])
applyFirmwareFromDeviceData()
cmds += zigbee.readAttribute(0x0400, 0x0000, [destEndpoint: 11])
cmds += zigbee.readAttribute(0x0402, 0x0000, [destEndpoint: 13])
return cmds
}
def parse(String description) {
if (logEnable) log.debug "Parsing: ${description}"
markOnline()
def descMap = zigbee.parseDescriptionAsMap(description)
def ep = descMap?.endpoint ? Integer.parseInt(descMap.endpoint, 16) : 0
if ((descMap?.cluster == "0000" || descMap?.clusterInt == 0x0000) &&
(descMap?.attrId == "0005" || descMap?.attrInt == 0x0005)) {
def hexVal = descMap.value ?: ""
def modelString = ""
try {
def hexData = hexVal.length() > 2 ? hexVal.substring(2) : hexVal
for (int i = 0; i + 1 < hexData.length(); i += 2) {
modelString += (char) Integer.parseInt(hexData.substring(i, i + 2), 16)
}
} catch (Exception e) {
modelString = hexVal
}
if (modelString) {
state.lastModelString = modelString
String fwVersion = extractFirmwareFromModelString(modelString) ?: "unknown"
sendEvent(name: "firmwareVersion", value: fwVersion, displayed: true)
}
return
}
if (descMap?.clusterInt == 0x0402 && descMap?.attrInt == 0x0000 && ep == 13) {
Integer raw = Integer.parseInt(descMap.value, 16)
processTemperature(raw)
return
}
if (descMap?.clusterInt == 0x0400 && descMap?.attrInt == 0x0000) {
if (ep == 11) {
Integer encoded = Integer.parseInt(descMap.value, 16)
processPressure(encoded)
}
return
}
def result = zigbee.getEvent(description)
if (result && result.name != "switch" && result.name != "illuminance" && result.name != "temperature") return result
}
def processPressure(Integer encodedPressure) {
// EP11 reports pressure in 0.1 hPa units. Keep the raw encoded value so
// preference changes can be re-applied without compounding any correction.
state.lastPressure = encodedPressure
def values = getPressureValues(encodedPressure)
// Standard pressure attribute now follows the selected pressure unit preference
sendEventIfChanged("pressure", getPreferredDisplayValue(values), getPressureUnitLabel())
// Summary / display attributes
sendEventIfChanged("Pressure Summary", buildPressureSummary(values), null, false)
updateSelectedPressureDisplay(values)
updateSLPPressure(values.hPa)
if (logEnable) {
log.debug "Pressure: raw ${values.rawHPa} hPa, offset ${formatSignedHpa(values.offsetHPa)} β ${values.hPa} hPa"
}
}
def processTemperature(Integer raw) {
int signed = raw > 32767 ? raw - 65536 : raw
BigDecimal tempC = roundValue(signed / 100.0, 2)
BigDecimal tempF = roundValue((tempC * 9.0 / 5.0) + 32.0, 1)
state.lastTempC = tempC
state.lastTempF = tempF
updateSelectedTemperatureAttribute(tempC, tempF)
updateTemperatureSummary(tempC, tempF)
updateSLPPressure()
}
def enableWiFiMode() {
log.info "Sending WiFi-only mode request to device..."
return zigbee.command(0x0006, 0x01, [destEndpoint: 12])
}
private void clearObsoleteCurrentStates() {
[
"(01) Pascals",
"(02) Hectopascals",
"(03) in Hg",
"(04) mm Hg",
"(05) PSI",
"Temperature C",
"Temperature F",
"(06) Raw Pascals",
"(07) Raw Hectopascals",
"(08) Raw in Hg",
"(09) Raw mm Hg",
"(10) Raw PSI",
"Raw Summary"
].each { attrName ->
try { device.deleteCurrentState(attrName) } catch (Exception ignored) { }
}
}
def scheduleHealthCheck() {
unschedule("deviceHealthCheck")
runEvery30Minutes("deviceHealthCheck")
}
def markOnline() {
Long nowMs = now()
state.lastObservation = nowMs
updateLastCheckin(nowMs)
if (device.currentValue("healthStatus") != "online") {
sendEvent(name: "healthStatus", value: "online")
}
}
def deviceHealthCheck() {
Long lastObservation = (state.lastObservation ?: 0L) as Long
Integer timeoutMinutes = (settings.healthTimeoutMinutes ?: 30) as Integer
Long timeoutMs = timeoutMinutes * 60L * 1000L
if (lastObservation == 0L || (now() - lastObservation) >= timeoutMs) {
if (device.currentValue("healthStatus") != "offline") {
sendEvent(name: "healthStatus", value: "offline")
}
}
}
private Map getPressureValues(Integer encodedPressure) {
BigDecimal rawHPa = roundValue(encodedPressure / 10.0, 1)
BigDecimal offsetHPa = getPressureOffsetHpa()
BigDecimal hPaValue = roundValue(rawHPa + offsetHPa, 1)
return [
pa : (hPaValue * 100).setScale(0, BigDecimal.ROUND_HALF_UP).intValue(),
hPa : hPaValue,
inHg : roundValue(hPaValue * 0.0295299830714, 3),
mmHg : roundValue(hPaValue * 0.750061683, 1),
psi : roundValue(hPaValue * 0.0145037738, 3),
rawHPa : rawHPa,
offsetHPa: offsetHPa
]
}
private Map slpValuesFromHpa(BigDecimal hPaValue) {
// Converts a final SLP hPa value to all display units.
// Unlike getPressureValues(), no offset is applied β SLP is already the final value.
BigDecimal rounded = roundValue(hPaValue, 1)
return [
pa : (rounded * 100).setScale(0, BigDecimal.ROUND_HALF_UP).intValue(),
hPa : rounded,
inHg: roundValue(rounded * 0.0295299830714, 3),
mmHg: roundValue(rounded * 0.750061683, 1),
psi : roundValue(rounded * 0.0145037738, 3)
]
}
private void updateSelectedPressureDisplay(Map values = null) {
if (values == null) {
Integer encoded = state.lastPressure != null ? (state.lastPressure as Integer) : null
if (encoded == null) return
values = getPressureValues(encoded)
}
def displayValue = getPreferredDisplayValue(values)
sendEventIfChanged("Selected Display", displayValue, getPressureUnitLabel())
}
private void updateSelectedPressureAttribute(Map values = null) {
if (values == null) {
Integer encoded = state.lastPressure != null ? (state.lastPressure as Integer) : null
if (encoded == null) return
values = getPressureValues(encoded)
}
sendEventIfChanged("pressure", getPreferredDisplayValue(values), getPressureUnitLabel())
}
private void updatePressureSummary(Map values = null) {
if (values == null) {
Integer encoded = state.lastPressure != null ? (state.lastPressure as Integer) : null
if (encoded == null) return
values = getPressureValues(encoded)
}
sendEventIfChanged("Pressure Summary", buildPressureSummary(values), null, false)
}
private void updateSelectedTemperatureAttribute(BigDecimal tempC = null, BigDecimal tempF = null) {
if (tempC == null || tempF == null) {
if (state.lastTempC == null || state.lastTempF == null) return
tempC = state.lastTempC as BigDecimal
tempF = state.lastTempF as BigDecimal
}
if ((tempUnitPreference ?: "F") == "F") {
sendEventIfChanged("temperature", tempF, "Β°F")
} else {
sendEventIfChanged("temperature", tempC, "Β°C")
}
}
private getPreferredDisplayValue(Map values) {
switch(unitPreference) {
case "Pa" : return values.pa
case "inHg": return values.inHg
case "mmHg": return values.mmHg
case "psi" : return values.psi
default: return values.hPa
}
}
private String getPressureUnitLabel() {
switch(unitPreference) {
case "Pa" : return "Pa"
case "inHg": return "inHg"
case "mmHg": return "mmHg"
case "psi" : return "psi"
default: return "hPa"
}
}
private String buildPressureSummary(Map values) {
String summary = "${values.pa} Pa | ${values.hPa} hPa | ${values.inHg} inHg | ${values.mmHg} mmHg | ${values.psi} psi"
if (values.offsetHPa != null && (values.offsetHPa as BigDecimal).compareTo(BigDecimal.ZERO) != 0) {
summary += " | raw ${values.rawHPa} hPa | offset ${formatSignedHpa(values.offsetHPa)}"
}
return summary
}
private BigDecimal getPressureOffsetHpa() {
return roundValue(settings.pressureOffsetHpa ?: 0.0, 1)
}
private String formatSignedHpa(value) {
BigDecimal hPa = value instanceof BigDecimal ? value : roundValue(value ?: 0.0, 1)
String sign = hPa.compareTo(BigDecimal.ZERO) > 0 ? "+" : ""
return "${sign}${hPa} hPa"
}
// ββ Sea-level pressure (SLP / QNH) helpers βββββββββββββββββββββββββββββββββββ
private BigDecimal computeSLP(BigDecimal stationHpa, BigDecimal tempC) {
// Returns null when elevation is not configured (0 or unset) β SLP is disabled.
def elevFeetSetting = settings.elevationFeet
if (elevFeetSetting == null || (elevFeetSetting as BigDecimal) == 0) return null
BigDecimal elevM = roundValue(
new BigDecimal(elevFeetSetting.toString()) * new BigDecimal("0.3048"), 2)
// Standard hypsometric formula:
// SLP = P_station Γ (T_K / (T_K β 0.0065 Γ h)) ^ 5.2559
// where T_K is temperature in Kelvin and h is elevation in metres.
// Uses live sensor temperature when available; falls back to ISA 15 Β°C.
BigDecimal T_K = (tempC ?: new BigDecimal("15.0")) + new BigDecimal("273.15")
double ratio = T_K.doubleValue() / (T_K.doubleValue() - 0.0065 * elevM.doubleValue())
double slpHpa = stationHpa.doubleValue() * Math.pow(ratio, 5.2559)
return roundValue(slpHpa, 1)
}
private void updateSLPPressure(BigDecimal stationHpa = null) {
// When called without an argument (from processTemperature or updated),
// reconstruct the corrected station pressure from the stored encoded integer.
if (stationHpa == null) {
Integer encoded = state.lastPressure != null ? (state.lastPressure as Integer) : null
if (encoded == null) return
stationHpa = roundValue(encoded / 10.0, 1) + getPressureOffsetHpa()
}
BigDecimal tempC = state.lastTempC != null ? (state.lastTempC as BigDecimal) : null
BigDecimal slpHpa = computeSLP(stationHpa, tempC)
if (slpHpa == null) {
// Elevation is 0 or unset β actively clear any previously published SLP values
// so stale data is not left in Current States after the user disables elevation.
state.remove("lastSLPHpa")
sendEventIfChanged("slpPressure", 0, getPressureUnitLabel())
sendEventIfChanged("SLP Summary", "Set Station Elevation in preferences to enable sea-level pressure", null, false)
return
}
state.lastSLPHpa = slpHpa
Map slpValues = slpValuesFromHpa(slpHpa)
String unitLabel = getPressureUnitLabel()
def elevFeet = settings.elevationFeet
BigDecimal elevM = roundValue(new BigDecimal(elevFeet.toString()) * new BigDecimal("0.3048"), 1)
String tempUsed = tempC != null ? "${tempC} Β°C" : "15.0 Β°C (ISA default)"
sendEventIfChanged("slpPressure", getPreferredDisplayValue(slpValues), unitLabel)
sendEventIfChanged("SLP Summary",
"SLP: ${slpValues.pa} Pa | ${slpValues.hPa} hPa | ${slpValues.inHg} inHg | ${slpValues.mmHg} mmHg | ${slpValues.psi} psi" +
" | Station: ${stationHpa} hPa | Elev: ${elevFeet} ft (${elevM} m) | Temp: ${tempUsed}",
null, false)
if (logEnable) log.debug "SLP: ${stationHpa} hPa station + ${elevFeet} ft (${elevM} m) + ${tempUsed} β ${slpHpa} hPa"
}
// ββ Temperature summary βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void updateTemperatureSummary(BigDecimal tempC = null, BigDecimal tempF = null) {
if (tempC == null || tempF == null) {
if (state.lastTempC == null || state.lastTempF == null) return
tempC = state.lastTempC as BigDecimal
tempF = state.lastTempF as BigDecimal
}
sendEventIfChanged("Temperature Summary", buildTemperatureSummary(tempC, tempF), null, false)
}
private String buildTemperatureSummary(BigDecimal tempC, BigDecimal tempF) {
return "${tempC} Β°C | ${tempF} Β°F"
}
// ββ Timestamp / firmware helpers ββββββββββββββββββββββββββββββββββββββββββββββ
private void updateLastCheckin(Long tsMs) {
if (tsMs == null || tsMs <= 0L) return
sendEventIfChanged("lastCheckin", formatTimestamp(tsMs))
}
private void republishLastCheckin() {
Long tsMs = (state.lastObservation ?: 0L) as Long
if (tsMs > 0L) {
updateLastCheckin(tsMs)
}
}
private String formatTimestamp(Long tsMs) {
TimeZone tz = location?.timeZone ?: TimeZone.getDefault()
return new Date(tsMs).format("yyyy-MM-dd HH:mm:ss", tz)
}
private void applyFirmwareFromDeviceData() {
String model = device.getDataValue("model") ?: ""
if (model) {
String fwVersion = extractFirmwareFromModelString(model) ?: "unknown"
sendEvent(name: "firmwareVersion", value: fwVersion, displayed: true)
}
}
private String extractFirmwareFromModelString(String modelString) {
if (!modelString) return null
if (modelString.contains("_v")) {
return modelString.split("_v")[1].replace("_", " ").trim()
}
return modelString.replace("_", " ").trim()
}
// ββ Utility βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private BigDecimal roundValue(value, Integer places) {
return new BigDecimal(value.toString()).setScale(places, BigDecimal.ROUND_HALF_UP)
}
private void sendEventIfChanged(String name, value, String unit = null, boolean forceInfoLog = false) {
def current = device.currentValue(name)
String newStr = value == null ? null : value.toString()
String curStr = current == null ? null : current.toString()
if (forceInfoLog || curStr != newStr) {
Map evt = [name: name, value: value]
if (unit != null) evt.unit = unit
sendEvent(evt)
if (forceInfoLog || logEnable) log.info "${name}: ${value}${unit ? ' ' + unit : ''}"
}
}
def logsOff() {
log.warn "Debug logging disabled..."
device.updateSetting("logEnable", [value: "false", type: "bool"])
}