Remote Humidity Monitor

Goal: track bathroom humidity. React early to prevent mold. Keep it simple, autonomous, readable months later.
Two parts:
- Remote sensor → measures humidity
- Main unit → displays status, handles logic
MicroPython. No blocking loops. Clear states.
Desired behavior
- Immediate visual signal on critical humidity
- Passive display with time and forecast
- Works even if Wi-Fi fails
Hardware
Main unit
- ESP32 S3 Supermini
- SH1106 OLED
- DS3231 RTC
- RGB LED
Remote sensor
- ESP32
- BME280
Design
- Sensor → main via ESP-NOW
- No Wi-Fi required
- Low latency, reliable indoors, router-independent
Wiring
Two I²C buses to avoid glitches. OLED updates frequently; RTC stays stable.
┌────────────────────────────┐
│ ESP32 S3 │
│ │
│ 3V3 ───────────┐ │
│ GND ───┐ │ │
│ ▼ ▼ │
│ ┌──────────────┐ │
│ │ SH1106 │ │
│ │ OLED │ │
│ └──────────────┘ │
│ SDA ←──── Pin 10 │
│ SCL ←──── Pin 11 │
│ VCC ←──── 3V3 │
│ GND ←──── GND │
│ │
│ ┌──────────────┐ │
│ │ DS3231 RTC │ │
│ └──────────────┘ │
│ SDA ←──── Pin 13 │
│ SCL ←──── Pin 12 │
│ VCC ←──── 3V3 │
│ GND ←──── GND │
│ │
│ ┌──────────────────┐ │
│ │ RGB LED │ │
│ └──────────────────┘ │
│ R (red) ←──── Pin 7 │
│ G (green) ←──── Pin 5 │
│ B (blue) ←──── Pin 6 │
│ GND ←──── GND │
└────────────────────────────┘
Operating Model
states, not procedures.
Main variables:
state["humidity"]state["alarm_level"]state["notification_pending"]state["cached_forecast_code"]state["last_sensor_update"]
Display reads state only.
Humidity Logic
Thresholds: Safe / Warning / Critical
LED:
- Green → normal
- Blue → elevated
- Blinking red → ventilate
LED depends only on humidity. No display code here.
Sensor Messages
Remote sends simple messages:
H=63%
H=71%
OK
Strict parsing.
Missing sensor updates:
- mark stale
- disable alarms
- dim LED
Stale Sensor Handling
Condition:
now - last_sensor_update > timeout
Actions: dim LED (~5%), clear alarms, reset notifications. Silent sensor → no false warnings.
Display
OLED shows:
- Clock
- Forecast icon
- Notifications
Rendering reads state only:
render(state)
Night Mode
Brightness drops at night (22:00–06:00). Applied to OLED and LED. Subtle illumination.
Weather Forecast
Optional, from DWD.
- Fetch tomorrow’s WMO code
- Convert to custom icons: sun, clouds, rain, snow, fog, thunderstorm
- Cache, update every few hours
- Non-blocking
Timekeeping
Primary: NTP Fallback: DS3231 RTC
Workflow: boot → Wi-Fi → NTP → RTC → offline fallback
Main Loop
Non-blocking, timed ticks:
loop:
handle_sensor_messages()
handle_events()
update_led()
update_display()
every 5 min:
fetch_forecast_if_needed()
attempt_ntp_sync()
Lessons
- Separate logic from rendering
- Handle missing sensors gracefully
- Dim LED signals sensor failure
- Keep messages small (
H=XX%) - Avoid blocking operations
Repository
Source
codeberg.org/duras/feuchtemelder
License
All third-party components under MIT:
- Adafruit uRTC
- Agave font
- BME280 driver
- SH1106 MicroPython driver
- MicroPython fonts