The Problem
The Stack

Voltage ≤ 47.0V → Grid ON
Voltage ≥ 48.2V → Grid OFF
SoC ≤ 5% → Grid ON
SoC ≥ 10% → Grid OFF
No load awareness. No compound conditions. No sustained-condition timers. Just raw thresholds triggering instant action.
Digging Into 30 Days of Data
Load Power Distribution
Percentile | Power (W) | What's Happening |
|---|---|---|
| P10 | 416 | Baseline — lights, standby, fridge |
| P25 | 698 | Light usage |
| P50 (Median) | 889 | Typical household |
| P75 | 1,358 | Active usage — cooking, some AC |
| P90 | 1,917 | Heavy usage — heaters & multiple AC |
| P95 | 2,373 | Peak usage |
| P99 | 3,478 | Extreme peak |
| MAX | 7,722 | Surge — all appliances simultaneously |
Grid Relay Events: The Smoking Gun

Duration | Count | % of Events | Assessment |
|---|---|---|---|
| ≤ 20 min | 7 | 37% | ❌ Unnecessary — transient load |
| 60–120 min | 5 | 26% | ⚠️ Borderline |
| 120–300 min | 4 | 21% | ✅ Justified |
| > 300 min (5h+) | 3 | 16% | ✅ Clearly needed |
The Apr 12 Event: A Perfect Case Study
Time (WIB) | Voltage | Load | SoC | Grid |
|---|---|---|---|---|
| 07:00 | 48.07V | ~600W | 30.4% | OFF |
| 07:25 | 47.67V | 2,009W | 30.2% | OFF |
| 07:30 | 47.60V → 46.97V spike | 2,041W | 30.0% | OFF |
| 07:40 | 47.49V | 1,849W | 29.7% | ON (triggered!) |
| 08:00 | 48.06V | 1,855W | 30.1% | OFF (recovered) |
The Solution: Three-Tier Hysteresis Engine

Tier 1: Critical Safety Floor
Javascript
// IMMEDIATE, unconditional — protects against deep discharge
if (voltage <= 45.5 || soc <= 3) {
flow.set("grid_state", "on");
msg.gridcmd = 1;
msg.tier = 1;
msg.gridsrc = "CRITICAL";
return msg;
}
Tier 2: Intelligent Activation
Javascript
// Three conditions must ALL be true:
const loadCondition = avgLoad > 1400; // Above P75 — genuinely high
const socCondition = soc < 30; // Battery actually depleted
const sustained = (now - sagStart) >= 90000; // Not a transient spike
if (voltage <= 47.0 && loadCondition && socCondition && sustained) {
// NOW activate the grid
}
- 1,400W power threshold: Just above P75 (1,358W). Anything above this means multiple appliances are running — not a brief spike.
- 30% SoC gate: My battery's usable capacity is 70% (displayed = raw − 30%). So displayed 30% = ~51% actual. At this level, the battery handles 2kW loads for hours. Below 30% = genuine concern.
- 90-second sustained timer: Most transient spikes (water heater on, kettle boils) resolve within 30–60 seconds. 90 seconds filters these out while still being responsive to real emergencies.
Tier 3: Graceful Release
Javascript
// All three must hold for 3 continuous minutes:
const voltOk = voltage >= 48.2; // Battery recovered
const loadOk = avgLoad < 1200; // Load has settled
const socOk = soc >= 30; // SoC is comfortable
if (voltOk && loadOk && socOk && heldFor180s) {
flow.set("grid_state", "off");
}
The 15-Minute Rolling Average
Javascript
const WINDOW = 900;
let buffer = flow.get("load_buffer") || [];
let bufIdx = flow.get("load_buf_idx") || 0;
if (buffer.length < WINDOW) {
buffer.push(load);
} else {
buffer[bufIdx % WINDOW] = load;
}
bufIdx++;
const avg = buffer.reduce((a, b) => a + b, 0) / buffer.length;
flow.set("avg_load_15m", avg);
Bugs Found During Deployment
Bug 1: Grid Stuck ON After Restart
Bug 2: SoC Paths Bypassed New Logic
Would It Have Worked? Backtesting the Logic
Event | Duration | SoC | Est. Avg Load | New Logic |
|---|---|---|---|---|
| Apr 12 (46.97V spike) | 20 min | 30% | ~2000W | ✅ Prevented — SoC ≥ 30% |
| Apr 6 #1 (oscillation) | 10 min | ~35% | ~1200W | ✅ Prevented — SoC ≥ 30% |
| Apr 6 #2 (oscillation) | 10 min | ~35% | ~1200W | ✅ Prevented — SoC ≥ 30% |
| Apr 1 (short trigger) | 20 min | ~12% | ~900W | ✅ Prevented — Load < 1400W |
| Mar 27 (short trigger) | 20 min | ~15% | ~800W | ✅ Prevented — Load < 1400W |
| Apr 8 (genuine, 8.2h) | 490 min | ~2% | ~1800W | ✅ Correctly activates |
Results
Metric | Before | After |
|---|---|---|
| Decision Logic | Single voltage threshold | Three-tier compound engine |
| Inputs Considered | Voltage only | Voltage + Load (15m avg) + SoC + Time |
| Expected Monthly Activations | ~19 | ≤ 10 (justified only) |
| False Triggers (≤20 min) | 7 (37%) | ~0 |
| Oscillation Events | Observed (Apr 6) | Prevented by holdoff timers |
| Safety Floor | None below 47.0V | Tier 1 at 45.5V (unconditional) |
| Startup Handling | Unsafe (stale context) | Relay sync + periodic check |
Key Takeaways
This is Part 3 of my home IoT series. Part 1: From Polling to Push: Real-Time Tuya Monitoring. Part 2: DHCP Lease Table Discovery. All optimizations are part of my ongoing Solar Home System project.