How to call a method on a custom component from lambda?

For some time, when I needed to call a method on my custom component from a lambda function, I used a complicated typecasting macro I saw on an example that goes like this (custom cover component): #define get_am25(constructor) static_cast<Am25 *>(const_cast<custom::CustomCoverConstructor *>(&constructor)->get_cover(0))

This is part of my custom component;

// custom_component.h
#include "esphome.h"
#define get_am25(constructor) static_cast<Am25 *>(const_cast<custom::CustomCoverConstructor *>(&constructor)->get_cover(0))

class Am25 : public Component, public Cover 
{
  public:
    // ... constructors, setup overrides, etc...
    void caltop ()
    {
      this->encoderpos = 0;
      // ...
    }
    // ...
}

Then, from the yaml file, I would call the method like this:

esphome:
  includes:
    - custom_component.h

cover:
  - platform: custom
    id: lblind
    lambda: |-
      auto lblind = new Am25(22, 1, id(lb_motor), id(lb_error), id(lb_error_str));
      App.register_component (lblind);
      return {lblind};
    covers:
      - name: "Window left blind"
        id: lblindcover
        device_class: blind

switch:
  - platform: template
    name: Window LB cal top
    turn_on_action:
      - lambda: 'get_am25(lblind)->caltop();'

It worked fine until ESPHome 1.17, 1.18 or something. Now, in ESPHome 2021.12, I get an error:

/config/esphome/window.yaml: In lambda function:
/config/esphome/window.yaml:510:16: error: 'lblind' was not declared in this scope
       - lambda: 'get_am25(lblind)->caltop();'
                ^
src/living_window_am25.h:5:97: note: in definition of macro 'get_am25'
 #define get_am25(constructor) static_cast<Am25 *>(const_cast<custom::CustomCoverConstructor *>(&constructor)->get_cover(0))                                      ^

Is there a proper way to call a method in a custom component class? Is there some bug on the newer versions of ESPHome, or was this some unavoidable side-effect of the various changes in the code?

My C++ is beyond poor, but it seems like youā€™re calling the constructor again - not something you usually want/need to do, as it temporarily instantiates the class, just so you can call one method.
Rather, you probably want to call the method via the existing instance of the class, which is called here ā€˜lblindā€™.

So maybe you need to reference it via the custom platform instanceā€™s ID, and not by calling the class itself, as in:
id(lblind)->caltop();
-or-
id(lblind).caltop();
(Iā€™m not sure which way to call it would be correct - per aforementioned C++ skills absence.)

Thanks for the response. I tried all those ways (I too get a little confused about when an object is considered a pointer), i get either base operand of '->' has non-pointer type 'const esphome::custom::CustomCoverConstructor, or 'class esphome::cover::Cover' has no member named 'caltop', or const class esphome::custom::CustomCoverConstructor' has no member named 'count'.

The typecast macro was to signal the compiler that the object I was referencing was not itā€™s parent class (Cover / CustomCoverConstructor), so it would not complain about ā€œno member namedā€, but it looks like the newer version might have shuffled some declarations. I see in the generated main.cpp that the Cover components are being declared at the beggining:

cover::Cover *lblindcover;

But not my custom cover lblind (which is the one that has the method).

Sounds like weā€™re both in sort of the same boat with the C++ mysteries of it.
Perhaps someone more knowlegeable than I will step in and provide a useful and correct answer.
Iā€™m sure thereā€™s a wayā€¦

Hi, I have the exact same question: How do we call a custom componentā€™s method from another component/lambda? Iā€™m sure there must be some proper way. Does anybody know it?

1 Like

Unfortunately, no, it does not work that way as already mentioned by @vesta
Does anybody know how to do it?

1 Like

Thanks a lot for that Makro, i searched for days for something like that, to access the parent class of the Constructor!

I did not test your exact code but had a similar issue.

try it with id(lblind) instead of lblind

1 Like

For anyone searching, consider this snippet:

custom_component:
  - id: mycomponent
    lambda: !lambda |-
      auto comp = new MyComponent();
      return {comp};

number:
  - platform: template
    # ...
    set_action: 
      then:
        - lambda: |-
            auto ptr = id(mycomponent).get_component(0);
            static_cast<MyComponent*>(ptr)->custom_method();

Where:

  • MyComponent is your component class
  • mycomponent is custom_component id
  • id(mycomponent) is of type CustomComponentConstructor
  • id(mycomponent).get_component(0) returns first registered Component* (pointer)
  • static_cast<MyComponent*>(ptr) converts pointer to an actual type, so you can call your custom method on it
4 Likes

Still doesnā€™t work for me.

my code

switch:
  - platform: custom
    id: rd1_switch_id
    lambda: |-
      auto rd1_switch = new Rd1Switch();
      App.register_component(rd1_switch);
      rd1_switch->poll();
      return {rd1_switch};
    switches:
      - name: "My Custom Sensor"
        
interval:
  - interval: 1sec
    then:
      -  lambda: |-
            auto ptr = id(rd1_switch_id).get_component(0);
            static_cast<MyComponent*>(ptr)->poll();

Error Iā€™m getting:

'const class esphome::custom::CustomSwitchConstructor' has no member named 'get_component'; did you mean 'Component'?

Replace MyComponent with Rd1Switch

or use
static_cast< Rd1Switch*> (id(rd1_switch_id).get_component(0))->poll()

Thank you, this is excellent, and is exactly what I was looking for!

Using this approach itā€™s possible to have a single C++ component that provides esphome entities of different type, as well as custom methods to be used in lambdas. As a bonus, if youā€™d like to keep the ugly casts out of your yaml, you can make a little helper method in C++.

custom_component:
  - id: zone_controller
    lambda: return {new ZoneController(id(...))};

switch:
  - platform: custom
    lambda: |-
      auto* controller = ZoneController::get(zone_controller);    
      return {
        controller->power(0), controller->bus(0),
        ...
      };
    switches:
      - { id: zone_power_1, name: "Zone Power 1" }
      - { id: zone_bus_1, name: "Zone Bus 1" }
      ...
class ZoneController : public Component, ... {
 public:
  ... 
  static ZoneController* get(
      const custom_component::CustomComponentConstructor& c) {
    return static_cast<ZoneController*>(c.get_component(0));
  }
1 Like

Well, I finally got around to trying again. @mdvorak and @mag1024 answers gave me some insight and are great alternatives depending on how you want to implement it. But in the end, I got my code working by simply using id(component) instead of component in the macro call (get_am25(id(lblind))->caltop();). I suppose using component in the macro call stopped working because object declarations got shuffled around on C++ code generation by newer ESPHome versions and the object had not been declared yet when it inserted the lambda code. The extremely ugly const_cast, as I see it, is to get around C++ protections against const potential changes via pointers.

Really if your are only planning to use your custom component simply as a library of functions, you can simply declare a global instance variable instantation of your class. For example for custom component class include of " my_component.h"

class myclass: xxxxx {

}

Add this after your class definition in your header file:

myclass myclassobjvar();

#or if you prefer a pointer declaration

myclass * myclassobjvar = new myclass();

No need for a custom component declaration in your yaml at all as you can now refer to your new global class object in your lambdas as:

myclassobjvar.myclassfunc();

or if using a pointer declaration

myclassobjvar->myclassfunc();

You can still use the global variable in your custom_component declaration, just donā€™t use the ā€œautoā€ attribute in front of the variable. In this case you will need to use the pointer variation to declare your global variable first (without instantatiating it). ie myclass * myclassobjvar; The instantiation in your customcomponent section will then assign to your previously declared global.

I should clarify that this works fine for custom_components but I have not tried it with App.register for sensors, switches, etc.

For more details see this other thread on a similar topic:

Thatā€™s an interesting approach, @Dilbert66. In my case, the class (window blinds) is an actual class for a custom cover, with properties, methods and gets instantiated to two objects (left and right blind). I had actually tried something similar (global pointer declaration in the header file), but could not get it to work at the time. Anyway, I am not going to mess with the code I just got to work ;-), but this may be useful for some other case.