ESPHome: Dynamic Light Control with Ultrasonic Sensors

I’m building a project using ESPHome and ultrasonic sensors to control an addressable WS2812B LED strip dynamically. As an object moves closer to one sensor, the LEDs on that side will light up red, indicating its position.

Project Setup:

I’m currently in the development phase, using two ultrasonic sensors connected to an ESP device for initial testing.

Desired Functionality:

  1. Ultrasonic Sensors: Two ultrasonic sensors will measure the distance to an object.
  2. Object Position: A template sensor will calculate the object’s position as a percentage based on readings from both sensors (higher percentage closer to sensor 1).
  3. Object Size (Optional): Another template sensor can estimate the object’s size based on the combined distance readings.
  4. Dynamic Light Control: The calculated object position percentage will control the number of LEDs illuminated on the Neopixel strip, visualizing the object’s location.

Challenge:

I initially used ChatGPT to generate an ESPHome configuration for this project. While the code seemed comprehensive, uploading it to ESPHome resulted in an error message stating “automation is not a component.”

Provided Configuration:

substitutions:
  name: ultrasonic_sensor_project
  friendly_name: Ultrasonic Sensor Project

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  name_add_mac_suffix: false
  project:
    name: esphome.ultrasonic
    version: '1.0'

esp8266:
  board: esp01_1m

logger:

api:

ota:

improv_serial:

wifi:
  ap: {}

captive_portal:

dashboard_import:
  package_import_url: github://esphome/example-configs/esphome-web/esp8266.yaml@main
  import_full_config: true

web_server:

sensor:
  - platform: ultrasonic
    trigger_pin: GPIO4
    echo_pin: GPIO5
    name: "Ultrasonic Sensor 1"
    id: ultrasonic_sensor_1
    update_interval: 1s
  - platform: ultrasonic
    trigger_pin: GPIO14
    echo_pin: GPIO12
    name: "Ultrasonic Sensor 2"
    id: ultrasonic_sensor_2
    update_interval: 1s

  - platform: template
    name: "Object Position Percentage"
    update_interval: 1s
    lambda: |-
      float distance1 = id(ultrasonic_sensor_1).state * 29.2407;  // Scale up the measured distance
      float distance2 = id(ultrasonic_sensor_2).state * 29.2407;  // Scale up the measured distance
      float max_threshold = 5.0;  // 5 meters threshold
      float wiggle_room = 0.005;  // Adjust this value as needed

      if (fabs(distance1 - distance2) <= wiggle_room) {
        return -1;  // No object detected between the sensors
      }

      // Calculate the total distance between the sensors
      float total_distance = distance1 + distance2;

      // Calculate the percentage value
      float position_percentage = ((distance1) / total_distance) * 100;

      // Ensure the position percentage is within [0, 100]
      position_percentage = constrain(position_percentage, 0, 100);
      
      return position_percentage;
    unit_of_measurement: '%'

  - platform: template
    name: "Object Size"
    update_interval: 1s
    lambda: |-
      float distance1 = id(ultrasonic_sensor_1).state * 29.2407;  // Scale up the measured distance
      float distance2 = id(ultrasonic_sensor_2).state * 29.2407;  // Scale up the measured distance
      float max_threshold = 5.0;  // 5 meters threshold
      float wiggle_room = 0.005;  // Adjust this value as needed

      if (fabs(distance1 - distance2) <= wiggle_room) {
        return 0;  // No object detected between the sensors
      }

      // Calculate the object size
      float object_size = max_threshold - (distance1 + distance2);
      object_size = object_size * 100; // Convert to centimeters

      return object_size;
    unit_of_measurement: 'cm'

light:
  - platform: neopixelbus
    type: GRB  # Color order
    pin: GPIO2  # Pin connected to the LED strip
    num_leds: 720  # Number of LEDs in your strip
    variant: WS2812B
    name: ws2812b_strip  # Name of the light entity

automation:
  - id: illuminate_object
    alias: Illuminate Object
    trigger:
      platform: homeassistant
      event: start
    action:
      - delay: 2s  # Wait for the sensors to initialize
      - while:
          condition:
            lambda: 'return id(ultrasonic_sensor_1).state == 0 && id(ultrasonic_sensor_2).state == 0;'  # Wait until both sensors have valid readings
          then:
            - delay: 1s  # Wait for sensors to stabilize
      - variables:
          scaled_distance_sensor_1_cm: !lambda |-
            return id(ultrasonic_sensor_1).state * 29.2407;  // Scale up the measured distance
          scaled_distance_sensor_2_cm: !lambda |-
            return id(ultrasonic_sensor_2).state * 29.2407;  // Scale up the measured distance
          length_of_strip_cm: 500  # Length of the strip in centimeters
          number_of_leds_to_light_up: !lambda |-
            return ((720 * (scaled_distance_sensor_1_cm - scaled_distance_sensor_2_cm)) / length_of_strip_cm) | int;
      - service: light.turn_off
        target:
          entity_id: light.ws2812b_strip
      - service: light.turn_on
        data_template:
          entity_id: light.ws2812b_strip
          brightness: 255  # Full brightness for illumination
          rgb_color: [255, 0, 0]  # Red color for illumination
          transition: 0

I’m looking for someone who worked on similar project to :

  • Help in understanding the “automation is not a component” error.
  • Give guidance on revising the configuration to achieve the desired functionality within ESPHome.

Thanks in advance for your help!

I’m not really prepared to help sorting out the nonsense ChatGPT has sprouted. However as stated by the error; there is no component called Automation in ESPHome. Automations, the kind outlined with service calls and data_templates, are normally set up in HA (not the lambda though).

In the future it’s probably best to leave out ChatGPT and instead just to ask for help, with your clearly stated goals. You’ll find the experienced people more willing to give pointers.

Edit: Elaborated on the answer because @Karosm is nitpicking :stuck_out_tongue:

And don’t forget that ESPHome, by default, reboots 15 minutes after having lost WiFi connection - unless you specifically tell it to not reboot in the WiFi config.

1 Like

I agree for chatGpt!
But esphome automations are fabulous and they are working even if HA or network is down for some reason.

Esphome automations are undervalued! :+1:

1 Like

Hey @Karosm and @zenzay42 , so i did a little bit of research , about automations: , and it seems chatgpt wants me to put that in the automations.yaml in home assistant , which doesn’t make sense so , i read the docs , i progressed a bit on the project , just to keep you up to date , here is the code that i’m using trying to achive this :

substitutions:
  name: ultrasonic_sensor_project
  friendly_name: Ultrasonic Sensor Project

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  name_add_mac_suffix: false
  project:
    name: esphome.ultrasonic
    version: '1.0'

esp8266:
  board: esp01_1m

logger:

api:

ota:

improv_serial:

wifi:
  ap: {}

captive_portal:

dashboard_import:
  package_import_url: github://esphome/example-configs/esphome-web/esp8266.yaml@main
  import_full_config: true

web_server:

sensor:
  - platform: ultrasonic
    trigger_pin: GPIO4
    echo_pin: GPIO5
    name: "Ultrasonic Sensor 1"
    id: ultrasonic_sensor_1
    update_interval: 100ms
  - platform: ultrasonic
    trigger_pin: GPIO14
    echo_pin: GPIO12
    name: "Ultrasonic Sensor 2"
    id: ultrasonic_sensor_2
    update_interval: 100ms

  - platform: template
    name: "Object Position Percentage"
    id: object_position_percentage
    update_interval: 100ms
    lambda: |-
      float distance1 = id(ultrasonic_sensor_1).state * 29.2407;  // Scale up the measured distance
      float distance2 = id(ultrasonic_sensor_2).state * 29.2407;  // Scale up the measured distance
      float max_threshold = 5.0;  // 5 meters threshold
      float wiggle_room = 0.005;  // Adjust this value as needed

      if (fabs(distance1 - distance2) <= wiggle_room) {
        return -1;  // No object detected between the sensors
      }

      // Calculate the total distance between the sensors
      float total_distance = distance1 + distance2;

      // Calculate the percentage value
      float position_percentage = ((distance1) / total_distance) * 100;

      // Ensure the position percentage is within [0, 100]
      position_percentage = constrain(position_percentage, 0, 100);
      
      return position_percentage;
    unit_of_measurement: '%'

  - platform: template
    name: "Object Size"
    update_interval: 10s
    lambda: |-
      float distance1 = id(ultrasonic_sensor_1).state * 29.2407;  // Scale up the measured distance
      float distance2 = id(ultrasonic_sensor_2).state * 29.2407;  // Scale up the measured distance
      float max_threshold = 5.0;  // 5 meters threshold
      float wiggle_room = 0.005;  // Adjust this value as needed

      if (fabs(distance1 - distance2) <= wiggle_room) {
        return 0;  // No object detected between the sensors
      }

      // Calculate the object size
      float object_size = max_threshold - (distance1 + distance2);
      object_size = object_size * 100; // Convert to centimeters

      return object_size;
    unit_of_measurement: 'cm'

light:
  - platform: neopixelbus
    type: GRB
    pin: GPIO2
    num_leds: 720
    variant: WS2812
    name: "Skull Eyes"
    id: ws2812b_strip
    effects:
      - automation:
          name: Change Light Color
          sequence:
            - light.addressable_set:
                id: ws2812b_strip
                range_from: 0
                range_to: 719
                red: 0%
                green: 0%
                blue: 0%
            - light.addressable_set:
                id: ws2812b_strip
                range_from: !lambda |-
                  int start_led = (int)((id(object_position_percentage).state * 700) / 100); // Calculate start position, leaving 20 LEDs for the range
                  return start_led;
                range_to: !lambda |-
                  int start_led = (int)((id(object_position_percentage).state * 700) / 100); // Calculate start position
                  return start_led + 20;
                red: 100%
                green: 75%
                blue: 0

Now this works , but it’s updating every 100 millisecond to update , and that causes the leds to jump around , i wanted to ask you if there is a smoother way to let the leds light up rather than this ? thank you !

I’ve never tried using two Ultrasonic Sensors to measure distance, and haven’t looked to closely at the code. However, I have a couple of observations.

I don’t know what Ultrasonic sensors you are using, but they can often give unstable readings, and a median filter could help stabilize the reading.

Do you really need to update the sensors every 100ms? How fast is the thing moving that you’re measuring the distance to? I would think 500ms would be fast enough for most cases.

The automation light effect you’ve created runs constantly. I would suggest to move the light logic to a script and only call the script when the distance measured actually change.