LD2410 esphome tips

I played with this sensor for the first time last night. I’ve been looking for a cheap & reliable room occupancy solution for years now. First impressions are that this thing is fantastic. My biggest challenge are ceiling fans (I have one in every room). I was able to tune the gate values to ignore the fan noise, but as soon as the fan was turned off the motionless gate thresholds were too high.

It occurs to me, now that ESPHome supports setting the gate values from within HA, it’s feasible to create different “sensibility profiles”. One when the fan is on and one where the fan is off. Then just trigger an automation to set the profile based on the fans on/off state.

There goes my weekend :wink:

EDIT: Looks like I spoke too soon. Seems these features aren’t quite implemented yet.

Esp8266 doesn’t have BT and can’t be used as a BT proxy.

1 Like

True :slight_smile: Same question but with esp32 then

Dunno, you might be the first to test that :slight_smile:

Well, i can say that i tried module connected to ESP32 and had BT connection via proxy to HA at the same time. What bothers me at BLE connection is potential occasional dropouts. Since ESPHome officially doesn’t support settings yet only option is to do changes of settings via phone - to do so you must reset LD module and connect it to phone before it connects to HA (two connections are not possible on BLE). Also, it happened that LD didn’t reconnect by itself after reset - i’ve had to click “reload” in integrations…

So, at the end i think that esphome version will be better and more stable than BLE. There’s already “beta” version of it:
LD2410 Sensor — ESPHome (deploy-preview-2680–esphome.netlify.app)

2 Likes

Interesting feedback. Have you now tried to keep your dev board as bluetooth proxy while controlling the LD with uart instead of bluetooth?

No, i only tested… for now i use my LD independently for light control and i only use my phone for settngs changes. Connection with HA is yet to come (on a different location), but i think i’ll do that with esphome, not via BLE, although i do have a BLE proxy network with 3 ESP modules in my house (for xiaomi thermometers).
But i did try to communicate with module via UART module and windows app - it also works fine. But, i can’t see any practical usefullnes in it…

It’s not a beta, it’s already an official component. I have a few of these on the D1 mini and they work without a problem.

really? on their official web site there’s no sign of switch and number sensors…?

You can find it here under sensors (motion)

It seems that you missed some points… indeed you can find LD2410, but there are only “read” sensors, no sensors for changing parametes yet. Those are for now only on my developer link. Open my link and you’ll see switch and number sensors which are missing on original web page - those are the ones you can change parameters of LD via esphome - all that is not possible with official version for now.

Ok, maybe misunderstood.
However, I set and adjusted the values for the sensor according to the table for my environment (official version) and everything works great without any dropouts.

1 Like

True, you can do that, but if you wish to change anything you must change in esphome’s yaml and re-upload FW, which is “PITA”. With dev version you won’t have to anymore - just change settings from HA’s device page. Let’s hope that it will be merged soon…

But, i agree that this sensor (for now) works like a charm!

What about connecting LD2410B via UART + ESP32 with BT Proxy connecting to other devices? Were you able to test that scenario?

I’m curious if the LD2410B would cause interference between proxy and other devices, being in such close proximity to the ESP32.

Do you mean that LD would be connected via uart to the same ESP who is already running BT proxy? No, i didn’t test that. It would be interesting to test, though…

That was precisely my question indeed but somehow I could not phrase it well enough

Yeah I thought so, borrowed and rephrased your question as I was really intrigued by it too :rofl:

Didn’t have esp32s to test :frowning:

Not sure if this was mentioned in this thread, but I stumbled on an alternate BLE integration that is working great for me and doesn’t require HASS reboots. It also has a toggle that disconnects it so you can use the android app to configure sensitivity (as the device can only be connected to one endpoint at a time). Once you are happy with your tweaks, toggle it back to reconnect to HASS.

Reading the documentation of the ld2410 integration on esphome, it seems we can disable the bluetooth adapter. That would prevent any possible interference you mentioned.

I am super frustrated with getting the LD2410 to work over UART on a Wemos D1 Mini 32 (ESP32-WROOM). Admittedly there is a lot going on so I was expecting to have issues with the high baud rate but even setting it at 9600, nothing…

substitutions:
  devicename: "office-air-quality-sensor"
  devicename_no_dashes: "office_air_quality_sensor"
  friendly_devicename: "Office Air Quality Sensor"
  device_description: "Air Gradient Air Quality Sensor"
  update_interval_s: "2s"
  update_interval_wifi: "120s"


esphome:
  name: ${devicename}
  comment: ${device_description}
  friendly_name: ${friendly_devicename}
  # Automatically add the mac address to the name
  # so you can use a single firmware for all devices
  # name_add_mac_suffix: true
  includes:
    - custom_components/ld2410/ld2410_uart.h
  on_boot:
    priority: 600
    # ...
    then:
      - lambda: |-
          auto uart_component = static_cast<LD2410 *>(ld2410);
          uart_component->setNumbers(maxMovingDistanceRange, maxStillDistanceRange, noneDuration);

esp32:
  board: wemos_d1_mini32
#  board: nodemcu-32s
#  framework:
#    type: esp-idf
#    version: recommended

# Enable logging
logger:
  baud_rate: 0


# Enable Home Assistant API
api: 
#  password: !secret api_pwd

ota:
  password: !secret ota_pwd

wifi:
  networks:
  - ssid: !secret iot_wifi_ssid
    password: !secret iot_wifi_password
  reboot_timeout: 15min

#Faster than DHCP. Also use if can't reach because of name change
  manual_ip:
    static_ip: 192.168.3.212
    gateway: 192.168.3.1
    subnet: 255.255.255.0
    dns1: 192.168.1.25
    dns2: 192.168.1.26

#Manually override what address to use to connect to the ESP.
#Defaults to auto-generated value. Example, if you have changed your
#static IP and want to flash OTA to the previously configured IP address.
  use_address: 192.168.3.212

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "${devicename}"
    password: !secret iot_wifi_password

switch:
  - platform: restart
    name: "Restart"

  - platform: template
    name: "Calibrate CO2 Sensor"
    id : "calibrate_co2_sensor"
    disabled_by_default: true
    turn_on_action:
        - senseair.background_calibration: co2_sensor
        - logger.log: "CO2 Sensor Calibration Triggered! Must be done OUTDOORS!"
        
  - platform: template
    name: "CO2 Sensor Calibration Result"
    id : co2_sensor_calibration_result
    disabled_by_default: true
    turn_on_action:
        - senseair.background_calibration_result: co2_sensor

  # Source: https://github.com/airgradienthq/arduino/blob/43f599a0a7d65524c49d00f546f814420aeaed6e/AirGradient.cpp#L123
  - platform: template
    name: "PMS5003"
    id: pms_switch
    optimistic: true
    turn_on_action:
      - uart.write:
          id: pms5003_uart
          data: [0x42, 0x4D, 0xE4, 0x00, 0x01, 0x01, 0x74]
    turn_off_action:
      - uart.write:
          id: pms5003_uart
          data: [0x42, 0x4D, 0xE4, 0x00, 0x00, 0x01, 0x73]

button:
  - platform: safe_mode
    name: "Restart (Safe Mode)"

  - platform: template
    name: "Reboot LD2410"
    on_press:
      lambda: 'static_cast<LD2410 *>(ld2410)->reboot();'
  - platform: template
    name: "Turn on config mode"
    on_press:
      - lambda: 'static_cast<LD2410 *>(ld2410)->setConfigMode(true);'
  - platform: template
    name: "Turn off config mode"
    on_press:
      - lambda: 'static_cast<LD2410 *>(ld2410)->setConfigMode(false);'
  - platform: template
    name: "Get config"
    on_press:
      - lambda: 'static_cast<LD2410 *>(ld2410)->queryParameters();'
  - platform: template
    name: "Set baud rate to 256000"
    on_press:
      - lambda: 'static_cast<LD2410 *>(ld2410)->setBaudrate(7);'
  - platform: template
    name: "Set baud rate to 115200"
    on_press:
      - lambda: 'static_cast<LD2410 *>(ld2410)->setBaudrate(5);'
  - platform: template
    name: "Set baud rate to 9600"
    on_press:
      - lambda: 'static_cast<LD2410 *>(ld2410)->setBaudrate(1);'

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

text_sensor:
  - platform: wifi_info
    ip_address:
      name: "IP"
      icon: "mdi:ip-outline"
      update_interval: ${update_interval_wifi}
    ssid:
      name: "SSID"
      icon: "mdi:wifi-settings"
      update_interval: ${update_interval_wifi}
    bssid:
      name: "BSSID"
      icon: "mdi:wifi-settings"
      update_interval: ${update_interval_wifi}
    mac_address:
      name: "MAC"
      icon: "mdi:network-outline"
    scan_results:
      name: "Wifi Scan"
      icon: "mdi:wifi-refresh"
      disabled_by_default: true

#https://esphome.io/guides/automations.html?highlight=restore_value#bonus-2-global-variables
globals: ##to set default reboot behavior
  # Wifi variables
  - id: wifi_connection
    type: bool
    restore_value: no
    initial_value: "false"

  - id: display_on_off
    type: bool
    restore_value: no
    initial_value: "true"

  - id: page_id
    type: int
    restore_value: no
    initial_value: "0"

  - id: last_page_id
    type: int
    restore_value: no
    initial_value: "3"

  - id: max_loops
    type: int
    restore_value: no
    initial_value: "3"

  - id: display_loops_counter
    type: int
    restore_value: no
    initial_value: "0"

  - id: debug_on_off
    type: bool
    restore_value: no
    initial_value: 'false'

i2c:
  sda: 21 #D2
  scl: 22 #D1

uart:
  - id: pms5003_uart
    rx_pin: 18 #D5
    tx_pin: 19 #D6
    baud_rate: 9600

  - id: co2_uart
    rx_pin: 16 #D4
    tx_pin: 17 #D3
    baud_rate: 9600

  - id: uart1
    rx_pin: 32
    tx_pin: 33
    baud_rate: 256000 # Change this according to your setting
    parity: NONE
    stop_bits: 1
    debug:
      direction: BOTH
      dummy_receiver: false
      after:
        delimiter: [0xF8,0xF7,0xF6,0xF5]

custom_component:
  - lambda: |-
      return {new LD2410(id(uart1))};
    components:
      - id: ld2410

    
font:
  # gfonts://family[@weight]
  - file: "gfonts://Roboto"
    id: roboto
    size: 12

  - file: "gfonts://Roboto"
    id: roboto_symbols
    size: 12
    glyphs: [
      "\U000000B5", #µ
      "\U00000067"  #g
      ]

  - file: "gfonts://Roboto"
    id: roboto_small
    size: 12

  - file: "gfonts://Roboto"
    id: roboto_medium
    size: 16
      
  - file: "gfonts://Roboto"
    id: roboto_large
    size: 32
 
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: wifi_icon_font
    size: 12
    glyphs: [
      "\U000F05A9", #wifi
      "\U000F05AA"  #no wifi
      ]

  - file: "fonts/materialdesignicons-webfont.ttf"
    id: face_icon_font
    size: 48
    glyphs: [
      "\U000F01F5", #mdi-emoticon-happy-outline
      "\U000F01F6", #mdi-emoticon-neutral-outline
      "\U000F01F8"  #mdi-emoticon-sad-outline
      ]

# https://www.co2meter.com/blogs/news/co2-levels-at-home
# ~400 ppm 	background (normal) outdoor air levels
# 400- 1,000 ppm 	typical levels found in occupied spaces with good air exchange
# 1,000 – 2,000 ppm	levels associated with complaints of drowsiness and poor air
# 2,000 – 5,000 ppm	levels associated with headaches, sleepiness, and stagnant, stale, stuffy air,
# poor concentration, loss of attention, increased heart rate and slight nausea may also be present
# >5,000 ppm	Exposure may lead to serious oxygen deprivation symptoms

display:
  - platform: ssd1306_i2c
    id: device_display
    model: "SH1106 128x64"
    address: 0x3C
    rotation: 180
    flip_x: false
    flip_y: false
    offset_y: 0
    offset_x: 0
    external_vcc: true
    update_interval: 1s
    pages:
      - id: display_auto_off_warning
        lambda: |-
          it.rectangle(0, 0, 128, 64);
          it.printf(4, 4, id(roboto_small),  "Display will turn off");
          it.printf(4, 23, id(roboto_small), "automatically.");
          it.printf(4, 42, id(roboto_small), "Touch logo to turn on.");
      - id: page1
        lambda: |-
          //it.rectangle(0, 0, 128, 64);
          it.printf(  6,  8, id(roboto_medium), "CO2 ");
          it.printf( 92,  8, id(roboto_medium), TextAlign::TOP_RIGHT, "%5.0f", id(co2).state);
          it.printf(120, 11, id(roboto), TextAlign::TOP_RIGHT, "ppm");
          it.line( 0, 32, 128,  32);
          it.line(64, 32,  64, 128);
          it.printf(  4, 34, id(roboto), "C");
          it.printf( 68, 34, id(roboto), "RH");
          it.printf( 54, 40, id(roboto_medium), TextAlign::TOP_RIGHT, "%3.1f°", id(temp).state);
          it.printf(120, 40, id(roboto_medium), TextAlign::TOP_RIGHT, "%2.0f%%", id(humidity).state);
      - id: page2
        lambda: |-
          //it.rectangle(0, 0, 128, 64);
          it.printf(4, 4, id(roboto_medium), "PM 1: ");
          it.printf(105, 4, id(roboto_medium), TextAlign::TOP_RIGHT, "%4.0f", id(pm1_0).state);
          it.printf(124, 7, id(roboto_symbols), TextAlign::TOP_RIGHT, "µg");
          it.printf(4, 23, id(roboto_medium), "PM 2.5: ");
          it.printf(105, 23, id(roboto_medium), TextAlign::TOP_RIGHT, "%4.0f", id(pm2_5).state);
          it.printf(124, 26, id(roboto_symbols), TextAlign::TOP_RIGHT, "µg");
          it.printf(4, 42, id(roboto_medium), "PM 10: ");
          it.printf(105, 42, id(roboto_medium), TextAlign::TOP_RIGHT, "%4.0f", id(pm10_0).state);
          it.printf(124, 45, id(roboto_symbols), TextAlign::TOP_RIGHT, "µg");
      - id: page3
        lambda: |-
          //it.rectangle(0, 0, 128, 64);
          if ((id(co2).state <= 1000.0) && (id(pm2_5).state < 35 )) {
            it.printf(8, 8, id(face_icon_font), "%s", "\U000F01F5");   //mdi-emoticon-happy-outline
            it.printf(76, 14, id(roboto_medium), "ALL");
            it.printf(68, 34, id(roboto_medium), "GOOD");
          } else if ((id(co2).state > 1000.0 && id(co2).state < 2000.0) || (id(pm2_5).state >= 35 && id(pm2_5).state <= 50)) {
            it.printf(8, 8, id(face_icon_font), "%s", "\U000F01F6");   //mdi-emoticon-neutral-outline
            it.printf(74, 14, id(roboto_medium), "NOT");
            it.printf(68, 34, id(roboto_medium), "GOOD");
          } else {
            it.printf(8, 8, id(face_icon_font), "%s", "\U000F01F8");   //mdi-emoticon-sad-outline
            it.printf(72, 14, id(roboto_medium), "NOT");
            it.printf(68, 34, id(roboto_medium), "SAFE");
          }



interval:
  - interval: 10s
    then:
      - if:
          condition:
            lambda: 'return id(display_on_off) == true;' 
          then:
            - display.page.show: !lambda |-
                ESP_LOGD("DEBUG", "page_id: %d", id(page_id));
                ESP_LOGD("DEBUG", "display_loops_counter: %d", id(display_loops_counter));
                ESP_LOGD("DEBUG", "max_loops: %d", id(max_loops));
                switch (id(page_id)) {
                  case 0:
                    return id(display_auto_off_warning);
                    break;
                  case 1:
                    return id(page1);
                    break;
                  case 2:
                    return id(page2);
                    break;
                  case 3:
                    return id(page3);
                    break;
                  default:
                    return (id(page1));
                    break;
                  }
            - component.update: device_display
            - lambda: |-
                if(id(display_on_off)){
                  id(page_id) += 1;
                  if(id(page_id) == id(last_page_id) + 1) {
                    id(page_id) = 1;
                    id(display_loops_counter) += 1;
                  }
                  if(id(display_loops_counter) >= id(max_loops)) {
                      id(display_loops_counter) = 1;
                      id(device_display).turn_off();
                      id(display_on_off) = false;
                      ESP_LOGD("DEBUG", "Reached max loops. Display turned off.");
                  } 
                }

            

  - interval: 20s
    then:
      if:
        condition:
          wifi.connected:
        then:
          - globals.set:
              id: wifi_connection
              value: "true"
        else:
          - globals.set:
              id: wifi_connection
              value: "false"

  - interval: 150s
    # Two-minute interval to extend the life span of the PMS5003 sensor
    then:
      - switch.turn_on: pms_switch
      - delay: 30s
      - switch.turn_off: pms_switch

sensor:
  - platform: wifi_signal
    name: "WiFi Signal"
    update_interval: ${update_interval_wifi}
    device_class: signal_strength

  - platform: sht3xd
    temperature:
      id: temp
      name: Temperature
    humidity:
      id: humidity
      name: Humidity
    address: 0x44
    update_interval: 10s
    
  - platform: pmsx003
    type: PMSX003
    uart_id: pms5003_uart
    pm_1_0:
      id: pm1_0
      name: "Particulate <1.0µm"
    pm_2_5:
      id: pm2_5
      name: "Particulate <2.5µm"
    pm_10_0:
      id: pm10_0
      name: "Particulate <10.0µm"

  - platform: custom
    lambda: |-
      auto uart_component = static_cast<LD2410 *>(ld2410);
      return {uart_component->movingTargetDistance,uart_component->movingTargetEnergy,uart_component->stillTargetDistance,uart_component->stillTargetEnergy,uart_component->detectDistance};
    sensors:
      - name: "Moving Target Distance"
        unit_of_measurement: "cm"
        accuracy_decimals: 0
      - name: "Moving Target Energy"
        unit_of_measurement: "%"
        accuracy_decimals: 0
      - name: "Still Target Distance"
        unit_of_measurement: "cm"
        accuracy_decimals: 0
      - name: "Still Target Energy"
        unit_of_measurement: "%"
        accuracy_decimals: 0
      - name: "Detect Distance"
        unit_of_measurement: "cm"
        accuracy_decimals: 0

  - platform: senseair
    id: co2_sensor
    uart_id: co2_uart
    co2:
      id: co2
      name: "CO2"
    update_interval: 60s

binary_sensor:
  - platform: gpio
    pin:
      number: 26
      inverted: true
      mode:
        input: true
        pullup: true
    name: "Left Touch Sensor"
    id: touch_sensor_lx
    on_press:
      then:
        lambda: |-
          id(display_toggle).execute();

  - platform: custom
    lambda: |-
      auto uart_component = static_cast<LD2410 *>(ld2410);
      return {uart_component->hasTarget,uart_component->hasMovingTarget,uart_component->hasStillTarget,uart_component->lastCommandSuccess};
    binary_sensors:
      - name: "Has Target"
      - name: "Has Moving Target"
      - name: "Has Still Target"
      - name: "Last Command Success"


  - platform: gpio
    pin:
      number: 23
      inverted: true
      mode:
        input: true
        pullup: true
    name: "Right Touch Sensor"
    id: touch_sensor_rx
    on_press:
      then:
        lambda: |-
          id(display_toggle).execute();

  - platform: gpio
    device_class: occupancy
    pin:
      number: 27
      inverted: false
    name: "Occupancy"
    filters:
      - delayed_off: 1000ms




script:
  - id: display_toggle
    then:
      - lambda: |-
          if(id(display_on_off)) {
            id(device_display).turn_off();
            id(display_on_off) = false;
            ESP_LOGD("DEBUG", "Display turned off.");
          } else {
            id(page_id) = 1;
            id(display_loops_counter) = 0;
            id(display_on_off) = true;
            id(display_first_page).execute();
            id(device_display).turn_on();
            ESP_LOGD("DEBUG", "Display turned on.");
          }

  - id: display_first_page
    then:
      - display.page.show: page1
      - component.update: device_display




number:        
  - platform: template
    name: "Max Moving Distance Range"
    id: maxMovingDistanceRange
    min_value: 1
    max_value: 8
    step: 1
    update_interval: never
    optimistic: true
    set_action:
      - lambda: |-
          auto uart_component = static_cast<LD2410 *>(ld2410);
          uart_component->setMaxDistancesAndNoneDuration(x,id(maxStillDistanceRange).state,id(noneDuration).state);
  - platform: template
    name: "Max Still Distance Range"
    id: maxStillDistanceRange
    min_value: 1
    max_value: 8
    step: 1
    update_interval: never
    optimistic: true
    set_action:
      - lambda: |-
          auto uart_component = static_cast<LD2410 *>(ld2410);
          uart_component->setMaxDistancesAndNoneDuration(id(maxMovingDistanceRange).state,x,id(noneDuration).state);
  - platform: template
    name: "None Duration"
    id: noneDuration
    min_value: 0
    max_value: 32767
    step: 1
    mode: box
    update_interval: never
    optimistic: true
    set_action:
      - lambda: |-
          auto uart_component = static_cast<LD2410 *>(ld2410);
          uart_component->setMaxDistancesAndNoneDuration(id(maxMovingDistanceRange).state,id(maxStillDistanceRange).state,x);

The pins I have left are the outer pins

I have tried 75, 25, 32, 33, 9, 10, and 12 in various configurations without success. The logs show nothing for the LD2410. When I press buttons, I see the name of the button pressed and a short string of hex, and then nothing else. The sensor works according to the BT app which is also what I used to change baud rates to match whatever I entered in the YAML. If all fails, I will have to just read the high/low output and configure the sensor via BT, but I would really like to have it all in ESPHome.

Device:

Any suggestions?