Problem with publish_state for template binary sensor

Last year, I built a smart mailbox that tells me when I have mail. At some point this year, it stopped working. Specifically, the one MQTT topic that gives a simple binary answer to the question ‘Got mail?’ seems to have stopped publishing. Everything else seems to work just fine. I’ve even checked MQTT Explorer and the topic just doesn’t seem to be written, despite the logs from the ESP32 saying it’s publishing it.

I’m pulling my hair out trying to see what’s wrong. Sorry to dump the whole script here, but if anyone has some insight or suggestions how to proceed, please let me know.

substitutions:
  devicename: "smart-mailbox"
  sub_ota: "1"
  sub_sun: "2"
  upd_prox1: "1"
  upd_prox2: "2"
  upd_adc: "4"

esphome:
  name: $devicename
  on_boot:
    - priority: -300
      then:
        - script.execute: check_stuff

esp32:
  board: esp32dev
  framework:
    type: esp-idf

wifi:
  ssid: !secret wifi_ext_ssid
  password: !secret wifi_password
  power_save_mode: LIGHT
  fast_connect: true

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Smart Mailbox Fallback Hotspot"
    password: !secret ota_smart_mailbox


# captive_portal:

# Enable logging
logger:
  level: DEBUG
  # baud_rate: 0
  # level: NONE

# Enable API
# api:


# Enable MQTT
mqtt:
  id: mqtt_cli
  broker: !secret mqtt_broker
  username: !secret mqtt_username
  password: !secret mqtt_password
  birth_message: 
  will_message: 


ota:
  - platform: esphome
    password: !secret ota_smart_mailbox
    on_end:
      then:
        - lambda: |-
            id(ota_mode) = 0;
            id(got_mail) = 0;
        - mqtt.publish:
            topic: "$devicename/ota"
            payload: "OFF"
            retain: true
        - lambda: |-
            id(ota_ready).publish_state(false);
            id(got_mail).publish_state(false);

globals:
  - id: updates
    type: int
    restore_value: no
    initial_value: '0'
  - id: subscriptions
    type: int
    restore_value: no
    initial_value: '0'
  - id: ota_mode
    type: int
    restore_value: no
    initial_value: '0'
  - id: sun_status
    type: std::string
    restore_value: no
    initial_value: '"unknown"'
  - id: naptime
    type: int
    restore_value: no
    initial_value: '300'


deep_sleep:
  id: sleepy


i2c:
  - id: bus_a
    sda: 21
    scl: 22
  - id: bus_b
    sda: 25
    scl: 26


external_components:
  source: github://dixonte/esphome-vcnl4010
  components: [vcnl4010_i2c_sensor]


script:
  - id: check_stuff
    then:
    # Announce device is online
    - mqtt.publish:
            topic: "$devicename/status"
            payload: "online"
            retain: true
    # Get status from Home Assistant
    - lambda: |-
        id(ota_ready).publish_state(false);
        id(mqtt_cli).subscribe("$devicename/ota", [=](const std::string &topic, const std::string &payload) {
          ESP_LOGD("mqtt", "Received OTA status: %s", payload.c_str());

          auto previous_ota = id(ota_mode);

          id(ota_mode) = (payload.compare("ON") == 0);
          id(subscriptions) |= $sub_ota;

          if (!id(ota_mode) && previous_ota) {
            ESP_LOGD("mailbox", "OTA mode aborted, going into immediate microsleep...");
            id(ota_ready).publish_state(false);
            id(sleepy).set_sleep_duration(1000);
            id(sleepy).begin_sleep(true);
          }
        });
        id(mqtt_cli).subscribe("sun/status", [=](const std::string &topic, const std::string &payload) {
          ESP_LOGD("mqtt", "Received sun status: %s", payload.c_str());
          id(sun_status) = payload;
          id(subscriptions) |= $sub_sun;
        });
    # Wait for status from Home Assistant
    - wait_until:
        lambda: |-
          return (id(subscriptions) == ($sub_ota | $sub_sun));
    # How long should we sleep after updating?
    - lambda: |-
        if (id(sun_status) == "above_horizon") {
          id(naptime) = 60; //600;
        } else if (id(sun_status) == "below_horizon") {
          id(naptime) = 3600;
        } else {
          id(naptime) = 60;
        }
    # Update sensors
    - lambda: |-
        ESP_LOGD("mailbox", "Updating sensors");
        id(sensor1).update();
        id(sensor2).update();
        id(lastUpdate).update();
        id(bat_voltage_raw).set_update_interval(1300);
        id(bat_voltage_raw).start_poller();
    # Wait for updates
    - wait_until:
        lambda: |-
          return (id(updates) == ($upd_prox1 | $upd_prox2 | $upd_adc));
    # Do we have mail?
    - lambda: |-
        //bool gotMail;

        if ((id(prox1).state > 30000) || (id(prox2).state > 30000)) {
          //gotMail = true;
          ESP_LOGD("mailbox", "Publishing got mail state: You've got mail");
          id(got_mail).publish_state(true);
        } else {
          //gotMail = false;
          ESP_LOGD("mailbox", "Publishing got mail state: No mail is good mail");
          id(got_mail).publish_state(false);
        }

        //ESP_LOGD("mailbox", "Publishing got mail state: %d", gotMail);
        //id(got_mail).publish_state(gotMail);
    - delay: 1s
    - logger.log:
        format: "Sleeping for %d seconds | OTA: %d"
        args: [ 'id(naptime)', 'id(ota_mode)' ]
    # Shut down indefinitely if battery low
    - if:
        condition:
          lambda: 'return (id(bat_voltage).state < 3.1);'
        then:
          - mqtt.publish:
                  topic: "$devicename/status"
                  payload: "offline"
                  retain: true
          - deep_sleep.enter:
              id: sleepy
              sleep_duration: 3650d
    # Deep sleep to save battery, if not OTA mode
    - if:
        condition:
          lambda: 'return id(ota_mode) == 0;'
        then:
          - deep_sleep.enter:
              id: sleepy
              sleep_duration: !lambda |- 
                  return id(naptime) * 1000;
        else:
          - lambda: |-
              ESP_LOGD("mqtt", "OTA ready.");
              id(ota_ready).publish_state(true);

button:
  - platform: template
    name: "Manual update"
    on_press:
      - logger.log: Checking mail on demand
      - component.update: sensor1
      - component.update: sensor2


sensor:
  - platform: vcnl4010_i2c_sensor
    id: sensor1
    i2c_id: bus_a
    update_interval: never
    proximity:
      name: Proximity sensor 1
      id: prox1
      on_value:
        lambda: |-
          id(updates) |= $upd_prox1;
    # ambient:
    #   name: Light sensor 1
  - platform: vcnl4010_i2c_sensor
    id: sensor2
    i2c_id: bus_b
    update_interval: never
    proximity:
      name: Proximity sensor 2
      id: prox2
      on_value:
        lambda: |-
          id(updates) |= $upd_prox2;
    # ambient:
    #   name: Light sensor 2

  - platform: adc
    pin: 33
    id: "bat_voltage_raw"
    name: "Battery voltage (raw)"
    accuracy_decimals: 3
    update_interval: never
    attenuation: auto
    # filters:
    #   - sliding_window_moving_average:
    #       window_size: 30
    #       send_every: 5
    #       send_first_at: 1
    filters:
      - median:
          window_size: 10
          send_every: 10
          send_first_at: 10
    on_value:
      lambda: |-
        id(bat_voltage_raw).stop_poller();
        id(bat_voltage).update();
        id(bat_percentage).update();
        id(updates) |= $upd_adc;


  - platform: template
    id: bat_voltage
    name: "Battery voltage"
    unit_of_measurement: 'V'
    icon: "mdi:battery"
    accuracy_decimals: 3
    update_interval: never
    lambda: |-
      return ((360.0f * id(bat_voltage_raw).state) + (1000.0f * id(bat_voltage_raw).state)) / 1000.0f;

  - platform: template
    id: bat_percentage
    name: "Battery level"
    unit_of_measurement: '%'
    icon: "mdi:battery-check"
    update_interval: never
    lambda: |-
      return id(bat_voltage).state;
    filters:
      - calibrate_linear:
        - 3.1 -> 0
        - 4.2 -> 100

  - platform: template
    id: lastUpdate
    name: "Last update"
    icon: "mdi:timer-sand-complete"
    state_class: "measurement"
    accuracy_decimals: 1
    update_interval: never
    lambda: |-
      return millis();


binary_sensor:
  - platform: template
    id: got_mail
    name: Got mail?
    icon: mdi:mailbox

  - platform: template
    id: ota_ready
    name: OTA ready?
    icon: mdi:download

Further testing suggests that mqtt.publish isn’t working for me, either. The $devicename/status is not being written to MQTT.

So… any specific reason why you use MQTT and not just the normal API ESP-Home uses?
Seems that could solve your problems quite easily

what about turning this into

topic: ${devicename}/status

since you didn’t define a substituation named “devicename/status”

Mainly it’s part of making it so the device can sleep for long periods without appearing offline to HA. That’s the idea, at least.

I’ll try that

So I seem to have worked around the issue, hopefully?

By a combination of adding this at the start of my script: tag

    # Wait for MQTT
    - wait_until: 
        condition:
          mqtt.connected:

and manually adding this after the lambda that updates got_mail

    - mqtt.publish:
        topic: "${devicename}/binary_sensor/got_mail_/state"
        payload: !lambda |-
          if (id(got_mail).state)
            return "ON";
          else
            return "OFF";