diff --git a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino index 960dd19..43d366e 100644 --- a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino +++ b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino @@ -17,6 +17,8 @@ #include "tz_lookup.h" // Timezone lookup, do not duplicate mapping here! #include "days_lookup.h" // Languages for the Days of the Week #include "months_lookup.h" // Languages for the Months of the Year +#include "index_html.h" // Web UI + #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 @@ -674,8 +676,8 @@ void printConfigToSerial() { Serial.println(customMessage); Serial.print(F("Total Runtime: ")); - if (totalUptimeSeconds > 0) { - Serial.println(formatUptime(totalUptimeSeconds)); + if (getTotalRuntimeSeconds() > 0) { + Serial.println(formatTotalRuntime()); } else { Serial.println(F("No runtime recorded yet.")); } @@ -695,7 +697,7 @@ void setupWebServer() { server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { Serial.println(F("[WEBSERVER] Request: /")); - request->send(LittleFS, "/index.html", "text/html"); + request->send_P(200, "text/html", index_html); }); server.on("/config.json", HTTP_GET, [](AsyncWebServerRequest *request) { @@ -1883,43 +1885,72 @@ void fetchWeather() { } +// ----------------------------- +// Load uptime from LittleFS +// ----------------------------- void loadUptime() { if (LittleFS.exists("/uptime.dat")) { File f = LittleFS.open("/uptime.dat", "r"); if (f) { totalUptimeSeconds = f.parseInt(); f.close(); + bootMillis = millis(); Serial.printf("[UPTIME] Loaded accumulated uptime: %lu seconds (%.2f hours)\n", totalUptimeSeconds, totalUptimeSeconds / 3600.0); } else { Serial.println(F("[UPTIME] Failed to open /uptime.dat for reading.")); + totalUptimeSeconds = 0; + bootMillis = millis(); } } else { Serial.println(F("[UPTIME] No previous uptime file found. Starting from 0.")); totalUptimeSeconds = 0; + bootMillis = millis(); } } +// ----------------------------- +// Save uptime to LittleFS +// ----------------------------- void saveUptime() { - // Add runtime since boot to total - unsigned long runtimeSeconds = (millis() - bootMillis) / 1000; - totalUptimeSeconds += runtimeSeconds; - - // Reset bootMillis so next period counts correctly - bootMillis = millis(); + // Use getTotalRuntimeSeconds() to include current session + totalUptimeSeconds = getTotalRuntimeSeconds(); + bootMillis = millis(); // reset session start File f = LittleFS.open("/uptime.dat", "w"); if (f) { f.print(totalUptimeSeconds); f.close(); - Serial.printf("[UPTIME] Saved accumulated uptime: %lu seconds (%.2f hours)\n", - totalUptimeSeconds, totalUptimeSeconds / 3600.0); + Serial.printf("[UPTIME] Saved accumulated uptime: %s\n", formatTotalRuntime().c_str()); } else { Serial.println(F("[UPTIME] Failed to write /uptime.dat")); } } + +// ----------------------------- +// Get total uptime including current session +// ----------------------------- +unsigned long getTotalRuntimeSeconds() { + return totalUptimeSeconds + (millis() - bootMillis) / 1000; +} + + +// ----------------------------- +// Format total uptime as HH:MM:SS +// ----------------------------- +String formatTotalRuntime() { + unsigned long secs = getTotalRuntimeSeconds(); + unsigned int h = secs / 3600; + unsigned int m = (secs % 3600) / 60; + unsigned int s = secs % 60; + char buf[16]; + sprintf(buf, "%02u:%02u:%02u", h, m, s); + return String(buf); +} + + void saveCustomMessageToConfig(const char *msg) { Serial.println(F("[CONFIG] Updating customMessage in config.json...")); @@ -2241,82 +2272,82 @@ void loop() { } -// ----------------------------- -// Dimming (auto + manual) -// ----------------------------- -time_t now_time = time(nullptr); -struct tm timeinfo; -localtime_r(&now_time, &timeinfo); -int curHour = timeinfo.tm_hour; -int curMinute = timeinfo.tm_min; -int curTotal = curHour * 60 + curMinute; + // ----------------------------- + // Dimming (auto + manual) + // ----------------------------- + time_t now_time = time(nullptr); + struct tm timeinfo; + localtime_r(&now_time, &timeinfo); + int curHour = timeinfo.tm_hour; + int curMinute = timeinfo.tm_min; + int curTotal = curHour * 60 + curMinute; -// ----------------------------- -// Determine dimming start/end -// ----------------------------- -int startTotal, endTotal; -bool dimActive = false; + // ----------------------------- + // Determine dimming start/end + // ----------------------------- + int startTotal, endTotal; + bool dimActive = false; -if (autoDimmingEnabled) { - startTotal = sunsetHour * 60 + sunsetMinute; - endTotal = sunriseHour * 60 + sunriseMinute; -} else if (dimmingEnabled) { - startTotal = dimStartHour * 60 + dimStartMinute; - endTotal = dimEndHour * 60 + dimEndMinute; -} else { - startTotal = endTotal = -1; // not used -} - -// ----------------------------- -// Check if dimming should be active -// ----------------------------- -if (autoDimmingEnabled || dimmingEnabled) { - if (startTotal < endTotal) { - dimActive = (curTotal >= startTotal && curTotal < endTotal); + if (autoDimmingEnabled) { + startTotal = sunsetHour * 60 + sunsetMinute; + endTotal = sunriseHour * 60 + sunriseMinute; + } else if (dimmingEnabled) { + startTotal = dimStartHour * 60 + dimStartMinute; + endTotal = dimEndHour * 60 + dimEndMinute; } else { - dimActive = (curTotal >= startTotal || curTotal < endTotal); // overnight + startTotal = endTotal = -1; // not used } -} -// ----------------------------- -// Apply brightness / display on-off -// ----------------------------- -static bool lastDimActive = false; // remembers last state -int targetBrightness = dimActive ? dimBrightness : brightness; + // ----------------------------- + // Check if dimming should be active + // ----------------------------- + if (autoDimmingEnabled || dimmingEnabled) { + if (startTotal < endTotal) { + dimActive = (curTotal >= startTotal && curTotal < endTotal); + } else { + dimActive = (curTotal >= startTotal || curTotal < endTotal); // overnight + } + } -// Log only when transitioning -if (dimActive != lastDimActive) { - if (dimActive) { - if (autoDimmingEnabled) - Serial.printf("[DISPLAY] Automatic dimming setting brightness to %d\n", targetBrightness); - else if (dimmingEnabled) - Serial.printf("[DISPLAY] Custom dimming setting brightness to %d\n", targetBrightness); + // ----------------------------- + // Apply brightness / display on-off + // ----------------------------- + static bool lastDimActive = false; // remembers last state + int targetBrightness = dimActive ? dimBrightness : brightness; + + // Log only when transitioning + if (dimActive != lastDimActive) { + if (dimActive) { + if (autoDimmingEnabled) + Serial.printf("[DISPLAY] Automatic dimming setting brightness to %d\n", targetBrightness); + else if (dimmingEnabled) + Serial.printf("[DISPLAY] Custom dimming setting brightness to %d\n", targetBrightness); + } else { + Serial.println(F("[DISPLAY] Waking display (dimming end)")); + } + lastDimActive = dimActive; + } + + // Apply brightness or shutdown + if (targetBrightness == -1) { + if (!displayOff) { + Serial.println(F("[DISPLAY] Turning display OFF (dimming -1)")); + P.displayShutdown(true); + P.displayClear(); + displayOff = true; + displayOffByDimming = dimActive; + displayOffByBrightness = !dimActive; + } } else { - Serial.println(F("[DISPLAY] Waking display (dimming end)")); + if (displayOff && ((dimActive && displayOffByDimming) || (!dimActive && displayOffByBrightness))) { + Serial.println(F("[DISPLAY] Waking display (dimming end)")); + P.displayShutdown(false); + displayOff = false; + displayOffByDimming = false; + displayOffByBrightness = false; + } + P.setIntensity(targetBrightness); } - lastDimActive = dimActive; -} - -// Apply brightness or shutdown -if (targetBrightness == -1) { - if (!displayOff) { - Serial.println(F("[DISPLAY] Turning display OFF (dimming -1)")); - P.displayShutdown(true); - P.displayClear(); - displayOff = true; - displayOffByDimming = dimActive; - displayOffByBrightness = !dimActive; - } -} else { - if (displayOff && ((dimActive && displayOffByDimming) || (!dimActive && displayOffByBrightness))) { - Serial.println(F("[DISPLAY] Waking display (dimming end)")); - P.displayShutdown(false); - displayOff = false; - displayOffByDimming = false; - displayOffByBrightness = false; - } - P.setIntensity(targetBrightness); -} // --- IMMEDIATE COUNTDOWN FINISH TRIGGER --- diff --git a/ESPTimeCast_ESP32/data/config.json b/ESPTimeCast_ESP32/data/config.json deleted file mode 100644 index 509a440..0000000 --- a/ESPTimeCast_ESP32/data/config.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "ssid": "", - "password": "", - "openWeatherApiKey": "", - "openWeatherCity": "", - "openWeatherCountry": "", - "clockDuration": 10000, - "weatherDuration": 5000, - "timeZone": "", - "weatherUnits": "metric", - "brightness": 10, - "flipDisplay": false, - "ntpServer1": "pool.ntp.org", - "ntpServer2": "time.nist.gov", - "twelveHourToggle": false, - "showDayOfWeek": true, - "showDate": false, - "showHumidity": false, - "colonBlinkEnabled": true, - "language": "en", - "dimmingEnabled": false, - "dimStartHour": 18, - "dimStartMinute": 0, - "dimEndHour": 8, - "dimEndMinute": 0, - "dimBrightness": 2, - "showWeatherDescription": false, - "countdown": { - "enabled": false, - "targetTimestamp": 0, - "label": "", - "isDramaticCountdown": true - } -} \ No newline at end of file diff --git a/ESPTimeCast_ESP32/data/index.html b/ESPTimeCast_ESP32/index_html.h similarity index 97% rename from ESPTimeCast_ESP32/data/index.html rename to ESPTimeCast_ESP32/index_html.h index 9cd1a5c..918d608 100644 --- a/ESPTimeCast_ESP32/data/index.html +++ b/ESPTimeCast_ESP32/index_html.h @@ -1,1636 +1,1641 @@ - - - - - -ESPTimeCast Settings - - - - -
- - -

WiFi Settings

- - - -
- - -
- - -

Clock Settings

- - - - - - -
-
- - - -
-
- - - -
-
- - -

Weather Settings

- - -
Required to fetch weather data. Get your API key here.
- - -
- - -
- - -
- Location format examples: City, Country Code - Osaka, JP | ZIP, Country Code - 94040, US | Latitude, Longitude - 34.6937, 135.5023 -
- -
-

Custom Message

- -
Allowed characters: A–Z, 0–9, space, and : ! ' - . ? , _ + % /
-
-
- - -
- - - - - - - -
- - - -
- - - \ No newline at end of file +#pragma once +// index_html.h + +const char index_html[] PROGMEM = R"rawliteral( + + + + + +ESPTimeCast Settings + + + + +
+ + +

WiFi Settings

+ + + +
+ + +
+ + +

Clock Settings

+ + + + + + +
+
+ + + +
+
+ + + +
+
+ + +

Weather Settings

+ + +
Required to fetch weather data. Get your API key here.
+ + +
+ + +
+ + +
+ Location format examples: City, Country Code - Osaka, JP | ZIP, Country Code - 94040, US | Latitude, Longitude - 34.6937, 135.5023 +
+ +
+

Custom Message

+ +
Allowed characters: A–Z, 0–9, space, and : ! ' - . ? , _ + % /
+
+
+ + +
+ + + + + + + +
+ + + +
+ + + +)rawliteral"; diff --git a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino index d760c14..d42b5ff 100644 --- a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino +++ b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino @@ -17,6 +17,8 @@ #include "tz_lookup.h" // Timezone lookup, do not duplicate mapping here! #include "days_lookup.h" // Languages for the Days of the Week #include "months_lookup.h" // Languages for the Months of the Year +#include "index_html.h" // Web UI + #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 @@ -671,8 +673,8 @@ void printConfigToSerial() { Serial.println(customMessage); Serial.print(F("Total Runtime: ")); - if (totalUptimeSeconds > 0) { - Serial.println(formatUptime(totalUptimeSeconds)); + if (getTotalRuntimeSeconds() > 0) { + Serial.println(formatTotalRuntime()); } else { Serial.println(F("No runtime recorded yet.")); } @@ -692,7 +694,7 @@ void setupWebServer() { server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { Serial.println(F("[WEBSERVER] Request: /")); - request->send(LittleFS, "/index.html", "text/html"); + request->send_P(200, "text/html", index_html); }); server.on("/config.json", HTTP_GET, [](AsyncWebServerRequest *request) { @@ -969,7 +971,6 @@ void setupWebServer() { }); }); - server.on("/ap_status", HTTP_GET, [](AsyncWebServerRequest *request) { Serial.print(F("[WEBSERVER] Request: /ap_status. isAPMode = ")); Serial.println(isAPMode); @@ -1104,7 +1105,6 @@ void setupWebServer() { request->send(200, "application/json", "{\"ok\":true}"); }); - server.on("/set_weatherdesc", HTTP_POST, [](AsyncWebServerRequest *request) { bool showDesc = false; if (request->hasParam("value", true)) { @@ -1644,7 +1644,6 @@ String normalizeWeatherDescription(String str) { return result; } - bool isNumber(const char *str) { for (int i = 0; str[i]; i++) { if (!isdigit(str[i]) && str[i] != '.' && str[i] != '-') return false; @@ -1661,7 +1660,6 @@ bool isFiveDigitZip(const char *str) { } - // ----------------------------------------------------------------------------- // Weather Fetching and API settings // ----------------------------------------------------------------------------- @@ -1878,42 +1876,73 @@ void fetchWeather() { http.end(); } + +// ----------------------------- +// Load uptime from LittleFS +// ----------------------------- void loadUptime() { if (LittleFS.exists("/uptime.dat")) { File f = LittleFS.open("/uptime.dat", "r"); if (f) { totalUptimeSeconds = f.parseInt(); f.close(); + bootMillis = millis(); Serial.printf("[UPTIME] Loaded accumulated uptime: %lu seconds (%.2f hours)\n", totalUptimeSeconds, totalUptimeSeconds / 3600.0); } else { Serial.println(F("[UPTIME] Failed to open /uptime.dat for reading.")); + totalUptimeSeconds = 0; + bootMillis = millis(); } } else { Serial.println(F("[UPTIME] No previous uptime file found. Starting from 0.")); totalUptimeSeconds = 0; + bootMillis = millis(); } } -void saveUptime() { - // Add runtime since boot to total - unsigned long runtimeSeconds = (millis() - bootMillis) / 1000; - totalUptimeSeconds += runtimeSeconds; - // Reset bootMillis so next period counts correctly - bootMillis = millis(); +// ----------------------------- +// Save uptime to LittleFS +// ----------------------------- +void saveUptime() { + // Use getTotalRuntimeSeconds() to include current session + totalUptimeSeconds = getTotalRuntimeSeconds(); + bootMillis = millis(); // reset session start File f = LittleFS.open("/uptime.dat", "w"); if (f) { f.print(totalUptimeSeconds); f.close(); - Serial.printf("[UPTIME] Saved accumulated uptime: %lu seconds (%.2f hours)\n", - totalUptimeSeconds, totalUptimeSeconds / 3600.0); + Serial.printf("[UPTIME] Saved accumulated uptime: %s\n", formatTotalRuntime().c_str()); } else { Serial.println(F("[UPTIME] Failed to write /uptime.dat")); } } + +// ----------------------------- +// Get total uptime including current session +// ----------------------------- +unsigned long getTotalRuntimeSeconds() { + return totalUptimeSeconds + (millis() - bootMillis) / 1000; +} + + +// ----------------------------- +// Format total uptime as HH:MM:SS +// ----------------------------- +String formatTotalRuntime() { + unsigned long secs = getTotalRuntimeSeconds(); + unsigned int h = secs / 3600; + unsigned int m = (secs % 3600) / 60; + unsigned int s = secs % 60; + char buf[16]; + sprintf(buf, "%02u:%02u:%02u", h, m, s); + return String(buf); +} + + void saveCustomMessageToConfig(const char *msg) { Serial.println(F("[CONFIG] Updating customMessage in config.json...")); @@ -2235,82 +2264,82 @@ void loop() { } -// ----------------------------- -// Dimming (auto + manual) -// ----------------------------- -time_t now_time = time(nullptr); -struct tm timeinfo; -localtime_r(&now_time, &timeinfo); -int curHour = timeinfo.tm_hour; -int curMinute = timeinfo.tm_min; -int curTotal = curHour * 60 + curMinute; + // ----------------------------- + // Dimming (auto + manual) + // ----------------------------- + time_t now_time = time(nullptr); + struct tm timeinfo; + localtime_r(&now_time, &timeinfo); + int curHour = timeinfo.tm_hour; + int curMinute = timeinfo.tm_min; + int curTotal = curHour * 60 + curMinute; -// ----------------------------- -// Determine dimming start/end -// ----------------------------- -int startTotal, endTotal; -bool dimActive = false; + // ----------------------------- + // Determine dimming start/end + // ----------------------------- + int startTotal, endTotal; + bool dimActive = false; -if (autoDimmingEnabled) { - startTotal = sunsetHour * 60 + sunsetMinute; - endTotal = sunriseHour * 60 + sunriseMinute; -} else if (dimmingEnabled) { - startTotal = dimStartHour * 60 + dimStartMinute; - endTotal = dimEndHour * 60 + dimEndMinute; -} else { - startTotal = endTotal = -1; // not used -} - -// ----------------------------- -// Check if dimming should be active -// ----------------------------- -if (autoDimmingEnabled || dimmingEnabled) { - if (startTotal < endTotal) { - dimActive = (curTotal >= startTotal && curTotal < endTotal); + if (autoDimmingEnabled) { + startTotal = sunsetHour * 60 + sunsetMinute; + endTotal = sunriseHour * 60 + sunriseMinute; + } else if (dimmingEnabled) { + startTotal = dimStartHour * 60 + dimStartMinute; + endTotal = dimEndHour * 60 + dimEndMinute; } else { - dimActive = (curTotal >= startTotal || curTotal < endTotal); // overnight + startTotal = endTotal = -1; // not used } -} -// ----------------------------- -// Apply brightness / display on-off -// ----------------------------- -static bool lastDimActive = false; // remembers last state -int targetBrightness = dimActive ? dimBrightness : brightness; + // ----------------------------- + // Check if dimming should be active + // ----------------------------- + if (autoDimmingEnabled || dimmingEnabled) { + if (startTotal < endTotal) { + dimActive = (curTotal >= startTotal && curTotal < endTotal); + } else { + dimActive = (curTotal >= startTotal || curTotal < endTotal); // overnight + } + } -// Log only when transitioning -if (dimActive != lastDimActive) { - if (dimActive) { - if (autoDimmingEnabled) - Serial.printf("[DISPLAY] Automatic dimming setting brightness to %d\n", targetBrightness); - else if (dimmingEnabled) - Serial.printf("[DISPLAY] Custom dimming setting brightness to %d\n", targetBrightness); + // ----------------------------- + // Apply brightness / display on-off + // ----------------------------- + static bool lastDimActive = false; // remembers last state + int targetBrightness = dimActive ? dimBrightness : brightness; + + // Log only when transitioning + if (dimActive != lastDimActive) { + if (dimActive) { + if (autoDimmingEnabled) + Serial.printf("[DISPLAY] Automatic dimming setting brightness to %d\n", targetBrightness); + else if (dimmingEnabled) + Serial.printf("[DISPLAY] Custom dimming setting brightness to %d\n", targetBrightness); + } else { + Serial.println(F("[DISPLAY] Waking display (dimming end)")); + } + lastDimActive = dimActive; + } + + // Apply brightness or shutdown + if (targetBrightness == -1) { + if (!displayOff) { + Serial.println(F("[DISPLAY] Turning display OFF (dimming -1)")); + P.displayShutdown(true); + P.displayClear(); + displayOff = true; + displayOffByDimming = dimActive; + displayOffByBrightness = !dimActive; + } } else { - Serial.println(F("[DISPLAY] Waking display (dimming end)")); + if (displayOff && ((dimActive && displayOffByDimming) || (!dimActive && displayOffByBrightness))) { + Serial.println(F("[DISPLAY] Waking display (dimming end)")); + P.displayShutdown(false); + displayOff = false; + displayOffByDimming = false; + displayOffByBrightness = false; + } + P.setIntensity(targetBrightness); } - lastDimActive = dimActive; -} - -// Apply brightness or shutdown -if (targetBrightness == -1) { - if (!displayOff) { - Serial.println(F("[DISPLAY] Turning display OFF (dimming -1)")); - P.displayShutdown(true); - P.displayClear(); - displayOff = true; - displayOffByDimming = dimActive; - displayOffByBrightness = !dimActive; - } -} else { - if (displayOff && ((dimActive && displayOffByDimming) || (!dimActive && displayOffByBrightness))) { - Serial.println(F("[DISPLAY] Waking display (dimming end)")); - P.displayShutdown(false); - displayOff = false; - displayOffByDimming = false; - displayOffByBrightness = false; - } - P.setIntensity(targetBrightness); -} // --- IMMEDIATE COUNTDOWN FINISH TRIGGER --- diff --git a/ESPTimeCast_ESP8266/data/config.json b/ESPTimeCast_ESP8266/data/config.json deleted file mode 100644 index 509a440..0000000 --- a/ESPTimeCast_ESP8266/data/config.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "ssid": "", - "password": "", - "openWeatherApiKey": "", - "openWeatherCity": "", - "openWeatherCountry": "", - "clockDuration": 10000, - "weatherDuration": 5000, - "timeZone": "", - "weatherUnits": "metric", - "brightness": 10, - "flipDisplay": false, - "ntpServer1": "pool.ntp.org", - "ntpServer2": "time.nist.gov", - "twelveHourToggle": false, - "showDayOfWeek": true, - "showDate": false, - "showHumidity": false, - "colonBlinkEnabled": true, - "language": "en", - "dimmingEnabled": false, - "dimStartHour": 18, - "dimStartMinute": 0, - "dimEndHour": 8, - "dimEndMinute": 0, - "dimBrightness": 2, - "showWeatherDescription": false, - "countdown": { - "enabled": false, - "targetTimestamp": 0, - "label": "", - "isDramaticCountdown": true - } -} \ No newline at end of file diff --git a/ESPTimeCast_ESP8266/data/index.html b/ESPTimeCast_ESP8266/index_html.h similarity index 97% rename from ESPTimeCast_ESP8266/data/index.html rename to ESPTimeCast_ESP8266/index_html.h index 9cd1a5c..918d608 100644 --- a/ESPTimeCast_ESP8266/data/index.html +++ b/ESPTimeCast_ESP8266/index_html.h @@ -1,1636 +1,1641 @@ - - - - - -ESPTimeCast Settings - - - - -
- - -

WiFi Settings

- - - -
- - -
- - -

Clock Settings

- - - - - - -
-
- - - -
-
- - - -
-
- - -

Weather Settings

- - -
Required to fetch weather data. Get your API key here.
- - -
- - -
- - -
- Location format examples: City, Country Code - Osaka, JP | ZIP, Country Code - 94040, US | Latitude, Longitude - 34.6937, 135.5023 -
- -
-

Custom Message

- -
Allowed characters: A–Z, 0–9, space, and : ! ' - . ? , _ + % /
-
-
- - -
- - - - - - - -
- - - -
- - - \ No newline at end of file +#pragma once +// index_html.h + +const char index_html[] PROGMEM = R"rawliteral( + + + + + +ESPTimeCast Settings + + + + +
+ + +

WiFi Settings

+ + + +
+ + +
+ + +

Clock Settings

+ + + + + + +
+
+ + + +
+
+ + + +
+
+ + +

Weather Settings

+ + +
Required to fetch weather data. Get your API key here.
+ + +
+ + +
+ + +
+ Location format examples: City, Country Code - Osaka, JP | ZIP, Country Code - 94040, US | Latitude, Longitude - 34.6937, 135.5023 +
+ +
+

Custom Message

+ +
Allowed characters: A–Z, 0–9, space, and : ! ' - . ? , _ + % /
+
+
+ + +
+ + + + + + + +
+ + + +
+ + + +)rawliteral";