Returning the level/value of a LEDC component?

Hi, I’m using a speed fan component to control dc motor shades. Maybe this isn’t the proper way to do it, but I got everything working as expected when controlling the LEDC speed pin directly, i.e.

- lambda: !lambda |-
     id(speed_pin).set_level(id(motor_speed));

But I can’t figure out how to return the level in a lambda to check if the motor is on.

id(speed_pin).state doesn’t work, id(speed_pin).level, etc. .get_level() doesn’t exist either.

Any suggestions?
Thank you

I assume you did not find a solution to directly access the PWM’s current value?

ESP-IDF does offer a function ledc_get_duty, but it needs to know the channel that is used for your output’s PWM. And channel_ is a protected attribute of the ESPHome LEDCOutput class.

If one can assume a specific channel is used, which is true when only using a single PWM pin, then with:

esphome:
  ...
  includes:
    - <driver/ledc.h>

esp32:
  variant: esp32
  framework:
    type: esp-idf

…this gets the details:

// The actual PWM channel is stored in a protected attribute,
// so cannot be accessed. Assume the first channel is used:
auto channel = LEDC_CHANNEL_0;

// Only valid with high speed support; see SOC_LEDC_SUPPORT_HS_MODE in
// https://github.com/esphome/esphome/blob/f20aaf398162cf9c19cf99ad7a22343ff04bef5e/esphome/components/ledc/ledc_output.cpp
auto speed_mode = channel < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE;

auto duty = ledc_get_duty(speed_mode, channel);
auto hpoint = ledc_get_hpoint(speed_mode, channel);

ESP_LOGI("my_app",
  "LEDC status: channel=%d; speed_mode=%d; duty=%d; hpoint=%d", 
  channel,
  speed_mode,
  duty == LEDC_ERR_DUTY ? -1 : duty,
  hpoint == LEDC_ERR_VAL ? -1 : hpoint
);

I only use the above for debugging. (Specifically for an issue with a display backlight that somehow does not adhere to id(my_ledc_output).set_level(my_brightness) if that code runs too early.) But I guess ESPHome expects us to keep the last known value in a sensor in Home Assistant, or in globals, or in a static variable in the lambda.

So, curious: is there a reason why you cannot use your id(motor_speed) to get the last known value? If you want to be truly sure the motor is running then you need a sensor that actually measures that, and not rely on the PWM’s internal housekeeping, I guess.

You can do it many ways but you are likely confused with two components here.
speed_pin is likely your output id , not the speed fan id.

This would return if your speed fan is on:

if (id(my_fan_id).state) {
      // Fan is ON, do something here
1 Like

Ah, indeed Fan’s documentation nicely shows:

  • state : Retrieve the current state (on/off) of the fan.

    // Within lambda, get the fan state and conditionally do something
    if (id(my_fan).state) {
      // Fan is ON, do something here
    } else {
      // Fan is OFF, do something else here
    }
    
  • speed : Retrieve the current speed of the fan.

    // Within lambda, get the fan speed and conditionally do something
    if (id(my_fan).speed == 2) {
      // Fan speed is 2, do something here
    } else {
      // Fan speed is not 2, do something else here
    }
    

Nice!

@EvanVanVan, I guess you should not use id(speed_pin).set_level either. Instead, I think you need something like:

id(my_fan).turn_on().set_speed(id(motor_speed)).perform();
// or:
id(my_fan).make_call().set_speed(id(motor_speed)).perform();
// or:
auto call = id(my_fan).turn_on();
call.set_speed(id(motor_speed));
call.perform();

I made the same mistake using set_level; details of my quest below:

Some obsolete notes on why I thought I needed my earlier workaround

TL;DR: using the wrong way to write values, also made reading values troublesome, thinking I needed the earlier workaround. But: no!

For a LEDC-controlled backlight of a TFT-display, I’m following this ESPHome Display documentation:

LCD Backlights

[…] may be able to be dimmed using a Monochromatic Light with a ESP32 LEDC Output.

This got me a PWM-controlled output as well:

output:
  - platform: ledc
    id: my_output_id
    ...

light:
  - platform: monochromatic
    id: my_light_id
    output: my_output_id
    ...

With the above, id(my_light_id).current_values.get_brightness() always seemed to yield 1.0, even after I changed the brightness. But I was setting the brightness in the wrong way, so the internal state of my Light was not updated.

Using id(my_light_id).turn_on().set_brightness(0.2).perform() as documented nicely changes the brightness. And makes get_brightness() work fine too.

But as I forgot to include .perform(), the call had no effect. Next, I found documentation that seems to suggest one can also use id(my_output_id).set_level(0.2). That indeed works as well. (Well, for me only if not executed in the first few runs of Display’s lambda.) But then get_brightness() always yields 1.0 as Light’s internal state does not know about the direct change I made.

Aside, @EvanVanVan, only now I see one can explicitly configure the channel:

Advanced options:

  • channel (Optional, int): Manually set the LEDC channel to use. Two adjacent channels share the same timer. Defaults to an automatic selection.

But given the example from Karosm, I guess you won’t need my earlier code to start with. :partying_face:

@Karosm, thanks a lot for making me realize I needed to investigate a bit more! :pray: