Sen5x start/stop measurement to improve battery life

The sen5x (e.g., SEN50, SEN54, SEN55) have the ability to be switched between idle mode, RHT/gas-only, and measurement mode. The current draw in measurement mode is 63 mA. The current draw in idle mode is about 2.6 mA. Sensirion’s documents indicate that I should set the device to measurement mode for ~30s (to accumulate statistics) before taking a reading. Assuming I want a measurement every 5 minutes, the ideal power consumption scenario would be:

  • Deep sleep for 4 minutes, 30 seconds.
  • Wake up, set device to measurement mode.
  • Deep sleep for 30 seconds.
  • Wake up, take readings. Post to MQTT.
  • Go back to step 1.

It looks like the current sen5x component is a PollingComponent and assumes that the device is always in measurement mode. I am happy to modify this for my needs, but I’m not sure what the best approach would be. I tried adding a line of code to start the measurement mode at the beginning of the update method and then added sleep(30). This seems to work, but Home Assistant does not like this (nor does esphome logs) as it causes ping to timeout. Is there a way I can configure this to temporarily pause execution of update (like a coroutine) to prevent HA API fro complaining?

Here’s my config to accomplish that:

esphome:
  name: esphome-web-c4025b
  friendly_name: Indoor Air Quality Sensor
  min_version: 2024.6.0
  name_add_mac_suffix: false
  project:
    name: esphome.web
    version: dev
  on_boot:
    priority: 300
    then:
      lambda: |-
        WiFi.setPhyMode(WIFI_PHY_MODE_11G);

external_components:
  - source: github://esphome/esphome@dev
    components: [sen5x]

esp8266:
  board: nodemcuv2

# Enable logging
logger:

# Enable Home Assistant API
api:

# Allow Over-The-Air updates
ota:
 - platform: esphome

# Allow provisioning Wi-Fi via serial
improv_serial:

wifi:
  ssid: 
  password: 
  power_save_mode: none
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap: {}

# In combination with the `ap` this allows the user
# to provision wifi credentials to the device via WiFi AP.
captive_portal:

# To have a "next url" for improv serial
web_server:

time:
  - platform: homeassistant
    id: homeassistant_time

i2c:
  sda: D5
  scl: D6
  scan: true

sensor:
  - platform: sen5x
    id: AQS
    pm_1_0:
      name: " PM <1µm Weight concentration"
      id: pm_1_0
      accuracy_decimals: 1
    pm_2_5:
      name: " PM <2.5µm Weight concentration"
      id: pm_2_5
      accuracy_decimals: 1
    pm_4_0:
      name: " PM <4µm Weight concentration"
      id: pm_4_0
      accuracy_decimals: 1
    pm_10_0:
      name: " PM <10µm Weight concentration"
      id: pm_10_0
      accuracy_decimals: 1
    temperature:
      name: "Temperature"
      filters:
        - lambda: return x * (9.0/5.0) + 32.0;
      unit_of_measurement: "°F"
      id: temp
      accuracy_decimals: 1
    humidity:
      name: "Humidity"
      id: humidity
      accuracy_decimals: 0
    voc:
      name: "VOC"
      id: voc
      algorithm_tuning:
        index_offset: 100
        learning_time_offset_hours: 12
        learning_time_gain_hours: 12
        gating_max_duration_minutes: 180
        std_initial: 50
        gain_factor: 230
    nox:
      name: "NOX"
      id: nox
      algorithm_tuning:
        index_offset: 100
        learning_time_offset_hours: 12
        learning_time_gain_hours: 12
        gating_max_duration_minutes: 180
        std_initial: 50
        gain_factor: 230
    temperature_compensation:
      offset: 0
      normalized_offset_slope: 0
      time_constant: 0
    acceleration_mode: low
    store_baseline: true
    address: 0x69
    update_interval: 15s
  - platform: template
    name: "Average Particulate Matter"
    id: avg_pm
    lambda: |-
      return (id(pm_1_0).state + id(pm_2_5).state + id(pm_4_0).state + id(pm_10_0).state) / 4.0;
    unit_of_measurement: "µg/m³"
    accuracy_decimals: 1
    icon: "mdi:air-filter"
    update_interval: 15s
    
text_sensor:
  - platform: template
    name: "SEN55 AQS Classification"
    icon: "mdi:checkbox-marked-circle-outline"
    lambda: |-
      if (int(id(avg_pm).state) <= 50) {
        return {"Excellent"};
      }
      else if (int(id(avg_pm).state) <= 100) {
        return {"Good"};
      }
      else if (int(id(avg_pm).state) <= 150) {
        return {"Lightly polluted"};
      }
      else if (int(id(avg_pm).state) <= 200) {
        return {"Moderately polluted"};
      }
      else if (int(id(avg_pm).state) <= 250) {
        return {"Heavily polluted"};
      }
      else if (int(id(avg_pm).state) <= 350) {
        return {"Severely polluted"};
      }
      else if (int(id(avg_pm).state) <= 500) {
        return {"Extremely polluted"};
      }
      else {
        return {"unknown"};
      }

interval:
  - interval: 1min
    then:
      - lambda: |-
          auto now = id(homeassistant_time).now();
          int hour = now.hour;
          if (hour >= 23 || hour < 5) {
            // Power-saving mode (23:00 to 05:00)
            id(AQS).set_update_interval(1800000);  // 30 minutes
          } else {
            // Normal mode
            id(AQS).set_update_interval(15000);  // 15 seconds
          }