Lambda Magic

Here are a couple recipes for various interesting things you can do with Lambdas in ESPHome. These things don’t need external or custom components, and show how powerful Lambda usage can be.

Display pages alternative

Some displays like lcd_pcf8574 Component don’t support pages natively, but you can easily implement them using Lambdas:

display:
  - platform: lcd_pcf8574
    dimensions: 20x4
    address: 0x27
    id: lcd
    lambda: |-
          switch (id(page)){
            case 1:
              it.print(0, 1, "Page1");
              break;
            case 2:
              it.print(0, 1, "Page2");
              break;
            case 3:
              it.print(0, 1, "Page3");
              break;
          }

globals:
- id: page
  type: int
  initial_value: "1"

interval:
- interval: 5s
  then:
    - lambda: |-
        id(page) = (id(page) + 1);
        if (id(page) > 3) {
          id(page) = 1;
        }

Send UDP commands

There are various network devices which can be commanded with UDP packets containing command strings. You can send such UDP commands from ESPHome using a Lambda in a script.

script:
- id: send_udp
  parameters:
    msg: string
    host: string
    port: int
  then:
    - lambda: |-
          int sock = ::socket(AF_INET, SOCK_DGRAM, 0);
          struct sockaddr_in destination, source;

          destination.sin_family = AF_INET;
          destination.sin_port = htons(port);
          destination.sin_addr.s_addr = inet_addr(host.c_str());

          // you can remove the next 4 lines if you don't want to set the source port for outgoing packets
          source.sin_family = AF_INET;
          source.sin_addr.s_addr = htonl(INADDR_ANY);
          source.sin_port = htons(64998);  // the source port number
          bind(sock, (struct sockaddr*)&source, sizeof(source));

          int n_bytes = ::sendto(sock, msg.c_str(), msg.length(), 0, reinterpret_cast<sockaddr*>(&destination), sizeof(destination));
          ESP_LOGD("lambda", "Sent %s to %s:%d in %d bytes", msg.c_str(), host.c_str(), port, n_bytes);
          ::close(sock);

button:
- platform: template
  id: button_udp_sender
  name: "Send UDP Command"
  on_press:
    - script.execute:
        id: send_udp
        msg: "Hello World!"
        host: "192.168.1.10"
        port: 5000

Tested on both arduino and esp-idf platforms.

Custom UART Text Sensor

Lots of devices communicate using the UART protocol. If you want to read lines from uart to a Text Sensor you can do so using this code example.

With this you can use automations or lambda to set switch or sensor states.

#include "esphome.h"

class UartReadLineSensor : public Component, public UARTDevice, public TextSensor {
 public:
  UartReadLineSensor(UARTComponent *parent) : UARTDevice(parent) {}

  void setup() override {
    // nothing to do here
  }

  int readline(int readch, char *buffer, int len)
  {
    static int pos = 0;
    int rpos;

    if (readch > 0) {
      switch (readch) {
        case '\n':
        case '\r': // Return on CR or newline
          buffer[pos] = 0; // Just to be sure, set last character 0
          rpos = pos;
          pos = 0;  // Reset position index ready for next time
          return rpos;
        default:
          if ((pos < len-1) && ( readch < 127 )) { // Filter on <127 to make sure it is a character
            buffer[pos++] = readch;
            buffer[pos] = 0;
          }
          else
          {
            buffer[pos] = 0; // Just to be sure, set last character 0
            rpos = pos;
            pos = 0;  // Reset position index ready for next time
            return rpos;
          }
      }
    }
    // No end of line has been found, so return -1.
    return -1;
  }

  void loop() override {
    const int max_line_length = 80;
    static char buffer[max_line_length];
    while (available()) {
      if(readline(read(), buffer, max_line_length) > 0) {
        publish_state(buffer);
      }
    }
  }
};

(Store this file in your configuration directory, for example uart_read_line_sensor.h)

And in YAML:

# Example configuration entry
esphome:
  includes:
    - uart_read_line_sensor.h

logger:
  level: VERBOSE #makes uart stream available in esphome logstream
  baud_rate: 0 #disable logging over uart

uart:
  id: uart_bus
  tx_pin: GPIOXX
  rx_pin: GPIOXX
  baud_rate: 9600

text_sensor:
- platform: custom
  lambda: |-
    auto my_custom_sensor = new UartReadLineSensor(id(uart_bus));
    App.register_component(my_custom_sensor);
    return {my_custom_sensor};
  text_sensors:
    id: "uart_readline"

For more details see Custom UART Device and UART Bus.

Custom UART Switch

Here is an example switch using the uart text sensor above to set switch state.

Here we use interval to request status from the device. The response will be stored in uart text sensor. Then the switch uses the text sensor state to publish its own state.

switch:
  - platform: template
    name: "Switch"
    lambda: |-
      if (id(uart_readline).state == "*POW=ON#") {
        return true;
      } else if(id(uart_readline).state == "*POW=OFF#") {
        return false;
      } else {
        return {};
      }
    turn_on_action:
      - uart.write: "\r*pow=on#\r"
    turn_off_action:
      - uart.write: "\r*pow=off#\r"

interval:
  - interval: 10s
    then:
      - uart.write: "\r*pow=?#\r"

Delaying Remote Transmissions

The solution below handles the problem of RF frames being sent out by RF Bridge Component (or Remote Transmitter) too quickly one after another when operating radio controlled covers. The cover motors seem to need at least 600-700ms of silence between the individual code transmissions to be able to recognize them.

This can be solved by building up a queue of raw RF codes and sending them out one after the other with (a configurable) delay between them. Delay is only added to the next commands coming from a list of covers which have to be operated at once from Home Assistant. This is transparent to the system, which will still look like they operate simultaneously.

rf_bridge:

number:
- platform: template
  name: Delay commands
  icon: mdi:clock-fast
  entity_category: config
  optimistic: true
  restore_value: true
  initial_value: 750
  unit_of_measurement: "ms"
  id: queue_delay
  min_value: 10
  max_value: 1000
  step: 50
  mode: box

globals:
- id: rf_code_queue
  type: 'std::vector<std::string>'

script:
- id: rf_transmitter_queue
  mode: single
  then:
    while:
      condition:
        lambda: 'return !id(rf_code_queue).empty();'
      then:
         - rf_bridge.send_raw:
             raw: !lambda |-
               std::string rf_code = id(rf_code_queue).front();
               id(rf_code_queue).erase(id(rf_code_queue).begin());
               return rf_code;
         - delay: !lambda 'return id(queue_delay).state;'

cover:
    # have multiple covers
  - platform: time_based
    name: 'My Room 1'
    disabled_by_default: false
    device_class: shutter
    assumed_state: true
    has_built_in_endstop: true

    close_action:
      - lambda: id(rf_code_queue).push_back("AAB0XXXXX..the.closing.code..XXXXXXXXXX");
      - script.execute: rf_transmitter_queue
    close_duration: 26s

    stop_action:
      - lambda: id(rf_code_queue).push_back("AAB0YXXXX..the.stopping.code..XXXXXXXXXX");
      - script.execute: rf_transmitter_queue

    open_action:
      - lambda: id(rf_code_queue).push_back("AAB0ZXXXX..the.opening.code..XXXXXXXXXX");
      - script.execute: rf_transmitter_queue
    open_duration: 27s

One Button Cover Control

The configuration below shows how with a single button you can control the motion of a motorized cover by cycling between: open->stop->close->stop->…

In this example a Time Based Cover is used with the GPIO configuration of a Sonoff Dual R2.

Note

Controlling the cover to quickly (sending new open/close commands within a minute of previous commands) might cause unexpected behaviour (eg: cover stopping halfway). This is because the delayed relay off feature is implemented using asynchronous automations. So every time an open/close command is sent a delayed relay off command is added and old ones are not removed.

esp8266:
  board: esp01_1m

binary_sensor:
- platform: gpio
  pin:
    number: GPIO10
    inverted: true
  id: button
  on_press:
    then:
      # logic for cycling through movements: open->stop->close->stop->...
      - lambda: |
          if (id(my_cover).current_operation == COVER_OPERATION_IDLE) {
            // Cover is idle, check current state and either open or close cover.
            if (id(my_cover).is_fully_closed()) {
              id(my_cover).open();
            } else {
              id(my_cover).close();
            }
          } else {
            // Cover is opening/closing. Stop it.
            id(my_cover).stop();
          }

switch:
- platform: gpio
  pin: GPIO12
  interlock: &interlock [open_cover, close_cover]
  id: open_cover
- platform: gpio
  pin: GPIO5
  interlock: *interlock
  id: close_cover

cover:
- platform: time_based
  name: "Cover"
  id: my_cover
  open_action:
    - switch.turn_on: open_cover
  open_duration: 60s
  close_action:
    - switch.turn_on: close_cover
  close_duration: 60s
  stop_action:
    - switch.turn_off: open_cover
    - switch.turn_off: close_cover

Update numeric values from text input

Sometimes it may be more confortable to use a Template Text to change some numeric values from the user interface. ESPHome has some nice helper functions among which theres’s one to convert text to numbers.

In the example below we have a text input and a template sensor which can be updated from the text input field. What the lambda does, is to parse and convert the text string to a number - which only succeedes if the entered string contains characters represesenting a float number (such as digits, - and .). If the entered string contains any other characters, the lambda will return NaN, which corresponds to unknown sensor state.

text:
  - platform: template
    name: "Number type in"
    optimistic: true
    min_length: 0
    max_length: 16
    mode: text
    on_value:
      then:
        - sensor.template.publish:
            id: num_from_text
            state: !lambda |-
              auto n = parse_number<float>(x);
              return n.has_value() ? n.value() : NAN;

sensor:
  - platform: template
    id: num_from_text
    name: "Number from text"

Factory reset after 5 quick reboots

One may want to restore factory settings (like Wi-Fi credentials set at runtime, or clear restore states) without having to disassemble or dismount the devices from their deployed location, whilst there’s no network access either. The example below shows how to achieve that using lambdas in a script by triggering the factory reset switch after the system rebooted 5 times with 10-second timeframes.

# Example config.yaml
esphome:
  name: "esphome_ld2410"
  on_boot:
    priority: 600.0
    then:
      - script.execute: fast_boot_factory_reset_script
esp32:
  board: esp32-c3-devkitm-1

substitutions:
  factory_reset_boot_count_trigger: 5

globals:
  - id: fast_boot
    type: int
    restore_value: yes
    initial_value: '0'

script:
  - id: fast_boot_factory_reset_script
    then:
      - if:
          condition:
            lambda: return ( id(fast_boot) >= ${factory_reset_boot_count_trigger});
          then:
            - lambda: |-
                ESP_LOGD("Fast Boot Factory Reset", "Performing factotry reset");
                id(fast_boot) = 0;
                fast_boot->loop();
                global_preferences->sync();
            - button.press: factory_reset_button
      - lambda: |-
          if(id(fast_boot) > 0)
            ESP_LOGD("Fast Boot Factory Reset", "Quick reboot %d/%d, do it %d more times to factory reset", id(fast_boot), ${factory_reset_boot_count_trigger}, ${factory_reset_boot_count_trigger} - id(fast_boot));
          id(fast_boot) += 1;
          fast_boot->loop();
          global_preferences->sync();
      - delay: 10s
      - lambda: |-
          id(fast_boot) = 0;
          fast_boot->loop();
          global_preferences->sync();

wifi:
  id: wifi_component
  ap:
    ap_timeout: 0s
  reboot_timeout: 0s

captive_portal:

button:
  - platform: factory_reset
    id: factory_reset_button
    name: "ESPHome: Factory reset"

See Also