Merge pull request 'Fix meter lifecycle and MQTT/HA integration bugs' (#1) from fix-attach-meters-detach into main

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-06-26 23:53:15 +00:00
+70 -31
View File
@@ -13,6 +13,9 @@
#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
#define MQTT_PAYLOAD_AVAILABLE "online"
#define MQTT_PAYLOAD_NOT_AVAILABLE "offline"
struct MeterConfig {
int pin;
@@ -138,12 +141,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);
}
}
}
@@ -238,6 +258,10 @@ 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;
doc["min"] = meters[i].rangeMin;
doc["max"] = meters[i].rangeMax;
doc["step"] = 0.1;
@@ -250,11 +274,21 @@ 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);
}
// 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() {
@@ -268,9 +302,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;
}
@@ -288,13 +322,18 @@ 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,
statusTopic, 0, true, "online: false");
bool ok = mqttClient.connect(clientId, user, pass,
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++)
@@ -306,14 +345,14 @@ 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();
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) {
@@ -353,6 +392,8 @@ static String escHtml(const String& s) {
char c = s.charAt(i);
switch (c) {
case '&': out += "&amp;"; break;
case '<': out += "&lt;"; break;
case '>': out += "&gt;"; break;
case '\'': out += "&#39;"; break;
case '"': out += "&quot;"; break;
default: out += c;
@@ -516,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)