Choisir un détecteurs à onde millimétrique (mmWave) de chez HLK pour ESPHome (LD2410, LD2450, etc)

Le problème

J’ai remarqué lors des discutions pour l’intégration dans ESPHome des capteurs HLK à onde millimétriques (le LD2450 par exemple) qu’il y avait un flou sur les capacités des capteurs, leurs points forts et leurs points faibles. J’ai tenté de faire une synthèse des caractéristiques qui me semblait importantes.
Ses caractéristiques proviennent toutes du site web de HLK. Il arrive, quand le datasheet est disponible, que les informations diffèrent un peu.

L’objectif

  • Aider chacun à choisir le capteur mmWave dont il a besoin.
  • Compléter les informations manquantes, non vérifiés ou incorrectes.
  • Découvrir de nouvelles applications, ou sélectionner un capteur mieux adapté ou moins chère.
  • Partager les codes sources pour le bon fonctionnement de chacun des capteurs.

Les caractéristiques

Module LD012-5G LD1125H LD2410 LD2410B LD2410C LD2410S LD2411 LD2411S LD2415H LD2420 LD2450 LD2461 LD6002
Fréquence (GHz) 5.8 24 24 24 24 24 24 24 24 24 24 24 60
Distance max (m) 6 à 10 9 5 5 5 4 à 8 3.5 à 6 3.5 à 6 180 8 6 5 à 8 1.5
Angle (°) Variable (~60) ±22 horizontal et ±24 vertical 60 60 60 60 ±30 à ±20 horizontal et ±40 vertical ±20 horizontal et ±45 vertical 20 60 ±60 horizontal et ±35 vertical 45 NC
Position NC Mur ou plafond Mur ou plafond Mur ou plafond Mur ou plafond Mur 45 cm au dessus du sol 45 cm au dessus du sol NC Mur ou plafond Mur Mur ou plafond NC
Tension (V) 5 à 12 3,3 à 5 5 à 12 5 à 12 5 à 12 3.3 5 5 9 à 24 3.3 5 5 3.3
Cible 1 1 1 1 1 1 1 1 1 1 3 5 1
Précision NC NC 9% 9% 9% NC (9%) NC NC NC NC 90% 90 % (3 personnes),≥ 80 % (5 personnes) 90%
Application _________________ Détection de mouvements : extérieur, faible consommation, compatible avec batterie. __________________________ Juge de l’existence du corps humain en détectant et en accumulant de minuscules mouvements tels que la respiration humaine __________________________ Détection de mouvements, micro-mouvements ou corps humains stationnaires. La distance de la cible peuvent être calculée __________________________ Détection de mouvements, micro-mouvements ou corps humains stationnaires. La distance de la cible peuvent être calculée __________________________ Détection de mouvement ou de micro-mouvement. La distance de la cible peuvent être calculée __________________________ Détection de mouvements, micro-mouvements ou corps humains stationnaires. La distance de la cible peuvent être calculée __________________________ Détection de distance humaine et de reconnaissance gestuelle (toilettes) __________________________ Détection du mouvement/micro mouvement et la distance du corps humain __________________________ Mesure de vitesse de 1 à 240 km/h __________________________ Détection précise des corps humains en mouvement, en micro-mouvements et debout __________________________ Détection de la position, de la distance, de l’angle et de la vitesse de la cible __________________________ Détection précise de plusieurs mouvements, micro-mouvements ou corps humains stationnaires __________________________ Détection respiratoire et de battement cardiaque __________________________
Paramétrable Oui Oui Non (PE) Oui Oui Oui Non (PE) Oui Non (PE) Non (PE) Non Non (PE) Non
Prix 1.2 ~12 2.77 2.78 2.64 2.84 2.83 2.82 25.7 1.8 4.25 14.98 11.76
Projet Oui, Non officiel : Oui, Non officiel : Oui, Officiel : NC Oui, Officiel : NC NC NC NC NC Oui, Officiel mais en attente : Oui, Non officiel : NC
Par patrick3399 Par patrick3399 Lien Lien Par HareeshMU Par Chreece
Oui, Non officiel :
Par Screek
Par 53l3cu5

Consulter le Google Sheet à jour du tableau ci-dessus

A découvrir

  • Le LD012-5G qui fonctionne sur batterie et en extérieure
  • Le LD6002 mesure le nombre de respiration et de battement cardiaque
  • Le LD2411 conçu pour les toilettes

Dernière édition : 26/04/2024

3 « J'aime »

Bonjour @selecus
J’ai repris ton tableau pour utiliser la fonction du forum cela permet d’avoir le scroll horizontal :wink:

Module LD012-5G LD6002 LD2410 LD2410B LD2410C LD2410S LD2411 LD2411S LD2415H LD2420 LD2450 LD2461
Fréquence (Ghz) 5.8 60 24 24 24 24 24 24 24 24 24 24
Distance max (m) 6 à 10 1.5 5 5 5 4 à 8 3.5 à 6 3.5 à 6 180 8 6 5 à 8
Angle (°) ~60° (variable) NC 60 60 60 60 ±30° à ±20° horizontal et ±40° vertical ±20° horizontal et ±45° vertical 20° 60° ±60° horizontal et ±35° vertical 45°
Position NC NC Mur ou plafond Mur ou plafond Mur ou plafond Mur 45 cm au dessus du sol 45 cm au dessus du sol NC Mur ou plafond Mur Mur ou plafond
Tension (V) 5 à 12 3.3 5 à 12 5 à 12 5 à 12 3.3 5 5 9 à 24 3.3 5 5
Cible 1 1 1 1 1 1 1 1 1 1 3 5
Précision NC 90% 9% 9% 9% NC (9%) NC NC NC NC 90% 90 % (3 personnes), ≥ 80 % (5 personnes)
Application Détection de mouvements : exterrieur, faible consommation, compatible avec baterie. Détection respiratoire et de battement cardiaque Détection de mouvements, micro-mouvements ou corps humains stationnaires. La distance de la cible peuvent être calculée Détection de mouvements, micro-mouvements ou corps humains stationnaires. La distance de la cible peuvent être calculée Detection de mouvement ou de micro-mouvement. La distance de la cible peuvent être calculée Détection de mouvements, micro-mouvements ou corps humains stationnaires. La distance de la cible peuvent être calculée Détection de distance humaine et de reconnaissance gestuelle (toilettes) Détection du mouvement/micro mouvement et la distance du corps humain Mesure de vitesse de 1 à 240 km/h Détection précise des corps humains en mouvement, en micro-mouvements et debout Détection de la position, de la distance, de l’angle et de la vitesse de la cible Détection précise de plusieurs mouvements, micro-mouvements ou corps humains statio
Paramétrable Oui Non Non (PE) Oui Oui Oui Non (PE) Oui Non (PE) Non (PE) Non Non (PE)
prix 1.2 11.76 2.77 2.78 2.64 2.84 2.83 2.82 25.7 1.8 4.25 14.98
5 « J'aime »

Je peux partager mon code pour le LD2450 :

Code ESPHome LD2450
#https://github.com/Emile86/HLK-2450/blob/main/2450.yaml
#https://community.home-assistant.io/t/hlk-ld2450-initial-experiments-to-connect-to-homeassistant/578878/185?page=10

substitutions:
  name: "pre-xxxxxxx"
  friendly_name: PRE xxxxxxx

esphome:
  name: ${name}
  comment: Human Sensor LD2450
  friendly_name:  ${friendly_name}
  name_add_mac_suffix: True
  platformio_options:
    board_build.flash_mode: dio
    # board_build.f_cpu: 80000000L
  project: 
    name: Screek.Human_Presence_Sensor
    version: 2A
  on_boot:
    - priority: 100
      then:
        lambda: |-
          id(cpu_speed) = ESP.getCpuFreqMHz();
    - priority: -200
      then:
        lambda: |-
          id(zone1_target_exsits).publish_state(false);
          id(zone2_target_exsits).publish_state(false);
          id(zone3_target_exsits).publish_state(false);
          id(zone_ex1_target_exsits).publish_state(false);

preferences:
    flash_write_interval: 5s

external_components:
  - source:
      type: git
      url: https://github.com/screekworkshop/custom_components_support_for_screek_2a
      ref: main
    components: [esp32, uart]
  
esp32:
  board: esp32dev
  framework:
    type: arduino
    # version: 2.0.9
    # platform_version: 6.3.0

globals:
  - id: cpu_speed
    type: int
    restore_value: no
    initial_value: '0'
  - id: last_update_ld2450
    type: unsigned long
    restore_value: no
    initial_value: '0'
  - id: init_zone_publish
    type: bool
    restore_value: no
    initial_value: "false"
  - id: last_illuminance
    type: float
    restore_value: no
    initial_value: "-1"
  - id: last_illuminance_timestamp
    type: int
    restore_value: no
    initial_value: "-1"

improv_serial:
  
logger:

debug:
  update_interval: 30s

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_encryption_key

ota:
  # use your own ota password plz. this is a words by Socrates.
  password: !secret ota_password
  safe_mode: False

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true
  # Optional manual IP
  manual_ip:
    static_ip: 192.168.xx.xx
    gateway: 192.168.yy.yy
    subnet: 255.255.255.0
  power_save_mode: LIGHT
  reboot_timeout: 10min
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "pre-xxxxxxx"
    password: !secret ap_password

# To have a "next url" for improv serial
web_server:
  port: 80

captive_portal:

text_sensor:
  # - platform: debug
  #   reset_reason:
  #     name: "ESP Reset Reason"
  #     icon: mdi:anchor
  #     disabled_by_default: True

  - platform: wifi_info
    ip_address:
      name: IP
      entity_category: "diagnostic"
      disabled_by_default: False
      icon: mdi:ip-network
    mac_address:
      name: MAC
      entity_category: "diagnostic"
      icon: mdi:ip-network
      disabled_by_default: False
    ssid:
      name: SSID
      entity_category: "diagnostic"
      disabled_by_default: False

  - platform: version
    name: Version
    hide_timestamp: true
    disabled_by_default: False
    icon: mdi:new-box
    entity_category: diagnostic

  - platform: template
    name: "Zone1 Info"
    id: tips_zone1_conf
    icon: mdi:information-outline
    entity_category: config
    lambda: |-
      return {"Configure below" };
    update_interval: 1000s

  - platform: template
    name: "Zone2 Info"
    id: tips_zone2_conf
    icon: mdi:information-outline
    entity_category: config
    lambda: |-
      return {"Configure below" };
    update_interval: 1000s

  - platform: template
    name: "Zone3 Info"
    id: tips_zone3_conf
    icon: mdi:information-outline
    entity_category: config
    lambda: |-
      return {"Configure below" };
    update_interval: 1000s

  - platform: template
    name: "Zout1 Info"
    id: tips_zone_ex1_conf
    icon: mdi:information-outline
    entity_category: config
    lambda: |-
      return {"Zone Exclusion 1" };
    update_interval: 1000s

  - platform: template
    name: "Any-Presence Info"
    id: tips_any_presence_conf
    icon: mdi:information-outline
    entity_category: config
    lambda: |-
      return {"Any Presence Config" };
    update_interval: 1000s

  - platform: template
    name: "Target1 Direction"
    id: target1_direction
    icon: mdi:directions

  - platform: template
    name: "Target2 Direction"
    id: target2_direction
    icon: mdi:directions

  - platform: template
    name: "Target3 Direction"
    id: target3_direction
    icon: mdi:directions

  - platform: template
    name: "Target1 Position"
    id: target1_position
    icon: mdi:directions

  - platform: template
    name: "Target2 Position"
    id: target2_position
    icon: mdi:directions

  - platform: template
    name: "Target3 Position"
    id: target3_position
    icon: mdi:directions

number:
  - platform: template
    name: "Any Presence Timeout"
    id: any_presence_timeout
    min_value: 0
    max_value: 600
    mode: box
    device_class: duration
    entity_category: config
    unit_of_measurement: s
    icon: mdi:timer-off
    step: 1
    optimistic: True
    initial_value: 0
    restore_value: True
  - platform: template
    name: "Zone1 Timeout"
    id: zone1_x_timeout
    min_value: 0
    max_value: 600
    mode: box
    device_class: duration
    entity_category: config
    unit_of_measurement: s
    icon: mdi:timer-off
    step: 1
    optimistic: True
    initial_value: 0
    restore_value: True
  - platform: template
    name: "Zone2 Timeout"
    id: zone2_x_timeout
    min_value: 0
    max_value: 600
    mode: box
    device_class: duration
    entity_category: config
    unit_of_measurement: s
    icon: mdi:timer-off
    step: 1
    optimistic: True
    initial_value: 0
    restore_value: True
  - platform: template
    name: "Zone3 Timeout"
    id: zone3_x_timeout
    min_value: 0
    max_value: 600
    mode: box
    device_class: duration
    entity_category: config
    unit_of_measurement: s
    icon: mdi:timer-off
    step: 1
    optimistic: True
    initial_value: 0
    restore_value: True
  # Zone 1
  - platform: template
    name: Zone1 X-Begin
    id: zone1_x_begin
    min_value: -4000
    max_value: 4000
    mode: box
    device_class: distance
    entity_category: config
    unit_of_measurement: mm
    icon: mdi:arrow-left-bold
    step: 10
    optimistic: True
    initial_value: 0
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zone1_vaild
  - platform: template
    name: Zone1 X-End
    id: zone1_x_end
    mode: box
    min_value: -4000
    max_value: 4000
    device_class: distance
    unit_of_measurement: mm
    entity_category: config
    icon: mdi:arrow-right-bold
    step: 10
    initial_value: 0
    optimistic: True
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zone1_vaild
  - platform: template
    name: Zone1 Y-Begin
    id: zone1_y_begin
    mode: box
    min_value: 0
    max_value: 6000
    device_class: distance
    entity_category: config
    icon: mdi:arrow-up-bold
    unit_of_measurement: mm
    step: 10
    initial_value: 0
    optimistic: True
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zone1_vaild
  - platform: template
    name: Zone1 Y-End
    id: zone1_y_end
    icon: mdi:arrow-down-bold
    mode: box
    min_value: 0
    max_value: 6000
    initial_value: 0
    entity_category: config
    device_class: distance
    unit_of_measurement: mm
    step: 10
    optimistic: True
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zone1_vaild
  
  # Zone 2
  - platform: template
    name: Zone2 X-Begin
    id: zone2_x_begin
    min_value: -4000
    max_value: 4000
    mode: box
    device_class: distance
    entity_category: config
    unit_of_measurement: mm
    icon: mdi:arrow-left-bold
    step: 10
    optimistic: True
    initial_value: 0
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zone2_vaild
  - platform: template
    name: Zone2 X-End
    id: zone2_x_end
    mode: box
    min_value: -4000
    max_value: 4000
    device_class: distance
    unit_of_measurement: mm
    entity_category: config
    icon: mdi:arrow-right-bold
    step: 10
    initial_value: 0
    optimistic: True
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zone2_vaild
  - platform: template
    name: Zone2 Y-Begin
    id: zone2_y_begin
    mode: box
    min_value: 0
    max_value: 6000
    device_class: distance
    entity_category: config
    icon: mdi:arrow-up-bold
    unit_of_measurement: mm
    step: 10
    initial_value: 0
    optimistic: True
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zone2_vaild
  - platform: template
    name: Zone2 Y-End
    id: zone2_y_end
    icon: mdi:arrow-down-bold
    mode: box
    min_value: 0
    max_value: 6000
    initial_value: 0
    entity_category: config
    device_class: distance
    unit_of_measurement: mm
    step: 10
    optimistic: True
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zone2_vaild

  # Zone 3
  - platform: template
    name: Zone3 X-Begin
    id: zone3_x_begin
    min_value: -4000
    max_value: 4000
    mode: box
    device_class: distance
    entity_category: config
    unit_of_measurement: mm
    icon: mdi:arrow-left-bold
    step: 10
    optimistic: True
    initial_value: 0
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zone3_vaild
  - platform: template
    name: Zone3 X-End
    id: zone3_x_end
    mode: box
    min_value: -4000
    max_value: 4000
    device_class: distance
    unit_of_measurement: mm
    entity_category: config
    icon: mdi:arrow-right-bold
    step: 10
    initial_value: 0
    optimistic: True
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zone3_vaild
  - platform: template
    name: Zone3 Y-Begin
    id: zone3_y_begin
    mode: box
    min_value: 0
    max_value: 6000
    device_class: distance
    entity_category: config
    icon: mdi:arrow-up-bold
    unit_of_measurement: mm
    step: 10
    initial_value: 0
    optimistic: True
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zone3_vaild
  - platform: template
    name: Zone3 Y-End
    id: zone3_y_end
    icon: mdi:arrow-down-bold
    mode: box
    min_value: 0
    max_value: 6000
    initial_value: 0
    entity_category: config
    device_class: distance
    unit_of_measurement: mm
    step: 10
    optimistic: True
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zone3_vaild
  
  # Zout1
  - platform: template
    name: Zout1 X-Begin
    id: zone_ex1_x_begin
    min_value: -4000
    max_value: 4000
    mode: box
    device_class: distance
    entity_category: config
    unit_of_measurement: mm
    icon: mdi:arrow-left-bold
    step: 10
    optimistic: True
    initial_value: 0
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zout1_vaild
  - platform: template
    name: Zout1 X-End
    id: zone_ex1_x_end
    mode: box
    min_value: -4000
    max_value: 4000
    device_class: distance
    unit_of_measurement: mm
    entity_category: config
    icon: mdi:arrow-right-bold
    step: 10
    initial_value: 0
    optimistic: True
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zout1_vaild
  - platform: template
    name: Zout1 Y-Begin
    id: zone_ex1_y_begin
    mode: box
    min_value: 0
    max_value: 6000
    device_class: distance
    entity_category: config
    icon: mdi:arrow-up-bold
    unit_of_measurement: mm
    step: 10
    initial_value: 0
    optimistic: True
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zout1_vaild
  - platform: template
    name: Zout1 Y-End
    id: zone_ex1_y_end
    icon: mdi:arrow-down-bold
    mode: box
    min_value: 0
    max_value: 6000
    initial_value: 0
    entity_category: config
    device_class: distance
    unit_of_measurement: mm
    step: 10
    optimistic: True
    restore_value: True
    on_value: 
      then:
        - script.execute: check_zout1_vaild

binary_sensor:
  - platform: template
    name: "Any Presence"
    id: any_target_exsits
    device_class: occupancy
    filters:
      - delayed_off: !lambda |-
          if (!id(init_zone_publish) || !id(zone_fn_enable).state) {
            return 0;
          };
          return id(any_presence_timeout).state * 1000.0;
  - platform: template
    name: "Zone1 Presence"
    id: zone1_target_exsits
    device_class: occupancy
    filters:
      - delayed_off: !lambda |-
          if (!id(init_zone_publish) || !id(zone_fn_enable).state) {
            return 0;
          }
          return id(zone1_x_timeout).state * 1000.0;
  - platform: template
    name: "Zone2 Presence"
    id: zone2_target_exsits
    device_class: occupancy
    filters:
      - delayed_off: !lambda |-
          if (!id(init_zone_publish) || !id(zone_fn_enable).state) {
            return 0;
          }
          return id(zone2_x_timeout).state * 1000.0;
  - platform: template
    name: "Zone3 Presence"
    id: zone3_target_exsits
    device_class: occupancy
    filters:
      - delayed_off: !lambda |-
          if (!id(init_zone_publish) || !id(zone_fn_enable).state) {
            return 0;
          }
          return id(zone3_x_timeout).state * 1000.0;
  - platform: template
    name: "Zout1 Presence"
    id: zone_ex1_target_exsits
    icon: mdi:account-multiple-remove
    device_class: occupancy

  - platform: status
    name: "PRE Couloir"

i2c:
  sda: GPIO21
  scl: GPIO22
  scan: true
  id: bus_a

script:
  - id: check_zone1_vaild
    then:
      - lambda: |-
          if (id(zone1_x_begin).state > id(zone1_x_end).state){
            id(tips_zone1_conf).publish_state("Err: X-Begin > X-End");
            return;
          }
          if (id(zone1_y_begin).state > id(zone1_y_end).state){
            id(tips_zone1_conf).publish_state("Err: Y-Begin > Y-End");
            return;
          }
          if (id(zone1_x_begin).state == 0 && id(zone1_x_end).state == 0 && id(zone1_y_begin).state == 0 && id(zone1_y_end).state == 0){
            id(tips_zone1_conf).publish_state("Configure below");
            return;
          }

          int x_size = id(zone1_x_end).state - id(zone1_x_begin).state;
          int y_size = id(zone1_y_end).state - id(zone1_y_begin).state;

          char combined[80]; 
          sprintf(combined, "Curr Size: %d x %d", x_size, y_size);
          id(tips_zone1_conf).publish_state(combined);
  - id: check_zone2_vaild
    then:
      - lambda: |-
          if (id(zone2_x_begin).state > id(zone2_x_end).state){
            id(tips_zone2_conf).publish_state("Err: X-Begin > X-End");
            return;
          }
          if (id(zone2_y_begin).state > id(zone2_y_end).state){
            id(tips_zone2_conf).publish_state("Err: Y-Begin > Y-End");
            return;
          }
          if (id(zone2_x_begin).state == 0 && id(zone2_x_end).state == 0 && id(zone2_y_begin).state == 0 && id(zone2_y_end).state == 0){
            id(tips_zone2_conf).publish_state("Configure below");
            return;
          }

          int x_size = id(zone2_x_end).state - id(zone2_x_begin).state;
          int y_size = id(zone2_y_end).state - id(zone2_y_begin).state;

          char combined[80]; 
          sprintf(combined, "Curr Size: %d x %d", x_size, y_size);
          id(tips_zone2_conf).publish_state(combined);
  - id: check_zone3_vaild
    then:
      - lambda: |-
          if (id(zone3_x_begin).state > id(zone3_x_end).state){
            id(tips_zone3_conf).publish_state("Err: X-Begin > X-End");
            return;
          }
          if (id(zone3_y_begin).state > id(zone3_y_end).state){
            id(tips_zone3_conf).publish_state("Err: Y-Begin > Y-End");
            return;
          }
          if (id(zone3_x_begin).state == 0 && id(zone3_x_end).state == 0 && id(zone3_y_begin).state == 0 && id(zone3_y_end).state == 0){
            id(tips_zone3_conf).publish_state("Configure below");
            return;
          }

          int x_size = id(zone3_x_end).state - id(zone3_x_begin).state;
          int y_size = id(zone3_y_end).state - id(zone3_y_begin).state;

          char combined[80]; 
          sprintf(combined, "Curr Size: %d x %d", x_size, y_size);
          id(tips_zone3_conf).publish_state(combined);
  - id: check_zout1_vaild
    then:
      - lambda: |-
          if (id(zone_ex1_x_begin).state > id(zone_ex1_x_end).state){
            id(tips_zone_ex1_conf).publish_state("Err: X-Begin > X-End");
            return;
          }
          if (id(zone_ex1_y_begin).state > id(zone_ex1_y_end).state){
            id(tips_zone_ex1_conf).publish_state("Err: Y-Begin > Y-End");
            return;
          }
          id(tips_zone_ex1_conf).publish_state("Zone Exclusion 1");

sensor:
  - platform: template
    name: "CPU Speed"
    accuracy_decimals: 0
    icon: mdi:cpu-32-bit
    unit_of_measurement: Mhz
    disabled_by_default: False
    lambda: |-
      return (id(cpu_speed));
    entity_category: "diagnostic"
    update_interval: 600s

  - platform: wifi_signal 
    name: RSSI
    id: wifi_signal_db
    update_interval: 60s
    entity_category: "diagnostic"

  - platform: copy
    source_id: wifi_signal_db
    name: "RSSI Percent"
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: " %"
    device_class: ""
    entity_category: "diagnostic"

  - platform: internal_temperature
    name: "Température"
    entity_category: "diagnostic"

  - platform: template
    id: esp_memory
    icon: mdi:memory
    name: Free Memory
    lambda: return heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024;
    unit_of_measurement: 'kB'
    state_class: measurement
    entity_category: "diagnostic"
    update_interval: 60s

  #-------------------------------------#
  # 高级雷达数据
  - platform: template
    name: "All Target Counts"
    id: all_target_count
    accuracy_decimals: 0
    icon: "mdi:counter"
    unit_of_measurement: "targets"

  - platform: template
    name: "Zone1 Target Counts"
    id: zone1_target_count
    accuracy_decimals: 0
    icon: "mdi:counter"
    unit_of_measurement: "targets"

  - platform: template
    name: "Zone2 Target Counts"
    id: zone2_target_count
    accuracy_decimals: 0
    icon: "mdi:counter"
    unit_of_measurement: "targets"

  - platform: template
    name: "Zone3 Target Counts"
    id: zone3_target_count
    accuracy_decimals: 0
    icon: "mdi:counter"
    unit_of_measurement: "targets"

  - platform: template
    name: "Zout1 Target Counts"
    id: zone_ex1_target_count
    accuracy_decimals: 0
    icon: mdi:account-multiple-minus-outline
    unit_of_measurement: "targets"

  # Target 1
  - platform: template
    name: "Target1 X"
    id: target1_x
    accuracy_decimals: 0
    unit_of_measurement: 'mm'
    state_class: measurement
    icon: mdi:focus-field-horizontal
    device_class: distance

  - platform: template
    name: "Target1 Y"
    id: target1_y
    accuracy_decimals: 0
    unit_of_measurement: 'mm'
    state_class: measurement
    device_class: distance
    icon: mdi:focus-field-vertical

  - platform: template
    name: "Target1 Speed"
    id: target1_speed
    accuracy_decimals: 2
    unit_of_measurement: 'm/s'
    state_class: measurement
    device_class: speed

  - platform: template
    name: "Target1 Resolution"
    id: target1_resolution
    accuracy_decimals: 0
    unit_of_measurement: 'mm'
    state_class: measurement
    device_class: distance
  
  # Target 2
  - platform: template
    name: "Target2 X"
    id: target2_x
    accuracy_decimals: 0
    unit_of_measurement: 'mm'
    state_class: measurement
    device_class: distance
    icon: mdi:focus-field-horizontal

  - platform: template
    name: "Target2 Y"
    id: target2_y
    accuracy_decimals: 0
    unit_of_measurement: 'mm'
    state_class: measurement
    device_class: distance
    icon: mdi:focus-field-vertical

  - platform: template
    name: "Target2 Speed"
    id: target2_speed
    accuracy_decimals: 0
    unit_of_measurement: 'm/s'
    state_class: measurement
    device_class: speed

  - platform: template
    name: "Target2 Resolution"
    id: target2_resolution
    accuracy_decimals: 0
    unit_of_measurement: 'mm'
    state_class: measurement
    device_class: distance

  # Target 3
  - platform: template
    name: "Target3 X"
    id: target3_x
    accuracy_decimals: 0
    unit_of_measurement: 'mm'
    state_class: measurement
    device_class: distance
    icon: mdi:focus-field-horizontal

  - platform: template
    name: "Target3 Y"
    id: target3_y
    accuracy_decimals: 0
    unit_of_measurement: 'mm'
    state_class: measurement
    device_class: distance
    icon: mdi:focus-field-vertical

  - platform: template
    name: "Target3 Speed"
    id: target3_speed
    accuracy_decimals: 0
    unit_of_measurement: 'm/s'
    state_class: measurement
    device_class: speed

  - platform: template
    name: "Target3 Resolution"
    id: target3_resolution
    accuracy_decimals: 0
    unit_of_measurement: 'mm'
    state_class: measurement
    device_class: distance

  - platform: template
    name: "Target1 Angle"
    id: target1_angle
    unit_of_measurement: 'º'
    accuracy_decimals: 1
    icon: mdi:angle-acute

  - platform: template
    name: "Target2 Angle"
    id: target2_angle
    accuracy_decimals: 1
    unit_of_measurement: 'º'
    icon: mdi:angle-acute

  - platform: template
    name: "Target3 Angle"
    id: target3_angle
    accuracy_decimals: 1
    unit_of_measurement: 'º'
    icon: mdi:angle-acute

# time:
#   - platform: sntp
#     id: time_now

switch:
  - platform: gpio
    pin: GPIO02
    name: "SWITCH BLEU"
    id: switch_led_bleu
    inverted: no
    entity_category: diagnostic
    restore_mode: ALWAYS_OFF

  # - platform: factory_reset
  #   name: Factory Reset
  #   disabled_by_default: True
  #   icon: mdi:heart-broken
  #   entity_category: diagnostic

  - platform: template
    name: Zout1 Enable
    id: zone_ex1_enable
    optimistic: True
    icon: mdi:account-cancel
    entity_category: config
    restore_mode: RESTORE_DEFAULT_OFF

  - platform: template
    name: Zone Enable
    id: zone_fn_enable
    optimistic: True
    icon: mdi:target-variant
    entity_category: config
    restore_mode: RESTORE_DEFAULT_ON

button:
  - platform: restart
    icon: mdi:restart
    name: "Reboot"
    entity_category: diagnostic

uart:
  id: uart_bus
  tx_pin: 
    number: GPIO17
    mode:
      input: true
      pullup: true
  rx_pin: 
    number: GPIO16
    mode:
      input: true
      pullup: true
  baud_rate: 256000
  parity: NONE
  stop_bits: 1
  data_bits: 8
  debug:
    direction: BOTH
    dummy_receiver: True
    after:
     delimiter: [0X55, 0XCC]
    sequence:
      - lambda: |-
          if ((millis() - id(last_update_ld2450)) <= 500) { 
            return;
          };
          id(last_update_ld2450) = millis();

          // p1
          int16_t p1_x = (uint16_t((bytes[5] << 8) | bytes[4] ));
          if ((bytes[5] & 0x80) >> 7){
            p1_x -= pow(2, 15); 
          }else{
            p1_x = 0 - p1_x;
          }

          int16_t p1_y = (uint16_t((bytes[7] << 8) | bytes[6] ));
          if ((bytes[7] & 0x80) >> 7){
            p1_y -= pow(2, 15);
          }else{
            p1_y = 0 - p1_y;
          }

          int p1_speed = (bytes[9] << 8 | bytes[8] );
          if ((bytes[9] & 0x80) >> 7){
            p1_speed -= pow(2, 15);
          }else{
            p1_speed = 0 - p1_speed;
          }
          int16_t p1_distance_resolution = (uint16_t((bytes[11] << 8) | bytes[10] )); 

          // p2
          int16_t p2_x = (uint16_t((bytes[13] << 8) | bytes[12] ));
          if ((bytes[13] & 0x80) >> 7){
            p2_x -=  pow(2, 15); 
          }else{
            p2_x = 0 - p2_x;
          }

          int16_t p2_y = (uint16_t((bytes[15] << 8) | bytes[14] ));
          if ((bytes[15] & 0x80) >> 7){
            p2_y -= pow(2, 15);
          }else{
            p2_y = 0 - p2_y;
          }

          int p2_speed = (bytes[17] << 8 | bytes[16] );
          if ((bytes[17] & 0x80) >> 7){
            p2_speed -= pow(2, 15);
          }else{
            p2_speed = 0 - p2_speed;
          }
          int16_t p2_distance_resolution = (uint16_t((bytes[19] << 8) | bytes[18] )); 

          // p3
          int16_t p3_x = (uint16_t((bytes[21] << 8) | bytes[20] ));
          if ((bytes[21] & 0x80) >> 7){
            p3_x -=  pow(2, 15); 
          }else{
            p3_x = 0 - p3_x;
          }

          int16_t p3_y = (uint16_t((bytes[23] << 8) | bytes[22] ));
          if ((bytes[23] & 0x80) >> 7){
            p3_y -= pow(2, 15);
          }else{
            p3_y = 0 - p3_y;
          }

          int p3_speed = (bytes[25] << 8 | bytes[24] );
          if ((bytes[25] & 0x80) >> 7){
            p3_speed -= pow(2, 15);
          }else{
            p3_speed = 0 - p3_speed;
          }
          
          int16_t p3_distance_resolution = (uint16_t((bytes[27] << 8) | bytes[26] )); 

          bool p1_vaild = (p1_x != 0 || p1_y > 0);
          bool p2_vaild = (p2_x != 0 || p2_y > 0);
          bool p3_vaild = (p3_x != 0 || p3_y > 0);

          // zone exlude 1

          int16_t target_count_in_zone_ex1 = 0;

          int16_t zone_ex1_x_min = id(zone_ex1_x_begin).state;
          int16_t zone_ex1_x_max = id(zone_ex1_x_end).state;
          int16_t zone_ex1_y_min = id(zone_ex1_y_begin).state;
          int16_t zone_ex1_y_max = id(zone_ex1_y_end).state;

          bool p1_zone_ex_enter = false;
          bool p2_zone_ex_enter = false;
          bool p3_zone_ex_enter = false;

          if (id(zone_ex1_enable).state){
            if (p1_vaild){
              if (p1_x >= zone_ex1_x_min && p1_x <= zone_ex1_x_max && p1_y >= zone_ex1_y_min && p1_y <= zone_ex1_y_max){
                  p1_zone_ex_enter = true;
                  target_count_in_zone_ex1 ++;
              }
            }
            if (p2_vaild){
              if (p2_x >= zone_ex1_x_min && p2_x <= zone_ex1_x_max && p2_y >= zone_ex1_y_min && p2_y <= zone_ex1_y_max){
                  p2_zone_ex_enter = true;
                  target_count_in_zone_ex1 ++;
              }
            }
            if (p3_vaild){
              if (p3_x >= zone_ex1_x_min && p3_x <= zone_ex1_x_max && p3_y >= zone_ex1_y_min && p3_y <= zone_ex1_y_max){
                  p3_zone_ex_enter = true;
                  target_count_in_zone_ex1 ++;
              }
            }
          }

          bool has_target_in_zone_ex1 = (target_count_in_zone_ex1 > 0);
          
          int16_t all_target_counts = 0;
          if (p1_vaild && !p1_zone_ex_enter){
            all_target_counts ++;
          }
          if (p2_vaild && !p2_zone_ex_enter){
            all_target_counts ++;
          }
          if (p3_vaild && !p3_zone_ex_enter){
            all_target_counts ++;
          }

          bool has_target_in_zone_all = (all_target_counts > 0);

          int16_t target_count_in_zone1 = 0;
          bool has_target_in_zone1 = false;

          int16_t target_count_in_zone2 = 0;
          bool has_target_in_zone2 = false;

          int16_t target_count_in_zone3 = 0;
          bool has_target_in_zone3 = false;

          if (id(zone_fn_enable).state){

            // zone 1 check

            int16_t zone1_x_min = id(zone1_x_begin).state;
            int16_t zone1_x_max = id(zone1_x_end).state;
            int16_t zone1_y_min = id(zone1_y_begin).state;
            int16_t zone1_y_max = id(zone1_y_end).state;

            if (p1_vaild && !p1_zone_ex_enter){
              if (p1_x >= zone1_x_min && p1_x <= zone1_x_max && p1_y >= zone1_y_min && p1_y <= zone1_y_max){
                  target_count_in_zone1 ++;
              }
            }
            if (p2_vaild && !p2_zone_ex_enter){
              if (p2_x >= zone1_x_min && p2_x <= zone1_x_max && p2_y >= zone1_y_min && p2_y <= zone1_y_max){
                  target_count_in_zone1 ++;
              }
            }
            if (p3_vaild && !p3_zone_ex_enter){
              if (p3_x >= zone1_x_min && p3_x <= zone1_x_max && p3_y >= zone1_y_min && p3_y <= zone1_y_max){
                  target_count_in_zone1 ++;
              }
            }
            has_target_in_zone1 = (target_count_in_zone1 > 0);

            // zone 2 check

            int16_t zone2_x_min = id(zone2_x_begin).state;
            int16_t zone2_x_max = id(zone2_x_end).state;
            int16_t zone2_y_min = id(zone2_y_begin).state;
            int16_t zone2_y_max = id(zone2_y_end).state;

            if (p1_vaild && !p1_zone_ex_enter){
              if (p1_x >= zone2_x_min && p1_x <= zone2_x_max && p1_y >= zone2_y_min && p1_y <= zone2_y_max){
                  target_count_in_zone2 ++;
              }
            }
            if (p2_vaild && !p2_zone_ex_enter){
              if (p2_x >= zone2_x_min && p2_x <= zone2_x_max && p2_y >= zone2_y_min && p2_y <= zone2_y_max){
                  target_count_in_zone2 ++;
              }
            }
            if (p3_vaild && !p3_zone_ex_enter){
              if (p3_x >= zone2_x_min && p3_x <= zone2_x_max && p3_y >= zone2_y_min && p3_y <= zone2_y_max){
                  target_count_in_zone2 ++;
              }
            }

            has_target_in_zone2 = (target_count_in_zone2 > 0);

            // zone 3 check

            int16_t zone3_x_min = id(zone3_x_begin).state;
            int16_t zone3_x_max = id(zone3_x_end).state;
            int16_t zone3_y_min = id(zone3_y_begin).state;
            int16_t zone3_y_max = id(zone3_y_end).state;

            if (p1_vaild && !p1_zone_ex_enter){
              if (p1_x >= zone3_x_min && p1_x <= zone3_x_max && p1_y >= zone3_y_min && p1_y <= zone3_y_max){
                  target_count_in_zone3 ++;
              }
            }
            if (p2_vaild && !p2_zone_ex_enter){
              if (p2_x >= zone3_x_min && p2_x <= zone3_x_max && p2_y >= zone3_y_min && p2_y <= zone3_y_max){
                  target_count_in_zone3 ++;
              }
            }
            if (p3_vaild && !p3_zone_ex_enter){
              if (p3_x >= zone3_x_min && p3_x <= zone3_x_max && p3_y >= zone3_y_min && p3_y <= zone3_y_max){
                  target_count_in_zone3 ++;
              }
            }
            has_target_in_zone3 = (target_count_in_zone3 > 0);

          }

          // Angle, Position and Direction, idea from walberjunior.

          float p1_angle = 0;
          if (p1_vaild){
            p1_angle = ((float)p1_x / (float)p1_y) * 180 / M_PI;;
          }

          std::basic_string<char> p1_position = "Static";
          if (p1_speed > 0) {
            p1_position = "Moving away";
          } else if (p1_speed < 0) {
            p1_position = "Approaching";
          } 

          std::basic_string<char> p1_direction = "None";
          if (p1_x > 0) {
            p1_direction = "Right";
          } else if (p1_x < 0) {
            p1_direction = "Left";
          } else if (p1_y > 0){
            p1_direction = "Middle";
          }

          float p2_angle = 0;
          if (p2_vaild){
            p2_angle = ((float)p2_x / (float)p2_y) * 180 / M_PI;;
          }

          std::basic_string<char> p2_position = "Static";;
          if (p2_speed > 0) {
            p2_position = "Moving away";
          } else if (p2_speed < 0) {
            p2_position = "Approaching";
          } 
          
          std::basic_string<char> p2_direction = "None";
          if (p2_x > 0) {
            p2_direction = "Right";
          } else if (p2_x < 0) {
            p2_direction = "Left";
          } else if (p2_y > 0){
            p2_direction = "Middle";
          }

          float p3_angle = 0;
          if (p3_vaild){
            p3_angle = ((float)p3_x / (float)p3_y) * 180 / M_PI;;
          }
          
          std::basic_string<char> p3_position = "Static";;
          if (p3_speed > 0) {
            p3_position = "Moving away";
          } else if (p3_speed < 0) {
            p3_position = "Approaching";
          } 

          std::basic_string<char> p3_direction = "None";
          if (p3_x > 0) {
            p3_direction = "Right";
          } else if (p3_x < 0) {
            p3_direction = "Left";
          } else if (p3_y > 0){
            p3_direction = "Middle";
          }

          if (id(target1_angle).state != p1_angle){
            id(target1_angle).publish_state(p1_angle);
          }
          if (id(target2_angle).state != p2_angle){
            id(target2_angle).publish_state(p2_angle);
          }
          if (id(target3_angle).state != p3_angle){
            id(target3_angle).publish_state(p3_angle);
          }

          if (p1_position != id(target1_position).state){
            id(target1_position).publish_state(p1_position);
          }
          if (p2_position != id(target2_position).state){
            id(target2_position).publish_state(p2_position);
          }
          if (p3_position != id(target3_position).state){
            id(target3_position).publish_state(p3_position);
          }

          if (p1_direction != id(target1_direction).state){
            id(target1_direction).publish_state(p1_direction);
          }
          if (p2_direction != id(target2_direction).state){
            id(target2_direction).publish_state(p2_direction);
          }
          if (p3_direction != id(target3_direction).state){
            id(target3_direction).publish_state(p3_direction);
          }

          // public all info

          if (id(target1_x).state != p1_x){
            id(target1_x).publish_state(p1_x);
          }
          if (id(target1_y).state != p1_y){
            id(target1_y).publish_state(p1_y);
          }

          float p1_m_speed = float(p1_speed) / 100.0;
          if (id(target1_speed).state != p1_m_speed){
            id(target1_speed).publish_state(p1_m_speed);
          }
          if (id(target1_resolution).state != p1_distance_resolution){
            id(target1_resolution).publish_state(p1_distance_resolution);
          }

          if (id(target2_x).state != p2_x){
            id(target2_x).publish_state(p2_x);
          }
          if (id(target2_y).state != p2_y){
            id(target2_y).publish_state(p2_y);
          }
          if (id(target2_speed).state != p2_speed){
            id(target2_speed).publish_state(p2_speed);
          }
          if (id(target2_resolution).state != p2_distance_resolution){
            id(target2_resolution).publish_state(p2_distance_resolution);
          }

          if (id(target3_x).state != p3_x){
            id(target3_x).publish_state(p3_x);
          }
          if (id(target3_y).state != p3_y){
            id(target3_y).publish_state(p3_y);
          }
          if (id(target3_speed).state != p3_speed){
            id(target3_speed).publish_state(p3_speed);
          }
          if (id(target3_resolution).state != p3_distance_resolution){
            id(target3_resolution).publish_state(p3_distance_resolution);
          }

          // publish target info
          
          if (id(all_target_count).state != all_target_counts){
            id(all_target_count).publish_state(all_target_counts);
            id(any_target_exsits).publish_state(has_target_in_zone_all);
          }else if (id(any_target_exsits).state != has_target_in_zone_all){
            id(any_target_exsits).publish_state(has_target_in_zone_all);
          }

          if (id(zone1_target_count).state != target_count_in_zone1){
            id(zone1_target_count).publish_state(target_count_in_zone1);
            id(zone1_target_exsits).publish_state(has_target_in_zone1);
          }else if (id(zone1_target_exsits).state != has_target_in_zone1){
            id(zone1_target_exsits).publish_state(has_target_in_zone1);
          }

          if (id(zone2_target_count).state != target_count_in_zone2){
            id(zone2_target_count).publish_state(target_count_in_zone2);
            id(zone2_target_exsits).publish_state(has_target_in_zone2);
          }else if (id(zone2_target_exsits).state != has_target_in_zone2){
            id(zone2_target_exsits).publish_state(has_target_in_zone2);
          }

          if (id(zone3_target_count).state != target_count_in_zone3){
            id(zone3_target_count).publish_state(target_count_in_zone3);
            id(zone3_target_exsits).publish_state(has_target_in_zone3);
          }else if (id(zone3_target_exsits).state != has_target_in_zone3){
            id(zone3_target_exsits).publish_state(has_target_in_zone3);
          }
          
          // zout
          if (id(zone_ex1_target_count).state != target_count_in_zone_ex1){
            id(zone_ex1_target_count).publish_state(target_count_in_zone_ex1);
          }

          if (id(zone_ex1_target_exsits).state != has_target_in_zone_ex1){
            id(zone_ex1_target_exsits).publish_state(has_target_in_zone_ex1);
          }

          if (!id(init_zone_publish)){
            id(init_zone_publish) = true;
          }

Et voici la configuration que j’ai mise (mais c’est discutable) :
image

image

Salut,
ton code est mal préformatté. Ca la fou mal pour un modo :rofl:

EDIT:
C’est bon la police a fais son taf :wink:

1 « J'aime »

Aïe mais que fait la police :slight_smile:

C’est corrigé, en tout cas merci

3 « J'aime »

A mon tour, je partage ma version pour le capteur LD2450. Je partage uniquement le lien du github car j’ai déjà détaillé son fonctionnement dans un autre sujet de ce forum.
Je précise juste qu’il a 6 zones de détection et 3 d’exclusion. :sunglasses: C’est le moment de l’auto-sponso :grin:

3 « J'aime »

Super tableau, petite suggestion : ajouter l’existence ou non d’une intégration esphome (officielle ou non).
Je me lance :
LD2410 : oui, officiel
LD2450: oui, non officielle, PR en cours pour intégration dans EspHome

1 « J'aime »

Merci d’ajouter ta pierre à l’édifice !
Je vais modifier le tableau en suivant ta suggestion et en profiter pour le remettre en forme.
Pour les projets non-officiels je propose de mettre un lien (pour chaque auteur qui le souhaite) dans le tableau qui renverra vers le github.

Concernant le projet non-officiel du LD2450, est-ce que tu peux m’en dire plus ? Il n’y a aucune explication sur ce que fait ce projet.

1 « J'aime »

Absolument remarquable !
Bravo et merci @selecus

Pour l’instant, j’essaie de bien comprendre le fonctionnement du LD2410 (réglages et entités à utiliser pour des automatisations pertinentes), ensuite je passerai au LD2450 que je n’ai même pas encore déballé.

Question : ton plotty-graph permet-il d’afficher en temps (presque) réel les positions détectées ?

Merci Patrick.

Le plotly-graph est rafraichi toutes les deux secondes. L’ESP32 quant à lui envoie en temps réel tout mouvement dès que le capteur le repère.

Bonjour.
Ma question était de savoir si les positions personnes détectées « apparaissent » dans la carte décrite, par exemple en tant que point qui se déplace.

Oui, toutes les 2 secondes leurs positions est mise à jour. Si les personnes se déplacent, les points sur la carte aussi.

Bonjour.
Ma question était de savoir si les positions personnes détectées « apparaissent » dans la carte décrite, par exemple en tant que point qui se déplace. En effet, le screenshot ne montre aucune détection.

2 « J'aime »

J’ai bien compris ta question et j’y ai répondu deux fois.

La réponse est :
Oui il affiche les personnes en temps réel.

De plus le graphique montre bien une détection, c’est le point bleu. Cela signifie dans mon cas que le frigo était en marche. C’est un faux positif comme je l’ai expliqué dans le sujet où tu as trouvé la carte.

S’il n’y a pas d’autre point, orange ou vert, c’est qu’il n’y avait personne dans les pièces au moment donné où j’ai fait la capture d’écran.

1 « J'aime »

Merci et désolé. Comme ils sont tout près du capteur, je n’avais pas compris que c’était une personne. Pour le frigo, oui j’avais bien lu.

My bad , le lien n’était effectivement pas le plus approprié.
Tout les détails dans Add LD2450 component by hareeshmu · Pull Request #5674 · esphome/esphome · GitHub. (ajouté dans le post initial).
C’est une intégration esphome pour récupérer les données de ce module.

Tu viens de me faire mettre le doigt dans quelque chose de bien complexe. Je viens de découvrir le monde des PR :sweat_smile: Et après avoir tout lu en détail je me suis joint à la conversation. :crazy_face:

J’ai donc ajouté ton lien au tableau.
Et en faisant des recherches sur la raison de la non-validation de la PR, j’ai découvert un projet en cours de développement le 2461 que j’ai également ajouté

1 « J'aime »

Je viens de me mettre à jour mon projet pour le LD2450. Vous pouvez trouver les sources soit sur Github soit sur cette autre sujet

Le gros changement est la possibilité de pouvoir mettre le capteur dans un coin et adapter l’angle de détection
Screenshot_20240420-155111_Zoom

2 « J'aime »

J’ai le plaisir de vous annoncer un petit venu le LD1125H !

En réalité il existe depuis au moins 2 ans. C’est le bon @Yannickinlive26 qui vient de m’en parler. Il s’en sert pour être sûr et certain d’être détecté quand il ne bouge pas dans le canapé. Et le bonus, c’est que nous avons un code pour le faire fonctionner dans ESPHome.

J’ai mis à jour le tableau en début de sujet si vous voulez avoir plus d’informations.

1 « J'aime »

Il y a encore du nouveau pour le LD2450 !
Je suis fier de vous annoncer que j’ai créé un générateur pour paramétrer entièrement le nombre de zones que vous souhaitez avoir sur votre tableau de bord. Il n’y a plus besoin de toucher au code, juste à faire des copier-coller !
Le générateur est disponible à cette adresse https://53l3cu5.github.io

4 « J'aime »