I Finally Did It: Wood Stove Temperature Sensor

First I want to thank @djh_wolf and @nclark for the inspiration to finally get off my a$$ and do it. Plus all the others who helped me with HA and the like.

I got a SeeStudio ESP32c development chip with the lugs already soldered to the pins. A Max6675 thermocouple with a K-type probe. I used ESPHome Builder to flash the device and add to HA. And then HADB to bring it into Hubitat. A little problem with excess decimal places, but there’s a workaround.

Now I have to figure out how to package it, lol.

First time doing this. Made a least one stupid mistake, but it got done.

4 Likes

You might look into this board if you want a local temp readout and direct Zigbee connection to Hubitat:

2 Likes

Now you tell me, lol.

Very cool. There’s a Hubitat driver for it?

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

2 Likes

Note also that the Nesso has a mechanical front button (the greenish bar) and the display screen is a programmable (touch, hold, etc.) touch screen, so you could, for example, use the touch interface to toggle the screen on or off, and the front button to activate some other device/automation. The device can be more compact than shown in the image, because the bottom third can be removed (it stores the LoRa antenna).

1 Like

what's the temp range b4 it melts

1 Like

Don’t know, lol. The cable has a metallic jacket. Type K goes to, what, 1200F, not sure, but weak point is cable. The thermocouple probe I have now on my wood stove has seen 900F, lol. This one was cheap, and probably not long enough.

2 Likes

would be nice to put one in our sauna stove to warn me when I forget to put wood in soon enough and it starts going out. Rule would be complex though as I am not sure you can sense a temp drop soon enough to get there b4 there are not enough coals to keep it going .

currently I have a ecowitt wn34d mounted outside the sauna with probe inside to monitor room temp. I think it goes to 247f. I've had it up to about 225

Why not? The setup reacts pretty quick. I have refresh set for 10 seconds, for now.

1 Like

I’m more worried about over firing. Your sauna stove is likely very small. Burns hot and short.

because I am not sure stove temp drops that quickly even if fire damps down?

true stove is small .. not sure sure what the max temp is?

also cable length would be an issue. not sure sensor receiver would survive inside the sauna with the heat and moisture when I throw water on the rocks

I’ve got my existing probe (for the fan) on the top plate (no drilled hole, lol). If you had access to the flue you could drill a hole and put a probe in there, sort of what I did with the VW TDI and the EGT gauge.

That’s the next frontier for me…project box I guess would be the term. Although my environment isn’t that harsh.

ya that would work … manual flue on exhaust pipe coming up out of stove. but still don't think receiver/ electronic package is rated to be in the room

It might be better if the sensor was outside and the probe extended to the inside, even for more accurate readings since it gets so hot in there. I’m on shaky ground there however, re: thermocouples.

stove with flue when I last noticed a leak and put in new pipes

1 Like

It was cool here today. lol.

edit: Nice chart from HA. Haven’t install Easy Dash on HE. Are they any good?

1 Like

I used a 6’ thermocouple probe on my stove.

If I had access to the pipe, I’d drill a hole, insert the probe, and screw on a nut from the inside to hole it in place.

You’d get accurate and fast readings in that location, vs the hysteresis from the stove top (which is still not bad).

It’s cool being able to ask Siri what the wood stove temperature is…from a reclining position on the sofa.

1 Like

@nclark, maybe you could clue me in, somewhere, about how you did the battery monitoring. My BM-2 is nice and all, but it does phone home.