Main

Reading CR2032 Voltage Curves: Why a Zigbee Sensor Reports 100% Until the Week It Dies

A Zigbee door sensor can report 100% battery for three years and then stop transmitting. That is not a bug in the sensor. It is what happens when a firmware-computed percentage is treated as a fuel gauge instead of what it is: a lossy, device-specific conversion of a voltage reading that sits on the flat top of a lithium coin cell’s discharge curve.

This article is about the diagnostic path I use when a battery-powered Zigbee device dies with a healthy-looking percentage, and the repeatable procedure that replaces percentage-based alerting with voltage-trend alerting and availability monitoring. It assumes a local-first stack: Home Assistant, Zigbee2MQTT, and MQTT, with no vendor cloud in the loop.

The failure pattern

The canonical report is in Zigbee2MQTT issue #8499. A user described a Xiaomi door/window sensor that had been in service for years and reported 100% battery until the last day before it died. The last voltage the sensor reported was 3035 mV. In the same thread, a contributor explained the mechanism: the sensor sends voltage, Zigbee2MQTT converts it to a percentage, and “over 3 V it will always show 100%.”

That is the whole story in two sentences. The percentage is a presentation layer. The voltage is the measurement. And near the top of a CR2032’s discharge curve, the voltage barely moves for most of the cell’s useful life.

Why the top of the curve is flat

Lithium manganese dioxide coin cells have a discharge curve that is close to flat across most of their capacity, then bends downward near end of life. The Sony CR2032 datasheet excerpt shows discharge characteristics plotted against a 2.5V cutoff at 23°C for 4.7k and 15k loads. The curve holds near nominal voltage for most of the discharge time and then falls steeply as the cell approaches cutoff.

That shape is why a linear voltage-to-percentage mapping produces a percentage that sits at or near 100% for most of the battery’s life and then collapses. A small voltage change near 3.0V maps to little or no percentage change. A small voltage change near 2.8V maps to a large percentage change. The percentage is not lying about the voltage; it is faithfully reporting a conversion that is nonlinear in practice and linear in code.

I want to be precise about what the datasheet does and does not tell us. It shows the shape of the curve under specific loads and a 2.5V cutoff. It does not tell you the cutoff voltage of your specific sensor’s radio and microcontroller, which is a separate number and often higher than 2.5V. That number has to be learned per device, or at least per device model, from observation.

The conversion layer is not one formula

Zigbee2MQTT publishes what the device sends. For most battery sensors, that is a voltage in millivolts. The percentage is computed by zigbee-herdsman-converters, and the mapping is not universal.

In Zigbee2MQTT issue #13814, the project owner stated that the battery percentage calculation was changed so that above 3000 mV is 100%, below 2800 mV is 0%, and values in between are linearly scaled. That is one mapping, quoted from a 2022 issue. It is not a guarantee about the device on your wall today.

Device definitions can override the mapping. In the discussion on zigbee-herdsman-converters pull request #7929, the project owner noted that Lumi devices use a voltageToPercentage min/max of 2850 and 3000 as an example, so the same voltage can map to different percentages across device types. The pull request itself proposed a revised table in which 3000 mV maps to 90% and 3200 mV maps to 100%, replacing an older table where 3000 mV already mapped to 100%. That pull request was closed as stale; I am citing it as evidence that the mapping is contested and version-dependent, not as a description of current behavior.

The practical consequence: do not build an alert on the percentage unless you have confirmed the mapping for your specific device definition and you are willing to re-verify it after every upgrade. Build the alert on the voltage.

Diagnostic path: find out what your sensor actually reports

Before changing anything, confirm what the device exposes. Some Zigbee sensors report voltage directly. Some report only a firmware-computed percentage. Some report nothing until they are queried. The procedure below assumes Zigbee2MQTT and Home Assistant, but the logic applies to any local-first stack.

  1. Open the device in Zigbee2MQTT. Look at the exposed attributes. If you see voltage in millivolts alongside battery in percent, you have the raw signal. If you see only battery, the device is not giving you the voltage and you will have to work with availability and time-in-service instead.

  2. Check the Home Assistant entity attributes. In Home Assistant, a sensor entity’s state is its current value, and attributes can carry related data. Open the entity’s details and look for a voltage attribute. If the integration exposes it, it will be there. If it does not, the entity is percentage-only.

  3. Look at history, not the current value. A sensor sitting at 100% today tells you nothing. Pull the history for the voltage attribute over the last 30 to 90 days. If the voltage has moved from 3.05V to 3.00V, you are on the flat part of the curve. If it has moved from 3.00V to 2.90V, you are approaching the knee.

  4. Record the last voltage before a known death. When a sensor dies, note the last voltage it reported. That number is your per-device floor, or at least a data point toward one. In issue #8499, one sensor’s last reported voltage was 3035 mV, which is unusually high and suggests that device’s radio cut out well before the cell was exhausted. That is exactly why a global threshold is a bad idea.

Procedure: log voltage, alert on trend

The goal is to keep the raw voltage in its own history path so the trend survives integration changes and conversion updates. Home Assistant’s sensor documentation notes that sensors with a state class are recorded in long-term statistics, and that the voltage device class is defined in V, mV, μV, kV, or MV. Use that.

Step 1: expose the voltage as its own entity

If Zigbee2MQTT publishes voltage as an attribute on the battery sensor, create a template sensor that copies it into a standalone entity with device_class: voltage and state_class: measurement. If the integration already creates a voltage entity, skip this step. The point is to have a numeric entity you can chart, threshold, and keep in statistics independent of the percentage entity.

If you are building the sensor side yourself with ESPHome, the ADC documentation is worth reading before you trust a voltage number. On the ESP8266, the chip pin sees 0.0V to 1.0V, and boards with an external voltage divider need a multiply filter to report the actual battery voltage. A mis-scaled divider produces a voltage trend that looks plausible and is wrong.

Step 2: set a voltage-floor alert per device

Do not alert on percentage crossing 20%. Alert on voltage crossing a floor you have learned for that device. A hypothetical example: if a sensor’s last reported voltage before death was 2.90V, alert at 2.95V. That gives you a margin of one reporting interval plus however long it takes you to buy and install a battery. If the sensor reports every few hours, 50 mV of margin may be days. If it reports once a day, 50 mV may be hours.

The floor is not a universal number. It depends on the device’s radio cutoff, the temperature, and the load profile. I have not seen enough cold-temperature data to state a rule for below 0°C; the Sony datasheet shows discharge characteristics at 0°C, 23°C, and 60°C, and the curve shifts, but how that maps to a specific sensor’s cutoff is device-specific. Treat cold-weather floors as uncertain until you have observed them.

Step 3: use availability as a second signal

Zigbee2MQTT’s availability feature marks battery-powered (passive) devices offline if they do not check in within a default of 25 hours. A check-in is any Zigbee message from the device that reaches Zigbee2MQTT, including internal updates that are not displayed. That means availability is a liveness signal independent of the battery percentage.

Treat “offline” as a reliability event on its own. A sensor that misses its 25-hour check-in is either out of range, out of battery, or broken. The voltage trend tells you which. If the voltage was trending down and the device goes offline, replace the battery. If the voltage was stable and the device goes offline, check the route and the coordinator before you replace anything.

One caveat from the availability documentation: the timeout is persisted between Zigbee2MQTT restarts, so a restart longer than the timeout will mark devices offline until they check in again. Do not treat a post-restart offline burst as a battery event.

Step 4: record time-in-service as a fallback

If the device does not expose voltage at all, you have two signals left: availability and time-in-service. Record the installation date, and set a reminder at a conservative fraction of the expected life. This is less precise than a voltage trend, but it is better than a percentage that never moves. For a sensor that has been in service for years and still reports 100%, time-in-service is the only honest signal you have.

Graceful degradation when the sensor drops

The reason this matters is not the battery. It is what the automation does when the sensor stops reporting. A door sensor that dies silently can leave a heating automation running, an alarm zone unarmed, or a light schedule stuck. The battery alert is the maintenance task; the degradation behavior is the reliability engineering.

Two rules I use:

  • Never let a battery-powered sensor be the only input to a safety or access decision. If the sensor is offline, the automation should fail to a known state, not to the last state. A door sensor that is unavailable should not be treated as “closed.”

  • Make the offline state visible. A dashboard tile that shows the sensor as unavailable is worth more than an alert that fires once. The point is to notice before the week the battery dies, not after.

If the automation cannot tolerate a missing sensor, a wired sensor or a mechanical switch is the smarter choice. A CR2032 and a radio are convenient, not reliable in the engineering sense. I would rather concede that than pretend a battery alert makes a coin-cell sensor into a safety device.

What to do when the percentage is all you have

Some devices only report a percentage. In that case, the percentage is still a lossy conversion, but you can sometimes recover the underlying voltage from the device definition or from the raw MQTT payload. Check the Zigbee2MQTT debug log for the device’s reported values. If the payload contains a voltage field that the integration is not exposing, you can parse it with an MQTT sensor in Home Assistant and get the raw signal back.

If the payload contains only a percentage, you are stuck with it. In that case, use availability and time-in-service, and set the percentage threshold low enough that it actually fires before the device dies. A threshold at 20% is useless if the device never displays below 100%. A threshold at 90% will fire early and often, which is annoying but at least gives you a trend. The right threshold is the one that fires before the device dies, and you can only find it by watching one device through a full cycle.

A repeatable checklist

  1. Confirm whether the device exposes voltage. If it does, create a standalone voltage entity with the voltage device class and measurement state class.

  2. Chart the voltage over 30 to 90 days. Identify whether you are on the flat part of the curve or approaching the knee.

  3. Record the last voltage before a known death. Use it to set a per-device floor with a margin of at least one reporting interval.

  4. Enable Zigbee2MQTT availability and verify the passive timeout is appropriate for your reporting cadence. The default is 25 hours.

  5. Alert on voltage crossing the floor, and on availability going offline. Do not alert on percentage alone.

  6. Record installation dates as a fallback for devices that do not expose voltage.

  7. Make the offline state visible on a dashboard, and make automations fail to a known state rather than the last state.

  8. After every Zigbee2MQTT or zigbee-herdsman-converters upgrade, re-check the voltage-to-percentage mapping for your device definitions. The mapping has changed before and will change again.

The percentage is a presentation layer. The voltage is the measurement. The device’s cutoff is the fact you have to learn. Build the alert on the measurement and the fact, and the percentage can stay at 100% for as long as it likes.

FAQ

Why does my sensor report 100% until it dies?

Because the percentage is computed from voltage, and the voltage-to-percentage mapping saturates at the top of the curve. In Zigbee2MQTT issue #8499, a contributor stated that over 3V the conversion will always show 100%. The sensor is reporting a real voltage; the percentage is a lossy conversion of it.

Can I change the voltage-to-percentage mapping?

Device definitions in zigbee-herdsman-converters can set a voltageToPercentage min and max, as the project owner noted for Lumi devices in the discussion on pull request #7929. Changing it for your own devices means editing or overriding the device definition, which is a maintenance burden. Logging the raw voltage and alerting on that is simpler and survives upgrades.

What voltage should I alert at?

There is no universal number. Record the last voltage before a known death for each device model, and set the floor above that with a margin of at least one reporting interval. A sensor that died at 2.90V should alert at 2.95V, not at 20%, because 20% may never be displayed.

Does temperature affect the voltage?

The Sony CR2032 datasheet shows discharge characteristics at 0°C, 23°C, and 60°C, and the curve shifts with temperature. How that maps to a specific sensor’s cutoff is device-specific. I have not seen enough cold-temperature data to state a rule for below 0°C; treat cold-weather floors as uncertain until you have observed them.

What if the device does not report voltage at all?

Use availability and time-in-service. Zigbee2MQTT’s availability feature marks passive devices offline after 25 hours without a check-in by default. Record the installation date and set a reminder at a conservative fraction of the expected life. It is less precise than a voltage trend, but it is better than a percentage that never moves.

Is a battery alert enough to make a sensor reliable?

No. A battery alert is a maintenance task. Reliability comes from making the automation fail to a known state when the sensor is unavailable, and from not using a battery-powered sensor as the only input to a safety or access decision. If the automation cannot tolerate a missing sensor, a wired sensor or a mechanical switch is the smarter choice.