Is a global lambda possible?

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?

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();