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

Cool project! working on smartifying my mailbox as well and will leverage some of your code. Thanks!

Curious what you did for powering the sensors? Did you power off a pin of the esp (or a pin with a mosfet) so they aren’t sipping power while the esp sleeps? Out just powering those all the time? I’ve also seen some designs using an nfet and pfet on the adc so the adc resistor bridge isn’t sipping power – will probably implement that as well.

I may not be able to do the solar on mine so increasing battery life is one of my goals.

Sorry to say, I didn’t think to do anything like that. That might explain why it loses power over time faster than I’d expect, though. If I ever do a revision I’ll give that a try.