Solar-Powered IoT: How Plant-Bot Has Run for 6 Months Without a Battery Change
A practical guide to building solar-powered IoT devices that actually work - including the mistakes that almost killed my first attempt

Solar-Powered IoT: How Plant-Bot Has Run for 6 Months Without a Battery Change
There's something deeply satisfying about deploying a device and forgetting about it. Not because you're lazy (okay, maybe a little), but because it just works. No battery swaps. No charging. No maintenance.
Plant-Bot has been monitoring my garden since August. Through Seattle's famously cloudy winters, through week-long rain stretches, through that weird cold snap in December. It's still going. No intervention needed.
This post is how I got there - including the firmware bug that almost drained the battery on day one.
What We're Building
Plant-Bot is a solar-powered plant monitoring system with:
- ESP32-C6 for WiFi 6 and ultra-low power sleep
- AHT20 temperature and humidity sensor
- Capacitive soil moisture sensor (resistive ones corrode)
- 6V 1W solar panel (surprisingly small)
- 2000mAh LiPo battery (about 3 days of cloudy reserve)
- 1-hour sleep cycles for data collection
Total cost: about $20 in components. It's been running for 6 months.
The Promise (and Reality) of Solar IoT
Let me be honest: solar-powered IoT is not plug-and-play. There are real engineering challenges:
- Energy harvesting is inconsistent: A cloudy week in December generates 10% of what a sunny week in July does
- Battery capacity degrades over time: Your safety margins shrink
- Temperature affects everything: Both solar efficiency and battery capacity
But here's the thing: once you solve these problems, you have a device that can run indefinitely. No more planning battery replacements for sensors in hard-to-reach places. No more guilt about disposable batteries.
Worth the effort? For outdoor sensors, absolutely.
Solar Power: The Engineering Reality
How Much Power Does a Solar Panel Actually Generate?
Marketing says my panel is "1W." Reality is more complicated.
Peak power (1W) happens under ideal conditions:
- Direct sunlight
- 1000 W/m² irradiance
- Panel facing the sun at optimal angle
- 25°C cell temperature
Actual power depends on:
- Weather (obviously)
- Panel orientation
- Time of day and season
- Shading (even partial shading kills output)
Here's what I actually measured in Seattle:
| Condition | Power Output |
|---|---|
| Direct summer sun | 850mW |
| Overcast summer | 200mW |
| Direct winter sun | 600mW |
| Overcast winter | 50mW |
| Heavy rain | 20mW |
The takeaway: design for your worst case, not your best case.
Energy Budget Math That Actually Works
Let me walk through the real calculation for Plant-Bot.
Device consumption:
Deep sleep: 8µA
Wake cycle: 120mA for 45 seconds (15s sensors + 30s WiFi)
Cycles per day: 24
Average current: 8µA × 0.9875 + 120mA × 0.0125 = 1.51mA
Daily energy: 1.51mA × 24h × 3.7V = 134mWh
Solar generation (worst case - rainy Seattle December):
Usable daylight: 8 hours
Average generation: 50mW (heavy overcast)
Daily energy: 50mW × 8h = 400mWh
Safety margin: 400mWh / 134mWh = 3x
That's tight but workable. On sunny days, I have 15-30x margin, which charges the battery back up.
Battery reserve:
Battery: 2000mAh × 3.7V = 7.4Wh = 7400mWh
Reserve days: 7400mWh / 134mWh = 55 days (theoretical)
Practical reserve: 3-5 days (don't drain below 20%)
The Reality Check
After 6 months of operation, here's what actually happened:
- Summer (June-August): Battery stayed 95-100%. Basically always full.
- Fall (Sept-October): Battery 85-95%. Starting to draw down on cloudy stretches.
- Winter (Nov-February): Battery fluctuated 60-85%. Got down to 52% during an 8-day overcast stretch.
- No missed readings: 100% uptime through all conditions.
The margins held. But they were margins, not guarantees.
Component Selection: What Actually Matters
Solar Panel: Bigger is Usually Better
For ESP32-based IoT, here's my sizing guide:
| Device Power | Minimum Panel | Recommended | Overkill |
|---|---|---|---|
| <1mA average | 5V 0.5W | 6V 1W | >2W |
| 1-5mA average | 6V 1W | 6V 2W | >3W |
| >5mA average | 6V 2W+ | Consider wired power | - |
I chose 6V 1W for Plant-Bot because:
- Small footprint (110mm × 60mm)
- Sufficient for Seattle weather with margin
- 6V output works well with LiPo charging ICs
If I were doing it again for a cloudier location, I'd go 2W without hesitation. The extra $3 isn't worth the stress.
Battery: Capacity vs Size Tradeoff
LiPo vs LiFePO4:
- LiPo (3.7V): Higher energy density, more cycles before degradation concerns
- LiFePO4 (3.2V): Longer cycle life, better temperature tolerance, slightly lower density
I went with LiPo for the energy density. For a 10-year deployment, I'd consider LiFePO4.
Capacity calculation:
Target reserve: 5 cloudy days
Daily consumption: 134mWh
Required capacity: 134mWh × 5 days = 670mWh
At 3.7V: 670mWh / 3.7V = 181mAh minimum
I used 2000mAh for 10x margin.
The extra capacity costs about $3 more and gives me peace of mind. Worth it.
Charge Controller: Don't Skip MPPT
For solar charging, you have two main options:
Linear charger (TP4056):
- Simple and cheap (under $1)
- ~65-70% efficiency
- Wastes 30-35% of your solar harvest
MPPT charger (CN3065 or similar):
- Slightly more complex (~$2)
- ~85-90% efficiency
- Gets 20-30% more energy from same panel
I use the CN3065 for MPPT. The 20-30% efficiency gain means the difference between "barely enough" and "comfortable margins" during winter.
Hardware Design: Lessons from Three Revisions
Rev 1: The Catastrophic Firmware Bug
My first Plant-Bot prototype had a subtle but deadly firmware bug. During WiFi connection failures, it would retry indefinitely instead of timing out and sleeping.
First cloudy day: WiFi was flaky. Device stayed awake for 6 hours trying to connect. Battery went from 100% to 40%.
The fix:
// ALWAYS set a connection timeout
WiFi.begin(ssid, password);
unsigned long startTime = millis();
while (WiFi.status() != WL_CONNECTED) {
if (millis() - startTime > 30000) { // 30 second timeout
// Give up and sleep
esp_sleep_enable_timer_wakeup(3600 * 1000000ULL);
esp_deep_sleep_start();
}
delay(100);
}Rule: Every network operation needs a timeout. No exceptions.
Rev 2: The Soil Sensor Corrosion Problem
Resistive soil moisture sensors use exposed metal electrodes. After 2 months in soil, they corrode and stop working.
The fix: Capacitive soil moisture sensors. They measure moisture through the PCB material without exposed metal. My Rev 2 sensor has been in soil for 6 months with no degradation.
Rev 3: The Current Design
What I landed on:
Solar Panel (6V 1W)
↓
CN3065 Charge Controller (MPPT)
↓
LiPo Battery (3.7V 2000mAh)
↓
AP2112 LDO (3.3V, 55µA quiescent)
↓
ESP32-C6 + Sensors
Key design decisions:
- Power gating for soil sensor: GPIO controls a P-channel MOSFET to cut sensor power
- Battery voltage monitoring: Voltage divider to ADC for state-of-charge estimation
- Low-quiescent LDO: AP2112 at 55µA is acceptable; MCP1700 at 1.6µA would be better
- No reverse polarity protection: It's permanently wired, not worth the voltage drop
Firmware: The Power Strategy
The Main Loop
Plant-Bot's firmware is deliberately simple:
void setup() {
// Check why we woke up
esp_sleep_wakeup_cause_t reason = esp_sleep_get_wakeup_cause();
// Read battery voltage first
float batteryVoltage = readBatteryVoltage();
// Adaptive behavior based on battery
if (batteryVoltage < 3.3) {
// Critical - sleep longer to allow charging
goToSleep(6 * 3600); // 6 hours
return;
}
// Power on sensors
digitalWrite(SENSOR_POWER_PIN, HIGH);
delay(100); // Stabilization
// Read sensors quickly
SensorData data = readAllSensors();
// Power off sensors immediately
digitalWrite(SENSOR_POWER_PIN, LOW);
// Try to upload (with timeout)
bool uploaded = uploadData(data, 30000); // 30s max
// Determine next sleep duration
int sleepHours = calculateSleepDuration(batteryVoltage, uploaded);
goToSleep(sleepHours * 3600);
}
void loop() {
// Never reached - we always sleep from setup()
}Adaptive Sleep Duration
When battery is low, extend sleep time to allow recovery:
int calculateSleepDuration(float voltage, bool uploadSuccess) {
// Base: 1 hour
int hours = 1;
// Low battery: sleep longer
if (voltage < 3.5) hours = 2;
if (voltage < 3.4) hours = 4;
if (voltage < 3.3) hours = 6;
// Failed upload: don't keep retrying
if (!uploadSuccess) hours = max(hours, 2);
// Nighttime: sleep longer (plant data doesn't change much at night)
// This would require RTC, which I didn't include
return hours;
}WiFi 6 Target Wake Time (TWT)
The ESP32-C6 supports WiFi 6's Target Wake Time feature, which coordinates sleep schedules with the router. On Plant-Bot, this reduced WiFi power consumption by about 15%.
// Enable WiFi 6 power save features
wifi_config_t wifi_config;
wifi_config.sta.listen_interval = 3;
esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
// TWT is negotiated automatically with compatible routersNote: TWT only works if your router supports WiFi 6. On older routers, the ESP32-C6 falls back to standard power save modes.
Enclosure: Surviving the Elements
An outdoor IoT device needs protection. Here's what works:
Enclosure requirements:
- IP65 or better waterproof rating
- UV-resistant materials (or paint it)
- Ventilation holes with membrane (prevents condensation)
- Cable glands for wire entry
Solar panel mounting:
- South-facing (northern hemisphere)
- 30-45° angle from horizontal
- Clear of shadows (even partial shading kills output)
- Secure against wind (mine is bolted to a fence post)
Temperature considerations:
- Avoid direct sun on the electronics box (separate from panel if needed)
- Black enclosures get hot; white or reflective is better
- LiPo batteries don't like extreme temperatures
I used a $10 IP65 junction box from Amazon, a $2 cable gland, and a 3D-printed bracket for the solar panel. Total enclosure cost: under $15.
Testing Before Deployment
Don't deploy a solar device without testing:
- Measure sleep current: Should be <50µA. If it's higher, find the leak.
- Verify wake cycle energy: Complete cycles, not just averages.
- Test WiFi timeout: Unplug your router and confirm the device sleeps.
- Validate charging: Measure charge current under various light conditions.
- Run it for a week indoors: Catch bugs before they're in your garden.
I ran Plant-Bot on my desk for 2 weeks before deployment. Found and fixed three bugs that would have caused problems in the field.
Results: 6 Months of Operation
What went well:
- 100% uptime (no missed readings)
- Battery never dropped below 52%
- WiFi 6 TWT measurably improved power efficiency
- Capacitive soil sensor shows no degradation
What I'd change:
- Larger solar panel (2W instead of 1W) for more winter margin
- Add RTC for time-aware sleep scheduling
- Better waterproofing on soil sensor cable entry (some moisture got in)
- Consider LoRa backup for WiFi failures
Actual maintenance performed:
- Cleaned bird droppings off solar panel once (September)
- That's it
The Bill of Materials
| Component | Cost | Notes |
|---|---|---|
| ESP32-C6-DevKitC-1 | $8 | Or design your own PCB |
| 6V 1W Solar Panel | $5 | Monocrystalline preferred |
| CN3065 Charge Module | $2 | MPPT charging |
| 2000mAh LiPo | $8 | With protection circuit |
| AHT20 Sensor | $2 | I2C, low power |
| Capacitive Soil Sensor | $2 | NOT resistive |
| IP65 Enclosure | $10 | Plus cable glands |
| Misc (wires, connectors) | $3 | |
| Total | ~$40 | Or $24.99 assembled from my store |
Get Your Own
Don't want to build it yourself? I sell assembled Plant-Bots in my store for $24.99, including solar panel, battery, and documentation.
All hardware and firmware are open source: github.com/tooyipjee/plantbot2
The Bottom Line
Solar-powered IoT is absolutely viable for low-power applications. The key principles:
- Design for worst case: Size your panel for cloudy weeks, not sunny days
- Build in battery reserve: 3-5 days minimum
- Sleep aggressively: Every µA matters
- Timeout everything: Network failures will happen
- Test before deploying: Catch bugs on your desk, not in the field
Plant-Bot has been running for 6 months with zero maintenance. That's the goal. That's achievable.
Building your own solar IoT project? Share your energy budget in the contact form and I'll help you check the math.