Community project
Storm Alert
Storm Alert is a weather monitoring system that uses an ESP32 to fetch real-time weather data and alert conditions via LED indicators. A red LED displays temperature status (cold, mild, or hot), while a blue LED shows precipitation and severe weather warnings. The system connects to Wi-Fi, retrieves weather information from Open-Meteo and the National Weather Service, and can send Telegram notifications when alerts are detected.
This guide provides a complete parts list, wiring diagram, and step-by-step assembly instructions for connecting the LEDs and resistor to the ESP32. You'll also get the full firmware code with configuration options for testing different weather scenarios before deploying the system to monitor live conditions in your area.
Wiring diagram

Gather all the parts
Assemble it in 4 steps
1. Keep the red temperature light in place
Leave the red LED and its 220 Ω resistor exactly as they are. The red LED’s long leg connects through its resistor to the board pin labelled D25, and its short leg beside the flat edge connects to GND.
- The resistor has no direction. The path is D25 → resistor → red LED long leg.
- Do not connect the red LED directly to D25 without its resistor — too much current can damage the LED or the board pin.
2. Place the blue rain light and resistor
Put the blue LED into two different numbered breadboard rows. Put one end of its second 220 Ω resistor into the same connected five-hole row as the blue LED’s long leg. Put the other resistor end into an unused row.
- For example, if the blue LED long leg is in A10, one resistor end can be in B10 because A10 through E10 are connected together.
- Make sure the blue LED’s two legs are not in the same connected breadboard row, or it cannot light.
3. Connect the blue rain light to D26
Run a jumper from the free end of the blue LED’s resistor to the board pin labelled D26. Run another jumper from the blue LED’s short leg, beside its flat edge, to any board pin labelled GND. This makes D26 → resistor → blue LED long leg (rain signal), and blue LED short leg → GND (ground).
- D26 is printed beside D25 on the same header row of your Elegoo board. Both LEDs may share a GND pin.
- Do not swap the blue LED connections: its short leg must go to GND, and its long leg must reach D26 through the 220 Ω resistor.
4. Set Wi-Fi and deploy
In main.cpp, replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own Wi-Fi name and password. Leave TEST_WEATHER_CODE as -1 for live Sutton weather, or temporarily set it to 63 to test rain or 95 to test a thunderstorm. Plug the board in with USB-C and deploy it.
- The red LED stays solid while joining Wi-Fi. The blue LED fades for rain, flashes rapidly for thunderstorms, and stays off when dry.
- Keep your Wi-Fi password private when sharing the project or its Serial Monitor output.
Review all connections
1. Connections between "led_1" and "ESP32"
2. Connections between "resistor_1" and "ESP32"
3. Connections between "resistor_2" and "ESP32"
4. Connections between "led_2" and "ESP32"
Deploy the firmware
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <math.h>
#include <string.h>
// Replace only these four credentials before using live Wi-Fi and Telegram.
enum RedLedMode : uint8_t { CONNECTING, WEATHER_BLINK, ERROR_PATTERN };
enum BlueLedMode : uint8_t { BLUE_OFF, BLUE_RAIN, BLUE_WATCH, BLUE_WARNING };
enum TemperatureBand : uint8_t { COLD, MILD, HOT };
struct AlertSummary {
String event;
String severity;
String headline;
};
// Forward declarations
bool alertsMatch(const AlertSummary *oldAlerts, size_t oldCount);
constexpr char WIFI_SSID[] = "Kigali";
constexpr char WIFI_PASSWORD[] = "kigali@250";
constexpr char TELEGRAM_BOT_TOKEN[] = "8631126143:AAGa_v8Yme9qhZ0G8jwAoweHUzIJkMgpHLU";
constexpr char TELEGRAM_CHAT_ID[] = "7931850982";
// NAN uses Open-Meteo's temperature. Use 30, 60, or 85 to test.
constexpr float TEST_TEMPERATURE_F = NAN;
// -1 uses Open-Meteo's weather code. Use 63 or 95 to test.
constexpr int TEST_WEATHER_CODE = -1;
// Empty uses live NWS alerts. Use "High Wind Warning" or "Flood Watch" to test.
constexpr char TEST_ALERT_EVENT[] = "";
// NAN uses Open-Meteo's gust value. Use 45 to test.
constexpr float TEST_GUST_MPH = NAN;
// Replace the example email before regular use; NWS requires a contactable User-Agent.
constexpr char NWS_USER_AGENT[] = "SuttonStormLight (your-email@example.com)";
// On this Elegoo board, GPIO25 is printed as D25 and GPIO26 is printed as D26.
constexpr int RED_LED_PIN = 25;
constexpr int BLUE_LED_PIN = 26;
constexpr char WEATHER_URL[] =
"http://api.open-meteo.com/v1/forecast?latitude=42.15&longitude=-71.76"
"¤t=temperature_2m,precipitation,weather_code,wind_gusts_10m"
"&temperature_unit=fahrenheit&precipitation_unit=inch&wind_speed_unit=mph";
constexpr char NWS_ALERTS_URL[] =
"https://api.weather.gov/alerts/active?point=42.15,-71.76";
constexpr unsigned long DRY_WEATHER_INTERVAL_MS = 10UL * 60UL * 1000UL;
constexpr unsigned long WET_WEATHER_INTERVAL_MS = 5UL * 60UL * 1000UL;
constexpr unsigned long ALERT_INTERVAL_MS = 5UL * 60UL * 1000UL;
constexpr unsigned long ALERT_STALE_MS = 30UL * 60UL * 1000UL;
constexpr unsigned long RETRY_INTERVAL_MS = 30UL * 1000UL;
constexpr unsigned long WIFI_CONNECT_TIMEOUT_MS = 20UL * 1000UL;
constexpr unsigned long TELEGRAM_POLL_INTERVAL_MS = 10UL * 1000UL;
constexpr unsigned long TELEGRAM_RATE_LIMIT_MS = 30UL * 1000UL;
constexpr unsigned long BLUE_FRAME_INTERVAL_MS = 50UL;
constexpr size_t MAX_ALERTS_TO_REPORT = 12;
volatile RedLedMode redLedMode = CONNECTING;
volatile BlueLedMode blueLedMode = BLUE_OFF;
volatile TemperatureBand temperatureBand = MILD;
volatile unsigned long redHalfPeriodMs = 600;
bool redLedOn = false;
unsigned long lastRedLedChangeMs = 0;
unsigned long errorPatternStartedMs = 0;
unsigned long lastBlueFrameMs = 0;
bool haveCurrentWeather = false;
bool currentRain = false;
bool gustWatchActive = false;
float currentGustMph = NAN;
int currentWeatherCode = -1;
float currentTemperatureF = NAN;
unsigned long lastWeatherSuccessMs = 0;
bool nwsWarningActive = false;
bool nwsOtherAlertActive = false;
bool haveNwsState = false;
unsigned long lastNwsSuccessMs = 0;
AlertSummary activeAlerts[MAX_ALERTS_TO_REPORT];
size_t activeAlertCount = 0;
size_t reportedAlertCount = 0;
String pendingTelegramMessage;
unsigned long lastTelegramMessageMs = 0;
unsigned long lastTelegramPollMs = 0;
long telegramUpdateOffset = 0;
bool telegramOnlineNoticePending = false;
bool haveEverBeenOnline = false;
unsigned long wifiOfflineStartedMs = 0;
unsigned long reconnectOfflineDurationMs = 0;
unsigned long updateTemperatureBand(float temperatureF);
const char *temperatureBandName(TemperatureBand band);
const char *weatherLabel(int weatherCode);
const char *blueModeName(BlueLedMode mode);
void setErrorState();
void chooseBlueLedMode();
void printStatus();
String statusSummary();
String urlEncode(const String &text);
void queueTelegram(const String &message);
bool telegramSendNow(const String &message);
void serviceTelegramSend();
void pollTelegramCommands();
bool fetchWeather();
bool fetchNwsAlerts();
void weatherTask(void *);
void updateRedLed();
void updateBlueLed();
unsigned long updateTemperatureBand(float temperatureF) {
switch (temperatureBand) {
case COLD:
if (temperatureF > 77.0f) temperatureBand = HOT;
else if (temperatureF > 47.0f) temperatureBand = MILD;
break;
case MILD:
if (temperatureF < 43.0f) temperatureBand = COLD;
else if (temperatureF > 77.0f) temperatureBand = HOT;
break;
case HOT:
if (temperatureF < 43.0f) temperatureBand = COLD;
else if (temperatureF < 73.0f) temperatureBand = MILD;
break;
}
if (temperatureBand == COLD) return 1500UL;
if (temperatureBand == HOT) return 150UL;
return 600UL;
}
const char *temperatureBandName(TemperatureBand band) {
if (band == COLD) return "COLD";
if (band == HOT) return "HOT";
return "MILD";
}
const char *weatherLabel(int code) {
switch (code) {
case 0: return "Clear sky"; case 1: return "Mainly clear"; case 2: return "Partly cloudy"; case 3: return "Overcast";
case 45: case 48: return "Fog"; case 51: return "Light drizzle"; case 53: return "Moderate drizzle";
case 55: return "Dense drizzle"; case 56: return "Light freezing drizzle"; case 57: return "Dense freezing drizzle";
case 61: return "Slight rain"; case 63: return "Moderate rain"; case 65: return "Heavy rain";
case 66: return "Light freezing rain"; case 67: return "Heavy freezing rain"; case 71: return "Slight snow";
case 73: return "Moderate snow"; case 75: return "Heavy snow"; case 77: return "Snow grains";
case 80: return "Slight rain showers"; case 81: return "Moderate rain showers"; case 82: return "Violent rain showers";
case 85: return "Slight snow showers"; case 86: return "Heavy snow showers"; case 95: return "Thunderstorm";
case 96: return "Thunderstorm with slight hail"; case 99: return "Thunderstorm with heavy hail";
default: return "Unknown weather";
}
}
const char *blueModeName(BlueLedMode mode) {
if (mode == BLUE_WARNING) return "WARNING strobe";
if (mode == BLUE_WATCH) return "WATCH/ADVISORY fast blink";
if (mode == BLUE_RAIN) return "RAIN breathing";
return "OFF";
}
void setErrorState() {
redLedMode = ERROR_PATTERN;
blueLedMode = BLUE_OFF;
errorPatternStartedMs = millis();
}
void chooseBlueLedMode() {
if (!haveCurrentWeather || redLedMode == ERROR_PATTERN || redLedMode == CONNECTING) return;
const bool testAlert = TEST_ALERT_EVENT[0] != '\0';
const bool warning = testAlert ? strstr(TEST_ALERT_EVENT, "Warning") != nullptr : nwsWarningActive;
const bool otherAlert = testAlert ? !warning : nwsOtherAlertActive;
const float gust = isnan(TEST_GUST_MPH) ? currentGustMph : TEST_GUST_MPH;
if (warning) blueLedMode = BLUE_WARNING;
else if (otherAlert || gustWatchActive) blueLedMode = BLUE_WATCH;
else if (currentRain) blueLedMode = BLUE_RAIN;
else blueLedMode = BLUE_OFF;
}
String statusSummary() {
String result;
result.reserve(900);
result += "Temperature: "; result += String(currentTemperatureF, 1); result += " F (";
result += temperatureBandName(temperatureBand); result += ")\nGusts: ";
const float gust = isnan(TEST_GUST_MPH) ? currentGustMph : TEST_GUST_MPH;
result += isnan(gust) ? "unknown" : String(gust, 1); result += " mph\nWeather: ";
result += weatherLabel(currentWeatherCode); result += "\nNWS alerts: "; result += String(reportedAlertCount);
for (size_t i = 0; i < activeAlertCount; ++i) {
result += "\n- "; result += activeAlerts[i].event; result += " ("; result += activeAlerts[i].severity; result += ")";
}
if (reportedAlertCount > activeAlertCount) result += "\n(additional alerts not listed)";
result += "\nBlue LED: "; result += blueModeName(blueLedMode);
if (lastWeatherSuccessMs != 0) {
result += "\nLast weather update: "; result += String((millis() - lastWeatherSuccessMs) / 1000UL); result += " seconds ago";
}
return result;
}
void printStatus() {
if (!haveCurrentWeather) return;
const float gust = isnan(TEST_GUST_MPH) ? currentGustMph : TEST_GUST_MPH;
Serial.printf("Temperature: %.1f F; band: %s; red LED: %lu ms on / %lu ms off\n", currentTemperatureF, temperatureBandName(temperatureBand), redHalfPeriodMs, redHalfPeriodMs);
Serial.printf("Gusts: %.1f mph; weather_code: %d (%s)\n", gust, currentWeatherCode, weatherLabel(currentWeatherCode));
if (TEST_ALERT_EVENT[0] != '\0') Serial.printf("NWS alerts: test override: %s\n", TEST_ALERT_EVENT);
else {
Serial.printf("NWS alerts: %u active\n", static_cast<unsigned>(reportedAlertCount));
for (size_t i = 0; i < activeAlertCount; ++i) Serial.printf(" %u: %s [%s]\n", static_cast<unsigned>(i + 1), activeAlerts[i].event.c_str(), activeAlerts[i].severity.c_str());
}
Serial.printf("Blue LED: %s\n", blueModeName(blueLedMode));
}
String urlEncode(const String &text) {
const char *hex = "0123456789ABCDEF";
String encoded; encoded.reserve(text.length() * 3);
for (size_t i = 0; i < text.length(); ++i) {
const uint8_t c = static_cast<uint8_t>(text[i]);
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.') encoded += char(c);
else if (c == ' ') encoded += '+';
else { encoded += '%'; encoded += hex[c >> 4]; encoded += hex[c & 0x0F]; }
}
return encoded;
}
void queueTelegram(const String &message) {
if (message.isEmpty()) return;
if (!pendingTelegramMessage.isEmpty()) pendingTelegramMessage += "\n\n";
pendingTelegramMessage += message;
}
bool telegramSendNow(const String &message) {
if (strcmp(TELEGRAM_BOT_TOKEN, "YOUR_BOT_TOKEN") == 0 || strcmp(TELEGRAM_CHAT_ID, "YOUR_CHAT_ID") == 0) {
Serial.println("Telegram credentials are placeholders; queued Telegram message was dropped.");
return true;
}
const String url = String("https://api.telegram.org/bot") + TELEGRAM_BOT_TOKEN + "/sendMessage";
const String body = "chat_id=" + urlEncode(TELEGRAM_CHAT_ID) + "&text=" + urlEncode(message);
for (uint8_t attempt = 1; attempt <= 3; ++attempt) {
WiFiClientSecure client; client.setInsecure();
HTTPClient https; https.useHTTP10(true); https.setConnectTimeout(10000); https.setTimeout(12000);
if (!https.begin(client, url)) { Serial.printf("Telegram send attempt %u could not start.\n", attempt); continue; }
https.addHeader("Content-Type", "application/x-www-form-urlencoded");
const int status = https.POST(body);
https.end();
if (status == HTTP_CODE_OK) { Serial.printf("Telegram sent: %s\n", message.c_str()); return true; }
Serial.printf("Telegram send attempt %u failed: HTTP %d\n", attempt, status);
}
Serial.println("Telegram message dropped after 3 failed attempts.");
return false;
}
void serviceTelegramSend() {
if (pendingTelegramMessage.isEmpty() || millis() - lastTelegramMessageMs < TELEGRAM_RATE_LIMIT_MS) return;
const String message = pendingTelegramMessage;
pendingTelegramMessage = "";
telegramSendNow(message);
lastTelegramMessageMs = millis();
}
void pollTelegramCommands() {
if (millis() - lastTelegramPollMs < TELEGRAM_POLL_INTERVAL_MS) return;
lastTelegramPollMs = millis();
if (strcmp(TELEGRAM_BOT_TOKEN, "YOUR_BOT_TOKEN") == 0 || strcmp(TELEGRAM_CHAT_ID, "YOUR_CHAT_ID") == 0) {
Serial.println("Telegram getUpdates poll skipped: credentials are placeholders.");
return;
}
WiFiClientSecure client; client.setInsecure();
const String url = String("https://api.telegram.org/bot") + TELEGRAM_BOT_TOKEN + "/getUpdates?timeout=2&offset=" + String(telegramUpdateOffset);
HTTPClient https; https.useHTTP10(true); https.setConnectTimeout(8000); https.setTimeout(8000);
if (!https.begin(client, url)) { Serial.println("Telegram getUpdates poll: could not start (HTTP unavailable; 0 updates)."); return; }
const int status = https.GET();
if (status != HTTP_CODE_OK) { Serial.printf("Telegram getUpdates poll: HTTP %d; 0 updates.\n", status); https.end(); return; }
JsonDocument document;
const DeserializationError error = deserializeJson(document, https.getStream());
https.end();
if (error) { Serial.printf("Telegram getUpdates poll: HTTP %d; response read error: %s; 0 updates.\n", status, error.c_str()); return; }
JsonArrayConst updates = document["result"].as<JsonArrayConst>();
Serial.printf("Telegram getUpdates poll: HTTP %d; %u updates.\n", status, static_cast<unsigned>(updates.size()));
for (JsonVariantConst update : updates) {
const long updateId = update["update_id"] | -1L;
if (updateId >= telegramUpdateOffset) telegramUpdateOffset = updateId + 1;
const JsonVariantConst message = update["message"];
const String chatId = message["chat"]["id"].as<String>();
const String text = message["text"].as<String>();
if (chatId.isEmpty()) { Serial.println("Telegram update ignored: no message chat ID."); continue; }
if (chatId != String(TELEGRAM_CHAT_ID)) {
Serial.printf("Telegram message: chat ID %s; text: %s; ignored: chat ID does not match.\n", chatId.c_str(), text.c_str());
continue;
}
if (!(text == "/status" || text.startsWith("/status@"))) {
Serial.printf("Telegram message: chat ID %s; text: %s; ignored: not a /status command.\n", chatId.c_str(), text.c_str());
continue;
}
Serial.printf("Telegram message: chat ID %s; text: %s; accepted.\n", chatId.c_str(), text.c_str());
queueTelegram(statusSummary());
}
}
bool fetchWeather() {
HTTPClient http;
http.useHTTP10(true); http.setConnectTimeout(10000); http.setTimeout(10000); http.begin(WEATHER_URL);
const int status = http.GET();
if (status != HTTP_CODE_OK) { Serial.printf("Weather request failed: HTTP %d\n", status); http.end(); return false; }
JsonDocument document;
const DeserializationError error = deserializeJson(document, http.getStream());
http.end();
if (error) { Serial.printf("Weather response could not be read: %s\n", error.c_str()); return false; }
JsonVariantConst current = document["current"];
if (current.isNull() || current["temperature_2m"].isNull() || current["precipitation"].isNull() || current["weather_code"].isNull() || current["wind_gusts_10m"].isNull()) { Serial.println("Weather response did not contain all current weather values."); return false; }
const bool hadWeather = haveCurrentWeather;
const TemperatureBand oldBand = temperatureBand;
const bool oldRain = currentRain;
const bool oldGustWatch = gustWatchActive;
const float apiTemp = current["temperature_2m"].as<float>();
currentTemperatureF = isnan(TEST_TEMPERATURE_F) ? apiTemp : TEST_TEMPERATURE_F;
const float precipitation = current["precipitation"].as<float>();
const int apiCode = current["weather_code"].as<int>();
currentWeatherCode = TEST_WEATHER_CODE == -1 ? apiCode : TEST_WEATHER_CODE;
currentGustMph = current["wind_gusts_10m"].as<float>();
currentRain = (currentWeatherCode >= 51 && currentWeatherCode <= 67) || (currentWeatherCode >= 80 && currentWeatherCode <= 82) || precipitation > 0.0f;
const float effectiveGust = isnan(TEST_GUST_MPH) ? currentGustMph : TEST_GUST_MPH;
if (!gustWatchActive && !isnan(effectiveGust) && effectiveGust >= 40.0f) gustWatchActive = true;
else if (gustWatchActive && (!isnan(effectiveGust) && effectiveGust < 35.0f)) gustWatchActive = false;
haveCurrentWeather = true;
redHalfPeriodMs = updateTemperatureBand(currentTemperatureF);
redLedMode = WEATHER_BLINK;
lastWeatherSuccessMs = millis();
chooseBlueLedMode();
if (hadWeather) {
if (oldBand != temperatureBand) queueTelegram(String("Temperature band changed to ") + temperatureBandName(temperatureBand) + " (" + String(currentTemperatureF, 1) + " F)");
if (oldRain != currentRain) queueTelegram(currentRain ? "Rain started in Sutton." : "Rain stopped in Sutton.");
if (oldGustWatch != gustWatchActive) queueTelegram(gustWatchActive ? String("Wind gusts reached ") + String(effectiveGust, 1) + " mph." : String("Wind gusts dropped below 35 mph (now ") + String(effectiveGust, 1) + " mph).");
}
printStatus();
if (!isnan(TEST_TEMPERATURE_F)) Serial.printf("Test temperature override is active; API temperature was %.1f F.\n", apiTemp);
if (TEST_WEATHER_CODE != -1) Serial.printf("Test weather-code override is active; API weather_code was %d.\n", apiCode);
if (!isnan(TEST_GUST_MPH)) Serial.println("Test gust override is active.");
return true;
}
bool alertsMatch(const AlertSummary *oldAlerts, size_t oldCount) {
if (oldCount != activeAlertCount) return false;
for (size_t i = 0; i < oldCount; ++i) if (oldAlerts[i].event != activeAlerts[i].event || oldAlerts[i].severity != activeAlerts[i].severity || oldAlerts[i].headline != activeAlerts[i].headline) return false;
return true;
}
bool fetchNwsAlerts() {
AlertSummary oldAlerts[MAX_ALERTS_TO_REPORT];
const size_t oldCount = activeAlertCount;
for (size_t i = 0; i < oldCount; ++i) oldAlerts[i] = activeAlerts[i];
const bool hadNws = haveNwsState;
WiFiClientSecure client; client.setInsecure();
HTTPClient https; https.useHTTP10(true); https.setConnectTimeout(15000); https.setTimeout(20000);
if (!https.begin(client, NWS_ALERTS_URL)) { Serial.println("NWS alert request could not start; keeping the last alert state."); return false; }
https.addHeader("User-Agent", NWS_USER_AGENT); https.addHeader("Accept", "application/geo+json");
const int status = https.GET();
if (status != HTTP_CODE_OK) { Serial.printf("NWS alert request failed: HTTP %d; keeping the last alert state.\n", status); https.end(); return false; }
JsonDocument filter; filter["features"][0]["properties"]["event"] = true; filter["features"][0]["properties"]["severity"] = true; filter["features"][0]["properties"]["headline"] = true;
JsonDocument document;
const DeserializationError error = deserializeJson(document, https.getStream(), DeserializationOption::Filter(filter));
https.end();
if (error) { Serial.printf("NWS alert response could not be read: %s; keeping the last alert state.\n", error.c_str()); return false; }
JsonArrayConst features = document["features"].as<JsonArrayConst>();
if (features.isNull()) { Serial.println("NWS alert response did not contain an alerts list; keeping the last alert state."); return false; }
nwsWarningActive = false; nwsOtherAlertActive = false; activeAlertCount = 0; reportedAlertCount = 0;
for (JsonVariantConst feature : features) {
JsonVariantConst props = feature["properties"]; const char *event = props["event"] | "Unnamed alert"; const char *severity = props["severity"] | "Unknown"; const char *headline = props["headline"] | "";
++reportedAlertCount;
if (strstr(event, "Warning") != nullptr) nwsWarningActive = true; else nwsOtherAlertActive = true;
if (activeAlertCount < MAX_ALERTS_TO_REPORT) activeAlerts[activeAlertCount++] = {event, severity, headline};
}
haveNwsState = true; lastNwsSuccessMs = millis(); chooseBlueLedMode();
if (hadNws && !alertsMatch(oldAlerts, oldCount)) {
if (activeAlertCount > oldCount) for (size_t i = oldCount; i < activeAlertCount; ++i) queueTelegram(String("New NWS alert: ") + activeAlerts[i].event + " (" + activeAlerts[i].severity + ")\n" + activeAlerts[i].headline);
else queueTelegram(String("An NWS alert ended. Active alerts now: ") + String(reportedAlertCount));
}
printStatus();
return true;
}
void weatherTask(void *) {
unsigned long nextWeatherMs = 0, nextAlertMs = 0;
bool wasConnected = false;
for (;;) {
const unsigned long now = millis();
const bool connected = WiFi.status() == WL_CONNECTED;
if (connected && !wasConnected) {
Serial.print("WiFi connected. IP address: "); Serial.println(WiFi.localIP());
const unsigned long offlineMs = wifiOfflineStartedMs ? now - wifiOfflineStartedMs : 0;
telegramOnlineNoticePending = !haveEverBeenOnline || offlineMs > 2UL * 60UL * 1000UL;
reconnectOfflineDurationMs = offlineMs;
haveEverBeenOnline = true; wifiOfflineStartedMs = 0; nextWeatherMs = 0; nextAlertMs = 0;
}
if (!connected && wasConnected) { Serial.println("WiFi disconnected."); wifiOfflineStartedMs = now; setErrorState(); nextWeatherMs = now + RETRY_INTERVAL_MS; nextAlertMs = now + RETRY_INTERVAL_MS; }
wasConnected = connected;
if (!connected && static_cast<long>(now - nextWeatherMs) >= 0) {
redLedMode = CONNECTING; blueLedMode = BLUE_OFF; Serial.printf("Connecting to WiFi: %s\n", WIFI_SSID); WiFi.disconnect(); WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
const unsigned long started = millis(); while (WiFi.status() != WL_CONNECTED && millis() - started < WIFI_CONNECT_TIMEOUT_MS) vTaskDelay(pdMS_TO_TICKS(250));
if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi connection failed; retrying in 30 seconds."); setErrorState(); nextWeatherMs = millis() + RETRY_INTERVAL_MS; }
} else if (connected) {
if (static_cast<long>(now - nextWeatherMs) >= 0) { if (fetchWeather()) nextWeatherMs = millis() + (currentRain ? WET_WEATHER_INTERVAL_MS : DRY_WEATHER_INTERVAL_MS); else { setErrorState(); nextWeatherMs = millis() + RETRY_INTERVAL_MS; } }
if (static_cast<long>(now - nextAlertMs) >= 0) { fetchNwsAlerts(); nextAlertMs = millis() + ALERT_INTERVAL_MS; }
if (telegramOnlineNoticePending && haveCurrentWeather && haveNwsState) {
String notice = "Sutton Storm Light online";
if (reconnectOfflineDurationMs > 2UL * 60UL * 1000UL) {
notice += " after "; notice += String(reconnectOfflineDurationMs / 60000UL); notice += " minutes offline";
}
notice += "\n"; notice += statusSummary();
queueTelegram(notice); telegramOnlineNoticePending = false;
}
if (haveNwsState && millis() - lastNwsSuccessMs > ALERT_STALE_MS) { Serial.println("NWS alert state is over 30 minutes old; clearing it."); haveNwsState = false; nwsWarningActive = false; nwsOtherAlertActive = false; activeAlertCount = 0; reportedAlertCount = 0; chooseBlueLedMode(); }
pollTelegramCommands(); serviceTelegramSend();
}
vTaskDelay(pdMS_TO_TICKS(250));
}
}
void updateRedLed() {
const unsigned long now = millis();
if (redLedMode == CONNECTING) { if (!redLedOn) { redLedOn = true; digitalWrite(RED_LED_PIN, HIGH); } return; }
if (redLedMode == WEATHER_BLINK) { if (now - lastRedLedChangeMs >= redHalfPeriodMs) { lastRedLedChangeMs = now; redLedOn = !redLedOn; digitalWrite(RED_LED_PIN, redLedOn ? HIGH : LOW); } return; }
const unsigned long position = (now - errorPatternStartedMs) % 1200UL; const bool on = position < 100UL || (position >= 200UL && position < 300UL);
if (on != redLedOn) { redLedOn = on; digitalWrite(RED_LED_PIN, redLedOn ? HIGH : LOW); }
}
void updateBlueLed() {
const unsigned long now = millis(); if (now - lastBlueFrameMs < BLUE_FRAME_INTERVAL_MS) return; lastBlueFrameMs = now;
if (blueLedMode == BLUE_OFF) ledcWrite(BLUE_LED_PIN, 0);
else if (blueLedMode == BLUE_WARNING) ledcWrite(BLUE_LED_PIN, (now % 160UL) < 80UL ? 255 : 0);
else if (blueLedMode == BLUE_WATCH) ledcWrite(BLUE_LED_PIN, (now % 600UL) < 300UL ? 255 : 0);
else { const float phase = (now % 2000UL) * (2.0f * PI / 2000.0f); ledcWrite(BLUE_LED_PIN, static_cast<uint8_t>((sinf(phase - PI / 2.0f) + 1.0f) * 127.5f)); }
}
void setup() {
Serial.begin(115200); pinMode(RED_LED_PIN, OUTPUT); digitalWrite(RED_LED_PIN, LOW);
ledcAttach(BLUE_LED_PIN, 5000, 8); ledcWrite(BLUE_LED_PIN, 0); WiFi.mode(WIFI_STA);
Serial.println("Sutton D25 temperature LED, D26 storm-alert LED, and Telegram starting.");
xTaskCreatePinnedToCore(weatherTask, "weatherTask", 16384, nullptr, 1, nullptr, 0);
}
void loop() { updateRedLed(); updateBlueLed(); delay(1); }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.




