Building a Native Home Assistant MmWave Presence Sensor With ESPHome on ESP32-C6

by Jaychouu in Circuits > Sensors

31 Views, 0 Favorites, 0 Comments

Building a Native Home Assistant MmWave Presence Sensor With ESPHome on ESP32-C6

1a98d24cde1941de80853a89609d8695.png
Building a Native Home Assistant mmWave Presence Sensor with ESPHome on ESP32-C6

When I first tried to automate lighting in Home Assistant, I assumed a simple PIR motion sensor would be enough. In practice, it failed to detect stationary people — lights turned off the moment I stopped moving.

I switched to mmWave presence sensors, but that introduced new issues: false triggers from environmental movement, slow response due to heavy signal smoothing, and the need to tweak parameters on a PC.

That changed with the C4002 mmWave Motion and Static Presence Detection Module. Designed for Home Assistant, it clearly separates motion, stationary presence, and absence, actively reduces environmental interference, and offers a wide 120° detection angle that works far better than PIR sensors.

Most importantly, the C4002 integrates natively with ESPHome. Detection distance and sensitivity can be adjusted directly in the Home Assistant UI — no coding, no flashing tools, and no custom firmware required.

In this article, I’ll show how to build a reliable Home Assistant mmWave presence sensor using ESPHome, ESP32-C6, and the C4002 module.

Supplies

  1. C4002 Millimeter-Wave Motion and Static Presence Detection Module
  2. FireBeetle 2 ESP32-C6 IoT Development Board (Other ESP32 development boards with 5V power supply are also compatible,such as ESP32-S2-DevKitM-1)
  3. USB 3.0 to Type-C Cable
  4. 3×AA Battery Holder ( DC2.1 Jack;Example power source; any stable ~5V supply is acceptable.)
  5. Jumper Wires 9" F/F
  6. DC Barrel Jack Adapter - Female
  7. Home Assistant Host (Home Assistant OS / Supervised / Container)

Why I Chose the C4002 for Home Assistant

f39ddac2b9dc4495a1a8b9f56d580584.png

Compared to many low-cost mmWave modules, the C4002 mmWave motion and static presence detection module offers clearer separation between motion, static presence, and absence, helping reduce false triggers caused by airflow or moving objects. Its 120° detection angle also provides better room coverage than traditional PIR sensors.

Most importantly, the C4002 supports native ESPHome integration. Detection range and sensitivity can be configured directly in the Home Assistant UI, with no custom code, no firmware flashing tools, and no PC-based tuning. This makes it a practical and maintainable choice for building a reliable Home Assistant presence sensor.

Wiring Diagram and Power Requirements

78d5335f115d444aa0041885bf58f7f1.png

Before connecting the ESP32 and the C4002 module, it is important to understand the power requirements.

According to the C4002 documentation, the operating voltage range of the module is 3.6V to 5.5V.

  1. If the ESP32 development board provides a regulated 5V output pin (for example, ESP32-S2-DevKitM-1), the C4002 module can be powered directly from the board.
  2. The FireBeetle 2 ESP32-C6 does not provide a dedicated 5V output pin suitable for powering external modules. Therefore, an external 5V power source is required.

ESP32-C6 and C4002 Power Supply Notes:

In this setup, the ESP32-C6 and the C4002 module must share the same external power source and a common ground. The external supply should provide a voltage around 5V, staying within the safe operating range of both devices.

When using battery power or other unregulated sources, ensure that the input voltage does not exceed the maximum rated voltage of either device, and be aware that low input voltage may cause unstable operation of the ESP32.

Power Distribution and Wiring

73a04c0a046d4af887e610f9977c6559.png
115c62f8119a4860874bd0f03b9e8880.png
338797bda69e42b5a6bd59f9f358a0fb.png
5eebb65bb3d24791b1c25f085035acb6.png
203f2bbf910640efb248a039f12c3144.png
ba4de51eac814e329f0d109d1e827ed7.png

In this tutorial, power is supplied using a 3×AA battery holder with a DC 2.1mm plug and a DC barrel jack adapter (female, screw terminal type).

This allows the battery power to be easily split and connected to both the ESP32-C6 and the C4002 mmWave module.

First, prepare two jumper wires to split the positive power line.Then, prepare another two jumper wires to split the negative (ground) power line.

Take two jumper wires.Strip a small section of insulation from one end of each wire.Twist the exposed copper wires together and solder them into a single joint, keeping both female headers intact.This creates one power line with two female outputs.

Repeat the same steps with another two jumper wires to create the ground line.

After preparing the two positive wires and the two ground wires, twist the ends of the positive wires together and insert them into the + terminal of the DC barrel jack. Tighten the screw securely to make sure the wires are firmly fixed and will not come loose.

Then, twist the ends of the ground wires together and insert them into the – terminal of the DC barrel jack, tightening the screw to secure the connection. Make sure that the positive and ground wires are separated and that no bare copper is exposed that could cause a short circuit.

Once both positive and ground wires are properly secured in the DC barrel jack, take the assembled DC barrel jack and plug it into the DC input of the battery holder.

Make sure the plug is fully inserted so that the connection is stable and will reliably supply power from the 3×AA batteries to your circuit. At this stage, your battery power is ready to be distributed to the ESP32-C6 and the C4002 module through the DC barrel jack.

Now, use the two female jumper outputs on the positive wire to connect the VIN input of the ESP32-C6 and the VIN input of the C4002 module. Similarly, use the two female jumper outputs on the ground wire to connect the GND pin of the ESP32-C6 and the GND input of the C4002 module.

Take two female-to-female jumper wires. Connect the RX pin of the C4002 module to the TX pin (GPIO5) of the ESP32-C6. Then connect the TX pin of the C4002 module to the RX pin (GPIO4) of the ESP32-C6.


Installing Home Assistant on the Host Device

The first step to getting started with Home Assistant is to install it on a device. Home Assistant provides multiple deployment methods for different application scenarios and skill levels of makers. You can follow the official documentation to complete the installation according to your hardware configuration:Home Assistant Installation.

Installing the ESPHome Add-on in Home Assistant

5d04a6444eef4667b3fe7157553cd804.png
c24106ee21184ce4b7c76bfee144daba.png
f29a3eeffd34471e8766e89c1015da3e.png
727efe9e589646a68db47bb0f3bee19e.png
fa3d8984a5874b4d95e66a672cea5cc5.png
1a7ee51f0e1f478e8bfd44a5d6b166c0.png
8a681fd5e29c4089a51f167bed5860fa.png

Once installed, create an account and follow through the initial set up instuctions.

Once configured, you will see the main dashboard of Home Assistant as shown below.

Next, we will install the ESPHome add-on in Home Assistant.

Go to the Home Assistant main page and click Settings in the left sidebar; find and click Add-ons.

After entering the Add-ons page, click Add-on store in the bottom right corner;

Enter ESPHome in the store's search bar and search; Select 'Esphome Device Builder ', then click Install;

After the add-on is installed successfully, go to its configuration page, enable all functional options, and then click Start;

Wait for the add-on to start completely. If the ESPHome Builder option appears in the left sidebar, and you can enter the corresponding management page by clicking it, it means the ESPHome add-on has been installed and started successfully.


Adding the ESP32-C6 Device to ESPHome

001aaa18f2ae4e91a474574800714c2f.png
2f142deeeb3a46bd8a5dc15dfb9a7243.png
9cc6c2d862124a5e9e6be64f8d00b071.png
02b38a007b264947ae233542274fe08a.png
aa11ade1ad44436eb81d5e5fcba8325f.png
a68adda332374848b3f746d56146efe8.png

STEP 1: On the ESPHome management page, click + NEW DEVICE.

STEP 2: On the device creation guide page, click CONTINUE directly.

STEP 3: Enter a custom device name (e.g., presence sensor), then click NEXT.

STEP 4: On the hardware model selection page, select the corresponding type ESP32-C6 for the FireBeetle 2 ESP32-C6 in use.

The page displays Configuration created, click 'SKIP' — because we will manually write the complete hardware and sensor configuration.

After the device is created successfully, go to the ESPHome device management list page, find the newly created presence-sensor device.

Click the 'SECRETS' button in the top right corner, and enter the Wi-Fi name and password.


Complete ESPHome YAML Configuration for C4002

b11fb129d41a4882b6b6ddd401a94ead.png
5ee0b125569447bab5dbef02626f01b7.png
2e174a29036b43c6a6624b216cc34e41.png
4ab35703ddc741c7a57621882c809e2c.png
ce287bed1d3940a8a418524621abaa94.png
b2a5aa2a86c546c39fecd3ffda9068f7.png
1410dc132b0f4405887e3d7cd4286ecb.png
a18979171ef44ca2852e538ebb21b4f0.png
215a48605bf34786b83474caeef8643b.png
c408c172265c4182839a6fea1646ac55.png
f32ece1b0eb3490b94e6d59fb73453b9.png
d6a72e46e09b43d29df633485ae886d4.png
52926652265f49d3a5fc1f6c36a4db3e.png
2e8e8d9854694a8f95d30aab777220b0.png
1a146793b0734f32b5f049f1268c39e1.png
c34a05a1bb3e4c96aa061da258052f20.png
260d40e73e594b2280e57d486697cc55.png
9c205acb19c445f8839c1fd74b11b283.png
a4308f536ec546ac91ac45665a4e7700.png
aa6166ebc2d94f3b91b72a38bc833897.png
d5eca1bd4eba4df9ac723402cf78a13c.png
c46a69fa01724bea99276f3c30e49e7e.png
b07df11d51834473835f931a4313f238.png

STEP 1:Click 'EDIT' — this will open the YAML configuration file of the device, where all subsequent hardware and sensor configurations will be completed;

The full page after opening is shown above.

STEP2: Paste the configuration code for FireBeetle 2 ESP32-C6 and C4002 sensor to the end of the YAML file.

# UART Configuration
uart:
id: uart_bus
tx_pin: GPIO5
rx_pin: GPIO4
baud_rate: 115200

# External components
external_components:
- source:
type: git
url: https://github.com/cdjq/esphome.git
ref: dev
components:
- dfrobot_c4002
refresh: 0s

# C4002 Component Configuration
dfrobot_c4002:
id: my_c4002
uart_id: uart_bus # Specify UART bus explicitly

# Sensor Configuration Section
sensor:
# C4002 Sensors
- platform: dfrobot_c4002
c4002_id: my_c4002
movement_distance:
name: "Motion Distance"
id: movement_distance_sensor
unit_of_measurement: "m"
accuracy_decimals: 2
icon: "mdi:ruler"
device_class: "distance"
state_class: "measurement"
existing_distance:
name: "Presence Distance"
id: existing_distance_sensor
unit_of_measurement: "m"
accuracy_decimals: 2
icon: "mdi:account"
device_class: "distance"
state_class: "measurement"
movement_speed:
name: "Motion Speed"
id: movement_speed_sensor
unit_of_measurement: "m/s"
accuracy_decimals: 2
icon: "mdi:speedometer"
device_class: "speed"
state_class: "measurement"
movement_direction:
name: "Motion Direction"
id: movement_direction_sensor
icon: "mdi:compass"
# internal: true # Comment out temporarily for data testing
target_status:
name: "Target Status"
id: target_status_sensor
icon: "mdi:target"
# internal: true # Comment out temporarily for data testing

# WiFi Signal Sensor
- platform: wifi_signal
name: "WiFi Signal Strength"
update_interval: 30s
unit_of_measurement: "dBm"
accuracy_decimals: 0
device_class: "signal_strength"
entity_category: "diagnostic"

text_sensor:
- platform: template
name: "Movement Direction Text"
id: movement_direction_text
icon: "mdi:directions"
lambda: |-
if (id(movement_direction_sensor).has_state()) {
int d = id(movement_direction_sensor).state;
if (d == 0) return {"Approaching"};
else if (d == 1) return {"No Direction"};
else if (d == 2) return {"Away"};
else return {"Unknown"};
}
return {"No Data"};
update_interval: 1s

- platform: template
name: "Target Status Text"
id: target_status_text
icon: "mdi:human-greeting"
lambda: |-
if (id(target_status_sensor).has_state()) {
int d = id(target_status_sensor).state;
if (d == 0) return {"No Target"};
else if (d == 1) return {"Static Presence"};
else if (d == 2) return {"Motion"};
else return {"Unknown"};
}
return {"No Data"};
update_interval: 1s

- platform: dfrobot_c4002
c4002_id: my_c4002
c4002_text_sensor:
name: "C4002 Log"
icon: "mdi:message-text-outline"

# Switch Configuration Section
switch:
- platform: dfrobot_c4002
c4002_id: my_c4002
switch_out_led:
name: "Out LED Switch"
icon: "mdi:led-on"
switch_run_led:
name: "Run LED Switch"
icon: "mdi:led-on"
switch_factory_reset:
name: "Factory Reset"
icon: "mdi:restart"
entity_category: "config"
switch_environmental_calibration:
name: "Sensor Calibration"
icon: "mdi:calibration"
entity_category: "config"

# Select Configuration Section - Fixed operating_mode
select:
- platform: dfrobot_c4002
c4002_id: my_c4002
operating_mode:
name: "OUT Mode"
icon: "mdi:account"
entity_category: "config"
options:
- "Mode_1"
- "Mode_2"
- "Mode_3"

# Number Configuration Section
number:
- platform: dfrobot_c4002
c4002_id: my_c4002
max_range:
name: "Max Detection Distance"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
min_range:
name: "Min Detection Distance"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
light_threshold:
name: "Light Threshold"
unit_of_measurement: "lx"
icon: "mdi:lightbulb"
entity_category: "config"
area1_min:
name: "Area 1 Min"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
area1_max:
name: "Area 1 Max"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
area2_min:
name: "Area 2 Min"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
area2_max:
name: "Area 2 Max"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
area3_min:
name: "Area 3 Min"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
area3_max:
name: "Area 3 Max"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
target_disappeard_delay_time:
name: "Target Disappear Delay"
unit_of_measurement: "s"
icon: "mdi:timer"
entity_category: "config"

The complete YAML content is as follows.

esphome:
name: presence-sensor
friendly_name: presence sensor

esp32:
board: esp32-c6-devkitc-1
framework:
type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:
encryption:
key: "mMdnZuFViavIWHEM3BhKbQvu/5n3QjUbCAwH11bHlL0="

ota:
- platform: esphome
password: "87f8c5878da3bfbfaa4bcf16bb54a32b"

wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password

# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Presence-Sensor Fallback Hotspot"
password: "1Ulcsc55x6Ou"

captive_portal:

# UART Configuration
uart:
id: uart_bus
tx_pin: GPIO5
rx_pin: GPIO4
baud_rate: 115200

# External components
external_components:
- source:
type: git
url: https://github.com/cdjq/esphome.git
ref: dev
components:
- dfrobot_c4002
refresh: 0s

# C4002 Component Configuration
dfrobot_c4002:
id: my_c4002
uart_id: uart_bus # Specify UART bus explicitly

# Sensor Configuration Section
sensor:
# C4002 Sensors
- platform: dfrobot_c4002
c4002_id: my_c4002
movement_distance:
name: "Motion Distance"
id: movement_distance_sensor
unit_of_measurement: "m"
accuracy_decimals: 2
icon: "mdi:ruler"
device_class: "distance"
state_class: "measurement"
existing_distance:
name: "Presence Distance"
id: existing_distance_sensor
unit_of_measurement: "m"
accuracy_decimals: 2
icon: "mdi:account"
device_class: "distance"
state_class: "measurement"
movement_speed:
name: "Motion Speed"
id: movement_speed_sensor
unit_of_measurement: "m/s"
accuracy_decimals: 2
icon: "mdi:speedometer"
device_class: "speed"
state_class: "measurement"
movement_direction:
name: "Motion Direction"
id: movement_direction_sensor
icon: "mdi:compass"
# internal: true # Comment out temporarily for data testing
target_status:
name: "Target Status"
id: target_status_sensor
icon: "mdi:target"
# internal: true # Comment out temporarily for data testing

# WiFi Signal Sensor
- platform: wifi_signal
name: "WiFi Signal Strength"
update_interval: 30s
unit_of_measurement: "dBm"
accuracy_decimals: 0
device_class: "signal_strength"
entity_category: "diagnostic"

text_sensor:
- platform: template
name: "Movement Direction Text"
id: movement_direction_text
icon: "mdi:directions"
lambda: |-
if (id(movement_direction_sensor).has_state()) {
int d = id(movement_direction_sensor).state;
if (d == 0) return {"Approaching"};
else if (d == 1) return {"No Direction"};
else if (d == 2) return {"Away"};
else return {"Unknown"};
}
return {"No Data"};
update_interval: 1s

- platform: template
name: "Target Status Text"
id: target_status_text
icon: "mdi:human-greeting"
lambda: |-
if (id(target_status_sensor).has_state()) {
int d = id(target_status_sensor).state;
if (d == 0) return {"No Target"};
else if (d == 1) return {"Static Presence"};
else if (d == 2) return {"Motion"};
else return {"Unknown"};
}
return {"No Data"};
update_interval: 1s

- platform: dfrobot_c4002
c4002_id: my_c4002
c4002_text_sensor:
name: "C4002 Log"
icon: "mdi:message-text-outline"

# Switch Configuration Section
switch:
- platform: dfrobot_c4002
c4002_id: my_c4002
switch_out_led:
name: "Out LED Switch"
icon: "mdi:led-on"
switch_run_led:
name: "Run LED Switch"
icon: "mdi:led-on"
switch_factory_reset:
name: "Factory Reset"
icon: "mdi:restart"
entity_category: "config"
switch_environmental_calibration:
name: "Sensor Calibration"
icon: "mdi:calibration"
entity_category: "config"

# Select Configuration Section - Fixed operating_mode
select:
- platform: dfrobot_c4002
c4002_id: my_c4002
operating_mode:
name: "OUT Mode"
icon: "mdi:account"
entity_category: "config"
options:
- "Mode_1"
- "Mode_2"
- "Mode_3"

# Number Configuration Section
number:
- platform: dfrobot_c4002
c4002_id: my_c4002
max_range:
name: "Max Detection Distance"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
min_range:
name: "Min Detection Distance"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
light_threshold:
name: "Light Threshold"
unit_of_measurement: "lx"
icon: "mdi:lightbulb"
entity_category: "config"
area1_min:
name: "Area 1 Min"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
area1_max:
name: "Area 1 Max"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
area2_min:
name: "Area 2 Min"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
area2_max:
name: "Area 2 Max"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
area3_min:
name: "Area 3 Min"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
area3_max:
name: "Area 3 Max"
unit_of_measurement: "m"
icon: "mdi:ruler"
entity_category: "config"
target_disappeard_delay_time:
name: "Target Disappear Delay"
unit_of_measurement: "s"
icon: "mdi:timer"
entity_category: "config"


STEP 3: Click SAVE in the upper right corner of the page to save the configuration.


STEP 4: Click INSTALL to trigger firmware compilation;

A pop-up window for installation method selection appears, select 'Plug into this computer', then click 'Download project' to start firmware compilation.

Compilation Completion Mark: The Download project button changes from gray to clickable (firmware compilation takes about 10-20 minutes, please wait and do not close the page);

After the firmware is compiled, click 'Download project', select the 'Factory format' mode in the flashing options, and download the firmware file (the file name is presence-sensor.factory.bin);


STEP 5: After the firmware is downloaded, click 'Open ESPHome Web'.

Ensure the FireBeetle 2 ESP32-C6 is connected to the computer via a USB cable.

Then click ' CONNECT 'on the flashing tool page.

In the pop-up serial port selection box, select the serial port corresponding to the FireBeetle 2 ESP32-C6, and click Connect to complete the device connection.


STEP 6: After the ESP32-C6 is successfully connected to the flashing tool, click INSTALL on the page.

Click 'Choose File', select the newly downloaded presence-sensor.factory.bin firmware file, and click INSTALL to start flashing.

Wait for the firmware flashing to complete,the page displays a flashing success.


STEP 7: Unplug the USB cable to disconnect the ESP32-C6 from the computer. Connect the battery pack, ESP32-C6, and C4002 motion and presence module according to the following wiring diagram.

Return to the Home Assistant page and you will see the presence sensor device go online.

If it shows offline, the board is most likely failing to connect to the WiFi. Please check if the WiFi SSID and password in the secrets.yaml file are correct.


STEP 8: After the presence sensor device is online, click Settings – a notification for new device discovery will pop up on the page, then click Add.

Locate the corresponding API key in the presence-sensor.yaml file.

Fill it in the designated input box of the pop-up.

Once completed, click 'Settings' and navigate to the 'Device & Services' page.

Click ESPHome and check that presence sensor appears on the page.

Click 'presence sensor' and you will see the following device information:


Sensor Data Items

  1. Motion Distance: Real-time distance between the target and the sensor when a moving object is detected
  2. Presence Distance: Real-time distance between the target and the sensor when a static human body is detected
  3. Motion Speed: Moving speed of the detected target
  4. Motion Direction (numeric): 0=Approaching, 1=No direction, 2=Moving away
  5. Target Status (numeric): 0=No target, 1=Static presence, 2=Motion detected


In addition to the sensor data, the following configurable options are available:

  1. Sensor OUT Indicator Light Switch: Controls the on/off state of the sensor's physical OUT LED
  2. Sensor RUN Indicator Light Switch: Controls the on/off state of the sensor's physical RUN LED
  3. Restore Factory Settings: Momentary trigger switch that auto-resets after activation and restores the sensor to factory default parameters
  4. Environmental Calibration: Momentary trigger switch that auto-resets after activation and calibrates the sensor to the current environment for improved detection accuracy
  5. Detection Distance Setting: Customize the sensor's effective detection range
  6. Light Sensing Threshold Setting: Configure the trigger threshold for the sensor's light-sensing detection
  7. Detection Exclusion Zone Setting: Customize up to 3 detection exclusion zones to avoid false detections caused by objects such as furniture


Visualizing MmWave Presence Data in the Home Assistant Dashboard

10521289ab0b471da9b9bde364d93315.png
6a024fd490cf4e1b8ebd2e7594ecea20.png
2067e6508bb848c28dd1a5a8bab1abd9.png
c217a7f719164611a9c2ca6a8db883c0.png
ef10ac766d2b45b3a7e0b85d0f13f4d2.png
4b6db9ec05e8459f87edfcd4e15e1529.png
1a98d24cde1941de80853a89609d8695.png

On the Overview page of Home Assistant, click the three-dot icon in the top right corner and select Edit Dashboard.

Then click '+ ADD CARD'.

If you want to display the C4002 sensor data via a card, select the the 'Entity card', then choose the relevant C4002 entities (e.g., Presence Distance).

You can customize the card name and icon, then click 'Save'. A dedicated data card will be created, allowing you to view the real-time human presence distance data detected by the C4002 Motion and Presence Detection Module.

Click 'Save'. You will see an entity card appear on the dashboard, displaying real-time human presence distance data detected by the C4002 Motion and Presence Detection Module.

In addition, you can select different card types to create various data visualization effects.

In addition, you can select different card types to create various data visualization effects. This is my practical project of a human presence sensor, which can detect human presence and motion status in the living room. Now it's your turn to make your own!