I built a smart mailbox

I posted this originally on lemmy.world, but someone suggested you might appreciate it here. Turns out as a new user I can’t post the whole thing here, so here’s a link to the original post.

tl;dr it’s a solar and battery powered ESP32 connected to a pair of VCNL4010 proximity sensors inside my mailbox, which lets me know when there’s mail to collect via ESPHome and Home Assistant.

2 Likes

You should post the code here using </> .

Sure, here it is.

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

esphome:
  name: $devicename
  platform: ESP32
  board: esp32dev
  on_boot:
    - priority: -300
      then:
        - script.execute: check_stuff


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:
  safe_mode: True
  password: !secret ota_smart_mailbox
  on_end:
    then:
      - lambda: |-
          id(ota_mode) = 0;
      - mqtt.publish:
          topic: "$devicename/ota"
          payload: "OFF"
          retain: true
      - lambda: |-
          id(ota_ready).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;
        } else {
          gotMail = false;
        }

        ESP_LOGD("mailbox", "Publishing got mail state: %d", gotMail);
        id(got_mail).publish_state(gotMail);
    - 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