This is a hard question to research, and even harder to title. Please bear with me as I try to explain:
One of my YAML configs uses a lot of lambdas to carry out actions for which there’s no built-in component.
That is, I need to set some C++ #define ’s and instantiating a couple objects which all the other lambda’s will need to use.
For now, I’m using ‘esphome: include:’ of a classic .h file, just to get those lines at a global scope.
It’d be nice to be able put those object instantiations and #define ’s in a lambda within (e.g., the ‘globals’ or perhaps the ‘esphome:’ section of) the YAML, so everything could be in one file.
Is that possible?
Karosm
(Karosm)
April 7, 2025, 10:31pm
2
While I’m probably not able to read your “monster lambdas”, don’t be too shy to post your code. It doesn’t hurt anybody…
The lambda code isn’t large at all, but might not make much sense since it is to operate a stepper motor in a special clock.
Here are the lines in the .h file I can’t migrate to the YAML.
Adafruit_MotorShield AFMS = Adafruit_MotorShield();
Adafruit_StepperMotor *myMotor = AFMS.getStepper(96, 2); // 96 steps per rev, board channel 2
To operate it requires that I instantiate the Adafruit Motor Shield library. Which I can only do in a C++ file (the aforementioned .h file).
Formerly, I had followed the pattern for a ‘Custom Component’ but as I found that I could factor out portions of the code into lambdas which could fit into automation elements like ‘on_time…’, I got it to the point where the only things left in the Custom Component were the few lines in .h that were C++ #define ’s and a couple instantiations.
The #define ’s can simply be YAML ‘globals’ that I treat as constants.
But the object creation isn’t one I can port to YAML.
Even the library inclusion was handled by the YAML ‘libraries’ section.
I went as far as trying an item under ‘globals:’ with type ‘Adafruit_MotorShield’ but ESPHome’s precompiler rejected it as undefined.
The YAML code. In the comments is the content of the .h file it references.
# Chrono -
# Linear Clock - GLD - Summer 2018
# Stepper motor control, NTP time sync, limit switch sensing
# Pointer tracking, in motor 'steps':
# zero is 'bottom.' Midnight is 2" up from there, at MIDNIGHT_POSITION
# Noon is 12 * STEPS_PER_HOUR further up, at NOON_POSITION
# the limit switch triggers about 1.5" above that (somewhere around 77277 steps)
# so at startup, we have to presume the pointer is out of bounds and needs to reach the limit switch
# but rather than have it do all that seeking when power bounces, we're going to presume it is already close to
# where it should be, and just recalibrate every noon, when the distance to the limit switch is smallest.
# 30" travel per 12 hours
# = 2.5" per hour
# Current threaded rod pitch: 24/inch
# Current servo-steps: 96/revolution
# Results in:
# 96 steps/minute
# = 5760 steps/hour
# = 1.6 steps/second
# = 8 steps / 5 seconds
# Motor is driven by an Adafruit "DC Motor + Stepper FeatherWing Add-on For All Feather Boards"
# Adafruit product # 2927
# https://learn.adafruit.com/adafruit-stepper-dc-motor-featherwing
# TODO:
# make it respond immediately to the limit switch (which it can't do if stepping more than 1 at a time)
# find a quiet stepping mode that's also non-blocking
# figure out how to put this into 'globals:'
# #define ABOVE_LIMIT_SWITCH 78336
# #define LIMIT_SWITCH_LOCATION 72777
# #define NOON_POSITION 69120
# #define MIDNIGHT_POSITION 0
# // 12 hours * 60 minutes * 96 steps/minute = 69120
# #define STEPS_PER_HOUR 5760
# #define STEPS_PER_MINUTE 96
# #define STEPS_PER_SECOND 1.6
# Adafruit_MotorShield AFMS = Adafruit_MotorShield();
# Adafruit_StepperMotor *myMotor = AFMS.getStepper(96, 2); // 96 steps per rev, board channel 2
esphome:
name: ${node_name}
includes:
- common/chrono.h
- <Adafruit_MotorShield.h>
libraries:
- "Wire"
- "SPI"
- "Adafruit BusIO"
- "Adafruit Motor Shield V2 Library"
# Motor Library reference: https://learn.adafruit.com/adafruit-stepper-dc-motor-featherwing/library-reference
on_boot:
priority: -100
then:
- lambda: |-
AFMS.begin(); // initialize, using the default PWM frequency 1.6KHz
myMotor->setSpeed( 60 ); // a modest speed seems mostly important to accurate SINGLE stepping mode, not MICROSTEP
myMotor->release();
id(afms_setup_complete) = true;
on_loop:
then:
# stepping has to be done one at a time, so it can properly handle changes to pointer position (e.g. limit switch)
- lambda: |-
// don't run this if AFMS isn't setup yet, it will crash
if( ! id(sntp_time).now().is_valid() || ! id(afms_setup_complete) ) { return; }
id(stepsNeeded) = (id(desired_pointer_position) - id(presumed_pointer_position));
if( id(stepsNeeded) != 0 ) {
myMotor->step( 1
, id(stepsNeeded) > 0 ? FORWARD : BACKWARD
, abs(id(stepsNeeded)) > STEPS_PER_MINUTE ? SINGLE : MICROSTEP
);
if( id(stepsNeeded) > 0 ) id(presumed_pointer_position)++; else id(presumed_pointer_position)--;
myMotor->release();
}
time:
- platform: sntp
servers: '10.1.1.1'
id: sntp_time
timezone: 'America/Indiana/Indianapolis'
on_time_sync:
then:
- script.execute: do_time_calc
- text_sensor.template.publish:
id: text_timesync
state: !lambda |-
return( id(sntp_time).now().strftime( "%c" ) );
on_time:
# every minute on the mark, choose the new pointer position
- seconds: 0
then:
- script.execute: do_time_calc
# every second, update the webpage time so it looks like something's happening
- seconds: '*'
then:
- text_sensor.template.publish:
id: text_time
state: !lambda |-
return( id(sntp_time).now().strftime( "%c" ) );
# at noon, if it's 1st of month or calibration needed after startup, do so
- seconds: 0
minutes: 0
hours: 12
then:
- if:
condition:
lambda: |-
return( id(sntp_time).now().is_valid() &&
( id(sntp_time).now().day_of_month == 1 || id(calibration_needed) ) );
then:
- script.execute: recalibrate_now
script:
- id: do_time_calc
then:
- lambda: |-
if( id(stepsNeeded)==0 && id(sntp_time).now().is_valid() ) {
unsigned long thisSecond = id(sntp_time).now().second;
if (id(sntp_time).now().hour < 12) {
// id(desired_pointer_position) = MIDNIGHT_POSITION + (id(sntp_time).now().hour * STEPS_PER_HOUR) + (id(sntp_time).now().minute * STEPS_PER_MINUTE) + (thisSecond * STEPS_PER_SECOND);
id(desired_pointer_position) = MIDNIGHT_POSITION + (id(sntp_time).now().hour * STEPS_PER_HOUR) + (id(sntp_time).now().minute * STEPS_PER_MINUTE);
} else {
// id(desired_pointer_position) = NOON_POSITION - ((id(sntp_time).now().hour - 12) * STEPS_PER_HOUR) - (id(sntp_time).now().minute * STEPS_PER_MINUTE) - (thisSecond * STEPS_PER_SECOND);
id(desired_pointer_position) = NOON_POSITION - ((id(sntp_time).now().hour - 12) * STEPS_PER_HOUR) - (id(sntp_time).now().minute * STEPS_PER_MINUTE);
}
if( id(time_first_pass) ) { // presume the pointer is already close to where it needs to be
id(presumed_pointer_position) = id(desired_pointer_position);
id(time_first_pass) = false;
}
}
- id: recalibrate_now
then:
- lambda: |-
id(presumed_pointer_position) = 0;
id(desired_pointer_position) = ABOVE_LIMIT_SWITCH;
binary_sensor:
platform: gpio
id: limit_switch
name: "${node_name} Limit Switch"
internal: true
pin:
number: 14
mode:
input: true
pullup: true
filters:
# - invert: # needed for normally-open switch
- delayed_on_off: 100ms
on_press:
then:
# note that the pointer is now pressing the limit switch
- lambda: |-
id(stepsNeeded) = 0; // stop any motion immediately (redundant with math done in loop)
id(calibration_needed) = false;
id(presumed_pointer_position) = LIMIT_SWITCH_LOCATION;
id(desired_pointer_position) = NOON_POSITION;
button:
- platform: template
name: "Recalibrate now"
on_press:
- logger.log: Recalibrate Button Pressed
- script.execute: recalibrate_now
globals:
- id: time_first_pass
type: boolean
initial_value: 'true'
- id: calibration_needed
type: boolean
initial_value: 'true'
- id: afms_setup_complete
type: boolean
initial_value: 'false'
- id: presumed_pointer_position
type: int
initial_value: '0'
- id: desired_pointer_position
type: int
initial_value: '0'
- id: stepsNeeded
type: int
initial_value: '0'
# =================================
# UI stuff is below here
text_sensor:
- platform: template
name: "${node_name} Time, SNTP"
id: text_time
internal: true
icon: mdi:clock-start
- platform: template
name: "${node_name} Time, last NTP sync"
id: text_timesync
internal: true
icon: mdi:clock-start
# number:
- platform: template
name: "${node_name} Pointer, Presumed Position"
internal: true
update_interval: 1s
lambda: |-
char buffer[32];
sprintf( buffer, "%u", id(presumed_pointer_position));
return {buffer};
# set_action:
# - lambda: |-
# id(presumed_pointer_position) = x;
# update_interval: 1s
# id: presumed_pointer_position_number
# mode: box
# entity_category: 'config'
# max_value: 80000
# min_value: 0
# step: 1
- platform: template
name: "${node_name} Pointer, Desired Position"
internal: true
update_interval: 1s
lambda: |-
char buffer[32];
sprintf( buffer, "%u", id(desired_pointer_position));
return {buffer};
# set_action:
# - lambda: |-
# id(desired_pointer_position) = x;
# update_interval: 1s
# id: desired_pointer_position_number
# mode: box
# entity_category: 'config'
# max_value: 80000
# min_value: 0
# step: 1
# =================================
output:
- platform: esp8266_pwm
id: output_blue_led
pin: GPIO2
inverted: True
- platform: esp8266_pwm
id: output_red_led
pin: GPIO0
inverted: True
i2c:
api:
reboot_timeout: 0s # ignore absence of HomeAssistant
esp8266:
board: huzzah
# framework:
# version: latest
#========================================
substitutions:
node_name: chrono
packages:
# diag_info: !include common/diag_info.yaml
## device_base seems to need to be last one in this block
device_base: !include common/device_base.yaml
logger:
# baud_rate: 0
level: DEBUG
# level: VERY_VERBOSE
logs:
number: INFO
text_sensor: INFO
sensor: INFO
component: ERROR # avoid seeing hundreds of complaints about servo steps being slow
ota:
- platform: esphome
on_begin:
then:
- lambda: |-
id(stepsNeeded) = 0;
myMotor->release();