Schematik build

UV Weather Alert Station

Schematik

Published July 31, 2026 · Updated July 31, 2026

ESP32Intermediate60 minutes3 components6 assembly steps
Remix this project
Photo of UV Weather Alert Station

This UV Weather Alert Station combines temperature, humidity, and pressure monitoring with real-time UV index detection to help users make informed decisions about sun protection. Built around an ESP32 microcontroller, the station uses a BME280 environmental sensor for atmospheric data and a GUVA-S12SD UV sensor module to track UV exposure levels, all powered by a compact LiPo battery for portable deployment.

The guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions to get the station operational. Firmware is included with configurable UV alert thresholds and Telegram notification support, plus a built-in web dashboard for real-time monitoring. After assembly and deployment, users can customize alert settings and track environmental conditions from any connected device.

Wiring diagram

Interactive · read-only
Wiring diagram for UV Weather Alert Station

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
BME280I2C, 3.3 V1Bosch BME280 environmental sensor on the exact Adafruit 2652 breakout. Supports I2C via SCK/SCL and SDI/SDA, optional SDO address select, and SPI pins when needed.
GUVA-S12SD UV sensor moduleAnalog UV1Analog ultraviolet photodiode sensor module. Its analog output is sampled at 3.3 V logic to estimate UV index.
LiPo 3.7V 1000mAh Battery3.7 V, 1000 mAh1Single-cell LiPo pack, nominal 3.7 V, 1000 mAh. Default rechargeable choice for portable ESP32 / Pico projects. Pair with a TP4056 charger for safe USB recharging.

Assembly

6 steps
  1. Disconnect USB before fitting the battery

    Unplug the XIAO ESP32-C6 from USB before connecting the LiPo. Check that the battery is a protected, single-cell 3.7 V LiPo with the connector and polarity intended for the XIAO battery input.

    • Tip: A 1000 mAh cell is a practical starting size for this weather station.
    • Tip: Never force a battery plug: verify positive-to-positive and negative-to-negative before connection.
    • Do not use a 2-cell (7.4 V nominal) battery, a 9 V battery, or an unprotected loose LiPo cell.
    • Never short, crush, pierce, or charge a swollen/damaged LiPo.
  2. Connect the LiPo to the XIAO battery input

    Connect LiPo positive (+V) to the XIAO ESP32-C6 BAT+ battery contact/JST battery connector positive, and LiPo negative (GND) to BAT- / connector negative. The LiPo powers the board through its built-in battery circuitry.

    • Tip: If your LiPo has a compatible 2-pin JST plug, use the board’s battery connector rather than loose wires.
    • Tip: USB can remain connected after the battery is attached for normal operation and charging.
    • Confirm connector polarity with the markings or a multimeter; JST-style plugs are not universally wired the same.
    • Do not connect the LiPo to the XIAO 3V3 pin or to a sensor power rail.
  3. Wire the BME280 over I²C

    Connect BME280 VCC to XIAO 3V3 and BME280 GND to XIAO GND. Connect BME280 SDA to XIAO D4 (GPIO23) and BME280 SCL to XIAO D5 (GPIO22).

    • Tip: Most BME280 I²C boards use address 0x76; the firmware also tries 0x77 automatically.
    • Tip: Place the BME280 in shade with airflow, away from heat produced by the XIAO.
    • Use a BME280 breakout that works with 3.3 V I²C logic.
  4. Wire the GUVA-S12SD analog UV module

    Connect GUVA-S12SD VCC to XIAO 3V3, GND to XIAO GND, and OUT to XIAO D0 (GPIO1).

    • Tip: Mount its optical window upward and clear of shade.
    • Tip: Compare its approximate UV reading against a trusted local UV report and tune the dashboard alert threshold if needed.
    • Power this module from 3.3 V only: a 5 V-powered module can drive OUT above the ESP32-C6 ADC safe voltage.
  5. Place and protect the station

    Install the board, LiPo, and sensors in a weather-resistant ventilated enclosure. Keep the battery dry, secured so it cannot move, and separated from direct sun or heat. Leave the UV window exposed and keep the BME280 shielded from rain and direct solar heating while allowing airflow.

    • Tip: A radiation shield and small downward-facing ventilation openings improve environmental measurements.
    • Tip: The approximate 8-hour battery estimate depends strongly on Wi-Fi signal strength and dashboard use.
    • Do not charge or operate a LiPo in a sealed, hot enclosure or direct summer sun.
    • Inspect the pack periodically and discontinue use if it becomes swollen, hot, or damaged.
  6. Configure the dashboard after deployment

    Use Schematik’s Deploy button with USB connected. Then join the XIAO-Weather-Setup Wi-Fi network and open http://192.168.4.1 to enter home Wi-Fi, the Telegram bot token, chat ID, and dangerous UV threshold.

    • Tip: USB power charges the directly connected battery through the XIAO’s onboard charging circuit.
    • Tip: The alert sends when UV crosses the selected threshold and is limited to once per hour while it remains high.
    • Treat the Telegram bot token as a password and do not share it.

Pin assignments

Board wiring reference
PinConnectionType
3V3bme280_1 VCCpower
GNDbme280_1 GNDground
GPIO 23bme280_1 SDAi2c
GPIO 22bme280_1 SCLi2c
3V3guva_s12sd_1 VCCpower
GNDguva_s12sd_1 GNDground
GPIO 1guva_s12sd_1 OUTanalog
EXTlipo_1 +VXIAO ESP32-C6 BAT+ pin / JST battery connector positivepower
EXTlipo_1 GNDXIAO ESP32-C6 BAT- pin / JST battery connector negativeground

Firmware

ESP32
main.cppDeploy to device
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <Wire.h>
#include <Preferences.h>
#include <Adafruit_BME280.h>


// Forward declarations
String enc(const String &s);
String advice();
String amount();
bool telegramReady();
bool telegram(const String &text);
void sample();
String field(const char *name,const String &value,const char *type);
String page();
void api();
void save();

const int UV_PIN = 1;
const int I2C_SDA_PIN = 23;
const int I2C_SCL_PIN = 22;
const unsigned long SAMPLE_MS = 5000;
const unsigned long ALERT_COOLDOWN_MS = 3600000UL;

WebServer server(80);
Preferences prefs;
Adafruit_BME280 bme;
bool bmeOK = false, uvWasHigh = false;
float tempC = NAN, humidity = NAN, pressure = NAN, uv = NAN, threshold = 6.0;
unsigned long lastSample = 0, lastAlert = 0;

String enc(const String &s) { const char *h="0123456789ABCDEF"; String r; for(size_t i=0;i<s.length();i++){uint8_t c=s[i]; if(isalnum(c)||c=='-'||c=='_'||c=='.'||c=='~')r+=(char)c;else{r+='%';r+=h[c>>4];r+=h[c&15];}} return r; }
String advice() {
  if (!isfinite(uv)) return "UV reading unavailable.";
  if (uv < 3) return "Low UV: sunscreen is optional for brief exposure; use SPF 30+ for prolonged outdoor time.";
  if (uv < 6) return "Moderate UV: apply broad-spectrum, water-resistant SPF 30+ to exposed skin.";
  if (uv < 8) return "High UV: apply broad-spectrum, water-resistant SPF 30+ (SPF 50+ for sensitive skin), plus hat and sunglasses.";
  if (uv < 11) return "Very high UV: use SPF 50+, protective clothing and shade; limit long midday exposure.";
  return "Extreme UV: avoid direct midday sun if possible; use SPF 50+, protective clothing, a hat, sunglasses and shade.";
}
String amount() { return "Apply generously: roughly 30 mL (one shot-glass) for an adult full body, 15 minutes before sun; reapply every 2 hours and after swimming or heavy sweating."; }
bool telegramReady() { return prefs.getString("token", "").length() > 10 && prefs.getString("chat", "").length() > 0; }
bool telegram(const String &text) {
  if (WiFi.status()!=WL_CONNECTED || !telegramReady()) return false;
  WiFiClientSecure c; c.setInsecure();
  HTTPClient http;
  String url="https://api.telegram.org/bot"+prefs.getString("token","")+"/sendMessage";
  if(!http.begin(c,url)) return false;
  http.addHeader("Content-Type","application/x-www-form-urlencoded");
  int rc=http.POST("chat_id="+enc(prefs.getString("chat",""))+"&text="+enc(text)); http.end(); return rc==200;
}
void sample() {
  uint32_t raw=analogRead(UV_PIN);
  float volts=raw*3.3f/4095.0f;
  uv=volts/0.1f;
  if(bmeOK){tempC=bme.readTemperature();humidity=bme.readHumidity();pressure=bme.readPressure()/100.0f;}
  bool high=isfinite(uv)&&uv>=threshold; unsigned long now=millis();
  if(high && (!uvWasHigh || now-lastAlert>=ALERT_COOLDOWN_MS)) {
    if(telegram("UV ALERT\nUV Index: "+String(uv,1)+" (threshold "+String(threshold,1)+")\n"+advice()+"\n"+amount())) lastAlert=now;
  }
  uvWasHigh=high;
}
String field(const char *name,const String &value,const char *type="text") { return String("<label>")+name+"</label><input name='"+name+"' type='"+type+"' value='"+value+"'>"; }
String page() {
  String addr=WiFi.status()==WL_CONNECTED?WiFi.localIP().toString():"Join XIAO-Weather-Setup, then open 192.168.4.1";
  String html = "<!doctype html><html><head><meta name='viewport' content='width=device-width,initial-scale=1'><title>XIAO Weather</title><style>body{font-family:Arial;background:#0b1625;color:#eaf4ff;margin:0;padding:18px}main{max-width:800px;margin:auto}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}.card,form{background:#15263b;border-radius:14px;padding:16px}.value{font-size:2em;color:#67d8ff}input{display:block;width:100%;box-sizing:border-box;padding:10px;margin:5px 0 12px;border-radius:6px;border:0}button{padding:11px 16px;background:#27a6d4;color:white;border:0;border-radius:6px}.note{color:#b9cee0}</style></head><body><main><h1>XIAO Weather Station</h1><p class='note'>Address: ";
  html += addr;
  html += "</p><div class='grid'><div class='card'>Temperature<div class='value' id='t'>--</div></div><div class='card'>Humidity<div class='value' id='h'>--</div></div><div class='card'>Pressure<div class='value' id='p'>--</div></div><div class='card'>UV index<div class='value' id='u'>--</div></div></div><div class='card'><h3>Sun protection guidance</h3><p id='a'>Loading...</p><p class='note'>General public-health guidance, not medical advice.</p></div><h2>Connection and Telegram alert</h2><form method='post' action='/save'>";
  html += field("ssid",prefs.getString("ssid",""));
  html += field("pass","","password");
  html += field("token","","password");
  html += field("chat",prefs.getString("chat",""));
  html += field("threshold",String(threshold,1),"number");
  html += "<button>Save settings</button><p class='note'>Password and token fields may be left blank to keep their saved values. Create a Telegram bot with BotFather, start a chat with it, then enter its token and your numeric chat ID.</p></form></main><script>async function r(){let d=await fetch('/api').then(x=>x.json());t.textContent=d.t+' C';h.textContent=d.h+' %';p.textContent=d.p+' hPa';u.textContent=d.u+(d.high?' !':'');a.textContent=d.a;}r();setInterval(r,5000)</script></body></html>";
  return html;
}
void api(){String j="{\"t\":\""+(isfinite(tempC)?String(tempC,1):"--")+"\",\"h\":\""+(isfinite(humidity)?String(humidity,0):"--")+"\",\"p\":\""+(isfinite(pressure)?String(pressure,0):"--")+"\",\"u\":\""+(isfinite(uv)?String(uv,1):"--")+"\",\"high\":"+String(uv>=threshold?"true":"false")+",\"a\":\""+advice()+" "+amount()+"\"}";server.send(200,"application/json",j);}
void save(){if(server.arg("ssid").length())prefs.putString("ssid",server.arg("ssid"));if(server.arg("pass").length())prefs.putString("pass",server.arg("pass"));if(server.arg("token").length())prefs.putString("token",server.arg("token"));if(server.arg("chat").length())prefs.putString("chat",server.arg("chat"));threshold=constrain(server.arg("threshold").toFloat(),0.0f,20.0f);prefs.putFloat("thr",threshold);server.send(200,"text/html","<meta http-equiv='refresh' content='5;url=/'><h2>Saved. Reconnecting...</h2>");WiFi.disconnect();if(prefs.getString("ssid","").length())WiFi.begin(prefs.getString("ssid","").c_str(),prefs.getString("pass","").c_str());}
void setup(){Serial.begin(115200);prefs.begin("weather",false);threshold=prefs.getFloat("thr",6.0f);analogReadResolution(12);Wire.begin(I2C_SDA_PIN,I2C_SCL_PIN);bmeOK=bme.begin(0x76)||bme.begin(0x77);WiFi.mode(WIFI_AP_STA);WiFi.softAP("XIAO-Weather-Setup");if(prefs.getString("ssid","").length())WiFi.begin(prefs.getString("ssid","").c_str(),prefs.getString("pass","").c_str());server.on("/",[](){server.send(200,"text/html",page());});server.on("/api",api);server.on("/save",HTTP_POST,save);server.begin();sample();}
void loop(){server.handleClient();if(millis()-lastSample>=SAMPLE_MS){lastSample=millis();sample();}}

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

Project files

Shared by the author

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik