Lambda 魔法

这里有一些使用 Lambda 在 ESPHome 中实现各种有趣事情的做法。 这些不需要外部组件,并展示了 Lambda 的强大功能。

显示页面替代方案

一些显示设备如 lcd_pcf8574 组件 本身不支持页面,但你可以使用 Lambda 轻松实现:

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;
        }        

发送 UDP 命令

有多种网络设备可以通过包含命令字符串的 UDP 数据包进行控制。 你可以使用 ESPHome 中的脚本中的 Lambda 发送这样的 UDP 命令。

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

          // 如果你不需要为发出的数据包设置源端口,可以移除接下来的 4 行
          source.sin_family = AF_INET;
          source.sin_addr.s_addr = htonl(INADDR_ANY);
          source.sin_port = htons(64998);  // 源端口编号
          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: "发送 UDP 命令"
  on_press:
    - script.execute:
        id: send_udp
        msg: "Hello World!"
        host: "192.168.1.10"
        port: 5000

arduinoesp-idf 平台上都经过测试。

延迟远程传输

下面的解决方案处理了在操作无线遥控窗帘时,由 RF Bridge Component(或 Remote Transmitter)发送的射频帧过于频繁的问题。窗帘电机似乎需要在单独的代码传输之间至少有 600-700ms 的静默时间才能识别它们。

这个问题可以通过构建一个原始射频代码队列,并使用(可配置的)延迟将它们一个接一个地发送来解决。延迟仅添加到接下来从需要同时从 Home Assistant 操作的窗帘列表中发送的命令。这对系统是透明的,系统仍然看起来像它们同时操作。

rf_bridge:

number:
- platform: template
  name: 延迟命令
  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:
    # 有多个窗帘
  - platform: time_based
    name: '我的房间 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..关闭的代码..XXXXXXXXXX");
      - script.execute: rf_transmitter_queue
    close_duration: 26s

    stop_action:
      - lambda: id(rf_code_queue).push_back("AAB0YXXXX..停止的代码..XXXXXXXXXX");
      - script.execute: rf_transmitter_queue

    open_action:
      - lambda: id(rf_code_queue).push_back("AAB0ZXXXX..开启的代码..XXXXXXXXXX");
      - script.execute: rf_transmitter_queue
    open_duration: 27s

单按钮窗帘控制

下面的配置展示了如何通过一个按钮控制电动窗帘的运动,通过循环切换:开启->停止->关闭->停止->…

在这个例子中,使用了 Time Based Cover,并配置了 Sonoff Dual R2 的 GPIO。

ℹ️ Note

快速控制窗帘(在之前的命令后一分钟内发送新的开启/关闭命令)可能会导致意外行为(例如:窗帘在中间停止)。这是因为延迟继电器关闭功能使用异步自动化实现的。因此,每次发送开启/关闭命令时,都会添加一个延迟继电器关闭命令,并且旧的命令不会被删除。
esp8266:
  board: esp01_1m

binary_sensor:
- platform: gpio
  pin:
    number: GPIO10
    inverted: true
  id: button
  on_press:
    then:
      # 循环切换运动的逻辑:开启->停止->关闭->停止->...
      - lambda: |
          if (id(my_cover).current_operation == COVER_OPERATION_IDLE) {
            // 窗帘处于空闲状态,检查当前状态,要么开启要么关闭窗帘。
            if (id(my_cover).is_fully_closed()) {
              auto call = id(my_cover).make_call();
              call.set_command_open();
              call.perform();
            } else {
              auto call = id(my_cover).make_call();
              call.set_command_close();
              call.perform();
            }
          } else {
            // 窗帘正在开启/关闭。停止它。
            auto call = id(my_cover).make_call();
            call.set_command_stop();
            call.perform();
          }          

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: "窗帘"
  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

从文本输入更新数值

有时使用 Template Text 从用户界面更改一些数值可能更方便。 ESPHome 有一些很棒的 辅助函数,其中包括一个将文本转换为数字的函数。

在下面的示例中,我们有一个文本输入和一个模板传感器,它们可以从文本输入字段更新。Lambda 做的事情是将文本字符串解析并转换为数字——这只有在输入的字符串包含表示浮点数的字符(如数字、-.)时才会成功。如果输入的字符串包含任何其他字符,Lambda 将返回 NaN,这对应于 未知 传感器状态。

text:
  - platform: template
    name: "数字类型输入"
    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: "从文本获取的数字"

参考文档