Repository: esphome/esphome
Pull request #8348: [ld2450] fix null exception & zone target_count not published
Author: login: mistic100
Labels: name: component: ld2450
Created: 2025-03-01T09:16:48Z | Merged: 2025-03-03T16:44:16Z

# What does this implement/fix?

This adds missing null-checks in the LD2450 when the default number components are not used.

The error occurs when one use for example template numbers, but none of the default ld2450 numbers.

I also checked other optional variable, everything else seems fine.

## Types of changes

- [x] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Code quality improvements to existing code or addition of tests
- [ ] Other

**Related issue or feature (if applicable):**

https://github.com/esphome/esphome/pull/8305

## Test Environment

- [x] ESP32
- [ ] ESP32 IDF
- [ ] ESP8266
- [ ] RP2040
- [ ] BK72xx
- [ ] RTL87xx

## Example entry for `config.yaml`:

Minimal excerpt

```yaml
ld2450:
  id: ld2450_radar
  uart_id: uart_ld2450

sensor:
  - platform: ld2450
    ld2450_id: ld2450_radar
    target_count:
      name: "Presence Target Count"

number:
  - platform: template
    name: "My number"
    min_value: 0
    max_value: 100
    step: 1
    initial_value: 0
    optimistic: true
    restore_value: true
```

## Checklist:
  - [x] The code change is tested and works locally.
  - [ ] Tests have been added to verify that the new code works (under `tests/` folder).

If user exposed functionality or configuration variables are added/changed:
  - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).



Diff:
diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp
index dd564e6ed577..5bd7635dec15 100644
--- a/esphome/components/ld2450/ld2450.cpp
+++ b/esphome/components/ld2450/ld2450.cpp
@@ -110,8 +110,10 @@ LD2450Component::LD2450Component() {}
 void LD2450Component::setup() {
   ESP_LOGCONFIG(TAG, "Setting up HLK-LD2450...");
 #ifdef USE_NUMBER
-  this->pref_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_object_id_hash());
-  this->set_presence_timeout();
+  if (this->presence_timeout_number_ != nullptr) {
+    this->pref_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_object_id_hash());
+    this->set_presence_timeout();
+  }
 #endif
   this->restart_and_read_all_info();
 }
@@ -171,17 +173,11 @@ void LD2450Component::dump_config() {
   }
 #endif
 #ifdef USE_NUMBER
-  for (number::Number *n : this->zone_x1_numbers_) {
-    LOG_NUMBER("  ", "ZoneX1Number", n);
-  }
-  for (number::Number *n : this->zone_y1_numbers_) {
-    LOG_NUMBER("  ", "ZoneY1Number", n);
-  }
-  for (number::Number *n : this->zone_x2_numbers_) {
-    LOG_NUMBER("  ", "ZoneX2Number", n);
-  }
-  for (number::Number *n : this->zone_y2_numbers_) {
-    LOG_NUMBER("  ", "ZoneY2Number", n);
+  for (auto n : this->zone_numbers_) {
+    LOG_NUMBER("  ", "ZoneX1Number", n.x1);
+    LOG_NUMBER("  ", "ZoneY1Number", n.y1);
+    LOG_NUMBER("  ", "ZoneX2Number", n.x2);
+    LOG_NUMBER("  ", "ZoneY2Number", n.y2);
   }
 #endif
 #ifdef USE_SELECT
@@ -281,10 +277,13 @@ void LD2450Component::process_zone_(uint8_t *buffer) {
     this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4);
     this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6);
 #ifdef USE_NUMBER
-    this->zone_x1_numbers_[index]->publish_state(this->zone_config_[index].x1);
-    this->zone_y1_numbers_[index]->publish_state(this->zone_config_[index].y1);
-    this->zone_x2_numbers_[index]->publish_state(this->zone_config_[index].x2);
-    this->zone_y2_numbers_[index]->publish_state(this->zone_config_[index].y2);
+    // only one null check as all coordinates are required for a single zone
+    if (this->zone_numbers_[index].x1 != nullptr) {
+      this->zone_numbers_[index].x1->publish_state(this->zone_config_[index].x1);
+      this->zone_numbers_[index].y1->publish_state(this->zone_config_[index].y1);
+      this->zone_numbers_[index].x2->publish_state(this->zone_config_[index].x2);
+      this->zone_numbers_[index].y2->publish_state(this->zone_config_[index].y2);
+    }
 #endif
   }
 }
@@ -480,21 +479,20 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
   uint8_t zone_moving_targets = 0;
   uint8_t zone_all_targets = 0;
   for (index = 0; index < MAX_ZONES; index++) {
+    zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false);
+    zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true);
+    zone_all_targets = zone_still_targets + zone_moving_targets;
+
     // Publish Still Target Count in Zones
     sensor::Sensor *szstc = this->zone_still_target_count_sensors_[index];
     if (szstc != nullptr) {
-      zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false);
       szstc->publish_state(zone_still_targets);
     }
     // Publish Moving Target Count in Zones
     sensor::Sensor *szmtc = this->zone_moving_target_count_sensors_[index];
     if (szmtc != nullptr) {
-      zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true);
       szmtc->publish_state(zone_moving_targets);
     }
-
-    zone_all_targets = zone_still_targets + zone_moving_targets;
-
     // Publish All Target Count in Zones
     sensor::Sensor *sztc = this->zone_target_count_sensors_[index];
     if (sztc != nullptr) {
@@ -820,10 +818,10 @@ void LD2450Component::set_direction_text_sensor(uint8_t target, text_sensor::Tex
 // Send Zone coordinates data to LD2450
 #ifdef USE_NUMBER
 void LD2450Component::set_zone_coordinate(uint8_t zone) {
-  number::Number *x1sens = this->zone_x1_numbers_[zone];
-  number::Number *y1sens = this->zone_y1_numbers_[zone];
-  number::Number *x2sens = this->zone_x2_numbers_[zone];
-  number::Number *y2sens = this->zone_y2_numbers_[zone];
+  number::Number *x1sens = this->zone_numbers_[zone].x1;
+  number::Number *y1sens = this->zone_numbers_[zone].y1;
+  number::Number *x2sens = this->zone_numbers_[zone].x2;
+  number::Number *y2sens = this->zone_numbers_[zone].y2;
   if (!x1sens->has_state() || !y1sens->has_state() || !x2sens->has_state() || !y2sens->has_state()) {
     return;
   }
@@ -834,10 +832,15 @@ void LD2450Component::set_zone_coordinate(uint8_t zone) {
   this->send_set_zone_command_();
 }
 
-void LD2450Component::set_zone_x1_number(uint8_t zone, number::Number *n) { this->zone_x1_numbers_[zone] = n; }
-void LD2450Component::set_zone_y1_number(uint8_t zone, number::Number *n) { this->zone_y1_numbers_[zone] = n; }
-void LD2450Component::set_zone_x2_number(uint8_t zone, number::Number *n) { this->zone_x2_numbers_[zone] = n; }
-void LD2450Component::set_zone_y2_number(uint8_t zone, number::Number *n) { this->zone_y2_numbers_[zone] = n; }
+void LD2450Component::set_zone_numbers(uint8_t zone, number::Number *x1, number::Number *y1, number::Number *x2,
+                                       number::Number *y2) {
+  if (zone < MAX_ZONES) {
+    this->zone_numbers_[zone].x1 = x1;
+    this->zone_numbers_[zone].y1 = y1;
+    this->zone_numbers_[zone].x2 = x2;
+    this->zone_numbers_[zone].y2 = y2;
+  }
+}
 #endif
 
 // Set Presence Timeout load and save from flash
diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h
index 2fed7dc0c9a2..32e4bc02e486 100644
--- a/esphome/components/ld2450/ld2450.h
+++ b/esphome/components/ld2450/ld2450.h
@@ -57,6 +57,15 @@ struct Zone {
   int16_t y2 = 0;
 };
 
+#ifdef USE_NUMBER
+struct ZoneOfNumbers {
+  number::Number *x1 = nullptr;
+  number::Number *y1 = nullptr;
+  number::Number *x2 = nullptr;
+  number::Number *y2 = nullptr;
+};
+#endif
+
 enum BaudRateStructure : uint8_t {
   BAUD_RATE_9600 = 1,
   BAUD_RATE_19200 = 2,
@@ -152,10 +161,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
 #endif
 #ifdef USE_NUMBER
   void set_zone_coordinate(uint8_t zone);
-  void set_zone_x1_number(uint8_t zone, number::Number *n);
-  void set_zone_y1_number(uint8_t zone, number::Number *n);
-  void set_zone_x2_number(uint8_t zone, number::Number *n);
-  void set_zone_y2_number(uint8_t zone, number::Number *n);
+  void set_zone_numbers(uint8_t zone, number::Number *x1, number::Number *y1, number::Number *x2, number::Number *y2);
 #endif
 #ifdef USE_SENSOR
   void set_move_x_sensor(uint8_t target, sensor::Sensor *s);
@@ -206,10 +212,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
   std::string mac_{};
 #ifdef USE_NUMBER
   ESPPreferenceObject pref_;  // only used when numbers are in use
-  std::vector<number::Number *> zone_x1_numbers_ = std::vector<number::Number *>(MAX_ZONES);
-  std::vector<number::Number *> zone_y1_numbers_ = std::vector<number::Number *>(MAX_ZONES);
-  std::vector<number::Number *> zone_x2_numbers_ = std::vector<number::Number *>(MAX_ZONES);
-  std::vector<number::Number *> zone_y2_numbers_ = std::vector<number::Number *>(MAX_ZONES);
+  ZoneOfNumbers zone_numbers_[MAX_ZONES];
 #endif
 #ifdef USE_SENSOR
   std::vector<sensor::Sensor *> move_x_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
diff --git a/esphome/components/ld2450/number/__init__.py b/esphome/components/ld2450/number/__init__.py
index 8e83de56a05e..d2098f613116 100644
--- a/esphome/components/ld2450/number/__init__.py
+++ b/esphome/components/ld2450/number/__init__.py
@@ -88,33 +88,34 @@ async def to_code(config):
         )
         await cg.register_parented(n, config[CONF_LD2450_ID])
         cg.add(ld2450_component.set_presence_timeout_number(n))
-    for x in range(MAX_ZONES):
-        if zone_conf := config.get(f"zone_{x + 1}"):
-            if zone_x1_config := zone_conf.get(CONF_X1):
-                n = cg.new_Pvariable(zone_x1_config[CONF_ID], x)
-                await number.register_number(
-                    n, zone_x1_config, min_value=-4860, max_value=4860, step=1
-                )
-                await cg.register_parented(n, config[CONF_LD2450_ID])
-                cg.add(ld2450_component.set_zone_x1_number(x, n))
-            if zone_y1_config := zone_conf.get(CONF_Y1):
-                n = cg.new_Pvariable(zone_y1_config[CONF_ID], x)
-                await number.register_number(
-                    n, zone_y1_config, min_value=0, max_value=7560, step=1
-                )
-                await cg.register_parented(n, config[CONF_LD2450_ID])
-                cg.add(ld2450_component.set_zone_y1_number(x, n))
-            if zone_x2_config := zone_conf.get(CONF_X2):
-                n = cg.new_Pvariable(zone_x2_config[CONF_ID], x)
-                await number.register_number(
-                    n, zone_x2_config, min_value=-4860, max_value=4860, step=1
-                )
-                await cg.register_parented(n, config[CONF_LD2450_ID])
-                cg.add(ld2450_component.set_zone_x2_number(x, n))
-            if zone_y2_config := zone_conf.get(CONF_Y2):
-                n = cg.new_Pvariable(zone_y2_config[CONF_ID], x)
-                await number.register_number(
-                    n, zone_y2_config, min_value=0, max_value=7560, step=1
-                )
-                await cg.register_parented(n, config[CONF_LD2450_ID])
-                cg.add(ld2450_component.set_zone_y2_number(x, n))
+    for zone_num in range(MAX_ZONES):
+        if zone_conf := config.get(f"zone_{zone_num + 1}"):
+            zone_x1_config = zone_conf.get(CONF_X1)
+            x1 = cg.new_Pvariable(zone_x1_config[CONF_ID], zone_num)
+            await number.register_number(
+                x1, zone_x1_config, min_value=-4860, max_value=4860, step=1
+            )
+            await cg.register_parented(x1, config[CONF_LD2450_ID])
+
+            zone_y1_config = zone_conf.get(CONF_Y1)
+            y1 = cg.new_Pvariable(zone_y1_config[CONF_ID], zone_num)
+            await number.register_number(
+                y1, zone_y1_config, min_value=0, max_value=7560, step=1
+            )
+            await cg.register_parented(y1, config[CONF_LD2450_ID])
+
+            zone_x2_config = zone_conf.get(CONF_X2)
+            x2 = cg.new_Pvariable(zone_x2_config[CONF_ID], zone_num)
+            await number.register_number(
+                x2, zone_x2_config, min_value=-4860, max_value=4860, step=1
+            )
+            await cg.register_parented(x2, config[CONF_LD2450_ID])
+
+            zone_y2_config = zone_conf.get(CONF_Y2)
+            y2 = cg.new_Pvariable(zone_y2_config[CONF_ID], zone_num)
+            await number.register_number(
+                y2, zone_y2_config, min_value=0, max_value=7560, step=1
+            )
+            await cg.register_parented(y2, config[CONF_LD2450_ID])
+
+            cg.add(ld2450_component.set_zone_numbers(zone_num, x1, y1, x2, y2))


Reviews:
user: login: kbx81
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-02T08:41:05Z
user: login: kbx81
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-03T04:09:19Z
user: login: mistic100
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T16:09:11Z
user: login: kbx81
body: Thanks! 🍻
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-03T16:44:04Z

Review comments:
diff_hunk: @@ -281,10 +277,13 @@ void LD2450Component::process_zone_(uint8_t *buffer) {
     this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4);
     this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6);
 #ifdef USE_NUMBER
-    this->zone_x1_numbers_[index]->publish_state(this->zone_config_[index].x1);
-    this->zone_y1_numbers_[index]->publish_state(this->zone_config_[index].y1);
-    this->zone_x2_numbers_[index]->publish_state(this->zone_config_[index].x2);
-    this->zone_y2_numbers_[index]->publish_state(this->zone_config_[index].y2);
+    // only one null check as all coordinates are required for a single zone
+    if (this->zone_numbers_[index].x1 != nullptr) {
path: esphome/components/ld2450/ld2450.cpp
user: login: kbx81
body: This was making me a bit nervous since we had individual setters for each of these pointers...so I did [`b58c3b2` (#8348)](https://github.com/esphome/esphome/pull/8348/commits/b58c3b2fb8e4970dac26eefa924267838b1b0a18) which essentially forces setting them all as a single unit. It's still not perfect but I think it's generally handled a bit better since you'd never have a case where all four wouldn't be set.

This change also reduces the size of the binary by over 300 bytes:

Before:
```
Linking .pioenvs/apollo-mtr-1/firmware.elf
RAM:   [=         ]  14.2% (used 46380 bytes from 327680 bytes)
Flash: [========= ]  91.8% (used 1684154 bytes from 1835008 bytes)
```
After:
```
Linking .pioenvs/apollo-mtr-1/firmware.elf
RAM:   [=         ]  14.2% (used 46380 bytes from 327680 bytes)
Flash: [========= ]  91.8% (used 1683818 bytes from 1835008 bytes)
```
created_at: 2025-03-02T08:41:05Z
author_association: MEMBER
diff_hunk: @@ -281,10 +277,13 @@ void LD2450Component::process_zone_(uint8_t *buffer) {
     this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4);
     this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6);
 #ifdef USE_NUMBER
-    this->zone_x1_numbers_[index]->publish_state(this->zone_config_[index].x1);
-    this->zone_y1_numbers_[index]->publish_state(this->zone_config_[index].y1);
-    this->zone_x2_numbers_[index]->publish_state(this->zone_config_[index].x2);
-    this->zone_y2_numbers_[index]->publish_state(this->zone_config_[index].y2);
+    // only one null check as all coordinates are required for a single zone
+    if (this->zone_numbers_[index].x1 != nullptr) {
path: esphome/components/ld2450/ld2450.cpp
user: login: kbx81
body: @mistic100 please confirm if this change works as expected for your configuration(s) -- if there are no new issues, I'll get this merged. 😄
created_at: 2025-03-03T04:09:19Z
author_association: MEMBER
diff_hunk: @@ -281,10 +277,13 @@ void LD2450Component::process_zone_(uint8_t *buffer) {
     this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4);
     this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6);
 #ifdef USE_NUMBER
-    this->zone_x1_numbers_[index]->publish_state(this->zone_config_[index].x1);
-    this->zone_y1_numbers_[index]->publish_state(this->zone_config_[index].y1);
-    this->zone_x2_numbers_[index]->publish_state(this->zone_config_[index].x2);
-    this->zone_y2_numbers_[index]->publish_state(this->zone_config_[index].y2);
+    // only one null check as all coordinates are required for a single zone
+    if (this->zone_numbers_[index].x1 != nullptr) {
path: esphome/components/ld2450/ld2450.cpp
user: login: mistic100
body: everything's fine
created_at: 2025-03-03T16:09:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: probot-esphome[bot]
created_at: 2025-03-01T09:16:57Z
body: Hey there @hareeshmu, mind taking a look at this pull request as it has been labeled with an integration (`ld2450`) you are listed as a [code owner](https://github.com/esphome/esphome/blob/dev/CODEOWNERS#L239) for? Thanks!
<sub><sup>(message by CodeOwnersMention)</sup></sub>
author_association: NONE
user: login: codecov-commenter
created_at: 2025-03-01T09:17:44Z
body: ## [Codecov](https://app.codecov.io/gh/esphome/esphome/pull/8348?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=esphome) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 54.32%. Comparing base [(`4d8b5ed`)](https://app.codecov.io/gh/esphome/esphome/commit/4d8b5edb1c9834b3f4340922e2168f5e79509676?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=esphome) to head [(`b58c3b2`)](https://app.codecov.io/gh/esphome/esphome/commit/b58c3b2fb8e4970dac26eefa924267838b1b0a18?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=esphome).
> Report is 2127 commits behind head on dev.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##              dev    #8348      +/-   ##
==========================================
+ Coverage   53.70%   54.32%   +0.61%     
==========================================
  Files          50       50              
  Lines        9408     9898     +490     
  Branches     1654     1353     -301     
==========================================
+ Hits         5053     5377     +324     
- Misses       4056     4195     +139     
- Partials      299      326      +27     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/esphome/esphome/pull/8348?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=esphome).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=esphome).

author_association: NONE
user: login: mistic100
created_at: 2025-03-01T09:17:55Z
body: @kbx81 :-)
author_association: CONTRIBUTOR
user: login: mistic100
created_at: 2025-03-01T15:09:35Z
body: In the original PR I mentioned the device became unresponsive as soon as I tried to set the builtin zone coordinates. I think this commit fix it : I had only declared the Zone 1 number components, thus having null pointer errors when reaching zones 2 and 3 publish state.
author_association: CONTRIBUTOR
user: login: mistic100
created_at: 2025-03-01T20:05:03Z
body: I just added a small change to fix the following bug : for a specific zone, the `target_count` is wrong if `still_target_count` or `moving_target_count` is not declared
author_association: CONTRIBUTOR