Production-grade environmental sensor hub running on a DFRobot Firebeetle 2 ESP32-E, publishing to Home Assistant via MQTT auto-discovery with a local 128×32 OLED display.
┌─────────────────────────────────────────────────┐
│ Firebeetle ESP32-E │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ DHT22 │ │ DHT11 ×2 │ │ BH1750 (I2C) │ │
│ │ GPIO 14 │ │ GPIO 27 │ │ 0x23 │ │
│ │ │ │ GPIO 26 │ │ │ │
│ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌────┴──────────────┴───────────────┴───────┐ │
│ │ Sensor Hub (scheduler) │ │
│ └────────────────────┬──────────────────────┘ │
│ │ │
│ ┌────────────┐ ┌────┴───────┐ ┌───────────┐ │
│ │ OLED 128×32│ │ MQTT Client│ │ OTA │ │
│ │ 0x3C (I2C) │ │ PubSubCli │ │ ArduinoOTA│ │
│ └────────────┘ └────┬───────┘ └───────────┘ │
│ │ │
└───────────────────────┼─────────────────────────┘
│ WiFi
▼
┌─────────────────┐
│ Mosquitto MQTT │
│ (HA Add-on) │
└────────┬────────┘
│ Auto-Discovery
▼
┌─────────────────┐
│ Home Assistant │
│ (VirtualBox) │
└─────────────────┘
| Component | Quantity | UK Source | Approx. Price |
|---|---|---|---|
| DFRobot Firebeetle 2 ESP32-E | 1 | The Pi Hut / Amazon UK | £12–15 |
| DHT22 (AM2302) module | 1 | Amazon UK / AliExpress | £3–5 |
| DHT11 module | 2 | Amazon UK / AliExpress | £1–3 each |
| BH1750 (GY-302) breakout | 1 | Amazon UK / AliExpress | £2–4 |
| SSD1306 OLED 0.91" 128×32 | 1 | Amazon UK / AliExpress | £3–5 |
| 4.7kΩ resistors | 3 | Any | £0.10 each |
| Breadboard + jumper wires | 1 set | Any | £5–8 |
| USB-C data cable | 1 | Any | £3–5 |
Firebeetle ESP32-E Pinout (relevant pins):
┌──────────────┐
│ USB-C │
│ │
3V3 ── │ 3V3 VIN │ ── 5V (USB power)
GND ── │ GND GND │ ── GND
│ D7/G27 D2 │
│ D6/G14 D3/G26│
│ D5/G25 D4 │
│ SDA/G21 D9 │
│ SCL/G22 D10 │
│ │
└──────────────┘
| Firebeetle Pin | GPIO | Wire To | Notes |
|---|---|---|---|
| SDA | GPIO 21 | BH1750 SDA, OLED SDA | Shared I2C data bus |
| SCL | GPIO 22 | BH1750 SCL, OLED SCL | Shared I2C clock bus |
| D6 | GPIO 14 | DHT22 DATA | + 4.7kΩ pull-up to 3V3 |
| D7 | GPIO 27 | DHT11-A DATA | + 4.7kΩ pull-up to 3V3 |
| D3 | GPIO 26 | DHT11-B DATA | + 4.7kΩ pull-up to 3V3 |
| 3V3 | — | BH1750 VCC, OLED VCC, DHT22 VCC, DHT11-A VCC, DHT11-B VCC | All sensors run at 3.3V |
| GND | — | ALL sensor GND pins | Common ground rail |
Critical wiring notes:
- Each DHT sensor needs a 4.7kΩ pull-up resistor between DATA and 3V3. Many DHT breakout boards include this resistor on-board — check yours. If the board has a 10kΩ on-board, add an external 4.7kΩ anyway (parallel = ~3.2kΩ, fine).
- BH1750 and OLED share the I2C bus on different addresses (0x23 and 0x3C) — no conflict.
- Most BH1750 breakouts have the ADDR pin low by default → address 0x23.
If yours reads 0x5C, the ADDR pin is high — either ground it or change
I2C_ADDR_BH1750inpins.h. - Keep wires under 30cm for reliable DHT reads. I2C works up to ~1m at 100kHz.
- The Firebeetle has an on-board voltage regulator — USB-C provides 5V, the 3V3 pin is regulated output.
| Device | Address | Function |
|---|---|---|
| BH1750 | 0x23 | Ambient light (lux) |
| SSD1306 OLED | 0x3C | 128×32 display |
Run an I2C scan if unsure — uncomment scan: true or add a scan sketch.
- PlatformIO: Install via VS Code extension or CLI (
pip install platformio). - USB driver: The Firebeetle ESP32-E uses a CH340 USB-serial chip.
- macOS: May need the WCH driver from wch-ic.com. After install, allow the kernel extension in System Settings → Privacy & Security.
- Verify:
ls /dev/cu.usb*should show the device when plugged in.
# Clone / download this project
cd firebeetle-sensor-hub
# Create your config with real credentials
cp include/config.h.example include/config.h
# Edit include/config.h with your WiFi SSID, password, MQTT broker IP, etc.
# Build
pio run
# Flash via USB (first time)
pio run --target upload
# Monitor serial output
pio device monitorAfter the first USB flash, subsequent updates go over WiFi via OTA:
# Uncomment the OTA lines in platformio.ini, set the device IP
pio run --target uploadEdit include/config.h:
| Setting | What to change | Example |
|---|---|---|
WIFI_SSID |
Your WiFi network name | "MyHomeWiFi" |
WIFI_PASSWORD |
Your WiFi password | "hunter2" |
MQTT_BROKER |
Home Assistant VM IP | "192.168.1.50" |
MQTT_USER |
Mosquitto username | "mqtt_user" |
MQTT_PASSWORD |
Mosquitto password | "strongpassword" |
DEVICE_NAME |
MQTT topic segment | "office-sensor-hub" |
OTA_PASSWORD |
OTA update password | "mysecretOTA" |
If not already installed:
- Settings → Add-ons → Add-on Store → Mosquitto broker → Install → Start
- Enable Start on Boot and Watchdog
- Create an MQTT user: Settings → People → Users → Add User
- Username:
mqtt_user(cannot usehomeassistantoraddons) - Set a strong password
- Check "Can only log in from local network"
- Username:
- Settings → Devices & Services → Add Integration → MQTT
- It should auto-discover the Mosquitto broker
- Enable MQTT Discovery
After flashing, the device will:
- Connect to WiFi (watch serial monitor for IP assignment)
- Connect to MQTT broker
- Publish discovery configs for all 8 entities
- Begin publishing sensor state every 30 seconds
Within seconds of the device connecting to MQTT, navigate to: Settings → Devices & Services → MQTT → Devices
You should see "Office Sensor Hub" (or your configured DEVICE_FRIENDLY_NAME)
with these entities:
| Entity ID | Type | Unit |
|---|---|---|
sensor.office_sensor_hub_dht22_temperature |
Temperature | °C |
sensor.office_sensor_hub_dht22_humidity |
Humidity | % |
sensor.office_sensor_hub_dht11_a_temperature |
Temperature | °C |
sensor.office_sensor_hub_dht11_a_humidity |
Humidity | % |
sensor.office_sensor_hub_dht11_b_temperature |
Temperature | °C |
sensor.office_sensor_hub_dht11_b_humidity |
Humidity | % |
sensor.office_sensor_hub_illuminance |
Illuminance | lx |
sensor.office_sensor_hub_wifi_signal |
Signal Strength | dBm |
type: custom:mushroom-chips-card
chips:
- type: template
entity: sensor.office_sensor_hub_dht22_temperature
icon: mdi:thermometer
icon_color: |-
{% set t = states('sensor.office_sensor_hub_dht22_temperature') | float(0) %}
{% if t >= 28 %}red{% elif t >= 25 %}orange{% elif t < 18 %}blue{% else %}green{% endif %}
content: "{{ states('sensor.office_sensor_hub_dht22_temperature') }}°C"
- type: template
entity: sensor.office_sensor_hub_dht22_humidity
icon: mdi:water-percent
icon_color: |-
{% set h = states('sensor.office_sensor_hub_dht22_humidity') | float(0) %}
{% if h > 70 %}red{% elif h > 60 %}orange{% elif h < 30 %}blue{% else %}green{% endif %}
content: "{{ states('sensor.office_sensor_hub_dht22_humidity') }}%"
- type: template
entity: sensor.office_sensor_hub_illuminance
icon: mdi:brightness-6
icon_color: |-
{% set l = states('sensor.office_sensor_hub_illuminance') | float(0) %}
{% if l > 500 %}amber{% elif l > 50 %}green{% else %}blue{% endif %}
content: "{{ states('sensor.office_sensor_hub_illuminance') }} lx"type: custom:mini-graph-card
name: Office Climate (24h)
entities:
- entity: sensor.office_sensor_hub_dht22_temperature
name: Temp (DHT22)
color: "#ff6384"
show_state: true
- entity: sensor.office_sensor_hub_dht22_humidity
name: Humidity (DHT22)
color: "#36a2eb"
y_axis: secondary
show_state: true
hours_to_show: 24
points_per_hour: 2
line_width: 2
show:
legend: true
fill: fadeCompare all three temperature sensors to cross-validate readings:
type: custom:mini-graph-card
name: Temperature Comparison
entities:
- entity: sensor.office_sensor_hub_dht22_temperature
name: DHT22
color: "#ff6384"
show_state: true
- entity: sensor.office_sensor_hub_dht11_a_temperature
name: DHT11-A
color: "#4bc0c0"
show_state: true
- entity: sensor.office_sensor_hub_dht11_b_temperature
name: DHT11-B
color: "#ff9f40"
show_state: true
hours_to_show: 24
points_per_hour: 2
show:
legend: true
fill: falsetype: entities
title: Office Sensor Hub
entities:
- entity: sensor.office_sensor_hub_dht22_temperature
name: Temperature (Primary)
- entity: sensor.office_sensor_hub_dht22_humidity
name: Humidity (Primary)
- type: divider
- entity: sensor.office_sensor_hub_dht11_a_temperature
name: Temp (DHT11-A)
- entity: sensor.office_sensor_hub_dht11_a_humidity
name: Humidity (DHT11-A)
- entity: sensor.office_sensor_hub_dht11_b_temperature
name: Temp (DHT11-B)
- entity: sensor.office_sensor_hub_dht11_b_humidity
name: Humidity (DHT11-B)
- type: divider
- entity: sensor.office_sensor_hub_illuminance
name: Light Level
- entity: sensor.office_sensor_hub_wifi_signal
name: WiFi Signalautomation:
- alias: "Office temperature alert"
mode: single
triggers:
- trigger: numeric_state
entity_id: sensor.office_sensor_hub_dht22_temperature
above: 28
for: "00:05:00"
actions:
- action: notify.mobile_app_your_phone
data:
title: "Office Too Warm"
message: >-
Temperature: {{ states('sensor.office_sensor_hub_dht22_temperature') }}°C.
Consider opening a window.
data:
tag: office_temp_alert
priority: highautomation:
- alias: "Office lights needed"
mode: restart
triggers:
- trigger: numeric_state
entity_id: sensor.office_sensor_hub_illuminance
below: 50
for: "00:01:00"
conditions:
- condition: time
after: "08:00:00"
before: "22:00:00"
actions:
- action: light.turn_on
target:
entity_id: light.office_desk
data:
brightness_pct: 80Add to configuration.yaml for derived values:
template:
- sensor:
# Average temperature across all three sensors
- name: "Office Average Temperature"
unique_id: office_avg_temp
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
state: >-
{% set sensors = [
states('sensor.office_sensor_hub_dht22_temperature') | float(none),
states('sensor.office_sensor_hub_dht11_a_temperature') | float(none),
states('sensor.office_sensor_hub_dht11_b_temperature') | float(none)
] | reject('none') | list %}
{% if sensors | length > 0 %}
{{ (sensors | sum / sensors | length) | round(1) }}
{% else %}
unavailable
{% endif %}
# Dew point from DHT22 (Arden Buck equation)
- name: "Office Dew Point"
unique_id: office_dew_point
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
state: >-
{% set T = states('sensor.office_sensor_hub_dht22_temperature') | float %}
{% set RH = states('sensor.office_sensor_hub_dht22_humidity') | float %}
{% set b = 18.678 %}{% set c = 257.14 %}{% set d = 234.5 %}
{% set gamma = log((RH/100) * e**((b-T/d)*(T/(c+T)))) %}
{{ ((c * gamma) / (b - gamma)) | round(1) }}
# Comfort assessment
- name: "Office Comfort"
unique_id: office_comfort
state: >-
{% set t = states('sensor.office_sensor_hub_dht22_temperature') | float %}
{% set h = states('sensor.office_sensor_hub_dht22_humidity') | float %}
{% if t >= 20 and t <= 24 and h >= 40 and h <= 60 %}Comfortable
{% elif t > 28 or t < 16 or h > 70 or h < 30 %}Poor
{% else %}Fair{% endif %}If not already tuned, add to configuration.yaml to control database growth:
recorder:
purge_keep_days: 10
commit_interval: 5
auto_purge: trueSensors with state_class: measurement (which all of ours have via discovery)
automatically generate long-term statistics — hourly min/mean/max stored
indefinitely, unaffected by purge_keep_days. You get months of history graphs
while keeping the database lean.
All topics under: home/office-sensor-hub/
| Topic | Payload | Retained | Purpose |
|---|---|---|---|
.../status |
online / offline |
Yes | LWT availability |
.../state |
JSON (all sensors) | No | Sensor telemetry |
State payload example:
{
"dht22_temperature": 22.3,
"dht22_humidity": 48.1,
"dht11a_temperature": 22.0,
"dht11a_humidity": 50.0,
"dht11b_temperature": 21.8,
"dht11b_humidity": 49.0,
"illuminance": 342.5,
"wifi_rssi": -42
}HA discovery topics (published retained on connect):
homeassistant/sensor/firebeetle_office_01/dht22_temperature/config
homeassistant/sensor/firebeetle_office_01/dht22_humidity/config
homeassistant/sensor/firebeetle_office_01/dht11a_temperature/config
homeassistant/sensor/firebeetle_office_01/dht11a_humidity/config
homeassistant/sensor/firebeetle_office_01/dht11b_temperature/config
homeassistant/sensor/firebeetle_office_01/dht11b_humidity/config
homeassistant/sensor/firebeetle_office_01/illuminance/config
homeassistant/sensor/firebeetle_office_01/wifi_rssi/config
| Symptom | Cause | Fix |
|---|---|---|
| No serial output | Wrong baud rate | Set monitor to 115200 |
[WiFi] Connecting... loops |
Wrong SSID/password | Check config.h |
[MQTT] Failed, rc=-2 |
Broker unreachable | Verify HA IP, port 1883, Mosquitto running |
[MQTT] Failed, rc=5 |
Auth rejected | Check MQTT username/password, ensure user exists in HA |
[Sensor] dht22_temperature: invalid |
Wiring issue | Check pull-up resistor, data pin, power |
[OLED] SSD1306 init FAILED |
I2C address mismatch | Run I2C scan; try 0x3C or 0x3D |
| Device not appearing in HA | Discovery disabled | Settings → Integrations → MQTT → Enable discovery |
| Stale/ghost entities in HA | Old retained messages | Publish empty payload to the config topic |
If I2C devices aren't detected, flash this temporary sketch to find addresses:
#include <Wire.h>
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0)
Serial.printf("Found device at 0x%02X\n", addr);
}
}
void loop() {}firebeetle-sensor-hub/
├── platformio.ini # Build config + library deps
├── include/
│ ├── config.h.example # Template (committed)
│ ├── config.h # Real credentials (.gitignore'd)
│ ├── pins.h # Firebeetle GPIO assignments
│ └── version.h # Firmware semver
├── src/
│ ├── main.cpp # Entry point — init + loop dispatch
│ ├── timer_utils.h # Non-blocking millis() timer
│ ├── wifi_manager.h/.cpp # WiFi with event-driven reconnection
│ ├── mqtt_client.h/.cpp # MQTT + HA auto-discovery
│ ├── ota_handler.h/.cpp # ArduinoOTA wrapper
│ ├── sensor_hub.h/.cpp # Sensor registry + scheduler
│ ├── oled_display.h/.cpp # SSD1306 128×32 page-cycling display
│ └── sensors/
│ ├── base_sensor.h # Abstract sensor interface
│ ├── dht_sensor.h # DHT22/DHT11 driver (shared device)
│ └── bh1750_sensor.h # BH1750 I2C lux driver
├── .gitignore
└── README.md
To add a new sensor:
- Create
src/sensors/my_sensor.hinheriting fromBaseSensor - Implement
name(),begin(),read(),range_min(),range_max() - Register in
main.cpp:sensor_hub::add_sensor(std::make_unique<MySensor>(args)) - Add its discovery call in
mqtt_client.cpp→publish_discovery() - Add its JSON field in
main.cpp→publish_sensor_state() - Add library to
platformio.ini→lib_deps
MIT