Hello,
I am building a wireless temperature sensor to be inserted in my water heater's thermostat chamber in order to precisely adjust heating temperature.
I decided to use an ESP32-H2 (supermini) and Zigbee for power efficiency, a 18650 battery, and a Dallas DS18B20 for its small footprint. Here is the schematic of my project:
As you can see, I am using a voltage divider (2x10kΩ) to read battery voltage in order to be able to broadcast both temperature and battery level to my Home Assistant instance. I also added a 100nF capacitor as I read online it could smooth out the voltage readings.
Below is the code I uploaded to my board:
#ifndef ZIGBEE_MODE_ED
#error "Zigbee end device mode is not selected in Tools->Zigbee mode"
#endif
#include "Zigbee.h"
#include "OneWireNg_CurrentPlatform.h"
#include "drivers/DSTherm.h"
#include "utils/Placeholder.h"
/* Zigbee temperature sensor configuration */
#define TEMP_SENSOR_ENDPOINT_NUMBER 10
uint8_t button = BOOT_PIN;
static Placeholder<OneWireNg_CurrentPlatform> ow;
// Optional Time cluster variables
struct tm timeinfo;
struct tm *localTime;
int32_t timezone;
const int numReadings = 100;
int readings[numReadings]; // the readings from the analog input
int readIndex = 0; // the index of the current reading
int inputPin = A3;
#define LOOPS_BETWEEN_TEMP_REPORTS 5
#define LOOPS_BETWEEN_BATTERY_REPORTS 10
int incrementTempLoopCounter = 0;
int incrementBatterySecondsCounter = 0;
ZigbeeTempSensor zbTempSensor = ZigbeeTempSensor(TEMP_SENSOR_ENDPOINT_NUMBER);
/* Calculate average from array excluding 10% extremes */
static float smoothedAverageVoltage(const int readings[], int numReadings){
int copiedReadings[numReadings];
// copying input array
for (int i=0 ; i < numReadings; i++){
copiedReadings[i] = readings[i];
}
// sorting copied array
for (int i = 0; i < numReadings - 1; ++i) {
for (int j = 0; j < numReadings - i - 1; ++j) {
if (copiedReadings[j] > copiedReadings[j + 1]) {
// Swap the elements
int temp = copiedReadings[j];
copiedReadings[j] = copiedReadings[j + 1];
copiedReadings[j + 1] = temp;
}
}
}
// excludings 10% top and bottom
int startIndex = 0.1 * numReadings;
int endIndex = 0.9 * numReadings;
int sum = 0;
for (int i = startIndex; i < endIndex; i++){
sum += copiedReadings[i];
}
// converting it to voltage (3.3v board, 2x10kOhm voltage divider, adjustment value)
float voltage = (3.3 / 1095. * 2. * ((float)sum/(endIndex-startIndex))) * 0.8918;
// rouding voltage
voltage = round(voltage*100) / 100;
return voltage;
}
static int voltageToBatteryLevel(float voltage){
float batteryLevel = 0;
// smooth out bad values
if (voltage > 4.19){
batteryLevel = 100;
}
else if (voltage < 3.31){
batteryLevel = 0;
}
// simple 3rd degree polynomial interpolation from voltage agains battery life chart
else{
batteryLevel = (-1.6123*voltage*voltage*voltage+17.945*voltage*voltage-65.164*voltage+77.579)*100;
}
// force bound result between 0 and 100
if (batteryLevel < 0)
batteryLevel = 0;
else if (batteryLevel > 100){
batteryLevel = 100;
}
batteryLevel = round(batteryLevel);
return (int)batteryLevel;
}
// retrieve temperature from DS18B20
static float getTemp(const DSTherm::Scratchpad& scrpd)
{
long temp = scrpd.getTemp2();
return (float)temp/16;
}
/********************* Arduino functions **************************/
void setup() {
new (&ow) OneWireNg_CurrentPlatform(1, false);
DSTherm drv(ow);
Serial.begin(115200);
// initializing table of battery voltage readings
for (int thisReading = 0; thisReading < numReadings; thisReading++) {
readings[thisReading] = 0;
}
// Init button switch
pinMode(button, INPUT_PULLUP);
// Optional: set Zigbee device name and model
zbTempSensor.setManufacturerAndModel("ClemCorp", "ZigbeeTempSensorBoiler");
// Set minimum and maximum temperature measurement value (10-50°C is default range for chip temperature measurement)
zbTempSensor.setMinMaxValue(-55, 125);
// Optional: Set tolerance for temperature measurement in °C (lowest possible value is 0.01°C)
zbTempSensor.setTolerance(0.5);
// Optional: Time cluster configuration (default params, as this device will revieve time from coordinator)
zbTempSensor.addTimeCluster();
zbTempSensor.setPowerSource(ZB_POWER_SOURCE_BATTERY, 100);
// Add endpoint to Zigbee Core
Zigbee.addEndpoint(&zbTempSensor);
Serial.println("Starting Zigbee...");
// When all EPs are registered, start Zigbee in End Device mode
if (!Zigbee.begin()) {
Serial.println("Zigbee failed to start!");
Serial.println("Rebooting...");
ESP.restart();
} else {
Serial.println("Zigbee started successfully!");
}
Serial.println("Connecting to network");
while (!Zigbee.connected()) {
Serial.print(".");
delay(100);
}
Serial.println();
// Optional: If time cluster is added, time can be read from the coordinator
timeinfo = zbTempSensor.getTime();
timezone = zbTempSensor.getTimezone();
Serial.println("UTC time:");
Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
time_t local = mktime(&timeinfo) + timezone;
localTime = localtime(&local);
Serial.println("Local time with timezone:");
Serial.println(localTime, "%A, %B %d %Y %H:%M:%S");
// Start Temperature sensor reading task
// xTaskCreate(temp_sensor_value_update, "temp_sensor_update", 2048, NULL, 10, NULL);
// Set reporting interval for temperature measurement in seconds, must be called after Zigbee.begin()
// min_interval and max_interval in seconds, delta (temp change in 0,1 °C)
// if min = 1 and max = 0, reporting is sent only when temperature changes by delta
// if min = 0 and max = 10, reporting is sent every 10 seconds or temperature changes by delta
// if min = 0, max = 10 and delta = 0, reporting is sent every 10 seconds regardless of temperature change
zbTempSensor.setReporting(0, 10, 0);
}
void loop() {
// Checking button for factory reset
if (digitalRead(button) == LOW) { // Push button pressed
// Key debounce handling
delay(100);
int startTime = millis();
while (digitalRead(button) == LOW) {
delay(50);
if ((millis() - startTime) > 3000) {
// If key pressed for more than 3secs, factory reset Zigbee and reboot
Serial.println("Resetting Zigbee to factory and rebooting in 1s.");
delay(1000);
Zigbee.factoryReset();
}
}
zbTempSensor.reportTemperature();
}
DSTherm drv(ow);
/* convert temperature on all sensors connected... */
drv.convertTempAll(DSTherm::MAX_CONV_TIME, true);
static Placeholder<DSTherm::Scratchpad> scrpd;
OneWireNg::ErrorCode ec = drv.readScratchpadSingle(scrpd);
if (ec == OneWireNg::EC_SUCCESS) {
float temp = getTemp(scrpd);
zbTempSensor.setTemperature(temp);
Serial.printf("Current Temperature is %.2f°C\r\n",temp);
} else if (ec == OneWireNg::EC_CRC_ERROR)
// Serial.println(" CRC error.");
// read from the sensor:
readings[readIndex] = analogRead(A3);
// advance to the next position in the array:
readIndex = readIndex + 1;
// if we're at the end of the array...
if (readIndex >= numReadings) {
// ...wrap around to the beginning:
readIndex = 0;
}
if (incrementTempLoopCounter < LOOPS_BETWEEN_TEMP_REPORTS){
incrementTempLoopCounter++;
}
else{
// Serial.println("Reporting temp");
zbTempSensor.reportTemperature();
incrementTempLoopCounter = 0;
}
float roundedVoltage=smoothedAverageVoltage(readings, numReadings);
Serial.printf("Current voltage is : %.2f\r\n",roundedVoltage);
int batteryLevel = voltageToBatteryLevel(roundedVoltage);
Serial.printf("Current battery level is : %d%\r\n",batteryLevel);
zbTempSensor.setBatteryPercentage(batteryLevel);
if (incrementBatterySecondsCounter < LOOPS_BETWEEN_BATTERY_REPORTS){
incrementBatterySecondsCounter++;
}
else{
// Serial.println("Reporting battery level");
zbTempSensor.reportBatteryPercentage();
incrementBatterySecondsCounter = 0;
}
Serial.println("----------");
delay(1000);
}
I based my code on: OneWireNG's DallasTemperature template, Arduino's Zigbee Temperature Sensor template, and Arduino's Analog > Smoothing Template.
I have issues reading the battery voltage. It starts going through the roof before slowly decreasing to eventually reach a null value (whereas 18650 voltage reading with a multimeter shows plenty of battery left). Here is the result I get in Home Assistant:
Did I miss an obvious error in my code? Is my assembly incorrect?
Below the chart I used for polynomial interpolation for conversion from voltage to battery percent:
PS: I am aware that my code is very calculation hungry, thus not very power efficient. I simply want to correct my battery level issue before simplifying it (and trying to use deep sleep).




