Ecrans ePaper Seeed Studio reTerminal E1001 et E1002

Test complet des nouveaux écrans ePaper de Seeed Studio : les reTerminal E1001 et E1002. Exemple d'intégration avec Home Assistant via ESPHome.
Ecrans ePaper Seeed Studio reTerminal E1001 et E1002

Sommaire

Introduction

Seeed Studio a dévoilé de nouveaux écrans reTerminal E composés du E1001 (monochrome) et du E1002 (couleur). Des terminaux HMI (Human Machine Interface) utilisant des écrans e-paper (ou "paper ink") basse consommation, conçus pour des applications comme les dashboards, la signalétique numérique ou les installations toujours sous tension. Ils sont équipés d'un ESP32-S3 avec écran E-paper.

L’ESP32-S3 est un microcontrôleur puissant de la famille ESP32 d’Espressif, conçu pour des applications embarquées nécessitant à la fois connectivité et performances. Il se distingue par son double cœur Xtensa LX7 cadencé jusqu’à 240 MHz, son support natif de l’IA et du traitement vectoriel, ainsi que ses capacités avancées de gestion de la mémoire et du multimédia. Grâce à sa connectivité Wi-Fi et Bluetooth Low Energy (BLE 5.0), il est particulièrement adapté pour des projets connectés et interactifs.

Couplé à un écran e-paper (ou paper ink), l’ESP32-S3 ouvre la voie à des solutions d’affichage basse consommation, idéales pour des appareils nécessitant une autonomie prolongée. L’e-paper présente en effet deux atouts majeurs :

  • une excellente lisibilité même en plein soleil,
  • une consommation énergétique quasi nulle lorsqu’il n’y a pas de rafraîchissement de l’écran.

Cette combinaison permet de concevoir des projets tels que des étiquettes électroniques intelligentes, des dashboards IoT basse consommation, des lecteurs de données environnementales ou encore des interfaces utilisateur minimalistes et élégantes.

Unboxing

reTerminal E1001 (Monochrome)

reTerminal E1002 (Couleur)

Spécification

reTerminal E1001 (Monochrome)

  • Processeur : ESP32-S3R8
  • Mémoire : 8MB PSRAM
  • Écran : 7.5 pouces Monochrome (noir et blanc)
  • Résolution : 800 x 480 Pixels
  • Stockage : 32MB Flash, Carte Micro SD/TF (Jusqu'à 32GB, optionnelle)
  • Connexion Sans fil : 2.4GHz 802.11 b/g/n Wi-Fi, Bluetooth 5.0
  • Capteur de température et d'humidité : Capteur SHT40 intégré situé dans le coin inférieur gauche à l'arrière de l'appareil pour la surveillance de l'environnement.
  • Microphone
  • Buzzer
  • Batterie : 2000 mHA
  • Tension d'entrée : DC 5V 1A
  • Dimension : 175mm x 120mm x 16.5mm

reTerminal E1002 (Couleur)

  • Processeur : ESP32-S3R8
  • Mémoire : 8MB PSRAM
  • Écran : 7.3 pouces Couleur (6 couleurs , inclus noir et blanc) avec technologie ACeP (Advanced Color ePaper)
  • Résolution : 800 x 480 Pixels
  • Stockage : 32MB Flash, Carte Micro SD/TF (Jusqu'à 32GB, optionnelle)
  • Connexion Sans fil : 2.4GHz 802.11 b/g/n Wi-Fi, Bluetooth 5.0
  • Capteur de température et d'humidité : Capteur SHT40 intégré situé dans le coin inférieur gauche à l'arrière de l'appareil pour la surveillance de l'environnement.
  • Microphone
  • Buzzer
  • Batterie : 2000 mHA
  • Tension d'entrée : DC 5V 1A
  • Dimension : 175mm x 120mm x 16.5mm

Description du produit

1 - Écran 7.5" Monochrome / 7.3 Couleur"
2 - Bouton
3 - Microphone
4 - Port carte MicroSD
5 - Interrupteur d'alimentation
6 - LED d'état (verte)
7 - LED d'alimentation (rouge)
8 - Port USB-C
9 - Port d'extension

Pin du port d'extension


Pin (from top to bottom)
LabelESP32-S3 PinFunctionDescription
1HEADER_3V3-Power3.3V power supply for external devices
2GND-GroundCommon ground reference
3ESP_IO46GPIO46GPIO/ADCGeneral purpose I/O with analog input capability
4ESP_IO2/ADC1_CH4GPIO2GPIO/ADCGeneral purpose I/O with analog input capability (ADC1 channel 4)
5ESP_IO17/TX1GPIO17GPIO/UART TXGPIO or UART transmit (TX) signal
6ESP_IO18/RX1GPIO18GPIO/UART RXGPIO or UART receive (RX) signal
7ESP_IO20/I2C0_SCLGPIO20GPIO/I2C SCLGPIO or I2C clock signal
8ESP_IO19/I2C0_SDAGPIO19GPIO/I2C SDAGPIO or I2C data signal

Utilisation

Préparation de l'appareil

Maintenant, nous allons préparer le firmware de l'appareil sous ESPHome.
Vous pouvez consulter ici le tutoriel pour installer et configurer ESPHome.

Allez dans le menu de ESPHome et créer un nouvel appareil en cliquant sur New device.

Vous indiquez un nom à l'appareil.

Sélectionner en type d'appareil ESP32-S3.

À la fenêtre suivante, faite skip.

Votre appareil est prêt à être flashé.

Démo

Je vous propose une démo des deux écrans e-paper, avec quasiment toutes les fonctionnalités de l'appareil (Sauf le Microphone).
Dans un premier temps, on va créer des modèles de capteurs, pour pouvoir récupérer la météo.

Préparation des modèles de capteurs

Installer l'intégration Météo-France, pour avoir les donnes météo.

Vous allez ensuite créer deux triggers template pour récupérer les prévisions météo par jour et heure, puis deux sensors template pour mettre ces informations dans des attributs. Puis une sensor template pour la direction du vent.

Pour cela, ajouter ce code dans votre fichier configuration.yaml ou votre template.yaml suivant comment vous avez configuré vos template.

#########################
#       TEMPLATE        #
#########################
template:
  - sensor:
      - name: Direction Vent Ma Ville
        unique_id: direction_vent_ma_ville
        icon: mdi:weather-windy
        state: >-
          {% set direction = ['N','NNE','NE','ENE','E','ESE','SE','SSE','S','SSO','SO','OSO','O','ONO','NO','NNO','N'] %}
          {% set degree = state_attr('weather.ma_ville', 'wind_bearing')|int(0) %}
          {% if degree > 1 %}
            {{ direction[((degree+11.25)/22.5)|int(0)] }}
          {% else %}
            {{ 'X' }}
          {% endif %}

      - name: Météo Ma Ville Jour epaper
        unique_id: meteo_ma_ville_jour_epaper
        state: "{{ state_attr('sensor.weather_forecast_jour_ma_ville','forecast')[0].condition | default(0) }}"
        attributes:
          conditiona: "{{ state_attr('sensor.weather_forecast_jour_ma_ville','forecast')[0].condition | default(0) }}"
          temperaturea: "{{ state_attr('sensor.weather_forecast_jour_ma_ville','forecast')[0].temperature | float(0) }}"
          templowa: "{{ state_attr('sensor.weather_forecast_jour_ma_ville','forecast')[0].templow | float(0) }}"
          humiditea: "{{ state_attr('sensor.weather_forecast_jour_ma_ville','forecast')[0].humidity | int(0) }}"
          precipitationa: "{{ state_attr('sensor.weather_forecast_jour_ma_ville','forecast')[0].precipitation | float(0) }}"
          conditiond: "{{ state_attr('sensor.weather_forecast_jour_ma_ville','forecast')[1].condition | default(0) }}"
          temperatured: "{{ state_attr('sensor.weather_forecast_jour_ma_ville','forecast')[1].temperature | float(0) }}"
          templowd: "{{ state_attr('sensor.weather_forecast_jour_ma_ville','forecast')[1].templow | float(0) }}"
          humidited: "{{ state_attr('sensor.weather_forecast_jour_ma_ville','forecast')[1].humidity | int(0) }}"
          precipitationd: "{{ state_attr('sensor.weather_forecast_jour_ma_ville','forecast')[1].precipitation | float(0) }}"
        availability:  "{{ states('sensor.weather_forecast_jour_ma_ville') not in ['unknown', 'unavailable', 'none'] }}"

      - name: Météo Ma Ville Heure Epaper
        unique_id: meteo_ma_ville_heure_epaper
        state: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[0].condition | default(0) }}"
        attributes:
          condition0: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[0].condition | default(0) }}"
          heure0: "{{ as_timestamp(state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[0].datetime) | int(0) | timestamp_custom('%H') }}"
          temperature0: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[0].temperature | float(0) }}"
          humidite0: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[0].humidity | int(0) }}"
          wind0: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[0].wind_speed | int(0) }}"
          precipitation0: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[0].precipitation | float(0) }}"
          condition1: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[1].condition | default(0) }}"
          heure1: "{{ as_timestamp(state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[1].datetime) | int(0) | timestamp_custom('%H') }}"
          temperature1: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[1].temperature | float(0) }}"
          humidite1: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[1].humidity | int(0) }}"
          wind1: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[1].wind_speed | int(0) }}"
          precipitation1: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[1].precipitation | float(0) }}"
          condition2: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[2].condition | default(0) }}"
          heure2: "{{ as_timestamp(state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[2].datetime) | int(0) | timestamp_custom('%H') }}"
          temperature2: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[2].temperature | float(0) }}"
          humidite2: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[2].humidity | int(0) }}"
          wind2: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[2].wind_speed | int(0) }}"
          precipitation2: "{{ state_attr('sensor.weather_forecast_heure_ma_ville','forecast')[2].precipitation | float(0) }}"
        availability:  "{{ states('sensor.weather_forecast_heure_ma_ville') not in ['unknown', 'unavailable', 'none'] }}"

  - trigger:
      - platform: time_pattern
        hours: /1
      - platform: homeassistant
        event: start
    action:
      - action: weather.get_forecasts
        data:
          type: daily
        target:
          entity_id: weather.ma_ville
        response_variable: daily
    sensor:
      - name: Weather Forecast Jour Ma Ville
        unique_id: weather_forecast_jour_ma_ville
        state: "{{ daily['weather.ma_ville'].forecast[0].condition }}"
        attributes:
          forecast: "{{ daily['weather.ma_ville'].forecast }}"
        availability:  "{{ states('weather.ma_ville') not in ['unknown', 'unavailable', 'none'] }}"

  - trigger:
      - platform: time_pattern
        hours: /1
      - platform: homeassistant
        event: start
    action:
      - action: weather.get_forecasts
        data:
          type: hourly
        target:
          entity_id: weather.ma_ville
        response_variable: hourly
    sensor:
      - name: Weather Forecast Heure Ma Ville
        unique_id: weather_forecast_heure_ma_ville
        state: "{{ hourly['weather.ma_ville'].forecast[0].condition }}"
        attributes:
          forecast: "{{ hourly['weather.ma_ville'].forecast }}"
        availability:  "{{ states('weather.ma_ville') not in ['unknown', 'unavailable', 'none'] }}"

Puis enregistrer et recharger la configuration des entités basées sur modèle. Aller dans outils de développement, onglet YAML et cliquer sur Entités basées sur modèle.

Vous obtiendrez deux entités météo par jour et heure, ainsi qu'une entité pour la direction du vent, qui serviront pour les données météo a utilisé dans le code ESPHome.

Installation du fichier icône

Maintenant, vous devrez copier le fichier materialdesignicons-webfont.ttf pour utiliser les icônes du site Material Design Icons dans le code ESPHome.
Il faudra le placer dans votre dossier esphome, puis créer à l'intérieur un dossier fonts et y coller le fichier.

Configuration du fichier secrets.yaml

Ensuite, vous devrez ajouter dans votre fichier secrets.yaml qui se trouve dans le dossier esphome ce code. Modifier par votre SSID de la box internet et sa clé Wifi, l'ip de votre box internet dans wifi_gtw et wifi_dns1. Puis les coordonnées GPS de votre maison dans latitude et longitude (pour les données du soleil).

wifi_ssid: "xxxxxxx"
wifi_password: "xxxxxxxxxxxxxxxxxxxxxxx"
wifi_gtw: "192.168.1.x"
wifi_sub: "255.255.255.0"
wifi_dns1: "192.168.1.x"
latitude: xx.xxxx°
longitude: x.xxxx°

À présent, votre ESPHome est prêt pour flasher le firmware.

Code ESPHome reTerminal E1001 (Monochrome)

substitutions:
  name: reterminal-e1001
  friendly_name: reTerminal E1001 

esphome:
  name: ${name}
  name_add_mac_suffix: false
  friendly_name: ${friendly_name}
  on_boot:
    - priority: 600
      then:
        - output.turn_on: bsp_sd_enable
        - output.turn_on: bsp_battery_enable
        - delay: 200ms
        - component.update: battery_voltage
        - component.update: battery_level
        - delay: 15s
        - component.update: sun_sunrise
        - component.update: sun_sunset
        - delay: 45s 
        - component.update: epaper_display
        - delay: 30s
        - lambda: |-
            auto time = id(ha_time).now();
            if (!time.is_valid()) {
              ESP_LOGI("custom", "Heure invalide (pas de synchro NTP). Deep sleep 60 min par défaut.");
              id(deep_sleep_1).set_sleep_duration(60 * 60 * 1000ULL);
              id(deep_sleep_1).begin_sleep(true);
              return;
            }
            
            int now_seconds = time.hour * 3600 + time.minute * 60 + time.second;
            
            int h = time.hour;
            int m = time.minute;
            int interval = id(sleep_interval_hours);
            int target_min = id(target_minute);
            
            //Calcul de la prochaine heure cible
            int next_target_hour = (h / interval) * interval;
            if (m >= target_min) {
              next_target_hour += interval;
            }
            if (next_target_hour >= 24) next_target_hour -= 24;
            
            int target_seconds = next_target_hour * 3600 + target_min * 60;
            if (target_seconds <= now_seconds) {
              target_seconds += 24 * 3600;  // bascule au jour suivant
            }
            
            int sleep_seconds = target_seconds - now_seconds;
            
            ESP_LOGI("custom", "Il est %02d:%02d:%02d", h, m, time.second);
            ESP_LOGI("custom", "Prochain réveil prévu à %02d:%02d (dans %d sec)", 
                     next_target_hour, target_min, sleep_seconds);
            
            id(deep_sleep_1).set_sleep_duration((uint64_t)sleep_seconds * 1000ULL);
            id(deep_sleep_1).begin_sleep(true);

esp32:
  #board: esp32-s3-devkitc-1
  variant: esp32s3
  framework:
    type: esp-idf
  flash_size: 32MB

psram:
  mode: octal

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "LYgjtfzoelYYwciqzqr1Z6dsZxxxxxxxxxxxxxxxxxxx"

ota:
  - platform: esphome
    password: "ce1cbb03ba47ca50xxxxxxxxxxxxxxxxxx"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true
  manual_ip:
    static_ip: 192.168.1.111
    gateway: !secret wifi_gtw
    subnet: !secret wifi_sub
    dns1: !secret wifi_dns1

deep_sleep:
  id: deep_sleep_1
  run_duration: 2min
  #sleep_duration: 58min 
  wakeup_pin: GPIO3  # Bouton vert
  wakeup_pin_mode: INVERT_WAKEUP

# SPI / I²C
spi:
  clk_pin: GPIO7
  mosi_pin: GPIO9
i2c:
  scl: GPIO20
  sda: GPIO19

globals:
  - id: page_index
    type: int
    restore_value: true
    initial_value: '0'
  - id: battery_glyph
    type: std::string
    restore_value: no
    initial_value: "\"\\U000F0079\""   # default full battery
  - id: sleep_interval_hours
    type: int
    restore_value: no
    initial_value: '1'  # Toutes les 1 heure
  - id: target_minute
    type: int
    restore_value: no
    initial_value: '2'  # À la minute 02

script:
  - id: buzzer_cycle
    mode: single
    then:
      - light.turn_on:
          id: buzzer
          brightness: "50%"
      - delay: 400ms
      - light.turn_off: buzzer
      - delay: 400ms
      - light.turn_on:
          id: buzzer
          brightness: "60%"
      - delay: 400ms
      - light.turn_off: buzzer
      - delay: 400ms
      - light.turn_on:
          id: buzzer
          brightness: "70%"
      - delay: 400ms
      - light.turn_off: buzzer
      - delay: 400ms
      - light.turn_on:
          id: buzzer
          brightness: "80%"
      - delay: 400ms
      - light.turn_off: buzzer
      - delay: 400ms
      - light.turn_on:
          id: buzzer
          brightness: "80%"
      - delay: 400ms
      - light.turn_off: buzzer



sensor:
  - platform: sht4x
    temperature:
      name: Température
      id: temp_sensor
    humidity:
      name: Humidité
      id: hum_sensor

  - platform: adc
    pin: GPIO1
    name: Tension Batterie
    id: battery_voltage
    update_interval: 60s
    attenuation: 12db
    filters:
      - multiply: 2.0
    on_value_range:
      - below: 3.41
        then:
          - script.execute: buzzer_cycle

  - platform: template
    name: Niveau Batterie
    id: battery_level
    unit_of_measurement: "%"
    icon: "mdi:battery"
    device_class: battery
    state_class: measurement
    lambda: 'return id(battery_voltage).state;'
    update_interval: 60s
    on_value:
      then:
        - lambda: |-
            int pct = int(x);
            if (pct <= 10)      id(battery_glyph) = "\U000F007A";
            else if (pct <= 20) id(battery_glyph) = "\U000F007B";
            else if (pct <= 30) id(battery_glyph) = "\U000F007C";
            else if (pct <= 40) id(battery_glyph) = "\U000F007D";
            else if (pct <= 50) id(battery_glyph) = "\U000F007E";
            else if (pct <= 60) id(battery_glyph) = "\U000F007F";
            else if (pct <= 70) id(battery_glyph) = "\U000F0080";
            else if (pct <= 80) id(battery_glyph) = "\U000F0081";
            else if (pct <= 90) id(battery_glyph) = "\U000F0082";
            else                id(battery_glyph) = "\U000F0079";
    filters:
      - calibrate_linear:
          - 4.15 -> 100.0
          - 3.96 -> 90.0
          - 3.91 -> 80.0
          - 3.85 -> 70.0
          - 3.80 -> 60.0
          - 3.75 -> 50.0
          - 3.68 -> 40.0
          - 3.58 -> 30.0
          - 3.49 -> 20.0
          - 3.41 -> 10.0
          - 3.30 -> 5.0
          - 3.27 -> 0.0
      - clamp:
          min_value: 0
          max_value: 100

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "temperaturea"
    id: today_temperaturea

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "templowa"
    id: today_templowa

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "humiditea"
    id: today_humiditea

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "precipitationa"
    id: today_precipitationa

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "temperatured"
    id: tomorrow_temperatured

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "templowd"
    id: tomorrow_templowd

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "humidited"
    id: tomorrow_humidited

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "precipitationd"
    id: tomorrow_precipitationd

  - platform: homeassistant
    entity_id: weather.ma_ville
    attribute: "temperature"
    id: now_weather_temperature

  - platform: homeassistant
    entity_id: weather.ma_ville
    attribute: "humidity"
    id: now_weather_humidity

  - platform: homeassistant
    entity_id: weather.ma_ville
    attribute: "wind_speed"
    id: now_weather_wind_speed

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "heure0"
    id: h1_weather_heure

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "temperature0"
    id: h1_weather_temperature

  - platform: homeassistant
    entity_id: sensor.meteo_ma_villex_heure_epaper
    attribute: "humidite0"
    id: h1_weather_humidite

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "wind0"
    id: h1_weather_wind

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "precipitation0"
    id: h1_weather_precipitation

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "heure1"
    id: h2_weather_heure

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "temperature1"
    id: h2_weather_temperature

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "humidite1"
    id: h2_weather_humidite

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "wind1"
    id: h2_weather_wind

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "precipitation1"
    id: h2_weather_precipitation

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "heure2"
    id: h3_weather_heure

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "temperature2"
    id: h3_weather_temperature

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "humidite2"
    id: h3_weather_humidite

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "wind2"
    id: h3_weather_wind

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "precipitation2"
    id: h3_weather_precipitation


output:
  - platform: gpio
    pin: GPIO6
    id: bsp_led
    inverted: true
  - platform: gpio
    pin: GPIO16
    id: bsp_sd_enable
  - platform: gpio
    pin: GPIO21
    id: bsp_battery_enable
  - platform: ledc   # CORRECTED: 'ledc' is the correct platform for ESP32 PWM.
    pin: GPIO45
    id: buzzer_pwm
    # The frequency determines the pitch of the buzzer's sound. 1000Hz is a mid-range tone.
    frequency: 1000Hz

# Onboard LED verte
light:
  - platform: binary
    name: Onboard LED
    output: bsp_led
    id: onboard_led
  - platform: monochromatic
    output: buzzer_pwm
    name: Buzzer
    id: buzzer
    # Setting transition length to 0s makes the buzzer turn on and off instantly.
    default_transition_length: 0s
    
binary_sensor:
  - platform: gpio    # Bouton page suivante
    pin:
      number: GPIO4
      mode: INPUT_PULLUP
      inverted: true
    id: key1
    name: "Bouton suivant"
    on_press:
      then:
        - lambda: |-
            id(page_index) = (id(page_index) + 1) % 2;
            id(epaper_display).update();

  - platform: gpio     # Bouton page précédente
    pin:
      number: GPIO5
      mode: INPUT_PULLUP
      inverted: true
    id: key2
    name: "Bouton précédent"
    on_press:
      then:
        - lambda: |-
            id(page_index) = (id(page_index) - 1 + 2) % 2;
            id(epaper_display).update();

#  - platform: gpio  # Bouton vert
#    pin:
#      number: GPIO3
#      mode: INPUT_PULLUP
#      inverted: true
#    id: key_green
#    name: "Bouton vert"
#    on_multi_click:
#      - timing:
#          - ON for 40ms to 400ms
#          - OFF for 40ms to 300ms
#          - ON for 40ms to 400ms
#          - OFF for at least 330ms
#        then:
#          - component.update: epaper_display 

# Home Assistant time
time:
  - platform: homeassistant
    id: ha_time

sun:
  latitude: !secret latitude
  longitude: !secret longitude

text_sensor:    
  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "conditiona"
    id: today_weather

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "conditiond"
    id: tomorrow_weather

  - platform: homeassistant
    entity_id: weather.ma_ville
    id: now_weather

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "condition0"
    id: h1_weather_condition

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "condition1"
    id: h2_weather_condition

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "condition2"
    id: h3_weather_condition

  - platform: sun
    name: Sun Next Sunrise
    type: sunrise
    format: "%H:%M"
    id: sun_sunrise
    internal: true

  - platform: sun
    name: Sun Next Sunset
    type: sunset
    format: "%H:%M"
    id: sun_sunset
    internal: true

  - platform: homeassistant
    entity_id: sensor.direction_vent_ma_ville
    id: now_weather_wind_bearing

# Fonts
font:
  - file: "gfonts://Inter@700"
    id: small_font
    size: 24
  - file: "gfonts://Inter@700"
    id: mid_font
    size: 36
    glyphs: "<>!'%()/+,-_.:;*=°?#0123456789AÀBCDEÉÈÊFGHIJKLMNOPQRSTUVWXYZ aàbcdeéèêfghijklmnopqrstuvwxyzôöç"
  - file: "gfonts://Inter@700"
    id: title_font
    size: 42
    glyphs: "<>!'%()/+,-_.:;*=°?#0123456789AÀBCDEÉÈÊFGHIJKLMNOPQRSTUVWXYZ aàbcdeéèêfghijklmnopqrstuvwxyzôöç"
  - file: "gfonts://Inter@700"
    id: big_font
    size: 180
  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: font_mdi_large
    size: 70
    glyphs:
      - "\U000F050F"  # thermometer
      - "\U000F058E"  # humidity
  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: font_bat_icon
    size: 24
    glyphs:
      - "\U000F007A"  # mdi-battery-10
      - "\U000F007B"  # mdi-battery-20
      - "\U000F007C"  # mdi-battery-30
      - "\U000F007D"  # mdi-battery-40
      - "\U000F007E"  # mdi-battery-50
      - "\U000F007F"  # mdi-battery-60
      - "\U000F0080"  # mdi-battery-70
      - "\U000F0081"  # mdi-battery-80
      - "\U000F0082"  # mdi-battery-90
      - "\U000F0079"  # mdi-battery

  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: icon_today
    size: 110
    glyphs: &mdi-weather-glyphs
      - "\U000F0590" # mdi-weather-cloudy
      - "\U000F0F2F" # mdi-weather-cloudy-alert
      - "\U000F0593" # mdi-weather-lightning
      - "\U000F067E" # mdi-weather-lightning-rainy
      - "\U000F0594" # mdi-weather-night
      - "\U000F0F31" # mdi-weather-night-partly-cloudy      
      - "\U000F0595" # mdi-weather-partly-cloudy
      - "\U000F0F32" # mdi-weather-partly-lightning
      - "\U000F0F33" # mdi-weather-partly-rainy
      - "\U000F0596" # mdi-weather-pouring
      - "\U000F0597" # mdi-weather-rainy
      - "\U000F0599" # mdi-weather-sunny
      - "\U000F059B" # mdi-weather-sunset-down
      - "\U000F059C" # mdi-weather-sunset-up
      - "\U000F0F37" # mdi-weather-sunny-alert
      - "\U000F14E4" # mdi-weather-sunny-off
      - "\U000F059A" # mdi-weather-sunset
      - "\U000F059D" # mdi-weather-windy
      - "\U000F059E" # mdi-weather-windy-variant
      - "\U000F0591" # mdi-weather-fog
      - "\U000F0592" # mdi-weather-hail
      - "\U000F0F30" # mdi-weather-hazy

  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: font_weather_icon
    size: 24
    glyphs:
      - "\U000F10C2"  # mdi-thermometer-high
      - "\U000F10C3"  # mdi-thermometer-low
      - "\U000F058E"  # mdi-water-percent
      - "\U000F058C"  # mdi-water
      - "\U000F059D"  # mdi-weather-windy
      - "\U000F15FA"  # mdi-windsock

  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: icon_sun
    size: 36
    glyphs:
      - "\U000F059B" # mdi-weather-sunset-down
      - "\U000F059C" # mdi-weather-sunset-up


# e-paper
display:
  - platform: waveshare_epaper
    id: epaper_display
    model: 7.50inv2
    cs_pin: GPIO10
    dc_pin: GPIO11
    reset_pin:
      number: GPIO12
      inverted: false
    busy_pin:
      number: GPIO13
      inverted: true
    update_interval: never
    lambda: |-
      // Map weather states to MDI characters.
      std::map<std::string, std::string> weather_icon_map
        {
          {"cloudy", "\U000F0590"},
          {"cloudy-alert", "\U000F0F2F"},
          {"fog", "\U000F0591"},
          {"hail", "\U000F0592"},
          {"hazy", "\U000F0F30"},
          {"lightning", "\U000F0593"},
          {"lightning-rainy", "\U000F067E"},
          {"clear-night", "\U000F0594"},
          {"night-partly-cloudy", "\U000F0F31"},          
          {"partlycloudy", "\U000F0595"},
          {"partly-lightning", "\U000F0F32"},
          {"partly-rainy", "\U000F0F33"},
          {"pouring", "\U000F0596"},
          {"rainy", "\U000F0597"},
          {"sunny", "\U000F0599"},
          {"sunny-alert", "\U000F0F37"},
          {"sunny-off", "\U000F14E4"},
          {"sunset", "\U000F059A"},
          {"sunset-down", "\U000F059B"},
          {"sunset-up", "\U000F059C"},
          {"windy", "\U000F059D"},
          {"windy-variant", "\U000F059E"},
        };

      // ----------  PAGE 0  ----------
      if (id(page_index) == 0) {
        const int scr_w = 800;
        const int scr_h = 480;

        // Batterie dans le coin supérieur droit
        it.printf(700, 7, id(font_bat_icon), "%s", id(battery_glyph).c_str());
        it.printf(730, 5, id(small_font), "%.0f%%", id(battery_level).state);

        //ligne verticale
        it.filled_rectangle(400, 100, 2, 280);

        // ---------------------------------------------------------
        // Horizontal split: two 400 px columns
        const int col_w = scr_w / 2;

        const int icon_y   = 100;   // Icon baseline
        const int value_y  = 220;   // Number baseline
        const int unit_y   = 300;   // Unit baseline
        const int label_y  = 380;   // Text label baseline

        const int icon_size = 70;   // icon font size
        const int val_size  = 120;  // number font size
        const int unit_size = 44;   // unit font size
        const int label_size= 36;   // label font size

        // --- colonne gauche : Température -----------------------------
        const int left_mid = col_w / 2 - 30;   // 200 px

        // Icon
        it.printf(left_mid, icon_y, id(font_mdi_large), TextAlign::CENTER, "\U000F050F");
        // Value
        it.printf(left_mid, value_y, id(big_font), TextAlign::CENTER, "%.0f", id(temp_sensor).state);
        // Unit
        it.printf(left_mid + 150, unit_y, id(mid_font), TextAlign::CENTER, "°C");
        // Label
        it.printf(left_mid, label_y, id(mid_font), TextAlign::CENTER, "Température");

        // --- colonne droite : Humidité -------------------------------
        const int right_mid = col_w + col_w / 2;   // 600 px

        // Icon
        it.printf(right_mid, icon_y, id(font_mdi_large), TextAlign::CENTER, "\U000F058E");
        // Value
        it.printf(right_mid, value_y, id(big_font), TextAlign::CENTER, "%.0f", id(hum_sensor).state);
        // Unit
        it.printf(right_mid + 150, unit_y, id(mid_font), TextAlign::CENTER, "%%");
        // Label
        it.printf(right_mid, label_y, id(mid_font), TextAlign::CENTER, "Humidité");
      }
      // ----------  PAGE 1  ----------
      else{
        // Batterie dans le coin supérieur droit
        it.printf(700, 7, id(font_bat_icon), "%s", id(battery_glyph).c_str());
        it.printf(730, 5, id(small_font), "%.0f%%", id(battery_level).state);

        // heure date
        const char* jours[] = {"Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"};
        const char* mois[] = {"Janv", "Févr", "Mars", "Avr", "Mai", "Juin", "Juill", "Août", "Sept", "Oct", "Nov", "Déc"};
        auto t = id(ha_time).now();
        if (t.is_valid()) {
          // Heure
          it.strftime(10, 5, id(mid_font), "%H:%M", t);
          // Affiche la date en français "Jeu 04 sept 2025"
          it.printf(10, 40, id(mid_font), "%s %d %s %d",
                    jours[t.day_of_week -1],
                    t.day_of_month,
                    mois[t.month - 1],
                    t.year);
        }

        // Soleil lever/coucher
        it.printf(379, 8, id(icon_sun), TextAlign::TOP_CENTER, "\U000F059C");
        it.printf(404, 5, id(mid_font), TextAlign::TOP_LEFT, "à %s", id(sun_sunrise).state.c_str());
        it.printf(379, 43, id(icon_sun), TextAlign::TOP_CENTER, "\U000F059B");
        it.printf(404, 40, id(mid_font), TextAlign::TOP_LEFT, "à %s", id(sun_sunset).state.c_str());

        //ligne verticale
        it.filled_rectangle(280, 90, 4, 375, BLACK);

        // --- colonne gauche : Météo -----------------------------
        // Titre météo
        it.printf(135, 90, id(title_font), TextAlign::TOP_CENTER, "MÉTÉO");

        // Titre aujourd'hui
        it.printf(135, 150, id(small_font), TextAlign::TOP_CENTER, "Aujourd'hui");

        // icône aujourd'hui
        it.printf(70, 190, id(icon_today), TextAlign::TOP_CENTER, "%s", weather_icon_map[id(today_weather).state.c_str()].c_str());
      
        // températurea
        it.printf(145, 192, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F10C2");
        it.printf(160, 190, id(small_font), TextAlign::TOP_LEFT, "%.1f°C", id(today_temperaturea).state);
        // templowa
        it.printf(145, 222, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F10C3");
        it.printf(160, 220, id(small_font), TextAlign::TOP_LEFT, "%.1f°C", id(today_templowa).state);
        // humiditéa
        it.printf(145, 252, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F058E");
        it.printf(160, 250, id(small_font), TextAlign::TOP_LEFT, "%.0f%%", id(today_humiditea).state);
        // précipitationa
        it.printf(145, 282, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F058C");
        it.printf(160, 280, id(small_font), TextAlign::TOP_LEFT, "%.1fmm", id(today_precipitationa).state);

        // Titre demain
        it.printf(130, 310, id(small_font), TextAlign::TOP_CENTER, "Demain");

        // icône demain
        it.printf(70, 350, id(icon_today), TextAlign::TOP_CENTER, "%s", weather_icon_map[id(tomorrow_weather).state.c_str()].c_str());
      
        // températured
        it.printf(145, 352, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F10C2");
        it.printf(160, 350, id(small_font), TextAlign::TOP_LEFT, "%.1f°C", id(tomorrow_temperatured).state);
        // templowd
        it.printf(145, 382, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F10C3");
        it.printf(160, 380, id(small_font), TextAlign::TOP_LEFT, "%.1f°C", id(tomorrow_templowd).state);
        // humiditéd
        it.printf(145, 412, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F058E");
        it.printf(160, 410, id(small_font), TextAlign::TOP_LEFT, "%.0f%%", id(tomorrow_humidited).state);
        // précipitationd
        it.printf(145, 442, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F058C");
        it.printf(160, 440, id(small_font), TextAlign::TOP_LEFT, "%.1fmm", id(tomorrow_precipitationd).state);

        // --- colonne droite : Prévision -----------------------------
        // Titre prévision
        it.printf(540, 90, id(title_font), TextAlign::TOP_CENTER, "PRÉVISION");

        // Titre maintenant
        it.printf(430, 150, id(small_font), TextAlign::TOP_CENTER, "Maintenant");

        // icône maintenant
        it.printf(360, 190, id(icon_today), TextAlign::TOP_CENTER, "%s", weather_icon_map[id(now_weather).state.c_str()].c_str());
      
        // now_weather_temperature
        it.printf(435, 192, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F10C2");
        it.printf(450, 190, id(small_font), TextAlign::TOP_LEFT, "%.1f°C", id(now_weather_temperature).state);
        // now_weather_humidity
        it.printf(435, 222, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F058E");
        it.printf(450, 220, id(small_font), TextAlign::TOP_LEFT, "%.0f%%", id(now_weather_humidity).state);
        // now_weather_wind_speed
        it.printf(435, 252, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F059D");
        it.printf(450, 250, id(small_font), TextAlign::TOP_LEFT, "%.0fkm/h", id(now_weather_wind_speed).state);
        // now_weather_wind_bearing
        it.printf(435, 282, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F15FA");
        it.printf(450, 280, id(small_font), TextAlign::TOP_LEFT, "%s", id(now_weather_wind_bearing).state.c_str());

        // Titre heure +1
        it.printf(670, 150, id(small_font), TextAlign::TOP_CENTER, "%.0f:00", id(h1_weather_heure).state);

        // icône heure +1
        it.printf(600, 190, id(icon_today), TextAlign::TOP_CENTER, "%s", weather_icon_map[id(h1_weather_condition).state.c_str()].c_str());
      
        // h1_weather_temperature
        it.printf(675, 192, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F10C2");
        it.printf(690, 190, id(small_font), TextAlign::TOP_LEFT, "%.1f°C", id(h1_weather_temperature).state);
        // h1_weather_humidite
        it.printf(675, 222, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F058E");
        it.printf(690, 220, id(small_font), TextAlign::TOP_LEFT, "%.0f%%", id(h1_weather_humidite).state);
        // h1_weather_wind
        it.printf(675, 252, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F059D");
        it.printf(690, 250, id(small_font), TextAlign::TOP_LEFT, "%.0fkm/h", id(h1_weather_wind).state);
        // h1_weather_precipitation
        it.printf(675, 282, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F058C");
        it.printf(690, 280, id(small_font), TextAlign::TOP_LEFT, "%.1fmm", id(h1_weather_precipitation).state);

        // Titre heure +2
        it.printf(430, 310, id(small_font), TextAlign::TOP_CENTER, "%.0f:00", id(h2_weather_heure).state);

        // icône heure +2
        it.printf(360, 350, id(icon_today), TextAlign::TOP_CENTER, "%s", weather_icon_map[id(h2_weather_condition).state.c_str()].c_str());
      
        // h2_weather_temperature
        it.printf(435, 352, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F10C2");
        it.printf(450, 350, id(small_font), TextAlign::TOP_LEFT, "%.1f°C", id(h2_weather_temperature).state);
        // h2_weather_humidite
        it.printf(435, 382, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F058E");
        it.printf(450, 380, id(small_font), TextAlign::TOP_LEFT, "%.0f%%", id(h2_weather_humidite).state);
        // h2_weather_wind
        it.printf(435, 412, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F059D");
        it.printf(450, 410, id(small_font), TextAlign::TOP_LEFT, "%.0fkm/h", id(h2_weather_wind).state);
        // h2_weather_precipitation
        it.printf(435, 442, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F058C");
        it.printf(450, 440, id(small_font), TextAlign::TOP_LEFT, "%.1fmm", id(h2_weather_precipitation).state);

        // Titre heure +3
        it.printf(670, 310, id(small_font), TextAlign::TOP_CENTER, "%.0f:00", id(h3_weather_heure).state);

        // icône heure +3
        it.printf(600, 350, id(icon_today), TextAlign::TOP_CENTER, "%s", weather_icon_map[id(h3_weather_condition).state.c_str()].c_str());
      
        // h3_weather_temperature
        it.printf(675, 352, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F10C2");
        it.printf(690, 350, id(small_font), TextAlign::TOP_LEFT, "%.1f°C", id(h3_weather_temperature).state);
        // h3_weather_humidite
        it.printf(675, 382, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F058E");
        it.printf(690, 380, id(small_font), TextAlign::TOP_LEFT, "%.0f%%", id(h3_weather_humidite).state);
        // h3_weather_wind
        it.printf(675, 412, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F059D");
        it.printf(690, 410, id(small_font), TextAlign::TOP_LEFT, "%.0fkm/h", id(h3_weather_wind).state);
        // h3_weather_precipitation
        it.printf(675, 442, id(font_weather_icon), TextAlign::TOP_CENTER, "\U000F058C");
        it.printf(690, 440, id(small_font), TextAlign::TOP_LEFT, "%.1fmm", id(h3_weather_precipitation).state);
      }
    

Code ESPHome reTerminal E1002 (Couleur)

substitutions:
  name: reterminal-e1002
  friendly_name: reTerminal E1002 

esphome:
  name: ${name}
  name_add_mac_suffix: false
  friendly_name: ${friendly_name}
  on_boot:
    - priority: 600
      then:
        - output.turn_on: bsp_sd_enable
        - output.turn_on: bsp_battery_enable
        - delay: 200ms
        - component.update: battery_voltage
        - component.update: battery_level
        - delay: 15s
        - component.update: sun_sunrise
        - component.update: sun_sunset
        - delay: 45s 
        - component.update: epaper_display
        - delay: 30s
        - lambda: |-
            auto time = id(ha_time).now();
            if (!time.is_valid()) {
              ESP_LOGI("custom", "Heure invalide (pas de synchro NTP). Deep sleep 60 min par défaut.");
              id(deep_sleep_1).set_sleep_duration(60 * 60 * 1000ULL);
              id(deep_sleep_1).begin_sleep(true);
              return;
            }
            
            int now_seconds = time.hour * 3600 + time.minute * 60 + time.second;
            
            int h = time.hour;
            int m = time.minute;
            int interval = id(sleep_interval_hours);
            int target_min = id(target_minute);
            
            //Calcul de la prochaine heure cible
            int next_target_hour = (h / interval) * interval;
            if (m >= target_min) {
              next_target_hour += interval;
            }
            if (next_target_hour >= 24) next_target_hour -= 24;
            
            int target_seconds = next_target_hour * 3600 + target_min * 60;
            if (target_seconds <= now_seconds) {
              target_seconds += 24 * 3600;  // bascule au jour suivant
            }
            
            int sleep_seconds = target_seconds - now_seconds;
            
            ESP_LOGI("custom", "Il est %02d:%02d:%02d", h, m, time.second);
            ESP_LOGI("custom", "Prochain réveil prévu à %02d:%02d (dans %d sec)", 
                     next_target_hour, target_min, sleep_seconds);
            
            id(deep_sleep_1).set_sleep_duration((uint64_t)sleep_seconds * 1000ULL);
            id(deep_sleep_1).begin_sleep(true);


external_components:
#  - source:
#      type: git
#      url: https://github.com/lublak/esphome
#      ref: dev
#    components: [ waveshare_epaper ]

  - source: github://pr#8416
    components: [waveshare_epaper]
    refresh: 1h

esp32:
  #board: esp32-s3-devkitc-1
  variant: esp32s3
  framework:
    type: esp-idf
  flash_size: 32MB

psram:
  mode: octal

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "LYgjtfzoelYYwciqzqr1Z6dsZjwccpxxxxxxxxxxx"

ota:
  - platform: esphome
    password: "ce1cbb03ba47ca50d022xxxxxxxxx"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true
  manual_ip:
    static_ip: 192.168.1.110
    gateway: !secret wifi_gtw
    subnet: !secret wifi_sub
    dns1: !secret wifi_dns1

deep_sleep:
  id: deep_sleep_1
  run_duration: 2min
  #sleep_duration: 58min 
  wakeup_pin: GPIO3  # Bouton vert
  wakeup_pin_mode: INVERT_WAKEUP

# SPI / I²C
spi:
  clk_pin: GPIO7
  mosi_pin: GPIO9
i2c:
  scl: GPIO20
  sda: GPIO19

globals:
  - id: page_index
    type: int
    restore_value: true
    initial_value: '0'
  - id: battery_glyph
    type: std::string
    restore_value: no
    initial_value: "\"\\U000F0079\""   # default full battery
  - id: sleep_interval_hours
    type: int
    restore_value: no
    initial_value: '1'  # Toutes les 1 heure
  - id: target_minute
    type: int
    restore_value: no
    initial_value: '2'  # À la minute 02

script:
  - id: buzzer_cycle
    mode: single
    then:
      - light.turn_on:
          id: buzzer
          brightness: "50%"
      - delay: 400ms
      - light.turn_off: buzzer
      - delay: 400ms
      - light.turn_on:
          id: buzzer
          brightness: "60%"
      - delay: 400ms
      - light.turn_off: buzzer
      - delay: 400ms
      - light.turn_on:
          id: buzzer
          brightness: "70%"
      - delay: 400ms
      - light.turn_off: buzzer
      - delay: 400ms
      - light.turn_on:
          id: buzzer
          brightness: "80%"
      - delay: 400ms
      - light.turn_off: buzzer
      - delay: 400ms
      - light.turn_on:
          id: buzzer
          brightness: "80%"
      - delay: 400ms
      - light.turn_off: buzzer



sensor:
  - platform: sht4x
    temperature:
      name: Température
      id: temp_sensor
    humidity:
      name: Humidité
      id: hum_sensor

  - platform: adc
    pin: GPIO1
    name: Tension Batterie
    id: battery_voltage
    update_interval: 60s
    attenuation: 12db
    filters:
      - multiply: 2.0
    on_value_range:
      - below: 3.41
        then:
          - script.execute: buzzer_cycle

  - platform: template
    name: Niveau Batterie
    id: battery_level
    unit_of_measurement: "%"
    icon: "mdi:battery"
    device_class: battery
    state_class: measurement
    lambda: 'return id(battery_voltage).state;'
    update_interval: 60s
    on_value:
      then:
        - lambda: |-
            int pct = int(x);
            if (pct <= 10)      id(battery_glyph) = "\U000F007A";
            else if (pct <= 20) id(battery_glyph) = "\U000F007B";
            else if (pct <= 30) id(battery_glyph) = "\U000F007C";
            else if (pct <= 40) id(battery_glyph) = "\U000F007D";
            else if (pct <= 50) id(battery_glyph) = "\U000F007E";
            else if (pct <= 60) id(battery_glyph) = "\U000F007F";
            else if (pct <= 70) id(battery_glyph) = "\U000F0080";
            else if (pct <= 80) id(battery_glyph) = "\U000F0081";
            else if (pct <= 90) id(battery_glyph) = "\U000F0082";
            else                id(battery_glyph) = "\U000F0079";
    filters:
      - calibrate_linear:
          - 4.15 -> 100.0
          - 3.96 -> 90.0
          - 3.91 -> 80.0
          - 3.85 -> 70.0
          - 3.80 -> 60.0
          - 3.75 -> 50.0
          - 3.68 -> 40.0
          - 3.58 -> 30.0
          - 3.49 -> 20.0
          - 3.41 -> 10.0
          - 3.30 -> 5.0
          - 3.27 -> 0.0
      - clamp:
          min_value: 0
          max_value: 100

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "temperaturea"
    id: today_temperaturea

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "templowa"
    id: today_templowa

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "humiditea"
    id: today_humiditea

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "precipitationa"
    id: today_precipitationa

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "temperatured"
    id: tomorrow_temperatured

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "templowd"
    id: tomorrow_templowd

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "humidited"
    id: tomorrow_humidited

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "precipitationd"
    id: tomorrow_precipitationd

  - platform: homeassistant
    entity_id: weather.ma_ville
    attribute: "temperature"
    id: now_weather_temperature

  - platform: homeassistant
    entity_id: weather.ma_ville
    attribute: "humidity"
    id: now_weather_humidity

  - platform: homeassistant
    entity_id: weather.ma_ville
    attribute: "wind_speed"
    id: now_weather_wind_speed

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "heure0"
    id: h1_weather_heure

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "temperature0"
    id: h1_weather_temperature

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "humidite0"
    id: h1_weather_humidite

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "wind0"
    id: h1_weather_wind

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "precipitation0"
    id: h1_weather_precipitation

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "heure1"
    id: h2_weather_heure

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "temperature1"
    id: h2_weather_temperature

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "humidite1"
    id: h2_weather_humidite

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "wind1"
    id: h2_weather_wind

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "precipitation1"
    id: h2_weather_precipitation

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "heure2"
    id: h3_weather_heure

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "temperature2"
    id: h3_weather_temperature

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "humidite2"
    id: h3_weather_humidite

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "wind2"
    id: h3_weather_wind

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "precipitation2"
    id: h3_weather_precipitation


output:
  - platform: gpio
    pin: GPIO6
    id: bsp_led
    inverted: true
  - platform: gpio
    pin: GPIO16
    id: bsp_sd_enable
  - platform: gpio
    pin: GPIO21
    id: bsp_battery_enable
  - platform: ledc   # CORRECTED: 'ledc' is the correct platform for ESP32 PWM.
    pin: GPIO45
    id: buzzer_pwm
    # The frequency determines the pitch of the buzzer's sound. 1000Hz is a mid-range tone.
    frequency: 1000Hz

# Onboard LED verte
light:
  - platform: binary
    name: Onboard LED
    output: bsp_led
    id: onboard_led
  - platform: monochromatic
    output: buzzer_pwm
    name: Buzzer
    id: buzzer
    # Setting transition length to 0s makes the buzzer turn on and off instantly.
    default_transition_length: 0s
    
binary_sensor:
  - platform: gpio    # Bouton page suivante
    pin:
      number: GPIO4
      mode: INPUT_PULLUP
      inverted: true
    id: key1
    name: "Bouton suivant"
    on_press:
      then:
        - lambda: |-
            id(page_index) = (id(page_index) + 1) % 2;
            id(epaper_display).update();

  - platform: gpio     # Bouton page précédente
    pin:
      number: GPIO5
      mode: INPUT_PULLUP
      inverted: true
    id: key2
    name: "Bouton précédent"
    on_press:
      then:
        - lambda: |-
            id(page_index) = (id(page_index) - 1 + 2) % 2;
            id(epaper_display).update();

#  - platform: gpio  # Bouton vert
#    pin:
#      number: GPIO3
#      mode: INPUT_PULLUP
#      inverted: true
#    id: key_green
#    name: "Bouton vert"
#    on_multi_click:
#      - timing:
#          - ON for 40ms to 400ms
#          - OFF for 40ms to 300ms
#          - ON for 40ms to 400ms
#          - OFF for at least 330ms
#        then:
#          - component.update: epaper_display 

# Home Assistant time
time:
  - platform: homeassistant
    id: ha_time

sun:
  latitude: !secret latitude
  longitude: !secret longitude

text_sensor:    
  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "conditiona"
    id: today_weather

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "conditiond"
    id: tomorrow_weather

  - platform: homeassistant
    entity_id: weather.ma_ville
    id: now_weather

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "condition0"
    id: h1_weather_condition

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "condition1"
    id: h2_weather_condition

  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "condition2"
    id: h3_weather_condition

  - platform: sun
    name: Sun Next Sunrise
    type: sunrise
    format: "%H:%M"
    id: sun_sunrise
    internal: true

  - platform: sun
    name: Sun Next Sunset
    type: sunset
    format: "%H:%M"
    id: sun_sunset
    internal: true

  - platform: homeassistant
    entity_id: sensor.direction_vent_ma_ville
    id: now_weather_wind_bearing

# Fonts
font:
  - file: "gfonts://Inter@700"
    id: small_font
    size: 24
  - file: "gfonts://Inter@700"
    id: mid_font
    size: 36
    glyphs: "<>!'%()/+,-_.:;*=°?#0123456789AÀBCDEÉÈÊFGHIJKLMNOPQRSTUVWXYZ aàbcdeéèêfghijklmnopqrstuvwxyzôöç"
  - file: "gfonts://Inter@700"
    id: title_font
    size: 42
    glyphs: "<>!'%()/+,-_.:;*=°?#0123456789AÀBCDEÉÈÊFGHIJKLMNOPQRSTUVWXYZ aàbcdeéèêfghijklmnopqrstuvwxyzôöç"
  - file: "gfonts://Inter@700"
    id: big_font
    size: 180
  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: font_mdi_large
    size: 70
    glyphs:
      - "\U000F050F"  # thermometer
      - "\U000F058E"  # humidity
  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: font_bat_icon
    size: 24
    glyphs:
      - "\U000F007A"  # mdi-battery-10
      - "\U000F007B"  # mdi-battery-20
      - "\U000F007C"  # mdi-battery-30
      - "\U000F007D"  # mdi-battery-40
      - "\U000F007E"  # mdi-battery-50
      - "\U000F007F"  # mdi-battery-60
      - "\U000F0080"  # mdi-battery-70
      - "\U000F0081"  # mdi-battery-80
      - "\U000F0082"  # mdi-battery-90
      - "\U000F0079"  # mdi-battery

  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: icon_today
    size: 110
    glyphs: &mdi-weather-glyphs
      - "\U000F0590" # mdi-weather-cloudy
      - "\U000F0F2F" # mdi-weather-cloudy-alert
      - "\U000F0593" # mdi-weather-lightning
      - "\U000F067E" # mdi-weather-lightning-rainy
      - "\U000F0594" # mdi-weather-night
      - "\U000F0F31" # mdi-weather-night-partly-cloudy      
      - "\U000F0595" # mdi-weather-partly-cloudy
      - "\U000F0F32" # mdi-weather-partly-lightning
      - "\U000F0F33" # mdi-weather-partly-rainy
      - "\U000F0596" # mdi-weather-pouring
      - "\U000F0597" # mdi-weather-rainy
      - "\U000F0599" # mdi-weather-sunny
      - "\U000F059B" # mdi-weather-sunset-down
      - "\U000F059C" # mdi-weather-sunset-up
      - "\U000F0F37" # mdi-weather-sunny-alert
      - "\U000F14E4" # mdi-weather-sunny-off
      - "\U000F059A" # mdi-weather-sunset
      - "\U000F059D" # mdi-weather-windy
      - "\U000F059E" # mdi-weather-windy-variant
      - "\U000F0591" # mdi-weather-fog
      - "\U000F0592" # mdi-weather-hail
      - "\U000F0F30" # mdi-weather-hazy

  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: font_weather_icon
    size: 24
    glyphs:
      - "\U000F10C2"  # mdi-thermometer-high
      - "\U000F10C3"  # mdi-thermometer-low
      - "\U000F058E"  # mdi-water-percent
      - "\U000F058C"  # mdi-water
      - "\U000F059D"  # mdi-weather-windy
      - "\U000F15FA"  # mdi-windsock

  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: icon_sun
    size: 36
    glyphs:
      - "\U000F059B" # mdi-weather-sunset-down
      - "\U000F059C" # mdi-weather-sunset-up


# e-paper
display:
  - platform: waveshare_epaper
    id: epaper_display
    model: 7.30in-e
    cs_pin: GPIO10
    dc_pin: GPIO11
    reset_pin:
      number: GPIO12
      inverted: false
    busy_pin:
      number: GPIO13
      inverted: true
    update_interval: never
    lambda: |-
      const auto BLACK   = Color(0,   0,   0,   0);
      const auto RED     = Color(255, 0,   0,   0);
      const auto GREEN   = Color(0,   255, 0,   0);
      const auto BLUE    = Color(0,   0,   255, 0);
      const auto YELLOW  = Color(255, 255, 0,   0);
      const auto WHITE   = Color(255, 255, 255);

      // Map weather states to MDI characters.
      std::map<std::string, std::string> weather_icon_map
        {
          {"cloudy", "\U000F0590"},
          {"cloudy-alert", "\U000F0F2F"},
          {"fog", "\U000F0591"},
          {"hail", "\U000F0592"},
          {"hazy", "\U000F0F30"},
          {"lightning", "\U000F0593"},
          {"lightning-rainy", "\U000F067E"},
          {"clear-night", "\U000F0594"},
          {"night-partly-cloudy", "\U000F0F31"},          
          {"partlycloudy", "\U000F0595"},
          {"partly-lightning", "\U000F0F32"},
          {"partly-rainy", "\U000F0F33"},
          {"pouring", "\U000F0596"},
          {"rainy", "\U000F0597"},
          {"sunny", "\U000F0599"},
          {"sunny-alert", "\U000F0F37"},
          {"sunny-off", "\U000F14E4"},
          {"sunset", "\U000F059A"},
          {"sunset-down", "\U000F059B"},
          {"sunset-up", "\U000F059C"},
          {"windy", "\U000F059D"},
          {"windy-variant", "\U000F059E"},
        };

      // ----------  PAGE 0  ----------
      if (id(page_index) == 0) {
        const int scr_w = 800;
        const int scr_h = 480;

        // Batterie dans le coin supérieur droit
        it.printf(700, 7, id(font_bat_icon), BLACK, "%s", id(battery_glyph).c_str());
        it.printf(730, 5, id(small_font), BLUE, "%.0f%%", id(battery_level).state);

        //ligne verticale
        it.filled_rectangle(400, 100, 2, 280, BLACK);

        // ---------------------------------------------------------
        // Horizontal split: two 400 px columns
        const int col_w = scr_w / 2;

        const int icon_y   = 100;   // Icon baseline
        const int value_y  = 220;   // Number baseline
        const int unit_y   = 300;   // Unit baseline
        const int label_y  = 380;   // Text label baseline

        const int icon_size = 70;   // icon font size
        const int val_size  = 120;  // number font size
        const int unit_size = 44;   // unit font size
        const int label_size= 36;   // label font size

        // --- colonne gauche : Température -----------------------------
        const int left_mid = col_w / 2 - 30;   // 200 px

        // Icon
        it.printf(left_mid, icon_y, id(font_mdi_large), BLACK, TextAlign::CENTER, "\U000F050F");
        // Value
        it.printf(left_mid, value_y, id(big_font), GREEN, TextAlign::CENTER, "%.0f", id(temp_sensor).state);
        // Unit
        it.printf(left_mid + 150, unit_y, id(mid_font), GREEN, TextAlign::CENTER, "°C");
        // Label
        it.printf(left_mid, label_y, id(mid_font), RED, TextAlign::CENTER, "Température");

        // --- colonne droite : Humidité -------------------------------
        const int right_mid = col_w + col_w / 2;   // 600 px

        // Icon
        it.printf(right_mid, icon_y, id(font_mdi_large), BLACK, TextAlign::CENTER, "\U000F058E");
        // Value
        it.printf(right_mid, value_y, id(big_font), BLUE, TextAlign::CENTER, "%.0f", id(hum_sensor).state);
        // Unit
        it.printf(right_mid + 150, unit_y, id(mid_font), BLUE, TextAlign::CENTER, "%%");
        // Label
        it.printf(right_mid, label_y, id(mid_font), RED, TextAlign::CENTER, "Humidité");
      }
      // ----------  PAGE 1  ----------
      else{
        // Batterie dans le coin supérieur droit
        it.printf(700, 7, id(font_bat_icon), BLACK, "%s", id(battery_glyph).c_str());
        it.printf(730, 5, id(small_font), BLUE, "%.0f%%", id(battery_level).state);

        // heure date
        const char* jours[] = {"Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"};
        const char* mois[] = {"Janv", "Févr", "Mars", "Avr", "Mai", "Juin", "Juill", "Août", "Sept", "Oct", "Nov", "Déc"};
        auto t = id(ha_time).now();
        if (t.is_valid()) {
          // Heure
          it.strftime(10, 5, id(mid_font), BLUE, "%H:%M", t);
          // Affiche la date en français "Jeu 04 sept 2025"
          it.printf(10, 40, id(mid_font), BLUE, "%s %d %s %d",
                    jours[t.day_of_week -1],
                    t.day_of_month,
                    mois[t.month - 1],
                    t.year);
        }

        // Soleil lever/coucher
        it.printf(379, 8, id(icon_sun), BLUE, TextAlign::TOP_CENTER, "\U000F059C");
        it.printf(404, 5, id(mid_font), BLUE, TextAlign::TOP_LEFT, "à %s", id(sun_sunrise).state.c_str());
        it.printf(379, 43, id(icon_sun), BLUE, TextAlign::TOP_CENTER, "\U000F059B");
        it.printf(404, 40, id(mid_font), BLUE, TextAlign::TOP_LEFT, "à %s", id(sun_sunset).state.c_str());

        //ligne verticale
        it.filled_rectangle(280, 90, 4, 375, BLACK);

        // --- colonne gauche : Météo -----------------------------
        // Titre météo
        it.printf(135, 90, id(title_font), BLACK, TextAlign::TOP_CENTER, "MÉTÉO");

        // Titre aujourd'hui
        it.printf(135, 150, id(small_font), RED, TextAlign::TOP_CENTER, "Aujourd'hui");

        // icône aujourd'hui
        it.printf(70, 190, id(icon_today), BLACK, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(today_weather).state.c_str()].c_str());
      
        // températurea
        it.printf(145, 192, id(font_weather_icon), RED, TextAlign::TOP_CENTER, "\U000F10C2");
        it.printf(160, 190, id(small_font), RED, TextAlign::TOP_LEFT, "%.1f°C", id(today_temperaturea).state);
        // templowa
        it.printf(145, 222, id(font_weather_icon), BLUE, TextAlign::TOP_CENTER, "\U000F10C3");
        it.printf(160, 220, id(small_font), BLUE, TextAlign::TOP_LEFT, "%.1f°C", id(today_templowa).state);
        // humiditéa
        it.printf(145, 252, id(font_weather_icon), BLUE, TextAlign::TOP_CENTER, "\U000F058E");
        it.printf(160, 250, id(small_font), BLUE, TextAlign::TOP_LEFT, "%.0f%%", id(today_humiditea).state);
        // précipitationa
        it.printf(145, 282, id(font_weather_icon), BLACK, TextAlign::TOP_CENTER, "\U000F058C");
        it.printf(160, 280, id(small_font), BLACK, TextAlign::TOP_LEFT, "%.1fmm", id(today_precipitationa).state);

        // Titre demain
        it.printf(130, 310, id(small_font), RED, TextAlign::TOP_CENTER, "Demain");

        // icône demain
        it.printf(70, 350, id(icon_today), BLACK, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(tomorrow_weather).state.c_str()].c_str());
      
        // températured
        it.printf(145, 352, id(font_weather_icon), RED, TextAlign::TOP_CENTER, "\U000F10C2");
        it.printf(160, 350, id(small_font), RED, TextAlign::TOP_LEFT, "%.1f°C", id(tomorrow_temperatured).state);
        // templowd
        it.printf(145, 382, id(font_weather_icon), BLUE, TextAlign::TOP_CENTER, "\U000F10C3");
        it.printf(160, 380, id(small_font), BLUE, TextAlign::TOP_LEFT, "%.1f°C", id(tomorrow_templowd).state);
        // humiditéd
        it.printf(145, 412, id(font_weather_icon), BLUE, TextAlign::TOP_CENTER, "\U000F058E");
        it.printf(160, 410, id(small_font), BLUE, TextAlign::TOP_LEFT, "%.0f%%", id(tomorrow_humidited).state);
        // précipitationd
        it.printf(145, 442, id(font_weather_icon), BLACK, TextAlign::TOP_CENTER, "\U000F058C");
        it.printf(160, 440, id(small_font), BLACK, TextAlign::TOP_LEFT, "%.1fmm", id(tomorrow_precipitationd).state);

        // --- colonne droite : Prévision -----------------------------
        // Titre prévision
        it.printf(540, 90, id(title_font), BLACK, TextAlign::TOP_CENTER, "PRÉVISION");

        // Titre maintenant
        it.printf(430, 150, id(small_font), RED, TextAlign::TOP_CENTER, "Maintenant");

        // icône maintenant
        it.printf(360, 190, id(icon_today), BLACK, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(now_weather).state.c_str()].c_str());
      
        // now_weather_temperature
        it.printf(435, 192, id(font_weather_icon), RED, TextAlign::TOP_CENTER, "\U000F10C2");
        it.printf(450, 190, id(small_font), RED, TextAlign::TOP_LEFT, "%.1f°C", id(now_weather_temperature).state);
        // now_weather_humidity
        it.printf(435, 222, id(font_weather_icon), BLUE, TextAlign::TOP_CENTER, "\U000F058E");
        it.printf(450, 220, id(small_font), BLUE, TextAlign::TOP_LEFT, "%.0f%%", id(now_weather_humidity).state);
        // now_weather_wind_speed
        it.printf(435, 252, id(font_weather_icon), GREEN, TextAlign::TOP_CENTER, "\U000F059D");
        it.printf(450, 250, id(small_font), GREEN, TextAlign::TOP_LEFT, "%.0fkm/h", id(now_weather_wind_speed).state);
        // now_weather_wind_bearing
        it.printf(435, 282, id(font_weather_icon), GREEN, TextAlign::TOP_CENTER, "\U000F15FA");
        it.printf(450, 280, id(small_font), GREEN, TextAlign::TOP_LEFT, "%s", id(now_weather_wind_bearing).state.c_str());

        // Titre heure +1
        it.printf(670, 150, id(small_font), RED, TextAlign::TOP_CENTER, "%.0f:00", id(h1_weather_heure).state);

        // icône heure +1
        it.printf(600, 190, id(icon_today), BLACK, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(h1_weather_condition).state.c_str()].c_str());
      
        // h1_weather_temperature
        it.printf(675, 192, id(font_weather_icon), RED, TextAlign::TOP_CENTER, "\U000F10C2");
        it.printf(690, 190, id(small_font), RED, TextAlign::TOP_LEFT, "%.1f°C", id(h1_weather_temperature).state);
        // h1_weather_humidite
        it.printf(675, 222, id(font_weather_icon), BLUE, TextAlign::TOP_CENTER, "\U000F058E");
        it.printf(690, 220, id(small_font), BLUE, TextAlign::TOP_LEFT, "%.0f%%", id(h1_weather_humidite).state);
        // h1_weather_wind
        it.printf(675, 252, id(font_weather_icon), GREEN, TextAlign::TOP_CENTER, "\U000F059D");
        it.printf(690, 250, id(small_font), GREEN, TextAlign::TOP_LEFT, "%.0fkm/h", id(h1_weather_wind).state);
        // h1_weather_precipitation
        it.printf(675, 282, id(font_weather_icon), BLACK, TextAlign::TOP_CENTER, "\U000F058C");
        it.printf(690, 280, id(small_font), BLACK, TextAlign::TOP_LEFT, "%.1fmm", id(h1_weather_precipitation).state);

        // Titre heure +2
        it.printf(430, 310, id(small_font), RED, TextAlign::TOP_CENTER, "%.0f:00", id(h2_weather_heure).state);

        // icône heure +2
        it.printf(360, 350, id(icon_today), BLACK, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(h2_weather_condition).state.c_str()].c_str());
      
        // h2_weather_temperature
        it.printf(435, 352, id(font_weather_icon), RED, TextAlign::TOP_CENTER, "\U000F10C2");
        it.printf(450, 350, id(small_font), RED, TextAlign::TOP_LEFT, "%.1f°C", id(h2_weather_temperature).state);
        // h2_weather_humidite
        it.printf(435, 382, id(font_weather_icon), BLUE, TextAlign::TOP_CENTER, "\U000F058E");
        it.printf(450, 380, id(small_font), BLUE, TextAlign::TOP_LEFT, "%.0f%%", id(h2_weather_humidite).state);
        // h2_weather_wind
        it.printf(435, 412, id(font_weather_icon), GREEN, TextAlign::TOP_CENTER, "\U000F059D");
        it.printf(450, 410, id(small_font), GREEN, TextAlign::TOP_LEFT, "%.0fkm/h", id(h2_weather_wind).state);
        // h2_weather_precipitation
        it.printf(435, 442, id(font_weather_icon), BLACK, TextAlign::TOP_CENTER, "\U000F058C");
        it.printf(450, 440, id(small_font), BLACK, TextAlign::TOP_LEFT, "%.1fmm", id(h2_weather_precipitation).state);

        // Titre heure +3
        it.printf(670, 310, id(small_font), RED, TextAlign::TOP_CENTER, "%.0f:00", id(h3_weather_heure).state);

        // icône heure +3
        it.printf(600, 350, id(icon_today), BLACK, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(h3_weather_condition).state.c_str()].c_str());
      
        // h3_weather_temperature
        it.printf(675, 352, id(font_weather_icon), RED, TextAlign::TOP_CENTER, "\U000F10C2");
        it.printf(690, 350, id(small_font), RED, TextAlign::TOP_LEFT, "%.1f°C", id(h3_weather_temperature).state);
        // h3_weather_humidite
        it.printf(675, 382, id(font_weather_icon), BLUE, TextAlign::TOP_CENTER, "\U000F058E");
        it.printf(690, 380, id(small_font), BLUE, TextAlign::TOP_LEFT, "%.0f%%", id(h3_weather_humidite).state);
        // h3_weather_wind
        it.printf(675, 412, id(font_weather_icon), GREEN, TextAlign::TOP_CENTER, "\U000F059D");
        it.printf(690, 410, id(small_font), GREEN, TextAlign::TOP_LEFT, "%.0fkm/h", id(h3_weather_wind).state);
        // h3_weather_precipitation
        it.printf(675, 442, id(font_weather_icon), BLACK, TextAlign::TOP_CENTER, "\U000F058C");
        it.printf(690, 440, id(small_font), BLACK, TextAlign::TOP_LEFT, "%.1fmm", id(h3_weather_precipitation).state);
      }
    

Utilisation de la démo

Après avoir flashé votre appareil, voici comment fonctionne le firmware de la démo.

  • Le bouton vert sert à réveiller l'appareil et rafraichira l'écran.
  • les boutons blanc page précédent ou suivant change de page. À utiliser après avoir appuyé sur le bouton vert. Ça changera de page et rafraichira la page.
  • l'appareil se réveille toutes les xxH02 (chaque heure +2 minute. Exemple : 14h02, 15h02…) pour actualiser les données et rafraichir l'écran. Puis repasse en sommeil pendant une heure.
  • Affichage du niveau de la batterie avec icône dynamique sur les deux pages
  • Le buzzer doit sonner cinq fois quand le niveau de batterie passe à 10%.
  • Affichage de l'heure, la date, le lever/coucher du soleil, de la météo de la journée sur deux jours et la prévision météo de la journée pour 4 heures sur la page 1.
  • Affichage de la température et humidité du SHT40 sur la page 2

Vous aurez ces entités disponibles dans l'intégration ESPHome sur Home assistant.

Avec cette démo, le ReTerminal devrait tenir plus de trois semaines sur la batterie (approximatif).

Explication du code de la démo

Deep sleep

Le composant deep_sleep permet de mettre automatiquement l'ESP32 en veille prolongée après un certain temps. Ceci est particulièrement utile pour les appareils fonctionnant sur batterie et nécessitant une économie d'énergie maximale.

  • run_duration est le temps que l'ESP reste allumé.
  • sleep_duration est le temps qui reste en sommeil.
  • wakeup_pin permet de choisir un pin GPIO pour réveiller l'appareil. Dans le code, j'utilise le bouton vert (GPIO03).
  • wakeup_pin_mode uniquement sur ESP32. Spécifie comment gérer le réveil de wakeup_pin.
deep_sleep:
  id: deep_sleep_1
  run_duration: 2min
  #sleep_duration: 58min 
  wakeup_pin: GPIO3  # Bouton vert
  wakeup_pin_mode: INVERT_WAKEUP

Pour réveiller l'ESP32 toutes les heures, j'ai opté pour un lambda qui permettra de choisir à quel intervalle de temps il doit se réveille. Dans la démo, je l'ai programmé toutes les xxH02 (chaque heure +2 minute. Exemple : 14h02, 15h02…), pour se réveiller et actualisé les informations des capteurs et repasser en mode sommeil après l'actualisation de l'écran.

        - lambda: |-
            auto time = id(ha_time).now();
            if (!time.is_valid()) {
              ESP_LOGI("custom", "Heure invalide (pas de synchro NTP). Deep sleep 60 min par défaut.");
              id(deep_sleep_1).set_sleep_duration(60 * 60 * 1000ULL);
              id(deep_sleep_1).begin_sleep(true);
              return;
            }
            
            int now_seconds = time.hour * 3600 + time.minute * 60 + time.second;
            
            int h = time.hour;
            int m = time.minute;
            int interval = id(sleep_interval_hours);
            int target_min = id(target_minute);
            
            //Calcul de la prochaine heure cible
            int next_target_hour = (h / interval) * interval;
            if (m >= target_min) {
              next_target_hour += interval;
            }
            if (next_target_hour >= 24) next_target_hour -= 24;
            
            int target_seconds = next_target_hour * 3600 + target_min * 60;
            if (target_seconds <= now_seconds) {
              target_seconds += 24 * 3600;  // bascule au jour suivant
            }
            
            int sleep_seconds = target_seconds - now_seconds;
            
            ESP_LOGI("custom", "Il est %02d:%02d:%02d", h, m, time.second);
            ESP_LOGI("custom", "Prochain réveil prévu à %02d:%02d (dans %d sec)", 
                     next_target_hour, target_min, sleep_seconds);
            
            id(deep_sleep_1).set_sleep_duration((uint64_t)sleep_seconds * 1000ULL);
            id(deep_sleep_1).begin_sleep(true);

Pour modifier les heures et minutes, c'est dans la partie globals.

globals:
  - id: sleep_interval_hours
    type: int
    restore_value: no
    initial_value: '1'  # Toutes les 1 heure
  - id: target_minute
    type: int
    restore_value: no
    initial_value: '2'  # À la minute 02

Sensor Home Assistant

Le Sensor Home assistant vous permet de créer des capteurs qui importent des états en numérique depuis votre instance Home Assistant à l'aide de l'API.

  • entity_id est pour le nom de l'entité numérique a importé de Home assistant.
  • attribute ( optionel ) est si vous voulez utiliser l'attribut d'une entité numérique.
  • id sert à donner un nom d'appel pour être utilisé dans le code.
sensor:
  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_heure_epaper
    attribute: "precipitation2"
    id: h3_weather_precipitation

Text sensor home assistant

Le composant text sensor home assistant vous permet de créer des capteurs qui importent des états en texte (ON/OFF) depuis votre instance Home Assistant à l'aide de l'API.

  • entity_id est pour le nom de l'entité texte a importé de Home assistant.
  • attribute ( optionel ) est si vous voulez utiliser l'attribut d'une entité texte.
  • id sert à donner un nom d'appel pour être utilisé dans le code.
text_sensor:    
  - platform: homeassistant
    entity_id: sensor.meteo_ma_ville_jour_epaper
    attribute: "conditiona"
    id: today_weather

Sun

Le composant sun permet de suivre la position du soleil dans le ciel. Les calculs sont effectués toutes les 60 secondes.

sun
il faut indiquer la latitude et longitude de votre maison.

  • latitude pour la coordonnéé GPS de la latitude de votre maison.
  • longitude pour la coordonnéé GPS de la longitude de votre maison.

time
Une source de temps est requise, on va utiliser la source de notre Home assistant.

text_sensor
Pour créer des capteurs texte pour le prochaine lever/coucher du soleil.

  • name est pour le nom de l'entité afficher sur Home assistant.
  • type est pour le type de valeur a suivre. Lever (sunrise) ou coucher (sunset) du soleil.
  • format format d'heure à afficher.
  • internal pour ne pas importer le sensor dans Home assistant.
sun:
  latitude: 48.8584°
  longitude: 2.2945°

time:
  - platform: homeassistant

text_sensor:
  - platform: sun
    name: Sun Next Sunrise
    type: sunrise
    format: "%H:%M"
    id: sun_sunrise
    internal: true

  - platform: sun
    name: Sun Next Sunset
    type: sunset
    format: "%H:%M"
    id: sun_sunset
    internal: true

Time

La meilleure façon de connaître l'heure dans ESPHome est d'utiliser Home Assistant. Avec la plateforme Time home assistant, la connexion à l'API de Home Assistant permet de synchroniser périodiquement l'heure actuelle.

  • id sert à donner un nom d'appel pour être utilisé dans le code.
time:
  - platform: homeassistant
    id: ha_time

Display

Le display composant, permet l'affichage de donnée ou forme sur un écran.
Pour cela, vous devez utiliser une police d'écriture a déclaré dans la partie font.

Police

ESPHome dispose d'un puissant gestionnaire de polices (police) qui s'intègre parfaitement au système. 

  • file permet de choisir une police ou icone.
    L'option gfonts:// permet d'utiliser des polices google. Dans l'exemple, j'utilise la police google Inter en gras 700.
  • id sert à donner un nom d'appel pour être utilisé dans le code.
  • Size permet de choisir la taille de la police.
  • glyphs permet une liste de caractère à utiliser pour la police.
font:
  - file: "gfonts://Inter@700"
    id: mid_font
    size: 36
    glyphs: "<>!'%()/+,-_.:;*=°?#0123456789AÀBCDEÉÈÊFGHIJKLMNOPQRSTUVWXYZ aàbcdeéèêfghijklmnopqrstuvwxyzôöç"


Pour afficher une icône du site Material design icons, il faut utiliser le fichier materialdesignicons-webfont.ttf et déclaré les icônes à utiliser dans glyphs.
Pour choisir l'icône, allez sur le site Material design icon, choisir une icône et cliquer dessus.

Puis copier le codepoint de l'icône, à utiliser dans la partie glyphs. Exemple "\U000F059C".

font:
  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: icon_sun
    size: 36
    glyphs:
      - "\U000F059B" # mdi-weather-sunset-down
      - "\U000F059C" # mdi-weather-sunset-up

Pour écrire sur l'écran, on va passer par un lambda.

Afficher une icône

lambda: |-
  it.printf(379, 8, id(icon_sun), BLUE, TextAlign::TOP_CENTER, "\U000F059C");
  • ip.printf = pour écrire une valeur
  • 379 = position X
  • 8 = positon Y
  • id(icon_sun) = la police a utilisé, mais dans notre cas, ça sera une icône.
  • BLUE = la couleur de l'icône
  • TextAlign = l'alignement du texte
  • "\U000F059C" = l'icône a affiché

Afficher un sensor et le formater

lambda: |-
  it.printf(160, 190, id(small_font), RED, TextAlign::TOP_LEFT, "%.1f°C", id(today_temperaturea).state);
  • ip.printf = pour écrire une valeur
  • 160 = position X
  • 190 = positon Y
  • id(small_font) = la police a utilisé
  • RED = la couleur de la valeur
  • TextAlign = l'alignement du texte
  • "%.1f°C" = permet d'afficher la valeur avec 1 chiffre après la virgule et ajoute l'unité °C à la valeur.
    Avec "%.0f" affiche la valeur sans décimal
    Avec "%.0f%%" afficher l'unité % et sans décimal
    Avec "%.1f%%" afficher l'unité % et un chiffre après la virgule
  • id(today_temperaturea).state = le ID d'une entité sensor (chiffre) à utiliser pour la valeur a affiché

Afficher un rectangle rempli

lambda: |-
  it.filled_rectangle(280, 90, 10, 375, BLACK);
  • it.filled_rectangle = afficher un rectangle rempli
  • 280 = position X
  • 90 = position Y
  • 10 = largeur
  • 375 = hauteur
  • BLACK = couleur

Afficher un rectangle vide

lambda: |-
  it.rectangle(5, 20, 30, 42);
  • it.rectangle = affiche un rectangle vide
  • 5 = position X
  • 20 = position Y
  • 30 = largeur
  • 42 = hauteur

Vous trouverez d'autre d'exemple de forme (cercle, triangle) dans la documentation de ESPHome.

Exemple de code pour afficher :

display:
  - platform: waveshare_epaper
    id: epaper_display
    model: 7.30in-e
    cs_pin: GPIO10
    dc_pin: GPIO11
    reset_pin:
      number: GPIO12
      inverted: false
    busy_pin:
      number: GPIO13
      inverted: true
    update_interval: never
    lambda: |-
      const auto BLACK   = Color(0,   0,   0,   0);
      const auto RED     = Color(255, 0,   0,   0);
      const auto GREEN   = Color(0,   255, 0,   0);
      const auto BLUE    = Color(0,   0,   255, 0);
      const auto YELLOW  = Color(255, 255, 0,   0);
      const auto WHITE   = Color(255, 255, 255);
      
      // now_weather_temperature
      it.printf(435, 192, id(font_weather_icon), RED, TextAlign::TOP_CENTER, "\U000F10C2");
      it.printf(450, 190, id(small_font), RED, TextAlign::TOP_LEFT, "%.1f°C", id(now_weather_temperature).state);
      // now_weather_humidity
      it.printf(435, 222, id(font_weather_icon), BLUE, TextAlign::TOP_CENTER, "\U000F058E");
      it.printf(450, 220, id(small_font), BLUE, TextAlign::TOP_LEFT, "%.0f%%", id(now_weather_humidity).state);
      // now_weather_wind_speed
      it.printf(435, 252, id(font_weather_icon), GREEN, TextAlign::TOP_CENTER, "\U000F059D");
      it.printf(450, 250, id(small_font), GREEN, TextAlign::TOP_LEFT, "%.0fkm/h", id(now_weather_wind_speed).state);
      // now_weather_wind_bearing
      it.printf(435, 282, id(font_weather_icon), GREEN, TextAlign::TOP_CENTER, "\U000F15FA");
      it.printf(450, 280, id(small_font), GREEN, TextAlign::TOP_LEFT, "%s", id(now_weather_wind_bearing).state.c_str());

Afficher un text_sensor

lambda: |-
  it.printf(404, 40, id(mid_font), BLUE, TextAlign::TOP_LEFT, "à %s", id(sun_sunset).state.c_str());
  • ip.printf = pour écrire une valeur
  • 404 = position X
  • 40 = positon Y
  • id(mid_font) = la police a utilisé
  • BLUE = la couleur du texte
  • TextAlign = l'alignement du texte
  • "à %s" = permet l'ajout d'un texte avant la valeur d'un text_sensor
    %s est pour afficher une valeur en text
  • id(sun_sunset).state.c_str() = le ID d'une entité text_sensor à utiliser pour la valeur a affiché

Exemple de code pour afficher :

display:
  - platform: waveshare_epaper
    id: epaper_display
    model: 7.30in-e
    cs_pin: GPIO10
    dc_pin: GPIO11
    reset_pin:
      number: GPIO12
      inverted: false
    busy_pin:
      number: GPIO13
      inverted: true
    update_interval: never
    lambda: |-
      const auto BLACK   = Color(0,   0,   0,   0);
      const auto RED     = Color(255, 0,   0,   0);
      const auto GREEN   = Color(0,   255, 0,   0);
      const auto BLUE    = Color(0,   0,   255, 0);
      const auto YELLOW  = Color(255, 255, 0,   0);
      const auto WHITE   = Color(255, 255, 255);

      // Soleil
      it.printf(379, 8, id(icon_sun), BLUE, TextAlign::TOP_CENTER, "\U000F059C");
      it.printf(404, 5, id(mid_font), BLUE, TextAlign::TOP_LEFT, "à %s", id(sun_sunrise).state.c_str());

Temps de rafraichissement des écrans

reTerminal E1001

5S

reTerminal E1002

20s

Conclusion

Ces deux modèles apportent une excellente combinaison entre longue autonomie, flexibilité logicielle et qualité matérielle. Le E1001 est parfait pour les contenus sobres, tandis que le E1002 délivre un rendu esthétique plus attrayant grâce à la couleur. Mais qui sera dépourvu d'un rafraîchissement d'écran lent.

Les plus :

✔️ Bonne qualité du produit
✔️ Tout en un (ESP32, Écran e-paper, Batterie, Boitier)
✔️ Capteur de température et Humidité SHT40
✔️ Autonomie de la batterie pouvant aller jusqu'à 3 mois d'utilisation (avec 4 rafraichissements par jour)
✔️ Compatible ESPHome
✔️ Accroche murale ou avec support à l'horizontal.

Les moins :

❌ Rafraîchissement de l'écran lent sur le E1002 couleur, mais plus rapide sur le E1001 monochrome.
❌ Pas d'accroche murale ou avec support à la verticale.

Merci à @hackdiy et @Nico.g2, pour leur aide sur le code du firmware ESPHome.

- Note HACF -
Nous tenons à remercier Seeed Studio, qui a gracieusement offert ce matériel.

Notre unique volonté est de partager, après test et intégration avec HA, nos avis sur du matériel nous semblant intéressant pour la communauté. Il est important de souligner que HACF reste totalement libre de sa ligne éditoriale et les auteurs de leurs propos.
Les médias HACF ont cependant des liens affiliés nous permettant de compléter nos revenus liés aux adhésions et dons, ce qui nous permet de mener nos différentes actions (voir https://www.hacf.fr/association-hacf/#financement)