# ============================================================ # Attic Climate Sensor — ESP32-C3 SuperMini (Battery-Powered) # Board: ESP32-C3 SuperMini (RISC-V, single-core, 160MHz) # Sensor: AHT20 + BMP280 combo (I2C) # Battery: 18650 Li-Ion # Schedule: Wake every 6 hours, report to HA, deep sleep # OTA: Hold GPIO3 LOW (jumper to GND) to prevent deep sleep # ============================================================ # # WIRING: # AHT20/BMP280 Combo Board: # SDA → GPIO4 # SCL → GPIO5 # VCC → 3.3V # GND → GND # # Battery Voltage Monitor (voltage divider): # BAT+ → 100kΩ → GPIO0 (ADC) → 100kΩ → GND # # OTA Stay-Awake Jumper: # GPIO3 → 2-pin header → GND (bridge to stay awake) # # NOTES: # - GPIO8 = onboard blue LED (active LOW / inverted) # - GPIO9 = BOOT button (avoid for general use) # - GPIO2, GPIO10 = strapping pins (avoid) # - All GPIO are 3.3V logic only # ============================================================ substitutions: device_name: "attic-climate-sensor" friendly_name: "Attic Climate Sensor" # Deep sleep duration: 6 hours = 21600 seconds sleep_duration: "21600s" # Max awake time before forced sleep (safety net) run_duration: "60s" esphome: name: ${device_name} friendly_name: ${friendly_name} on_boot: priority: -10 then: - wait_until: condition: api.connected: timeout: 45s # Give sensors time to stabilize and publish - delay: 5s - if: condition: binary_sensor.is_on: ota_stay_awake then: - logger.log: "OTA jumper detected — staying awake for maintenance" - deep_sleep.prevent: deep_sleep_control - light.turn_on: onboard_led else: - logger.log: "Publishing complete — entering deep sleep" - deep_sleep.enter: deep_sleep_control esp32: board: esp32-c3-devkitm-1 variant: esp32c3 framework: type: arduino # ── Logging ──────────────────────────────────────────────── logger: level: DEBUG # The C3 SuperMini uses native USB — hardware UART is on GPIO20/21 # For production, reduce to save power: # level: WARN # ── Network ──────────────────────────────────────────────── wifi: ssid: !secret wifi_iot_ssid password: !secret wifi_password # Static IP dramatically reduces WiFi connect time (~3-5s saved per wake) manual_ip: static_ip: !secret attic_sensor_ip gateway: !secret gateway subnet: !secret subnet dns1: !secret dns1 fast_connect: true power_save_mode: none ap: ssid: "${device_name}-fallback" password: !secret wifi_password captive_portal: # ── API & OTA ────────────────────────────────────────────── api: encryption: key: !secret api_encryption_key ota: - platform: esphome password: !secret ota_password # ── I2C Bus ──────────────────────────────────────────────── # AHT20: 0x38 | BMP280: 0x77 (or 0x76 depending on board) # Using GPIO4 (SDA) and GPIO5 (SCL) — safe general-purpose pins i2c: sda: GPIO4 scl: GPIO5 scan: true # ── Deep Sleep ───────────────────────────────────────────── deep_sleep: id: deep_sleep_control run_duration: ${run_duration} sleep_duration: ${sleep_duration} # ── Status LED ───────────────────────────────────────────── # Onboard blue LED on GPIO8 (active LOW / inverted) output: - platform: gpio pin: number: GPIO8 inverted: true id: blue_led_output light: - platform: binary name: "${friendly_name} Status LED" output: blue_led_output id: onboard_led entity_category: diagnostic # ── Binary Sensors ───────────────────────────────────────── binary_sensor: # OTA Stay-Awake Jumper # Wire a 2-pin header from GPIO3 → GND # Bridge jumper = stay awake for OTA | Remove = normal deep sleep - platform: gpio pin: number: GPIO3 mode: input: true pullup: true inverted: true # LOW (jumper bridged) = ON = stay awake name: "${friendly_name} OTA Stay Awake" id: ota_stay_awake entity_category: diagnostic on_press: - logger.log: "OTA jumper enabled — preventing deep sleep" - deep_sleep.prevent: deep_sleep_control - light.turn_on: onboard_led on_release: - logger.log: "OTA jumper removed — allowing deep sleep" - deep_sleep.allow: deep_sleep_control - light.turn_off: onboard_led # ── Sensors ──────────────────────────────────────────────── sensor: # ── AHT20: Temperature & Humidity ── - platform: aht10 variant: AHT20 temperature: name: "${friendly_name} Temperature" unit_of_measurement: "°F" accuracy_decimals: 1 filters: - lambda: return x * 9.0f / 5.0f + 32.0f; - sliding_window_moving_average: window_size: 3 send_every: 1 humidity: name: "${friendly_name} Humidity" accuracy_decimals: 1 filters: - sliding_window_moving_average: window_size: 3 send_every: 1 update_interval: 5s # ── BMP280: Barometric Pressure (+ backup temp) ── - platform: bmp280_i2c address: 0x77 # Change to 0x76 if your board uses that temperature: name: "${friendly_name} BMP280 Temperature" unit_of_measurement: "°F" accuracy_decimals: 1 filters: - lambda: return x * 9.0f / 5.0f + 32.0f; disabled_by_default: true pressure: name: "${friendly_name} Barometric Pressure" accuracy_decimals: 1 unit_of_measurement: "inHg" filters: - multiply: 0.0295299831 update_interval: 5s # ── Battery Voltage via ADC ── # Voltage divider: 100kΩ + 100kΩ from BAT+ to GND, midpoint → GPIO0 # GPIO0 is ADC1_CH0 on the C3 — safe for ADC use # 2:1 divider ratio — multiply by 2 to get actual battery voltage - platform: adc pin: GPIO0 name: "${friendly_name} Battery Voltage" id: battery_voltage accuracy_decimals: 2 update_interval: 5s attenuation: 11db filters: - multiply: 2.0 # ── Battery Percentage (estimated) ── # 18650 range: ~3.0V (empty) to ~4.2V (full) - platform: template name: "${friendly_name} Battery Percentage" unit_of_measurement: "%" device_class: battery accuracy_decimals: 0 update_interval: 5s lambda: |- float voltage = id(battery_voltage).state; if (voltage >= 4.2) return 100.0; if (voltage <= 3.0) return 0.0; return (voltage - 3.0) / (4.2 - 3.0) * 100.0; # ── WiFi Signal ── - platform: wifi_signal name: "${friendly_name} WiFi Signal" update_interval: 10s entity_category: diagnostic # ── Text Sensors ─────────────────────────────────────────── text_sensor: - platform: wifi_info ip_address: name: "${friendly_name} IP Address" entity_category: diagnostic # ── Switches ─────────────────────────────────────────────── switch: # Manual deep sleep trigger from HA (for testing) - platform: template name: "${friendly_name} Force Sleep" icon: "mdi:sleep" entity_category: config turn_on_action: - deep_sleep.enter: deep_sleep_control