A closed-loop edge-computing system for real-time environmental monitoring and AI-assisted energy-saving control. Designed for competition demos and engineering-grade reliability — the system stays stable even when the AI goes down.
Core philosophy: AI is the advisor, not the executor. Rule-based controllers with hysteresis, anti-short-cycle protection, and a self-healing orchestrator ensure safe operation regardless of cloud connectivity.
| Layer | Role |
|---|---|
| Physical | ESP32-S3 collects environmental data (temp, humidity, light, air quality, power) via sensors + INA219 energy metering; relays & actuators close the control loop |
| Edge (Python) | Rule-based control (time-of-use setpoints, deadband, min-run/min-stop), AI advisor (DeepSeek/OpenAI-compatible with candidate pool, circuit breaker, local fallback), self-healing orchestrator (I2C recovery, MCU reset, rate limiting) |
| Dashboard | React web UI + Streamlit dashboard; real-time curves, baseline-vs-saving energy comparison, AI candidate metrics, comfort scores |
- Python 3.11+
- Node.js 18+ (for the React frontend)
- (Optional) ESP32-S3 with sensors for hardware mode
# 1. Install Python dependencies (core: API / firmware tooling / tests)
pip install -r requirements.txt
# ...and, only if you want the Streamlit dashboard below (adds streamlit/pandas/matplotlib):
# pip install -r requirements-dashboard.txt
# 2. Run simulation comparison (baseline vs energy-saving)
python -m energy_system.simulation.compare
# 3. Launch the Streamlit dashboard
streamlit run dashboard.py# Terminal 1: Start the Python API server
python api_server.py --port 8080
# Terminal 2: Start the React frontend
cd app
npm install
npm run devOpen http://localhost:3000 — the frontend auto-connects to the backend API, and falls back to simulated data if the backend is down.
# 1. Configure params.yaml with your serial port and settings
# 2. Run the main control loop
python main.py
# 3. Start the dashboard (in a separate terminal)
streamlit run dashboard.pyecosentinel/
├── main.py # Hardware-mode main control loop
├── api_server.py # FastAPI backend for React frontend
├── dashboard.py # Streamlit dashboard entry
│
├── energy_system/ # Core Python modules
│ ├── algorithms/ # AI advisor, comfort evaluation, energy savings
│ ├── config/ # YAML config loader, runtime settings
│ ├── core/ # Rule-based controller, thermal model, logging
│ ├── hardware/ # Serial bridge, MQTT client, mock drivers
│ ├── power/ # Energy accounting (Wh/kWh integration)
│ ├── resilience/ # Self-healing orchestrator (stale detection → recovery)
│ ├── simulation/ # Digital twin simulator + data generator
│ └── utils/ # Helpers, env loader, file I/O, process manager
│
├── dashboard/ # Streamlit dashboard modules
│ ├── realtime_view.py # Real-time sensor curves
│ ├── energy_view.py # Baseline vs saving energy comparison
│ ├── simulation_view.py # Digital twin visualization
│ └── ...
│
├── app/ # React (TypeScript + Tailwind) frontend
│ ├── src/
│ │ ├── components/ # UI components (charts, metric cards, controls)
│ │ ├── pages/ # Dashboard, Realtime, Energy, Simulation, AI pages
│ │ └── services/ # API client
│ └── package.json
│
├── firmware/ # ESP32-S3 Arduino firmware
│ └── esp32_s3_competition/
│
├── scripts/ # Utility scripts (DeepSeek test, serial tools)
├── tests/ # pytest test suite
├── docs/ # Hardware guide & supplementary docs
├── logs/ # Runtime logs (gitignored)
├── LICENSE # MIT
└── CITATION.cff # Citation metadata
- Time-of-use setpoints — different temperature/lighting targets per time-of-day
- Hysteresis + deadband — ±0.5°C deadband prevents chattering near setpoints
- Anti-short-cycle — minimum 15-min run time, minimum 10-min stop time for compressor protection
- State-aware actuation — commands are only sent when relay state actually changes
- Suggestion mode (default) — AI proposes actions; the rule controller decides
- Candidate pool — multiple API configs with exploration/exploitation + EMA scoring
- Circuit breaker — consecutive failures auto-skip a candidate; half-open probing for recovery
- Local fallback — when the cloud is unreachable, heuristic strategies take over (ventilation, curtain control based on eCO₂/TVOC/temperature)
- Compatible with DeepSeek and any OpenAI-compatible API
- Stale data detection — triggers safe mode after configurable timeout
- Auto-recovery actions — I2C bus recovery, MCU soft reset (with cooldown + rate limiting)
- Audit trail — every self-healing action is logged to
logs/incidents_<label>.jsonl
- INA219 energy metering — integrates power over time → Wh/kWh
- Baseline vs Saving comparison — run two labels, dashboard shows savings rate
- Comfort score —
⚠️ retracted claim: this used to say "proving savings aren't from sacrificing comfort". That is not supported. The unified 0–1 score barely moves (baseline 0.493 → saving 0.491), but that is because its temperature term is clamped to 0 outside the 24–28 °C band in both modes while the humidity term (~0.956) carries the score. Under the IPMVP-native metric — percentage of time inside the comfort band [23, 26] °C — the picture reverses: baseline 41.7 % → saving 20.6 %, i.e. the saving strategy halves the time spent in the comfort band. Both numbers are pinned bytests/unit/test_comfort_band.py. Band source: ISO 7730 (PMV ∈ ±0.5 ⇒ ≈23–26.5 °C) and Sinergym (range_comfort_summer = (23.0, 26.0)), which agree.
⚠️ This model has no thermal inertia (established in the second teammate audit, verified here):τ = C_AIR/(U_WALL·A_WALL) = 25000/1800 ≈ 13.9 swhile the step isdt = 300 s, soexp(−dt/τ) = 4.16e-10— every step lands on that step's steady state and the room's heat storage is effectively absent. Consequences: the time-of-day setpoint schedule (including the 07–09 pre-cooling) cannot store coolth, so any claim that depends on when energy is used is not representable in this model; the savings figure therefore reflects a per-step decision, not a scheduling strategy. The parameters are left unchanged on purpose — co-calibrating them (UA ≥ 600 W/Kfor the AC to have authority, andC_AIR = τ·UAfor a 1–3 h time constant) requires measured data; seedocs/hardware-runbook.mdandscripts/identify_rc.py. Until then this limitation must be stated wherever the 17.6% is quoted.
Benchmarking study: report-benchmarks.md (BOPTEST / Beobench / FlexDRL / rl-testbed). BOPTEST is the only
one worth learning from, and three of its decisions are now implemented — each with a gate and a mutation test:
| KPI | 口径 | 出处 |
|---|---|---|
| energy | kWh over the run | — |
| comfort band share | % of time inside [23, 26] °C | IPMVP Core Concepts ("percentage of time in comfort band") + Sinergym range_comfort_summer; teammate audit |
| comfort violation integral | out-of-band amount integrated over time (K·h; humidity %·h; illuminance lx·h), split into three components | BOPTEST kpis/kpi_calculator.py:237-287 (slack integral); component split from rl-testbed reward decomposition (:242) |
| peak power | 15-minute window mean, then max (plus the raw single-sample max, reported side by side) | BOPTEST kpi_calculator.py:404-406 — a single-sample max at dt=300 s is a solver artefact, not a grid event |
area_m2, code_version, short_cycle_blocks |
reporting only | area: BOPTEST normalisation; code_version: Beobench version gate idea; blocks: rl-testbed F2 (countable hard constraints) |
Three measured results these KPIs exist to expose (3 days, seed 42 — all reproducible via
scripts/control_groups.py and the gates):
- "Saving energy" is not an achievement on its own.
do_nothinguses 13.05 kWh vs the controller's 199.10 kWh (−93.4 %) — but its mean room temperature is 29.91 °C. Any savings claim must be stated together with the comfort constraint, otherwise "just switch the AC off" beats it. - The saving strategy is worse than doing nothing on the IPMVP metric: band share 20.6 % vs 25.6 %, and its out-of-band integral is 141.88 K·h vs 95.03 K·h for the fixed-setpoint baseline (+49 %). The old 0–1 score hid this (0.493 → 0.491) partly because its humidity component is identical in all three modes (248.44 %·h) — none of them controls humidity, so that term dilutes any temperature signal.
- The anti-short-cycle logic works, and its counter explains the above: the fixed-setpoint baseline is blocked 111 times in 3 days, while the saving strategy is blocked 0 times — not because it is efficient, but because it barely actuates (its 26–28 °C setpoints sit far above the achievable ~27.3 °C). Its comfort cost is inaction, not a trade-off it chose.
- Dual-channel metering (optional) — measure both load consumption + solar input simultaneously
⚠️ Simulation numbers are not field measurements. With the current (uncalibrated, demo-grade) building parameters, the deterministic 3-day simulation reports baseline 199.10 kWh vs saving 164.01 kWh → 17.6% savings (comfort score 49.3% vs 49.1%). Earlier revisions of this repo quoted 29.8% / 82.1%: those came from a numerically diverging thermal model (explicit Euler at dt=300 s, above the 2τ≈27.8 s stability limit) and are retracted. The savings claim still needs real-hardware, alternating baseline/saving runs before it can be compared with literature values.
Two estimators, not one. The 17.6% above is a paired comparison (run baseline 3 days, then saving 3 days). A second, IPMVP-Option-C-style estimator lives in
energy_system/simulation/aso_experiment.py: it alternates the strategy inside one continuous run (ASO) and extrapolates a outdoor-temperature regression baseline to the report periods — for 6 days at seed 42 it reports 19.12% ± 4.21 kWh (95%),R²=0.82. The two numbers are different estimators and must not be averaged or mixed; each must be quoted with its own method and caveat (seedocs/refactor-baseline.md).
Every sample written to JSONL includes:
- Energy:
power_w,energy_wh,solar_power_w,solar_energy_wh - Comfort:
comfort_score+ raw sensor fields - AI:
ai_source,ai_candidate_id,ai_latency_ms,ai_score,ai_cloud_error - Safety:
safe_mode,safe_reason,resilience.*
All runtime parameters live in energy_system/config/params.yaml:
| Section | Key settings |
|---|---|
app |
use_hardware (true/false), run_label (baseline/saving) |
serial |
port, baudrate, timeout_s |
ai |
enabled, control_mode (suggest/execute), model, api_base, candidate pool |
control |
enable_rule_control, safe_mode_stale_s, actuate_min_interval_s |
self_healing |
enabled, auto_i2c_recover, auto_reset, cooldown settings |
API keys are NEVER stored in the YAML config. Inject them via:
- Environment variable:
DEEPSEEK_API_KEY,OPENAI_API_KEY, orAI_API_KEY - Local
.envfile (gitignored — see.env.example)
- Run
baseline(comfort-first, no energy saving) for 30–60 seconds - Run
saving(energy-saving with comfort floor) for 30–60 seconds - Open the dashboard: compare power curves + energy savings rate, verify
comfort_scorehasn't collapsed - Bonus: shine a flashlight on BH1750 or block the sensor — watch adaptive lighting/curtain strategy change in real time
pytest tests/ -qSee docs/hardware-guide.md for the complete hardware guide: BOM (budget vs. competition-grade), wiring diagrams, pin assignments, firmware upload instructions, and serial protocol reference.
If you use EcoSentinel in your research, please cite:
@software{ecosentinel2026,
author = {Yifan Wu},
title = {EcoSentinel: Smart Environmental Monitoring \& AI Energy-Saving System},
year = 2026,
version = {1.0.0},
url = {https://github.com/Atlas-did/ecosentinel},
license = {MIT},
}GitHub will also display a "Cite this repository" button thanks to CITATION.cff.
MIT — see LICENSE for details.