From 5bfe3890f28d0a62a6d18a8f63cc32c34234343a Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Sat, 27 Jun 2026 01:33:48 +0200 Subject: [PATCH 1/8] Detach freed PWM channels in attachMeters() --- src/main.cpp | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 6e23cb7..5a8700b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -138,12 +138,29 @@ static void saveConfig() { // PWM helpers // --------------------------------------------------------------------------- +// Tracks the GPIO currently bound to each PWM channel so channels that get +// freed (meter count shrinks, or a pin is changed/cleared) are detached +// instead of being left driving PWM on their old pin. +static int8_t attachedPin[8] = { -1, -1, -1, -1, -1, -1, -1, -1 }; + static void attachMeters() { - for (int i = 0; i < meterCount && i < 8; i++) { - if (meters[i].pin > 0) { + for (int i = 0; i < 8; i++) { + int pin = (i < meterCount && meters[i].pin > 0) ? meters[i].pin : -1; + + // Release the channel if it's no longer used or its pin changed. + if (attachedPin[i] != -1 && attachedPin[i] != pin) { + ledcDetachPin(attachedPin[i]); + pinMode(attachedPin[i], OUTPUT); + digitalWrite(attachedPin[i], LOW); // drive freed pin low so the meter reads zero + Serial.printf("[PWM] detach ch%d pin%d\n", i, attachedPin[i]); + attachedPin[i] = -1; + } + + if (pin > 0 && attachedPin[i] != pin) { ledcSetup(i, PWM_FREQ, PWM_RES); - ledcAttachPin(meters[i].pin, i); - Serial.printf("[PWM] attach ch%d pin%d\n", i, meters[i].pin); + ledcAttachPin(pin, i); + attachedPin[i] = pin; + Serial.printf("[PWM] attach ch%d pin%d\n", i, pin); } } } From a09f1dec878ec44a433047fa61117e75984743fe Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Sat, 27 Jun 2026 01:34:57 +0200 Subject: [PATCH 2/8] Clear retained HA discovery for removed meters --- src/main.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 5a8700b..f71d842 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -272,6 +272,16 @@ static void mqttPublishDiscovery() { bool pubOk = mqttClient.publish(topic, payload, true); Serial.printf("[MQTT] discovery %s -> %s (ok=%d)\n", topic, payload, pubOk); } + + // Clear retained discovery for slots no longer in use (meters removed) so + // Home Assistant drops the stale entities instead of keeping them forever. + for (int i = meterCount; i < MAX_METERS; i++) { + String objId = String(devId) + "_meter_" + String(i) + "_current"; + char topic[128]; + snprintf(topic, sizeof(topic), "homeassistant/number/%s/config", objId.c_str()); + mqttClient.publish(topic, "", true); + Serial.printf("[MQTT] discovery clear %s\n", topic); + } } static void mqttSubscribe() { From 5450d21c34cf9035e89366d1ffa9a17db4a1b441 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Sat, 27 Jun 2026 01:36:47 +0200 Subject: [PATCH 3/8] Shorten MQTT probe timeout to limit web server stalls --- src/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index f71d842..210e399 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #define PWM_RES 10 #define MQTT_MANUFACTURER "Baumann Enkataleiptics" #define AUTH_USERNAME "admin" +#define MQTT_PROBE_TIMEOUT_MS 400 // keep short: blocks loop()/handleClient() per reconnect attempt struct MeterConfig { int pin; @@ -340,7 +341,7 @@ static void mqttLoop() { if (now > mqttReconnectAt) { Serial.printf("[MQTT] probing %s:%d\n", mqttCfg.host, mqttCfg.port); WiFiClient probe; - bool reachable = probe.connect(mqttCfg.host, mqttCfg.port, 1500); + bool reachable = probe.connect(mqttCfg.host, mqttCfg.port, MQTT_PROBE_TIMEOUT_MS); probe.stop(); if (reachable) { From 409eeb489540a38b4ea21f9b1a416670fbba8f2b Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Sat, 27 Jun 2026 01:39:26 +0200 Subject: [PATCH 4/8] Allow anonymous MQTT broker connections --- src/main.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 210e399..21f9f19 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -296,9 +296,9 @@ static void mqttSubscribe() { } static bool mqttConnect() { - if (!mqttCfg.enabled || strlen(mqttCfg.host) == 0 || strlen(mqttCfg.user) == 0 || strlen(mqttCfg.pass) == 0) { - Serial.printf("[MQTT] connect skipped en=%d host=%d user=%d pass=%d\n", - mqttCfg.enabled, strlen(mqttCfg.host) > 0, strlen(mqttCfg.user) > 0, strlen(mqttCfg.pass) > 0); + if (!mqttCfg.enabled || strlen(mqttCfg.host) == 0) { + Serial.printf("[MQTT] connect skipped en=%d host=%d\n", + mqttCfg.enabled, strlen(mqttCfg.host) > 0); return false; } @@ -316,8 +316,13 @@ static bool mqttConnect() { mqttClient.setCallback(mqttCallback); mqttClient.setBufferSize(1024); + // Pass NULL for empty credentials so the broker treats it as an anonymous + // connection rather than an empty-string login (which some brokers reject). + const char* user = strlen(mqttCfg.user) > 0 ? mqttCfg.user : nullptr; + const char* pass = strlen(mqttCfg.pass) > 0 ? mqttCfg.pass : nullptr; + Serial.printf("[MQTT] connecting to %s:%d as %s\n", mqttCfg.host, mqttCfg.port, clientId); - bool ok = mqttClient.connect(clientId, mqttCfg.user, mqttCfg.pass, + bool ok = mqttClient.connect(clientId, user, pass, statusTopic, 0, true, "online: false"); if (ok) { @@ -334,7 +339,7 @@ static bool mqttConnect() { } static void mqttLoop() { - if (!mqttCfg.enabled || strlen(mqttCfg.host) == 0 || strlen(mqttCfg.user) == 0 || strlen(mqttCfg.pass) == 0) return; + if (!mqttCfg.enabled || strlen(mqttCfg.host) == 0) return; if (!mqttClient.connected()) { unsigned long now = millis(); From eafdf87563845a79d9252620e8cafce1561bd66e Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Sat, 27 Jun 2026 01:40:57 +0200 Subject: [PATCH 5/8] Wire MQTT availability into HA discovery --- src/main.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 21f9f19..c49747d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,8 @@ #define MQTT_MANUFACTURER "Baumann Enkataleiptics" #define AUTH_USERNAME "admin" #define MQTT_PROBE_TIMEOUT_MS 400 // keep short: blocks loop()/handleClient() per reconnect attempt +#define MQTT_PAYLOAD_AVAILABLE "online" +#define MQTT_PAYLOAD_NOT_AVAILABLE "offline" struct MeterConfig { int pin; @@ -256,6 +258,9 @@ static void mqttPublishDiscovery() { doc["name"] = name; doc["state_topic"] = stat; doc["command_topic"] = stat + "/set"; + doc["availability_topic"] = String(mqttCfg.prefix) + "/status"; + doc["payload_available"] = MQTT_PAYLOAD_AVAILABLE; + doc["payload_not_available"] = MQTT_PAYLOAD_NOT_AVAILABLE; doc["min"] = meters[i].rangeMin; doc["max"] = meters[i].rangeMax; doc["step"] = 0.1; @@ -268,7 +273,7 @@ static void mqttPublishDiscovery() { char topic[128]; snprintf(topic, sizeof(topic), "homeassistant/number/%s/config", objId.c_str()); - char payload[512]; + char payload[768]; serializeJson(doc, payload, sizeof(payload)); bool pubOk = mqttClient.publish(topic, payload, true); Serial.printf("[MQTT] discovery %s -> %s (ok=%d)\n", topic, payload, pubOk); @@ -323,11 +328,11 @@ static bool mqttConnect() { Serial.printf("[MQTT] connecting to %s:%d as %s\n", mqttCfg.host, mqttCfg.port, clientId); bool ok = mqttClient.connect(clientId, user, pass, - statusTopic, 0, true, "online: false"); + statusTopic, 0, true, MQTT_PAYLOAD_NOT_AVAILABLE); if (ok) { Serial.printf("[MQTT] connected to %s:%d\n", mqttCfg.host, mqttCfg.port); - mqttClient.publish(statusTopic, "online: true", true); + mqttClient.publish(statusTopic, MQTT_PAYLOAD_AVAILABLE, true); mqttSubscribe(); mqttPublishDiscovery(); for (int i = 0; i < meterCount; i++) From acf63a3753d9a078498cd6472971eb7b65a2b396 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Sat, 27 Jun 2026 01:44:32 +0200 Subject: [PATCH 6/8] Escape < and > in escHtml to prevent stored XSS --- src/main.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index c49747d..69eebce 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -391,6 +391,8 @@ static String escHtml(const String& s) { char c = s.charAt(i); switch (c) { case '&': out += "&"; break; + case '<': out += "<"; break; + case '>': out += ">"; break; case '\'': out += "'"; break; case '"': out += """; break; default: out += c; From b10c89505ca81a93851b7b0bc660795f9c656904 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Sat, 27 Jun 2026 01:44:32 +0200 Subject: [PATCH 7/8] Retain HA command messages so values survive restarts --- src/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.cpp b/src/main.cpp index 69eebce..b909983 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -258,6 +258,7 @@ static void mqttPublishDiscovery() { doc["name"] = name; doc["state_topic"] = stat; doc["command_topic"] = stat + "/set"; + doc["retain"] = true; // HA publishes commands retained so values survive restarts doc["availability_topic"] = String(mqttCfg.prefix) + "/status"; doc["payload_available"] = MQTT_PAYLOAD_AVAILABLE; doc["payload_not_available"] = MQTT_PAYLOAD_NOT_AVAILABLE; From 96998fb9e2e12a1264ca15e09f34571260563e67 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Sat, 27 Jun 2026 01:48:35 +0200 Subject: [PATCH 8/8] Persist MQTT settings when disabling MQTT --- src/main.cpp | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index b909983..b1f17bd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -557,24 +557,22 @@ static void handleConfig() { val.toCharArray(hostname, sizeof(hostname)); } - if (server.hasArg("mqtt_en")) { - mqttCfg.enabled = true; - if (server.hasArg("mqtt_host")) - strlcpy(mqttCfg.host, server.arg("mqtt_host").c_str(), sizeof(mqttCfg.host)); - if (server.hasArg("mqtt_port")) - mqttCfg.port = server.arg("mqtt_port").toInt(); - if (server.hasArg("mqtt_user")) - strlcpy(mqttCfg.user, server.arg("mqtt_user").c_str(), sizeof(mqttCfg.user)); - if (server.hasArg("mqtt_pass") && server.arg("mqtt_pass").length() > 0) - strlcpy(mqttCfg.pass, server.arg("mqtt_pass").c_str(), sizeof(mqttCfg.pass)); - if (server.hasArg("mqtt_prefix")) - strlcpy(mqttCfg.prefix, server.arg("mqtt_prefix").c_str(), sizeof(mqttCfg.prefix)); - Serial.printf("[HTTP] mqtt enabled host=%s port=%u user=%s prefix=%s\n", - mqttCfg.host, mqttCfg.port, mqttCfg.user, mqttCfg.prefix); - } else { - mqttCfg.enabled = false; - Serial.println("[HTTP] mqtt disabled"); - } + // Persist the connection fields regardless of the enable checkbox so edits + // made while disabling MQTT aren't discarded. + if (server.hasArg("mqtt_host")) + strlcpy(mqttCfg.host, server.arg("mqtt_host").c_str(), sizeof(mqttCfg.host)); + if (server.hasArg("mqtt_port")) + mqttCfg.port = server.arg("mqtt_port").toInt(); + if (server.hasArg("mqtt_user")) + strlcpy(mqttCfg.user, server.arg("mqtt_user").c_str(), sizeof(mqttCfg.user)); + if (server.hasArg("mqtt_pass") && server.arg("mqtt_pass").length() > 0) + strlcpy(mqttCfg.pass, server.arg("mqtt_pass").c_str(), sizeof(mqttCfg.pass)); + if (server.hasArg("mqtt_prefix")) + strlcpy(mqttCfg.prefix, server.arg("mqtt_prefix").c_str(), sizeof(mqttCfg.prefix)); + + mqttCfg.enabled = server.hasArg("mqtt_en"); + Serial.printf("[HTTP] mqtt enabled=%d host=%s port=%u user=%s prefix=%s\n", + mqttCfg.enabled, mqttCfg.host, mqttCfg.port, mqttCfg.user, mqttCfg.prefix); if (server.hasArg("auth_en")) { if (server.hasArg("auth_pass") && server.arg("auth_pass").length() > 0)