Bonjour,
Je suis en train de porter sous ESP32 un capteur de niveau de pellets pour mon poêle (non connecté.
J’avais fait ca sur Arduino avant.
Là c’est pas la même chose avec ESP builder et le yaml ( )
Je suis passé parle micro mircro M5nanaoC6.
1port usb C pour l’alim. un port Grove 4pins (5v, gnd G1 et G2) 1 led de status en G7 et une led RGB en G20 (datat) et G19 (pwr).
Le capteur était un ultrasonic v2.0 de Grove qui s’est avéré impossible à reprendre - seulement 3 pins; la 4eme inutilisé. J’ai du reprendre le “même’ en version HC-SR04.
J’ai voulu utilisé la led RGB pour afficher des couleurs selon le niveau restant mais a priori c’est impossible en esphome
(neopixel n’existe pas ) C’est là toute la question que je pose ici si quelqu’un sais faire ?
voici le yaml flashé dans sa dernière version.
Des stats son ajoutée dans configuration.yaml
(désolé j’arrive pas à le mettre formatté ici)
EDIT
Je modifie mon sujet avec la version actuelle bien plus aboutie.
J’abandonne définitivement l’usage de la led RGB donc la gestion est non compatible avec cet ESP.
J’utilise la Led de statut avec diverses patterns de clignotement pour simplement indiquer si en veille (clignotement de type “respiration”) ou en mode actif (à chaque ping de sonar).
-
Les fonctionnalités actuelles sont:
-
Mesure de la distance adéquate dans la cuve de pellet.
J’ai dû faire un tableau de mesures empiriques hauteur occupée/poids réel afin d’avoir une relativement bonne fiabilité notamment pour la partie inférieur du réservoir qui n’est plus un simple parallélépipède puisque de forme complexe façon entonnoir guidant les pellets jusqu’à la vis sans fin du poêle. J’ai reformé la forme de cône inversé que prnd le tas de pellets au fil de sa descente. Le sonar doit donc toujours être placé sus le couvercle au même endroit (son boitier tient par un aimant). -
Le programme fait une interpolation entre les diverses étapes des mesures que j’ai fourni.
-
Les distances entre le sonar et le haut du tas sont remontées et interprétées comme des kg.
-
on conserve les stats pour les conso/jour /sem. /mois /année.
-
on gère le fait que le couvercle est ouvert : il faut stopper toute action d’enregistrement des valeurs sinon on va enregistrer une cuve vide ce qui va fausser toutes le stats. Donc si on mesure une distance incongrue (dans mon cas >60cm) alors le couvercle est ouvert et on passe en mode spécifique jusqu’à ce que les mesures reviennent dans des valeurs acceptables.
-
Un mode veille que je déclenche par un switch dans le dashboard stoppe toute mesure (et aussi toute écrire en flash) afin de limiter le grillage de la flash sur des années.
-
il y a probablement encore des bugs qui trainent et des entités fantômes avec tous les changements que j’ai fait…
esphome:
name: pelletslevel
friendly_name: "Pellets Level"
esp32:
board: esp32-c6-devkitc-1
framework:
type: esp-idf
wifi:
ssid: "*********"
password: "*************"
output_power: 20dB
reboot_timeout: 15min
power_save_mode: none
ap: {}
api:
encryption:
key: "tWv6CMGEJoh1***********ODPkijkxk="
services:
- service: set_consumption_value
variables:
value: float
then:
- lambda: |-
id(pellets_total_consumed) = value;
id(last_saved_consumption) = value;
global_preferences->sync();
ESP_LOGI("pellets", "Nouvelle consommation: %.1f kg", value);
- service: set_new_pellets_consumption_value
variables:
value: float
then:
- globals.set:
id: pellets_total_consumed
value: !lambda 'return value;'
- lambda: |-
// Mettre à jour la référence pour éviter sur- ou sous-cumuls
static float &last_ref = *((float*)&id(pellet_consumption)); // référence au static last
last_ref = id(pellet_weight).state;
- logger.log:
format: "⚙️ Nouvelle valeur de consommation fixée à %.1f kg via Home Assistant"
args: ['value']
- text_sensor.template.publish:
id: pellet_last_refill_text
state: "Synchronisation manuelle"
# Synchronisation de l'heure avec Home Assistant
time:
- platform: homeassistant
id: homeassistant_time
ota:
platform: esphome
password: "52421d46ad0397*******47e272869"
logger:
level: INFO
baud_rate: 115200
# Sauvegarde en flash toutes les 3h
preferences:
flash_write_interval: 3h
output:
- platform: ledc
pin: GPIO7
id: status_led
frequency: 1000Hz
- platform: ledc #ne sert pas incompatible
pin: GPIO19
id: rgb_power
channel: 0
# Scripts pour gérer les patterns LED et màj conso.to
script:
# Clignotement lent pour monitoring désactivé (300ms ON / 3s OFF)
- id: led_slow_blink
mode: restart
then:
- while:
condition:
lambda: 'return !id(monitoring_enabled);'
then:
# Fade in (montée progressive) fade_status_led_index
- output.set_level:
id: status_led
level: 0%
- output.ledc.set_frequency:
id: status_led
frequency: 1000Hz
# --- BLOC CORRIGÉ 1 : Fade In ---
# --- FADE IN
- globals.set:
id: fade_status_led_index
value: '0' # Réinitialiser le compteur
- while:
condition:
lambda: 'return id(fade_status_led_index) < 30;'
then:
- output.set_level:
id: status_led
# Utilise le compteur global pour le calcul
level: !lambda |-
return (id(fade_status_led_index) * 3.3) / 100.0;
- delay: 10ms
- globals.set:
id: fade_status_led_index
value: !lambda 'return id(fade_status_led_index) + 1;' # Incrémenter le compteur
# Maintien 100ms
- delay: 100ms
# Fade out (descente progressive)
- globals.set:
id: fade_status_led_index
value: '0' # Réinitialiser le compteur
- while:
condition:
lambda: 'return id(fade_status_led_index) < 30;'
then:
- output.set_level:
id: status_led
# Utilise le compteur global pour le calcul (inversé)
level: !lambda |-
return (100.0 - id(fade_status_led_index) * 3.3) / 100.0;
- delay: 10ms
- globals.set:
id: fade_status_led_index
value: !lambda 'return id(fade_status_led_index) + 1;' # Incrémenter le compteur
# Pause
- output.set_level:
id: status_led
level: 0%
- delay: 7700ms
# Clignotement rapide pour niveau critique < 10%
- id: led_critical_blink
mode: restart
then:
- while:
condition:
lambda: 'return id(low_pellets_alert).state;'
then:
- output.turn_on: status_led
- delay: 200ms
- output.turn_off: status_led
- delay: 300ms
# Flash visible au ping du sonar (300ms)
- id: led_ping_flash
mode: queued
then:
- output.turn_on: status_led
- delay: 300ms
- output.turn_off: status_led
# Mise à jour manuelle conso totale
- id: update_pellets_consumption
mode: restart
then:
- lambda: |-
id(pellets_total_consumed) = id(new_pellets_consumption_value);
id(last_saved_consumption) = id(new_pellets_consumption_value);
global_preferences->sync();
ESP_LOGI("pellets", "Consommation mise à jour à %.1f kg", id(new_pellets_consumption_value));
- script.execute: led_ping_flash
globals:
- id: fade_status_led_index
type: int
initial_value: '0'
- id: last_valid_distance
type: float
initial_value: 'NAN'
# Stockage persistant de la consommation totale
- id: pellets_total_consumed
type: float
restore_value: no
initial_value: '10.3'
# Pour bouton de reset conso/modif valeur manuelle
- id: new_pellets_consumption_value
type: float
initial_value: '0.0'
# Pour détecter les changements significatifs avant sauvegarde
- id: last_saved_consumption
type: float
restore_value: no
initial_value: '0.0'
# Date et heure du dernier remplissage
- id: last_refill_timestamp
type: std::string
restore_value: yes
initial_value: '"Jamais"'
#initial_value: '"07/10/2025 20:00"'
# Flag pour détecter si couvercle ouvert (distance invalide)
- id: lid_open
type: bool
restore_value: no
initial_value: 'false'
# Flag pour activer/désactiver le monitoring (contrôlé par switch)
- id: monitoring_enabled
type: bool
restore_value: yes
initial_value: 'true'
text_sensor:
- platform: template
name: "Pellets Last Refill"
id: pellet_last_refill_text
icon: mdi:calendar-clock
lambda: |-
return {id(last_refill_timestamp)};
update_interval: 60s
- platform: template
name: "Pellets Lid Status"
id: pellet_lid_status
icon: mdi:window-open-variant
lambda: |-
if (!id(monitoring_enabled)) {
return {"Monitoring OFF"};
} else if (id(lid_open)) {
return {"Ouvert"};
} else {
return {"Fermé"};
}
- platform: template
name: "Conso. Totale (kg) à appliquer"
id: pellets_conso_new_value
lambda: |-
return {to_string(id(new_pellets_consumption_value)) + " kg"};
sensor:
- platform: ultrasonic
name: "Pellet Distance"
id: tank_distance
trigger_pin: GPIO1
echo_pin: GPIO2
update_interval: 60s
timeout: 5m
pulse_time: 10us
unit_of_measurement: "cm"
filters:
- median:
window_size: 5
- multiply: 100.0
on_value:
then:
- logger.log:
format: "Distance: %.1f cm"
args: ["x"]
- if:
condition:
lambda: 'return id(monitoring_enabled);'
then:
- script.execute: led_ping_flash
- lambda: |-
// Garder la dernière valeur valide même si monitoring OFF
if (!isnan(x) && x > 0) {
id(last_valid_distance) = x;
}
// NE RIEN TRAITER si monitoring désactivé
if (!id(monitoring_enabled)) {
return;
}
// Détection couvercle ouvert : distance > 60cm uniquement
if (!isnan(x) && x > 60.0) {
if (!id(lid_open)) {
id(lid_open) = true;
ESP_LOGW("pellets", "⚠️ Couvercle ouvert détecté (distance: %.1f cm) - Mesures suspendues", x);
}
} else if (!isnan(x)) {
// Distance valide : <= 60cm (couvercle fermé)
if (id(lid_open)) {
id(lid_open) = false;
ESP_LOGI("pellets", "✅ Couvercle refermé - Mesures reprises");
}
}
- platform: template
name: "Pellet Level %"
id: pellet_level
unit_of_measurement: "%"
accuracy_decimals: 1
lambda: |-
float d = id(last_valid_distance);
if (isnan(d)) return {};
float d_min = 3.0f; // Plein (100%)
float d_max = 55.0f; // Vide (0%)
float level = 100.0f * (d_max - d) / (d_max - d_min);
return max(0.0f, min(100.0f, level));
- platform: template
name: "Pellet Weight"
id: pellet_weight
unit_of_measurement: "kg"
accuracy_decimals: 1
state_class: measurement
device_class: weight
lambda: |-
float d = id(last_valid_distance);
if (isnan(d)) return {};
// Tableau de calibration basé sur mesures réelles
// Format: {distance_cm, poids_kg}
const float calibration[][2] = {
{3.0f, 27.0f}, // Estimé max (sac 15kg + 9kg + marge)
{10.0f, 24.0f}, // Mesuré: sac 15kg + 9kg
{33.0f, 9.0f}, // Mesuré
{37.0f, 7.0f}, // Mesuré
{40.0f, 5.0f}, // Mesuré
{43.0f, 3.0f}, // Mesuré
{49.0f, 1.0f}, // Mesuré
{55.0f, 0.3f} // Mesuré: résiduel
};
const int n_points = 8;
// Interpolation linéaire entre les points de mesure
if (d <= calibration[0][0]) {
return calibration[0][1]; // Au-dessus du max
}
if (d >= calibration[n_points-1][0]) {
return calibration[n_points-1][1]; // Vide
}
// Trouver les deux points encadrants
for (int i = 0; i < n_points - 1; i++) {
if (d >= calibration[i][0] && d <= calibration[i+1][0]) {
float d1 = calibration[i][0];
float w1 = calibration[i][1];
float d2 = calibration[i+1][0];
float w2 = calibration[i+1][1];
// Interpolation linéaire
float ratio = (d - d1) / (d2 - d1);
return w1 + ratio * (w2 - w1);
}
}
return 0.0f; // Fallback
- platform: template
name: "Pellets Consumption"
id: pellet_consumption
unit_of_measurement: "kg"
state_class: total_increasing
device_class: weight
accuracy_decimals: 1
lambda: |-
static bool initialized = false;
float current = id(pellet_weight).state;
static float last = current;
// Initialisation
if (!initialized) {
initialized = true;
last = current;
id(last_saved_consumption) = id(pellets_total_consumed);
ESP_LOGI("pellets", "Consommation restaurée: %.2f kg", id(pellets_total_consumed));
}
// Conditions d'inhibition
if (!id(monitoring_enabled) || id(lid_open)) {
return id(pellets_total_consumed);
}
// 1. Détection de consommation (baisse > 0.1 kg)
if (last - current > 0.1) {
float consumed = last - current;
float new_total = id(pellets_total_consumed) + consumed;
id(pellets_total_consumed) = new_total;
last = current;
// Sauvegarde si variation > 0.3 kg
float diff = abs(new_total - id(last_saved_consumption));
if (diff > 0.3) {
id(last_saved_consumption) = new_total;
global_preferences->sync();
ESP_LOGI("pellets", "Consommation: +%.2f kg (total: %.2f kg)", consumed, new_total);
}
}
// 2. Détection de remplissage (augmentation > 1 kg)
else if (current - last > 1.0) { // Seuil à 1kg comme demandé
auto time = id(homeassistant_time).now();
char timestamp[20];
sprintf(timestamp, "%02d/%02d/%04d %02d:%02d",
time.day_of_month, time.month, time.year,
time.hour, time.minute);
id(last_refill_timestamp) = timestamp;
// On ne met PAS à jour last tout de suite : on attend la stabilisation
ESP_LOGW("pellets", "Remplissage détecté (%.2f kg) - Attente stabilisation avant reset", current - last);
}
return id(pellets_total_consumed);
update_interval: 60s
binary_sensor:
- platform: status
name: "Pellet Online"
on_press:
then:
- script.stop: led_slow_blink
- script.stop: led_critical_blink
- if:
condition:
lambda: 'return id(monitoring_enabled);'
then:
- output.turn_on: status_led
else:
- script.execute: led_slow_blink
on_release:
then:
- output.turn_off: status_led
- script.stop: led_slow_blink
- script.stop: led_critical_blink
- platform: template
name: "Low Pellets Alert"
id: low_pellets_alert
device_class: problem
lambda: |-
if (!id(monitoring_enabled) || id(lid_open)) {
return false;
}
return id(pellet_level).state < 10.0;
on_press:
then:
- logger.log: "⚠️ Niveau critique : moins de 10 pcents de pellets"
- script.stop: led_slow_blink
- script.execute: led_critical_blink
on_release:
then:
- script.stop: led_critical_blink
- if:
condition:
lambda: 'return id(monitoring_enabled);'
then:
- output.turn_on: status_led
- platform: template
name: "Low Pellets Warning"
id: low_pellets_warning
device_class: problem
lambda: |-
if (!id(monitoring_enabled) || id(lid_open)) {
return false;
}
return id(pellet_level).state < 20.0;
on_press:
then:
- logger.log: "⚠️ Niveau bas : moins de 20 pcents de pellets"
switch:
- platform: restart
name: "Restart Pellet Monitor"
id: restart_switch
- platform: template
name: "Pellets Monitoring"
id: pellets_monitoring_switch
icon: mdi:home-thermometer
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
turn_on_action:
- lambda: |-
id(monitoring_enabled) = true;
ESP_LOGI("pellets", "🔥 Monitoring activé - Saison de chauffe");
- script.stop: led_slow_blink
- output.turn_on: status_led
turn_off_action:
- lambda: |-
id(monitoring_enabled) = false;
ESP_LOGI("pellets", "☀️ Monitoring désactivé - Hors saison");
- script.execute: led_slow_blink
lambda: |-
return id(monitoring_enabled);
- platform: template
name: "Mettre à jour conso. totale"
id: update_pellets_consumption_btn
icon: mdi:content-save
optimistic: true
turn_on_action:
- script.execute: update_pellets_consumption
les automations (dans automations.yaml):
- id: pellets_critical_alert
alias: Alerte pellets critique <10%
description: Notification urgente quand niveau < 10%
trigger:
- platform: state
entity_id: binary_sensor.pelletslevel_low_pellets_alert
to: 'on'
condition:
- condition: state
entity_id: switch.pelletslevel_pellets_monitoring
state: 'on'
action:
- service: persistent_notification.create
data:
title: "\U0001F6A8 Pellets CRITIQUE"
message: 'Niveau très bas : {{ states(''sensor.pelletslevel_pellet_level'')
}}% Stock restant : {{ states(''sensor.pelletslevel_pellet_weight'') }}kg
Remplissage URGENT nécessaire !
'
- id: pellets_warning_alert
alias: Alerte pellets niveau bas <20%
description: Notification quand niveau < 20%
trigger:
- platform: state
entity_id: binary_sensor.pelletslevel_low_pellets_warning
to: 'on'
condition:
- condition: state
entity_id: switch.pelletslevel_pellets_monitoring
state: 'on'
action:
- service: persistent_notification.create
data:
title: ⚠️ Pellets niveau bas
message: 'Niveau : {{ states(''sensor.pelletslevel_pellet_level'') }}% Stock
restant : {{ states(''sensor.pelletslevel_pellet_weight'') }}kg
Prévoir un remplissage prochainement.
'
- id: pellets_refill_detected
alias: Notification remplissage pellets
description: Confirme la détection d'un remplissage
trigger:
- platform: state
entity_id: text_sensor.pelletslevel_pellets_last_refill
condition:
- condition: template
value_template: '{{ trigger.to_state.state != ''Jamais'' and trigger.from_state.state
!= trigger.to_state.state }}'
action:
- service: persistent_notification.create
data:
title: ✅ Remplissage détecté
message: 'Remplissage effectué le {{ states(''text_sensor.pelletslevel_pellets_last_refill'')
}} Nouveau stock : {{ states(''sensor.pelletslevel_pellet_weight'') }}kg
'
- id: pellets_daily_report
alias: Rapport quotidien pellets
description: Résumé de la consommation à 21h
trigger:
- platform: time
at: '21:00:00'
condition:
- condition: state
entity_id: switch.pelletslevel_pellets_monitoring
state: 'on'
- condition: numeric_state
entity_id: sensor.pellets_daily
above: 0
action:
- service: persistent_notification.create
data:
title: "\U0001F4CA Rapport pellets du jour"
message: 'Consommation aujourd''hui : {{ states(''sensor.pellets_daily'') }}kg
Stock restant : {{ states(''sensor.pelletslevel_pellet_weight'') }}kg ({{
states(''sensor.pelletslevel_pellet_level'') }}%) Consommation totale : {{
states(''sensor.pelletslevel_pellets_consumption'') }}kg'
- id: '1759910087843'
alias: set_new_pellets_consumption_value2
description: ''
triggers:
- trigger: state
entity_id:
- input_number.pellets_consumption_reset
conditions: []
actions:
- action: esphome.pelletslevel_set_new_pellets_consumption_value
metadata: {}
data: {}
mode: single
et dans configuration.yaml:- platform: derivative
source: sensor.pelletslevel_pellet_weight
name: "Pellets Consumption Rate"
round: 2
unit_time: h
unit: "kg/h"
time_window: "01:00:00"
# UTILITY METERS - utilisent directement le capteur ESPHome
# cummuls des conso (kg) de pellets
utility_meter:
pellets_daily:
source: sensor.pelletslevel_pellets_consumption
cycle: daily
pellets_weekly:
source: sensor.pelletslevel_pellets_consumption
cycle: weekly
pellets_monthly:
source: sensor.pelletslevel_pellets_consumption
cycle: monthly
pellets_yearly:
source: sensor.pelletslevel_pellets_consumption
cycle: yearly
# TEMPLATE pour sauvegarder la valeur (optionnel mais recommandé)
template:
- sensor:
- name: "Pellets Stock Actuel"
unique_id: pellets_stock_actuel
unit_of_measurement: "kg"
device_class: weight
state_class: measurement
state: "{{ states('sensor.pelletslevel_pellet_weight') | float(0) }}"
- name: "Pellets Niveau %"
unique_id: pellets_niveau_pct
unit_of_measurement: "%"
state: "{{ states('sensor.pelletslevel_pellet_level') | float(0) }}"
# Helper pour stocker la consommation totale persistante
input_number:
pellets_consumption_backup:
name: "Backup consommation pellets"
min: 0
max: 100000
step: 0.1
unit_of_measurement: "kg"
mode: box
pellets_consumption_reset:
name: "Nouvelle conso.Tot. Pellets (kg)"
min: 0
max: 10000
step: 0.1
mode: box
unit_of_measurement: kg
icon: mdi:counter