Added Home Assistant Support

Added Home Assistant Support.
Bug fixes
This commit is contained in:
M-Factory
2025-11-08 17:37:45 +09:00
parent f83f3d1c46
commit 7096653bca
6 changed files with 1030 additions and 359 deletions

View File

@@ -30,6 +30,7 @@ AsyncWebServer server(80);
// --- Global Scroll Speed Settings ---
const int GENERAL_SCROLL_SPEED = 85; // Default: Adjust this for Weather Description and Countdown Label (e.g., 50 for faster, 200 for slower)
const int IP_SCROLL_SPEED = 115; // Default: Adjust this for the IP Address display (slower for readability)
int messageScrollSpeed = 85; // default fallback
// --- Nightscout setting ---
const unsigned int NIGHTSCOUT_IDLE_THRESHOLD_MIN = 10; // minutes before data is considered outdated
@@ -59,6 +60,8 @@ bool showHumidity = false;
bool colonBlinkEnabled = true;
char ntpServer1[64] = "pool.ntp.org";
char ntpServer2[256] = "time.nist.gov";
char customMessage[121] = "";
char lastPersistentMessage[128] = "";
// Dimming
bool dimmingEnabled = false;
@@ -101,6 +104,8 @@ bool shouldFetchWeatherNow = false;
unsigned long lastSwitch = 0;
unsigned long lastColonBlink = 0;
int displayMode = 0; // 0: Clock, 1: Weather, 2: Weather Description, 3: Countdown
int prevDisplayMode = -1;
bool clockScrollDone = false;
int currentHumidity = -1;
bool ntpSyncSuccessful = false;
@@ -147,7 +152,11 @@ const unsigned long descriptionScrollPause = 300; // 300ms pause after scroll
// --- Safe WiFi credential and API getters ---
const char *getSafeSsid() {
return isAPMode ? "********" : ssid;
if (isAPMode && strlen(ssid) == 0) {
return "";
} else {
return isAPMode ? "********" : ssid;
}
}
const char *getSafePassword() {
@@ -179,6 +188,7 @@ textEffect_t getEffectiveScrollDirection(textEffect_t desiredDirection, bool isF
return desiredDirection;
}
// -----------------------------------------------------------------------------
// Configuration Load & Save
// -----------------------------------------------------------------------------
@@ -252,10 +262,12 @@ void loadConfig() {
strlcpy(ssid, doc["ssid"] | "", sizeof(ssid));
strlcpy(password, doc["password"] | "", sizeof(password));
strlcpy(openWeatherApiKey, doc["openWeatherApiKey"] | "", sizeof(openWeatherApiKey)); // Corrected typo here
strlcpy(openWeatherApiKey, doc["openWeatherApiKey"] | "", sizeof(openWeatherApiKey));
strlcpy(openWeatherCity, doc["openWeatherCity"] | "", sizeof(openWeatherCity));
strlcpy(openWeatherCountry, doc["openWeatherCountry"] | "", sizeof(openWeatherCountry));
strlcpy(weatherUnits, doc["weatherUnits"] | "metric", sizeof(weatherUnits));
strlcpy(customMessage, doc["customMessage"] | "", sizeof(customMessage));
strlcpy(lastPersistentMessage, customMessage, sizeof(lastPersistentMessage));
clockDuration = doc["clockDuration"] | 10000;
weatherDuration = doc["weatherDuration"] | 5000;
strlcpy(timeZone, doc["timeZone"] | "Etc/UTC", sizeof(timeZone));
@@ -325,7 +337,6 @@ void loadConfig() {
}
// -----------------------------------------------------------------------------
// WiFi Setup
// -----------------------------------------------------------------------------
@@ -449,6 +460,7 @@ void connectWiFi() {
}
}
void clearWiFiCredentialsInConfig() {
DynamicJsonDocument doc(2048);
@@ -482,6 +494,7 @@ void clearWiFiCredentialsInConfig() {
Serial.println(F("[SECURITY] Cleared WiFi credentials in config.json."));
}
// -----------------------------------------------------------------------------
// Time / NTP Functions
// -----------------------------------------------------------------------------
@@ -584,6 +597,8 @@ void printConfigToSerial() {
Serial.println(countdownLabel);
Serial.print(F("Dramatic Countdown Display: "));
Serial.println(isDramaticCountdown ? "Yes" : "No");
Serial.print(F("Custom Message: "));
Serial.println(customMessage);
Serial.print(F("Total Runtime: "));
if (totalUptimeSeconds > 0) {
Serial.println(formatUptime(totalUptimeSeconds));
@@ -594,6 +609,7 @@ void printConfigToSerial() {
Serial.println();
}
// -----------------------------------------------------------------------------
// Web Server and Captive Portal
// -----------------------------------------------------------------------------
@@ -793,6 +809,7 @@ void setupWebServer() {
Serial.println(F("[SAVE] Config verification successful."));
DynamicJsonDocument okDoc(128);
strlcpy(customMessage, doc["customMessage"] | "", sizeof(customMessage));
okDoc[F("message")] = "Saved successfully. Rebooting...";
String response;
serializeJson(okDoc, response);
@@ -1101,6 +1118,110 @@ void setupWebServer() {
request->send(200, "application/json", "{\"ok\":true}");
});
// --- Custom Message Endpoint ---
server.on("/set_custom_message", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("message", true)) {
String msg = request->getParam("message", true)->value();
msg.trim();
String sourceHeader = request->header("X-Source");
bool isFromUI = (sourceHeader == "UI");
bool isFromHA = !isFromUI;
// --- Local speed variable (does not modify global GENERAL_SCROLL_SPEED) ---
int localSpeed = GENERAL_SCROLL_SPEED; // Default for UI messages
if (request->hasParam("speed", true)) {
localSpeed = constrain(request->getParam("speed", true)->value().toInt(), 10, 200);
Serial.printf("[MESSAGE] Custom Message scroll speed set to %d\n", localSpeed);
}
// --- CLEAR MESSAGE ---
if (msg.length() == 0) {
if (isFromUI) {
// Web UI clear: remove everything
customMessage[0] = '\0';
lastPersistentMessage[0] = '\0';
displayMode = 0;
Serial.println(F("[MESSAGE] All messages cleared by UI. Returning to normal mode."));
request->send(200, "text/plain", "CLEARED (UI)");
// --- SAVE CLEAR STATE ---
saveCustomMessageToConfig("");
} else {
// HA clear: remove only temporary message
customMessage[0] = '\0';
if (strlen(lastPersistentMessage) > 0) {
// Restore the last persistent message
strncpy(customMessage, lastPersistentMessage, sizeof(customMessage));
messageScrollSpeed = GENERAL_SCROLL_SPEED; // Use global speed for persistent
Serial.printf("[MESSAGE] Temporary HA message cleared. Restored persistent message: '%s' (speed=%d)\n",
customMessage, messageScrollSpeed);
request->send(200, "text/plain", "CLEARED (HA temporary, persistent restored)");
} else {
displayMode = 0;
Serial.println(F("[MESSAGE] Temporary HA message cleared. No persistent message to restore."));
request->send(200, "text/plain", "CLEARED (HA temporary, no persistent)");
}
}
return;
}
// --- SANITIZE MESSAGE ---
msg.toUpperCase();
String filtered = "";
for (size_t i = 0; i < msg.length(); i++) {
char c = msg[i];
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == ' ' || c == ':' || c == '!' || c == '\'' || c == '-' || c == '.' || c == ',' || c == '_' || c == '+' || c == '%' || c == '/' || c == '?') {
filtered += c;
}
}
filtered.toCharArray(customMessage, sizeof(customMessage));
// --- STORE MESSAGE ---
if (isFromHA) {
// --- Preserve the current persistent message before overwriting ---
char prevMessage[sizeof(customMessage)];
strlcpy(prevMessage, customMessage, sizeof(prevMessage));
// --- Overwrite customMessage with new temporary HA message ---
filtered.toCharArray(customMessage, sizeof(customMessage));
messageScrollSpeed = localSpeed; // Use HA-specified scroll speed
// --- If no persistent message stored yet, keep the previous one ---
if (strlen(lastPersistentMessage) == 0 && strlen(prevMessage) > 0) {
strlcpy(lastPersistentMessage, prevMessage, sizeof(lastPersistentMessage));
}
Serial.printf("[HA] Temporary HA message received: %s (persistent=%s)\n",
customMessage, lastPersistentMessage);
} else {
// --- UI-originated message: permanent ---
filtered.toCharArray(customMessage, sizeof(customMessage));
strlcpy(lastPersistentMessage, customMessage, sizeof(lastPersistentMessage));
messageScrollSpeed = GENERAL_SCROLL_SPEED; // Always global for UI
Serial.printf("[UI] Persistent message stored: %s (speed=%d)\n",
customMessage, messageScrollSpeed);
// --- Persist to config.json immediately ---
saveCustomMessageToConfig(customMessage);
}
// --- Activate display ---
displayMode = 6;
prevDisplayMode = 0;
String response = String(isFromHA ? "OK (HA message, speed=" : "OK (UI message, speed=") + String(localSpeed) + ")";
request->send(200, "text/plain", response);
} else {
Serial.println(F("[MESSAGE] Error: missing 'message' parameter in request."));
request->send(400, "text/plain", "Missing message parameter");
}
});
server.on("/uptime", HTTP_GET, [](AsyncWebServerRequest *request) {
if (!LittleFS.exists("/uptime.dat")) {
request->send(200, "text/plain", "No uptime recorded yet.");
@@ -1291,6 +1412,7 @@ void setupWebServer() {
Serial.println(F("[WEBSERVER] Web server started"));
}
void handleCaptivePortal(AsyncWebServerRequest *request) {
Serial.print(F("[WEBSERVER] Captive Portal triggered for URL: "));
Serial.println(request->url());
@@ -1307,6 +1429,7 @@ void handleCaptivePortal(AsyncWebServerRequest *request) {
}
}
String normalizeWeatherDescription(String str) {
// Serbian Cyrillic → Latin
str.replace("а", "a");
@@ -1467,7 +1590,6 @@ bool isFiveDigitZip(const char *str) {
}
// -----------------------------------------------------------------------------
// Weather Fetching and API settings
// -----------------------------------------------------------------------------
@@ -1506,7 +1628,6 @@ String buildWeatherURL() {
}
void fetchWeather() {
Serial.println(F("[WEATHER] Fetching weather data..."));
if (WiFi.status() != WL_CONNECTED) {
@@ -1603,6 +1724,7 @@ void fetchWeather() {
http.end();
}
void loadUptime() {
if (LittleFS.exists("/uptime.dat")) {
File f = LittleFS.open("/uptime.dat", "r");
@@ -1620,6 +1742,7 @@ void loadUptime() {
}
}
void saveUptime() {
// Add runtime since boot to total
unsigned long runtimeSeconds = (millis() - bootMillis) / 1000;
@@ -1639,6 +1762,41 @@ void saveUptime() {
}
}
void saveCustomMessageToConfig(const char *msg) {
Serial.println(F("[CONFIG] Updating customMessage in config.json..."));
DynamicJsonDocument doc(2048);
// Load existing config.json (if present)
File configFile = LittleFS.open("/config.json", "r");
if (configFile) {
DeserializationError err = deserializeJson(doc, configFile);
configFile.close();
if (err) {
Serial.print(F("[CONFIG] Error reading existing config: "));
Serial.println(err.f_str());
}
}
// Update only customMessage
doc["customMessage"] = msg;
// Safely write back to config.json
if (LittleFS.exists("/config.json")) {
LittleFS.rename("/config.json", "/config.bak");
}
File f = LittleFS.open("/config.json", "w");
if (!f) {
Serial.println(F("[CONFIG] ERROR: Failed to open /config.json for writing"));
return;
}
size_t bytesWritten = serializeJson(doc, f);
f.close();
Serial.printf("[CONFIG] Saved customMessage='%s' (%u bytes written)\n", msg, bytesWritten);
}
// Returns formatted uptime (for web UI or logs)
String formatUptime(unsigned long seconds) {
unsigned long days = seconds / 86400;
@@ -1655,7 +1813,6 @@ String formatUptime(unsigned long seconds) {
}
// -----------------------------------------------------------------------------
// Main setup() and loop()
// -----------------------------------------------------------------------------
@@ -1664,9 +1821,11 @@ DisplayMode key:
0: Clock
1: Weather
2: Weather Description
3: Countdown (NEW)
3: Countdown
4: Nightscout
5: Date
6: Custom Message
*/
void setup() {
Serial.begin(115200);
delay(1000);
@@ -1718,8 +1877,8 @@ void setup() {
}
void advanceDisplayMode() {
prevDisplayMode = displayMode;
int oldMode = displayMode;
String ntpField = String(ntpServer2);
bool nightscoutConfigured = ntpField.startsWith("https://");
@@ -1788,18 +1947,26 @@ void advanceDisplayMode() {
displayMode = 0;
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Countdown)"));
}
} else if (displayMode == 4) { // Nightscout -> Clock
} else if (displayMode == 4) { // Nightscout -> Custom Message
displayMode = 6;
Serial.println(F("[DISPLAY] Switching to display mode: CUSTOM MESSAGE (from Nightscout)"));
} else if (displayMode == 6) { // Custom Message -> Clock
displayMode = 0;
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Nightscout)"));
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Custom Message)"));
}
// --- Common cleanup/reset logic remains the same ---
if ((displayMode == 0) && strlen(customMessage) > 0 && oldMode != 6) {
displayMode = 6;
Serial.println(F("[DISPLAY] Custom Message display before returning to CLOCK"));
}
lastSwitch = millis();
}
void advanceDisplayModeSafe() {
int attempts = 0;
const int MAX_ATTEMPTS = 6; // Number of possible modes + 1
const int MAX_ATTEMPTS = 7; // Number of possible modes + 1
int startMode = displayMode;
bool valid = false;
do {
@@ -1816,6 +1983,7 @@ void advanceDisplayModeSafe() {
else if (displayMode == 2 && showWeatherDescription && weatherAvailable && weatherDescription.length() > 0) valid = true;
else if (displayMode == 3 && countdownEnabled && !countdownFinished && ntpSyncSuccessful) valid = true;
else if (displayMode == 4 && nightscoutConfigured) valid = true;
else if (displayMode == 6 && strlen(customMessage) > 0) valid = true;
// If we've looped back to where we started, break to avoid infinite loop
if (displayMode == startMode) break;
@@ -1831,6 +1999,7 @@ void advanceDisplayModeSafe() {
lastSwitch = millis();
}
//config save after countdown finishes
bool saveCountdownConfig(bool enabled, time_t targetTimestamp, const String &label) {
DynamicJsonDocument doc(2048);
@@ -1894,7 +2063,6 @@ void loop() {
const unsigned long fetchInterval = 300000; // 5 minutes
// AP Mode animation
static unsigned long apAnimTimer = 0;
static int apAnimFrame = 0;
@@ -1977,7 +2145,6 @@ void loop() {
}
// --- IMMEDIATE COUNTDOWN FINISH TRIGGER ---
if (countdownEnabled && !countdownFinished && ntpSyncSuccessful && countdownTargetTimestamp > 0 && now_time >= countdownTargetTimestamp) {
countdownFinished = true;
@@ -2011,7 +2178,6 @@ void loop() {
}
// --- BRIGHTNESS/OFF CHECK ---
if (brightness == -1) {
if (!displayOff) {
@@ -2024,7 +2190,6 @@ void loop() {
}
// --- NTP State Machine ---
switch (ntpState) {
case NTP_IDLE: break;
@@ -2097,7 +2262,6 @@ void loop() {
}
// --- MODIFIED WEATHER FETCHING LOGIC ---
if (WiFi.status() == WL_CONNECTED) {
if (!weatherFetchInitiated || shouldFetchWeatherNow || (millis() - lastFetch > fetchInterval)) {
@@ -2177,11 +2341,6 @@ void loop() {
}
// Persistent variables (declare near top of file or loop)
static int prevDisplayMode = -1;
static bool clockScrollDone = false;
// --- CLOCK Display Mode ---
if (displayMode == 0) {
P.setCharSpacing(0);
@@ -2231,6 +2390,8 @@ void loop() {
shouldScrollIn = true; // first boot or other special modes
} else if (prevDisplayMode == 2 && weatherDescription.length() > 8) {
shouldScrollIn = true; // only scroll in if weather was scrolling
} else if (prevDisplayMode == 6) {
shouldScrollIn = true; // scroll in when coming from custom message
}
if (shouldScrollIn && !clockScrollDone) {
@@ -2259,10 +2420,6 @@ void loop() {
}
}
// --- update prevDisplayMode ---
prevDisplayMode = displayMode;
// --- WEATHER Display Mode ---
static bool weatherWasAvailable = false;
@@ -2299,12 +2456,22 @@ void loop() {
}
// --- WEATHER DESCRIPTION Display Mode ---
if (displayMode == 2 && showWeatherDescription && weatherAvailable && weatherDescription.length() > 0) {
String desc = weatherDescription;
// --- Check if humidity is actually visible ---
bool humidityVisible = showHumidity && weatherAvailable && strlen(openWeatherApiKey) == 32 && strlen(openWeatherCity) > 0 && strlen(openWeatherCountry) > 0;
// --- Conditional padding ---
bool addPadding = false;
if (prevDisplayMode == 1 && humidityVisible) {
addPadding = true;
}
if (addPadding) {
desc = " " + desc; // 4-space padding before scrolling
}
// prepare safe buffer
static char descBuffer[128]; // large enough for OWM translations
desc.toCharArray(descBuffer, sizeof(descBuffer));
@@ -2617,10 +2784,17 @@ void loop() {
}
String fullString = String(buf);
bool addPadding = false;
bool humidityVisible = showHumidity && weatherAvailable && strlen(openWeatherApiKey) == 32 && strlen(openWeatherCity) > 0 && strlen(openWeatherCountry) > 0;
// --- Add a leading space only if showDayOfWeek is true ---
if (showDayOfWeek) {
fullString = " " + fullString;
// Padding logic
if (prevDisplayMode == 0 && (showDayOfWeek || colonBlinkEnabled)) {
addPadding = true;
} else if (prevDisplayMode == 1 && humidityVisible) {
addPadding = true;
}
if (addPadding) {
fullString = " " + fullString; // 4 spaces
}
// Display the full string and scroll it
@@ -2898,6 +3072,53 @@ void loop() {
}
}
// --- Custom Message Display Mode (displayMode == 6) ---
if (displayMode == 6) {
if (strlen(customMessage) == 0) {
advanceDisplayMode();
yield();
return;
}
String msg = String(customMessage);
// Replace standard digits 09 with your custom font character codes
for (int i = 0; i < msg.length(); i++) {
if (isDigit(msg[i])) {
int num = msg[i] - '0'; // 09
msg[i] = 145 + ((num + 9) % 10); // Maps 0→154, 1→145, ... 9→153
}
}
// --- Determine if we need left padding based on previous mode ---
bool addPadding = false;
bool humidityVisible = showHumidity && weatherAvailable && strlen(openWeatherApiKey) == 32 && strlen(openWeatherCity) > 0 && strlen(openWeatherCountry) > 0;
// If coming from CLOCK mode
if (prevDisplayMode == 0 && (showDayOfWeek || colonBlinkEnabled)) {
addPadding = true;
} else if (prevDisplayMode == 1 && humidityVisible) {
addPadding = true;
}
// Apply padding (4 spaces) if needed
if (addPadding) {
msg = " " + msg;
}
// --- Display scrolling message ---
P.setTextAlignment(PA_LEFT);
P.setCharSpacing(1);
textEffect_t actualScrollDirection = getEffectiveScrollDirection(PA_SCROLL_LEFT, flipDisplay);
extern int messageScrollSpeed; // declare globally once at the top of your sketch
P.displayScroll(msg.c_str(), PA_LEFT, actualScrollDirection, messageScrollSpeed);
while (!P.displayAnimate()) yield();
P.setTextAlignment(PA_CENTER);
advanceDisplayMode();
yield();
return;
}
unsigned long currentMillis = millis();
unsigned long runtimeSeconds = (currentMillis - bootMillis) / 1000;
unsigned long currentTotal = totalUptimeSeconds + runtimeSeconds;

File diff suppressed because one or more lines are too long

View File

@@ -47,7 +47,7 @@ MD_MAX72XX::fontType_t mFactory[] PROGMEM =
17, 130, 186, 198, 254, 134, 234, 134, 254, 250, 130, 250, 254, 134, 234, 134, 254, 124, // 41 - ')'
20, 250, 130, 250, 254, 130, 170, 186, 254, 130, 250, 226, 250, 134, 254, 130, 234, 234, 246, 254, 124, // 42 - '*'
5, 8, 8, 62, 8, 8, // 43 - '+'
1, 192, // 44 - ','
2, 128, 64, // 44 - ','
2, 8, 8, // 45 - '-'
1, 64, // 46 - '.'
3, 96, 24, 6, // 47 - '/'

View File

@@ -28,9 +28,9 @@ MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);
AsyncWebServer server(80);
// --- Global Scroll Speed Settings ---
const int GENERAL_SCROLL_SPEED = 85; // Default: Adjust this for Weather Description and Countdown Label (e.g., 50 for faster, 200 for slower)
const int IP_SCROLL_SPEED = 115; // Default: Adjust this for the IP Address display (slower for readability)
const int GENERAL_SCROLL_SPEED = 85; // Default: Adjust this for Weather Description and Countdown Label (e.g., 50 for faster, 200 for slower)
const int IP_SCROLL_SPEED = 115; // Default: Adjust this for the IP Address display (slower for readability)
int messageScrollSpeed = 85; // default fallback
// --- Nightscout setting ---
const unsigned int NIGHTSCOUT_IDLE_THRESHOLD_MIN = 10; // minutes before data is considered outdated
@@ -60,6 +60,8 @@ bool showHumidity = false;
bool colonBlinkEnabled = true;
char ntpServer1[64] = "pool.ntp.org";
char ntpServer2[256] = "time.nist.gov";
char customMessage[121] = "";
char lastPersistentMessage[128] = "";
// Dimming
bool dimmingEnabled = false;
@@ -102,6 +104,8 @@ bool shouldFetchWeatherNow = false;
unsigned long lastSwitch = 0;
unsigned long lastColonBlink = 0;
int displayMode = 0; // 0: Clock, 1: Weather, 2: Weather Description, 3: Countdown
int prevDisplayMode = -1;
bool clockScrollDone = false;
int currentHumidity = -1;
bool ntpSyncSuccessful = false;
@@ -148,7 +152,11 @@ const unsigned long descriptionScrollPause = 300; // 300ms pause after scroll
// --- Safe WiFi credential and API getters ---
const char *getSafeSsid() {
return isAPMode ? "********" : ssid;
if (isAPMode && strlen(ssid) == 0) {
return "";
} else {
return isAPMode ? "********" : ssid;
}
}
const char *getSafePassword() {
@@ -253,10 +261,12 @@ void loadConfig() {
strlcpy(ssid, doc["ssid"] | "", sizeof(ssid));
strlcpy(password, doc["password"] | "", sizeof(password));
strlcpy(openWeatherApiKey, doc["openWeatherApiKey"] | "", sizeof(openWeatherApiKey)); // Corrected typo here
strlcpy(openWeatherApiKey, doc["openWeatherApiKey"] | "", sizeof(openWeatherApiKey));
strlcpy(openWeatherCity, doc["openWeatherCity"] | "", sizeof(openWeatherCity));
strlcpy(openWeatherCountry, doc["openWeatherCountry"] | "", sizeof(openWeatherCountry));
strlcpy(weatherUnits, doc["weatherUnits"] | "metric", sizeof(weatherUnits));
strlcpy(customMessage, doc["customMessage"] | "", sizeof(customMessage));
strlcpy(lastPersistentMessage, customMessage, sizeof(lastPersistentMessage));
clockDuration = doc["clockDuration"] | 10000;
weatherDuration = doc["weatherDuration"] | 5000;
strlcpy(timeZone, doc["timeZone"] | "Etc/UTC", sizeof(timeZone));
@@ -327,7 +337,6 @@ void loadConfig() {
}
// -----------------------------------------------------------------------------
// WiFi Setup
// -----------------------------------------------------------------------------
@@ -484,6 +493,7 @@ void clearWiFiCredentialsInConfig() {
Serial.println(F("[SECURITY] Cleared WiFi credentials in config.json."));
}
// -----------------------------------------------------------------------------
// Time / NTP Functions
// -----------------------------------------------------------------------------
@@ -521,6 +531,7 @@ void setupTime() {
}
}
// -----------------------------------------------------------------------------
// Utility
// -----------------------------------------------------------------------------
@@ -586,6 +597,8 @@ void printConfigToSerial() {
Serial.println(countdownLabel);
Serial.print(F("Dramatic Countdown Display: "));
Serial.println(isDramaticCountdown ? "Yes" : "No");
Serial.print(F("Custom Message: "));
Serial.println(customMessage);
Serial.print(F("Total Runtime: "));
if (totalUptimeSeconds > 0) {
Serial.println(formatUptime(totalUptimeSeconds));
@@ -795,6 +808,7 @@ void setupWebServer() {
Serial.println(F("[SAVE] Config verification successful."));
DynamicJsonDocument okDoc(128);
strlcpy(customMessage, doc["customMessage"] | "", sizeof(customMessage));
okDoc[F("message")] = "Saved successfully. Rebooting...";
String response;
serializeJson(okDoc, response);
@@ -1105,6 +1119,110 @@ void setupWebServer() {
request->send(200, "application/json", "{\"ok\":true}");
});
// --- Custom Message Endpoint ---
server.on("/set_custom_message", HTTP_POST, [](AsyncWebServerRequest *request) {
if (request->hasParam("message", true)) {
String msg = request->getParam("message", true)->value();
msg.trim();
String sourceHeader = request->header("X-Source");
bool isFromUI = (sourceHeader == "UI");
bool isFromHA = !isFromUI;
// --- Local speed variable (does not modify global GENERAL_SCROLL_SPEED) ---
int localSpeed = GENERAL_SCROLL_SPEED; // Default for UI messages
if (request->hasParam("speed", true)) {
localSpeed = constrain(request->getParam("speed", true)->value().toInt(), 10, 200);
Serial.printf("[MESSAGE] Custom Message scroll speed set to %d\n", localSpeed);
}
// --- CLEAR MESSAGE ---
if (msg.length() == 0) {
if (isFromUI) {
// Web UI clear: remove everything
customMessage[0] = '\0';
lastPersistentMessage[0] = '\0';
displayMode = 0;
Serial.println(F("[MESSAGE] All messages cleared by UI. Returning to normal mode."));
request->send(200, "text/plain", "CLEARED (UI)");
// --- SAVE CLEAR STATE ---
saveCustomMessageToConfig("");
} else {
// HA clear: remove only temporary message
customMessage[0] = '\0';
if (strlen(lastPersistentMessage) > 0) {
// Restore the last persistent message
strncpy(customMessage, lastPersistentMessage, sizeof(customMessage));
messageScrollSpeed = GENERAL_SCROLL_SPEED; // Use global speed for persistent
Serial.printf("[MESSAGE] Temporary HA message cleared. Restored persistent message: '%s' (speed=%d)\n",
customMessage, messageScrollSpeed);
request->send(200, "text/plain", "CLEARED (HA temporary, persistent restored)");
} else {
displayMode = 0;
Serial.println(F("[MESSAGE] Temporary HA message cleared. No persistent message to restore."));
request->send(200, "text/plain", "CLEARED (HA temporary, no persistent)");
}
}
return;
}
// --- SANITIZE MESSAGE ---
msg.toUpperCase();
String filtered = "";
for (size_t i = 0; i < msg.length(); i++) {
char c = msg[i];
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == ' ' || c == ':' || c == '!' || c == '\'' || c == '-' || c == '.' || c == ',' || c == '_' || c == '+' || c == '%' || c == '/' || c == '?') {
filtered += c;
}
}
filtered.toCharArray(customMessage, sizeof(customMessage));
// --- STORE MESSAGE ---
if (isFromHA) {
// --- Preserve the current persistent message before overwriting ---
char prevMessage[sizeof(customMessage)];
strlcpy(prevMessage, customMessage, sizeof(prevMessage));
// --- Overwrite customMessage with new temporary HA message ---
filtered.toCharArray(customMessage, sizeof(customMessage));
messageScrollSpeed = localSpeed; // Use HA-specified scroll speed
// --- If no persistent message stored yet, keep the previous one ---
if (strlen(lastPersistentMessage) == 0 && strlen(prevMessage) > 0) {
strlcpy(lastPersistentMessage, prevMessage, sizeof(lastPersistentMessage));
}
Serial.printf("[HA] Temporary HA message received: %s (persistent=%s)\n",
customMessage, lastPersistentMessage);
} else {
// --- UI-originated message: permanent ---
filtered.toCharArray(customMessage, sizeof(customMessage));
strlcpy(lastPersistentMessage, customMessage, sizeof(lastPersistentMessage));
messageScrollSpeed = GENERAL_SCROLL_SPEED; // Always global for UI
Serial.printf("[UI] Persistent message stored: %s (speed=%d)\n",
customMessage, messageScrollSpeed);
// --- Persist to config.json immediately ---
saveCustomMessageToConfig(customMessage);
}
// --- Activate display ---
displayMode = 6;
prevDisplayMode = 0;
String response = String(isFromHA ? "OK (HA message, speed=" : "OK (UI message, speed=") + String(localSpeed) + ")";
request->send(200, "text/plain", response);
} else {
Serial.println(F("[MESSAGE] Error: missing 'message' parameter in request."));
request->send(400, "text/plain", "Missing message parameter");
}
});
server.on("/uptime", HTTP_GET, [](AsyncWebServerRequest *request) {
if (!LittleFS.exists("/uptime.dat")) {
request->send(200, "text/plain", "No uptime recorded yet.");
@@ -1604,6 +1722,7 @@ void fetchWeather() {
http.end();
}
void loadUptime() {
if (LittleFS.exists("/uptime.dat")) {
File f = LittleFS.open("/uptime.dat", "r");
@@ -1640,6 +1759,41 @@ void saveUptime() {
}
}
void saveCustomMessageToConfig(const char *msg) {
Serial.println(F("[CONFIG] Updating customMessage in config.json..."));
DynamicJsonDocument doc(2048);
// Load existing config.json (if present)
File configFile = LittleFS.open("/config.json", "r");
if (configFile) {
DeserializationError err = deserializeJson(doc, configFile);
configFile.close();
if (err) {
Serial.print(F("[CONFIG] Error reading existing config: "));
Serial.println(err.f_str());
}
}
// Update only customMessage
doc["customMessage"] = msg;
// Safely write back to config.json
if (LittleFS.exists("/config.json")) {
LittleFS.rename("/config.json", "/config.bak");
}
File f = LittleFS.open("/config.json", "w");
if (!f) {
Serial.println(F("[CONFIG] ERROR: Failed to open /config.json for writing"));
return;
}
size_t bytesWritten = serializeJson(doc, f);
f.close();
Serial.printf("[CONFIG] Saved customMessage='%s' (%u bytes written)\n", msg, bytesWritten);
}
// Returns formatted uptime (for web UI or logs)
String formatUptime(unsigned long seconds) {
unsigned long days = seconds / 86400;
@@ -1656,7 +1810,6 @@ String formatUptime(unsigned long seconds) {
}
// -----------------------------------------------------------------------------
// Main setup() and loop()
// -----------------------------------------------------------------------------
@@ -1667,8 +1820,9 @@ DisplayMode key:
2: Weather Description
3: Countdown
4: Nightscout
5: Date
6: Custom Message
*/
void setup() {
Serial.begin(115200);
delay(1000);
@@ -1719,7 +1873,9 @@ void setup() {
saveUptime();
}
void advanceDisplayMode() {
prevDisplayMode = displayMode;
int oldMode = displayMode;
String ntpField = String(ntpServer2);
bool nightscoutConfigured = ntpField.startsWith("https://");
@@ -1788,18 +1944,26 @@ void advanceDisplayMode() {
displayMode = 0;
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Countdown)"));
}
} else if (displayMode == 4) { // Nightscout -> Clock
} else if (displayMode == 4) { // Nightscout -> Custom Message
displayMode = 6;
Serial.println(F("[DISPLAY] Switching to display mode: CUSTOM MESSAGE (from Nightscout)"));
} else if (displayMode == 6) { // Custom Message -> Clock
displayMode = 0;
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Nightscout)"));
Serial.println(F("[DISPLAY] Switching to display mode: CLOCK (from Custom Message)"));
}
// --- Common cleanup/reset logic remains the same ---
if ((displayMode == 0) && strlen(customMessage) > 0 && oldMode != 6) {
displayMode = 6;
Serial.println(F("[DISPLAY] Custom Message display before returning to CLOCK"));
}
lastSwitch = millis();
}
void advanceDisplayModeSafe() {
int attempts = 0;
const int MAX_ATTEMPTS = 6; // Number of possible modes + 1
const int MAX_ATTEMPTS = 7; // Number of possible modes + 1
int startMode = displayMode;
bool valid = false;
do {
@@ -1816,6 +1980,7 @@ void advanceDisplayModeSafe() {
else if (displayMode == 2 && showWeatherDescription && weatherAvailable && weatherDescription.length() > 0) valid = true;
else if (displayMode == 3 && countdownEnabled && !countdownFinished && ntpSyncSuccessful) valid = true;
else if (displayMode == 4 && nightscoutConfigured) valid = true;
else if (displayMode == 6 && strlen(customMessage) > 0) valid = true;
// If we've looped back to where we started, break to avoid infinite loop
if (displayMode == startMode) break;
@@ -1831,6 +1996,7 @@ void advanceDisplayModeSafe() {
lastSwitch = millis();
}
//config save after countdown finishes
bool saveCountdownConfig(bool enabled, time_t targetTimestamp, const String &label) {
DynamicJsonDocument doc(2048);
@@ -1894,7 +2060,6 @@ void loop() {
const unsigned long fetchInterval = 300000; // 5 minutes
// AP Mode animation
static unsigned long apAnimTimer = 0;
static int apAnimFrame = 0;
@@ -2086,7 +2251,6 @@ void loop() {
}
// Only advance mode by timer for clock/weather, not description!
unsigned long displayDuration = (displayMode == 0) ? clockDuration : weatherDuration;
if ((displayMode == 0 || displayMode == 1) && millis() - lastSwitch > displayDuration) {
@@ -2094,7 +2258,6 @@ void loop() {
}
// --- MODIFIED WEATHER FETCHING LOGIC ---
if (WiFi.status() == WL_CONNECTED) {
if (!weatherFetchInitiated || shouldFetchWeatherNow || (millis() - lastFetch > fetchInterval)) {
@@ -2117,7 +2280,6 @@ void loop() {
}
const char *const *daysOfTheWeek = getDaysOfWeek(language);
const char *daySymbol = daysOfTheWeek[timeinfo.tm_wday];
@@ -2174,9 +2336,6 @@ void loop() {
advanceDisplayMode();
}
// Persistent variables (declare near top of file or loop)
static int prevDisplayMode = -1;
static bool clockScrollDone = false;
// --- CLOCK Display Mode ---
if (displayMode == 0) {
@@ -2226,6 +2385,8 @@ void loop() {
shouldScrollIn = true; // first boot or other special modes
} else if (prevDisplayMode == 2 && weatherDescription.length() > 8) {
shouldScrollIn = true; // only scroll in if weather was scrolling
} else if (prevDisplayMode == 6) {
shouldScrollIn = true; // scroll in when coming from custom message
}
if (shouldScrollIn && !clockScrollDone) {
@@ -2254,9 +2415,6 @@ void loop() {
}
}
// --- update prevDisplayMode ---
prevDisplayMode = displayMode;
// --- WEATHER Display Mode ---
static bool weatherWasAvailable = false;
@@ -2297,6 +2455,18 @@ void loop() {
if (displayMode == 2 && showWeatherDescription && weatherAvailable && weatherDescription.length() > 0) {
String desc = weatherDescription;
// --- Check if humidity is actually visible ---
bool humidityVisible = showHumidity && weatherAvailable && strlen(openWeatherApiKey) == 32 && strlen(openWeatherCity) > 0 && strlen(openWeatherCountry) > 0;
// --- Conditional padding ---
bool addPadding = false;
if (prevDisplayMode == 1 && humidityVisible) {
addPadding = true;
}
if (addPadding) {
desc = " " + desc; // 4-space padding before scrolling
}
// prepare safe buffer
static char descBuffer[128]; // large enough for OWM translations
desc.toCharArray(descBuffer, sizeof(descBuffer));
@@ -2609,10 +2779,17 @@ void loop() {
}
String fullString = String(buf);
bool addPadding = false;
bool humidityVisible = showHumidity && weatherAvailable && strlen(openWeatherApiKey) == 32 && strlen(openWeatherCity) > 0 && strlen(openWeatherCountry) > 0;
// --- Add a leading space only if showDayOfWeek is true ---
if (showDayOfWeek) {
fullString = " " + fullString;
// Padding logic
if (prevDisplayMode == 0 && (showDayOfWeek || colonBlinkEnabled)) {
addPadding = true;
} else if (prevDisplayMode == 1 && humidityVisible) {
addPadding = true;
}
if (addPadding) {
fullString = " " + fullString; // 4 spaces
}
// Display the full string and scroll it
@@ -2890,6 +3067,53 @@ void loop() {
}
}
// --- Custom Message Display Mode (displayMode == 6) ---
if (displayMode == 6) {
if (strlen(customMessage) == 0) {
advanceDisplayMode();
yield();
return;
}
String msg = String(customMessage);
// Replace standard digits 09 with your custom font character codes
for (int i = 0; i < msg.length(); i++) {
if (isDigit(msg[i])) {
int num = msg[i] - '0'; // 09
msg[i] = 145 + ((num + 9) % 10); // Maps 0→154, 1→145, ... 9→153
}
}
// --- Determine if we need left padding based on previous mode ---
bool addPadding = false;
bool humidityVisible = showHumidity && weatherAvailable && strlen(openWeatherApiKey) == 32 && strlen(openWeatherCity) > 0 && strlen(openWeatherCountry) > 0;
// If coming from CLOCK mode
if (prevDisplayMode == 0 && (showDayOfWeek || colonBlinkEnabled)) {
addPadding = true;
} else if (prevDisplayMode == 1 && humidityVisible) {
addPadding = true;
}
// Apply padding (4 spaces) if needed
if (addPadding) {
msg = " " + msg;
}
// --- Display scrolling message ---
P.setTextAlignment(PA_LEFT);
P.setCharSpacing(1);
textEffect_t actualScrollDirection = getEffectiveScrollDirection(PA_SCROLL_LEFT, flipDisplay);
extern int messageScrollSpeed; // declare globally once at the top of your sketch
P.displayScroll(msg.c_str(), PA_LEFT, actualScrollDirection, messageScrollSpeed);
while (!P.displayAnimate()) yield();
P.setTextAlignment(PA_CENTER);
advanceDisplayMode();
yield();
return;
}
unsigned long currentMillis = millis();
unsigned long runtimeSeconds = (currentMillis - bootMillis) / 1000;
unsigned long currentTotal = totalUptimeSeconds + runtimeSeconds;

File diff suppressed because one or more lines are too long

View File

@@ -47,7 +47,7 @@ MD_MAX72XX::fontType_t mFactory[] PROGMEM =
17, 130, 186, 198, 254, 134, 234, 134, 254, 250, 130, 250, 254, 134, 234, 134, 254, 124, // 41 - ')'
20, 250, 130, 250, 254, 130, 170, 186, 254, 130, 250, 226, 250, 134, 254, 130, 234, 234, 246, 254, 124, // 42 - '*'
5, 8, 8, 62, 8, 8, // 43 - '+'
1, 192, // 44 - ','
2, 128, 64, // 44 - ','
2, 8, 8, // 45 - '-'
1, 64, // 46 - '.'
3, 96, 24, 6, // 47 - '/'