J’utilise plusieurs capteurs de températures Zigbee au sein de mon système domotique Home Assistant. Si ils fonctionnent très bien, je trouve que leur autonomie est trop faible. Devoir changer de pile Lithium CR2032 tous les 4 à 6 mois ne me convient pas. J’ai donc décidé de fabriquer un capteur de température Zigbee conçu pour avoir une grande autonomie et alimenté par une batterie Li-Ion rechargeable 18650.
Pour ce tuto j’utilise le module Seeed Studio XIAO ESP32C6. Un choix poussé par la très faible consommation de ce module lorsqu’il est en veille profonde (15 μA) et par sa gestion de recharge de la batterie lorsqu’il est alimenté par son port USB-C.
J’utilise un capteur BME280 sous 3.3V, il permet de mesurer la température, l’humidité et la pression et communique avec l’ESP32C6 via le protocole I²C.
La clé de l’autonomie du capteur réside dans la mise en veille profonde du microcontrôleur. Je vais donc commencer par détailler les modes veille de ESP32C6 et son impact sur la liaison radio Zigbee.
Modes veille de l’ESP32C6
La documentation de l’ESP32C6 indique qu’il supporte 2 modes de veille, la veille légère (Light Sleep) et la veille profonde (Deep Sleep).
En veille légère, la tension d’alimentation et l’horloge des composants (RAM, CPU, périphériques…) sont limitées. Mais ils sont maintenus dans un état leur permettant d’être réactivés avec une reprise rapide de l’exécution.
En veille profonde, presque tous les composants de l’ESP32 sont désactivés, y compris le CPU et la plupart des périphériques. La mémoire RAM est également désactivée, ce qui signifie que l’état du programme n’est pas conservé.
Afin d’optimiser l’autonomie de notre capteur, nous allons mettre notre ESP32C6 en veille profonde après chaque mesure de température. Notez que le CPU redémarrant en sortie de veille profonde, le code de la fonction ‘setup()’ de notre programme va être exécuté à chaque sortie du mode veille profonde.



Source : https://lastminuteengineers.com/esp32-sleep-modes-power-consumption/
Zigbee Sleepy End Device
Notre dispositif Zigbee est de type Sleepy End Device, c’est à dire qu’il passe la majeure partie de son temps en mode veille profonde et se réveille périodiquement pour communiquer. Lorsque l’ESP32 est en veille profonde, la radio est désactivée, il faut donc bien porter une attention aux délais utilisés par Zigbee.
Le premier délai à considérer est le délai d’indisponibilité du côté du coordinateur afin d’éviter que le capteur soit vu comme ‘indisponible‘. La configuration de ce délai est faite dans Home Assistant dans le paramétrage du module Zigbee. Par défaut ce délai est positionné à 6 heures (21600 secondes) pour les appareils alimentés par batterie. Puisque notre capteur va communiquer toutes les 10 minutes, ce délai dans le coordinateur Zigbee ne va pas poser de problème pour notre capteur.

Le second délai à considérer est le paramètre ‘.ed_timeout‘ dans la configuration Zigbee de l’ESP32. Ce paramètre spécifie que si un dispositif Zigbee ne communique pas avec le réseau, il sera considéré comme inactif au bout de ce délai. Cela permet au réseau de maintenir une liste à jour des dispositifs actifs et de libérer des ressources pour les dispositifs qui ne sont plus présents ou actifs. Ce paramètre est configuré par défaut à 64 minutes(ESP_ZB_ED_AGING_TIMEOUT_64MIN
) dans la pile Zigbee de l’ESP32.
Le troisième délai à considérer est le paramètre ‘.keep_alive‘ de la configuration Zigbee de l’ESP32. Un message ‘Keep Alive‘ est envoyé périodiquement par l’ESP32 au coordinateur pour signaler qu’il est toujours actif et connecté au réseau. Ce paramètre est configuré par défaut à 3 secondes dans la pile Zigbee de l’ESP32. Mais dans l’exemple ‘Zigbee_Temp_Hum_Sensor_Sleepy‘ il est configuré à 10 secondes pour éviter les interférences avec les données envoyées. Mais je ne vois pas l’utilité d’une telle configuration puisque la connexion Zigbee est courte (<3 secondes), j’ai donc repositionné la valeur par défaut dans mon code.
Le dernier délai à considérer est le paramètre ‘begin timeout‘. Ce paramètre définit le délai d’attente pour l’initialisation du réseau Zigbee lors du démarrage de l’ESP32C6. Après ce temps d’attente, l’ESP32C6 va considérer que l’initialisation a échoué et il va redémarrer grâce à l’appel de la fonction ‘ESP.restart()‘. Par défaut ce paramètre est configuré à 30 secondes. Il est réduit à 10 secondes dans l’exemple ‘Zigbee_Temp_Hum_Sensor_Sleepy‘ , mais je n’ai pas trouvé ce choix pertinent. Ce délai est surtout utile lors de l’initialisation du réseau. Lors de la sortie de veille profonde, l’initialisation est très rapide normalement.
#define ZB_BEGIN_TIMEOUT_DEFAULT 30000 // 30 seconds
#define ZIGBEE_DEFAULT_ED_CONFIG() \
{ \
.esp_zb_role = ESP_ZB_DEVICE_TYPE_ED, .install_code_policy = false, \
.nwk_cfg = { \
.zed_cfg = \
{ \
.ed_timeout = ESP_ZB_ED_AGING_TIMEOUT_64MIN, \
.keep_alive = 3000, \
}, \
}, \
}
Le programme
Le code suivant est fortement inspiré du programme ‘Zigbee_Temp_Hum_Sensor_Sleepy‘ donné en exemple sur le GitHub d’Espressif. J’ai simplement ajouté la gestion du capteur BME280 et la mesure de la tension de la batterie.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @brief Zigbee temperature and humidity sensor Zigbee Sleepy End Device.
*
* https://tutoduino.fr/tutoriels/esp32c6-zigbee/
* This code is based on example "Zigbee temperature and humidity sensor Sleepy device" created by Jan Procházka
* https://github.com/espressif/arduino-esp32/tree/master/libraries/Zigbee/examples/Zigbee_Temp_Hum_Sensor_Sleepy
*/
#ifndef ZIGBEE_MODE_ED
#error "Zigbee end device mode is not selected in Tools->Zigbee mode"
#endif
// Uncomment the following line to display debug traces in serial monitor of Arduino IDE
//#define DEBUG_TRACE
#include "Zigbee.h"
#include <BME280I2C.h>
#include <Wire.h>
/* Zigbee temperature + humidity sensor configuration */
#define TEMP_SENSOR_ENDPOINT_NUMBER 10
#define uS_TO_S_FACTOR 1000000ULL /* Conversion factor for micro seconds to seconds */
#define TIME_TO_SLEEP 600 /* Sleep for 10 minutes */
ZigbeeTempSensor zbTempSensor = ZigbeeTempSensor(TEMP_SENSOR_ENDPOINT_NUMBER);
/* BME280 sensor */
BME280I2C sensor;
// 3.7 V Li-Ion battery voltage
const float minVoltage = 3.0;
const float maxVoltage = 4.0;
// Mapp float values to percentage
uint8_t mapFloat(float x, float in_min, float in_max) {
float val;
val = (x - in_min) * (100) / (in_max - in_min);
if (val < 0) {
val = 0;
} else if (val > 100) {
val = 100;
}
return (uint8_t)val;
}
// Get battery voltage en V
float getVbatt() {
uint32_t Vbatt = 0;
for (int i = 0; i < 16; i++) {
Vbatt += analogReadMilliVolts(A0); // Read and accumulate ADC voltage
}
return (2 * Vbatt / 16 / 1000.0); // Adjust for 1:2 divider and convert to volts
}
// Get data from BME280 sensor and go to deep sleep mode
void meausureAndSleep() {
// Measure temperature sensor value
float temperature(NAN), humidity(NAN), pressure(NAN);
uint8_t percentage;
float vBat;
BME280::TempUnit tempUnit(BME280::TempUnit_Celsius);
BME280::PresUnit presUnit(BME280::PresUnit_hPa);
// Read temperature and humidity on BME280 sensor
sensor.read(pressure, temperature, humidity, tempUnit, presUnit);
// Measure battery voltage
vBat = getVbatt();
percentage = mapFloat(vBat, minVoltage, maxVoltage);
#ifdef DEBUG_TRACE
Serial.printf("Battery: %.2fV (%d%%)\n", vBat, percentage);
#endif
// Update battery percentage
zbTempSensor.setBatteryPercentage(percentage);
zbTempSensor.setBatteryVoltage(vBat * 10); // voltage in 100mV
// Update temperature and humidity values in Temperature sensor EP
zbTempSensor.setTemperature(temperature);
zbTempSensor.setHumidity(humidity);
// Report values
zbTempSensor.report();
zbTempSensor.reportBatteryPercentage();
#ifdef DEBUG_TRACE
Serial.printf("Reported temperature: %.2f°C, Humidity: %.2f%%\r\n", temperature, humidity);
#endif
// Turn on the builtin LED for a very short time
flashLED();
// Add small delay to allow the data to be sent before going to sleep
delay(500);
// Put device to deep sleep
#ifdef DEBUG_TRACE
Serial.println("Going to sleep now");
#endif
esp_deep_sleep_start();
}
// Internal Led flash
void flashLED() {
// Turn on LED for 100ms
digitalWrite(LED_BUILTIN, LOW);
delay(100);
digitalWrite(LED_BUILTIN, HIGH);
}
/********************* Arduino functions **************************/
void setup() {
#ifdef DEBUG_TRACE
Serial.begin(115200);
delay(100);
Serial.println();
Serial.println("Tutoduino Zigbee temperature sensor start!");
#endif
// Configure use of external antenna
pinMode(WIFI_ENABLE, OUTPUT); // pinMode(3, OUTPUT);
digitalWrite(WIFI_ENABLE, LOW); // digitalWrite(3, LOW); // Activate RF switch control
delay(100);
pinMode(WIFI_ANT_CONFIG, OUTPUT); // pinMode(14, OUTPUT);
digitalWrite(WIFI_ANT_CONFIG, HIGH); // digitalWrite(14, HIGH); // Use external antenna
// Configure builtin LED and turn it OFF (HIGH)
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
// Init BME280 sensor
Wire.begin();
while (!sensor.begin()) {
#ifdef DEBUG_TRACE
Serial.println("Could not find BME280 sensor!");
#endif
delay(1000);
}
// Configure A0 as ADC input for reading battery voltage
pinMode(A0, INPUT);
// Configure the wake up source and set to wake up every 5 seconds
esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
// Optional: set Zigbee device name and model
zbTempSensor.setManufacturerAndModel("Tutoduino", "ESP32C6TempSensor");
// Set minimum and maximum temperature measurement value
zbTempSensor.setMinMaxValue(-20, 80);
// Set tolerance for temperature measurement in °C (lowest possible value is 0.01°C)
zbTempSensor.setTolerance(1);
// Set power source to battery, battery percentage and battery voltage (now 100% and 3.5V for demonstration)
// The value can be also updated by calling zbTempSensor.setBatteryPercentage(percentage) or zbTempSensor.setBatteryVoltage(voltage) anytime after Zigbee.begin()
zbTempSensor.setPowerSource(ZB_POWER_SOURCE_BATTERY, 100, 35);
// Add humidity cluster to the temperature sensor device with min, max and tolerance values
zbTempSensor.addHumiditySensor(0, 100, 1);
// Add endpoint to Zigbee Core
Zigbee.addEndpoint(&zbTempSensor);
// Create a default Zigbee configuration for End Device
esp_zb_cfg_t zigbeeConfig = ZIGBEE_DEFAULT_ED_CONFIG();
#ifdef DEBUG_TRACE
Serial.println("Starting Zigbee");
#endif
// When all EPs are registered, start Zigbee in End Device mode
if (!Zigbee.begin(&zigbeeConfig, false)) {
// If Zigbee does not start with 30s default timeout (ZB_BEGIN_TIMEOUT_DEFAULT) then restart
#ifdef DEBUG_TRACE
Serial.println("Zigbee failed to start!");
Serial.println("Rebooting ESP32!");
#endif
ESP.restart(); // If Zigbee failed to start, reboot the device and try again
}
#ifdef DEBUG_TRACE
Serial.println("Connecting to network");
#endif
while (!Zigbee.connected()) {
#ifdef DEBUG_TRACE
Serial.print(".");
#endif
delay(100);
}
#ifdef DEBUG_TRACE
Serial.println("Successfully connected to Zigbee network");
#endif
// Delay approx 1s (may be adjusted) to allow establishing proper connection with coordinator, needed for sleepy devices
delay(1000);
// Call the function to measure temperature and put the device to deep sleep
meausureAndSleep();
}
void loop() {
// No actions are performed in the loop (the ESP32C6 enters the setup function when it exits deep sleep).
}
Le matériel
L’ESP32C6 est alimenté par une batterie Li-Ion externe de 3.7 V reliée sur les 2 broches ‘+’ et ‘-‘ présentes sur la face arrière du module.



L’ESP32C6 est connecté au capteur BME280 via bus I2C (broches SDA et SCL).


Le capteur BME280 est alimenté par la sortie 3.3 V de l’ESP32.

Consommation du capteur BME280
Pour limiter la consommation, le capteur doit être en veille lorsque aucune mesure n’est nécessaire (ESP32C6 en veille). La documentation du capteur indique qu’il consomme 0.1 μA en veille et quelques microampère lorsqu’il effectue les mesure. Mais afin de minimiser la consommation, nous configurons le capteur en mode ‘Forced‘ afin que le capteur reste en veille lorsqu’il n’est pas interrogé par l’ESP32C6.
La librairie BME280 utilisée positionne le mode en ‘Forced‘ dans la configuration du capteur, il n’y a donc rien de spécifique à faire dans notre code.

Autonomie du dispositif
La consommation de l’ESP32C6 en veille profonde est de 15.48 μA, conformément à la documentation de l’ESP32C6. En fonctionnement (connexion au réseau Zigbee et envoi des données), le dispositif consomme 74 mA (soit 5000 fois plus qu’en veille profonde !).


Le dispositif reste en mode veille profonde pendant 10 minutes (600 secondes), passe en mode actif pendant environ 3 secondes (le temps de se connecter au réseau Zigbee et d’envoyer les données) puis retourne en mode veille profonde pendant 10 minutes.
Pour calculer la consommation moyenne en courant du dispositif, nous allons utiliser la formule :

- Calcul de la charge consommée pendant chaque période :
- Q1 = I1×t1 = 0,000015×600 = 0,009 C
- Q2 = I2×t2 = 0,064×3 = 0,192 C
- Charge totale :
- Qtotal = Q1+Q2 = 0,009+0,192 = 0,201 C
- Courant moyen :
- Imoyen = 0,201/603 = 0,000333 A
La consommation moyenne est d’environ 333 μA.
Une batterie Li-Ion 18650 ayant une capacité d’environ 3500 mAh, notre capteur devrait avoir une autonomie de 10500 heures, soit 437 jours.
Mesure de la tension de la batterie
Pour mesurer la tension aux bornes de la batterie, un simple pont diviseur de tension réalisé avec des résistances de 220 kΩ permet de mesurer sur la broche A0 la tension de la batterie (en multipliant par 2 la mesure lue sur la broche A0 car le pont diviseur divise la tension de la batterie par 2).

Voici le code inspiré de l’exemple pour l’ESP32C3 qui permet de mesurer la tension sur la broche A0 et de la transformer en % de charge de la batterie :
// Function to mapp values to percentage
float mapFloat(float x, float in_min, float in_max) {
return (x - in_min) * (100) / (in_max - in_min);
}
// Measure battery voltage
pinMode(A0, INPUT);
vBat = 2 * analogReadMilliVolts(A0) / 1000.0;
percentage = (uint8_t) mapFloat(vBat, minVoltage, maxVoltage);
Serial.printf("Battery: %.2fV %d\n", vBat, percentage);
Intégration du capteur dans Home Assistant
Comme je l’ai indiqué, mon installation domotique est contrôlée par Home Assistant sur un Raspberry Pi équipé d’un dongle Sonoff ZBDongle-E. L’intégration du capteur se fait très simplement, en ajoutant un appareil Zigbee depuis le menu ‘Paramètres‘.

Une fois ajouté, le capteur apparaît dans la liste des appareils. On voit bien la température et l’humidité mesurée par le BME280 ainsi que la charge de la batterie.

Et vous pouvez bien entendu organiser la visualisation du capteur comme bon vous semble dans Home Assistant, voici un exemple :

Problème potentiel avec Zigbee en sortie de veille profonde
Si tout se déroule bien, vous devriez voir ces traces dans le monitor série de l’IDE Arduino :
08:51:44.105 -> Tutoduino Zigbee temperature sensor start!
08:51:44.105 -> Starting Zigbee
08:51:44.105 -> Connecting to network
08:51:44.105 -> Successfully connected to Zigbee network
08:51:45.105 -> Battery: 4.01V (100%)
08:51:45.105 -> Reported temperature: 24.64°C, Humidity: 45.38%
08:51:45.621 -> Going to sleep now
Cependant, dans certains cas, la sortie du mode veille prolongée peut entraîner des temps de commissionnement Zigbee trop longs. Dans ce cas, les traces sur le moniteur série indiquent que le démarrage de Zigbee échoue. Dans ce cas, l’ESP32 redémarre après le délai d’expiration ‘begin timeout‘ (configuré sur 30 s, comme vous pouvez le voir dans la capture d’écran ci-dessous).
Ce problème est généralement dû à la portée limitée de Zigbee. Il faut y prêter attention, car s’il se reproduit, ce problème impacte fortement l’autonomie de la batterie (consommation de 74 mA pendant près d’une minute).
08:46:32.311 -> Tutoduino Zigbee temperature sensor start!
08:46:32.311 -> Starting Zigbee
08:47:02.365 -> Zigbee failed to start!
08:47:02.365 -> ESP-ROM:esp32c6-20220919
08:47:02.365 -> Build:Sep 19 2022
08:47:02.365 -> rst:0xc (SW_CPU),boot:0x1e (SPI_FAST_FLASH_BOOT)
08:47:02.365 -> Saved PC:0x4001975a
08:47:02.365 -> SPIWP:0xee
08:47:02.365 -> mode:DIO, clock div:2
08:47:02.365 -> load:0x40875720,len:0x1260
08:47:02.365 -> load:0x4086c110,len:0xdc4
08:47:02.404 -> load:0x4086e610,len:0x3018
08:47:02.404 -> entry 0x4086c110
08:47:02.718 ->
08:47:02.718 -> Tutoduino Zigbee temperature sensor start!
08:47:02.750 -> Starting Zigbee
08:47:24.940 -> Connecting to network
08:47:24.940 -> Successfully connected to Zigbee network
08:47:25.935 -> Battery: 4.07V (100%)
08:47:25.935 -> Reported temperature: 24.30°C, Humidity: 46.30%
08:47:26.458 -> Going to sleep now
Utilisation d’une antenne externe sur ESP32C6
Pour éviter ce type de problème et améliorer la portée du capteur, je recommande fortement d’utiliser une antenne externe sur le module ESP32C6, connectée à son connecteur uFL. Il s’agit d’une antenne 2,4 GHz compatible Wi-Fi, BLE, Zigbee et Thread.


Comme expliqué dans la Getting Started de l’ESP32C6, vous devez configurer l’utilisation de son antenne externe. Voici le code à positionner dans la fonction ‘setup()‘ :
// Configure use of external antenna
pinMode(WIFI_ENABLE, OUTPUT); // pinMode(3, OUTPUT);
digitalWrite(WIFI_ENABLE, LOW); // digitalWrite(3, LOW); // Activate RF switch control
delay(100);
pinMode(WIFI_ANT_CONFIG, OUTPUT); // pinMode(14, OUTPUT);
digitalWrite(WIFI_ANT_CONFIG, HIGH); // digitalWrite(14, HIGH); // Use external antenna
Réinitialisation des réglages usine de Zigbee
Si votre capteur n’arrive vraiment pas à se connecter à votre réseau Zigbee, je vous invite à suivre la procédure suivante :
- Configurer le paramètre ‘Erase All Flash Before Sketch Upload‘ à ‘Enabled‘ dans l’IDE Arduino
- Téléverser le programme de réinitialisation des réglages usine de Zigbee (code ci-dessous) sur l’ESP23C6
- Débrancher l’ESP32C6 de l’ordinateur, appuyer sur son bouton ‘BOOT‘ en le rebranchant
- Supprimer l’appareil de Home Assistant et redémarrer Home Assistant (voir redémarrer le Raspberry Pi)
- Téléverser le croquis du programme principal du capteur de température
- Appuyer sur le bouton ‘RESET‘ de l’ESP32C6
- Refaites la procédure d’ajout de l’appareil dans Home Assistant

Le programme suivant permet de réinitialiser les réglages usine de Zigbee, il met l’ESP32 en veille profonde indéfiniment une fois la réinitialisation terminée. Pour téléverser un nouveau programme sur l’ESP32 il faut le débrancher du PC, puis maintenir appuyer son bouton ‘BOOT‘ en le reconnectant au PC.
#include "Zigbee.h"
#ifndef ZIGBEE_MODE_ED
#error "Zigbee end device mode is not selected in Tools->Zigbee mode"
#endif
void setup() {
Serial.begin(115200);
Serial.println("Zigbee factory reset and going to endless sleep");
esp_zb_cfg_t zigbeeConfig = ZIGBEE_DEFAULT_ED_CONFIG();
// When all EPs are registered, start Zigbee in End Device mode
if (!Zigbee.begin(&zigbeeConfig, false)) {
Serial.println("Zigbee failed to start!");
ESP.restart(); // If Zigbee failed to start, reboot the device and try again
}
Zigbee.factoryReset(false);
delay(500);
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_TIMER);
esp_deep_sleep_start();
}
void loop() {
// put your main code here, to run repeatedly:
}
Modification du programme après ajout de l’appareil dans Home Assistant
Il est possible d’apporter des modifications au programme après avoir ajouté le capteur dans Home Assistant. Mais le capteur passe la grande partie de son temps en mode veille profonde. Il est donc déconnecté de l’IDE Arduino et il n’est pas possible de téléverser le programme. Il faut donc connecter l’ESP32C6 au PC en maintenant son bouton ‘BOOT‘ appuyé pour pouvoir y téléverser le programme. Il devrait apparaître automatiquement dans Home Assistant.
Si vous avez activé le paramètre ‘Erase All Flash Before Sketch Upload‘ à ‘Enabled‘ dans l’IDE Arduino avant le téléversement, il faut refaire la procédure d’ajout d’appareil dans Home Assistant. Il n’est pas nécessaire de supprimer l’appareil de Home Assistant au préalable.
Le montage final
J’ai réalisé un prototype, il m’a permis de valider le code et de vérifier la portée radio et la consommation du dispositif.

Une fois le prototype validé, il ne reste plus qu’à designer la carte sur KiCad et à produire un PCB digne de ce nom.
