fix: publish offline status on clean mqtt disconnect

This commit is contained in:
Jared Dohrman
2026-04-30 16:50:31 +10:00
parent ac3552383d
commit 14d517d29c
5 changed files with 35 additions and 4 deletions
+2
View File
@@ -50,6 +50,8 @@ Notes:
- new repeater MQTT installs default `mqtt.iata` to `UNSET` - new repeater MQTT installs default `mqtt.iata` to `UNSET`
- `letsmesh-eu` and `letsmesh-us` remain off by default unless already configured in saved prefs - `letsmesh-eu` and `letsmesh-us` remain off by default unless already configured in saved prefs
- if `mqtt.iata` is `UNSET`, `eastmesh-au`, `letsmesh-eu`, and `letsmesh-us` will not connect even if they are toggled on - if `mqtt.iata` is `UNSET`, `eastmesh-au`, `letsmesh-eu`, and `letsmesh-us` will not connect even if they are toggled on
- turning off a connected broker publishes a retained MQTT status update with `"status":"offline"` before the client disconnects
- changing `mqtt.iata` away from a configured value also publishes retained offline status to the old status topic before switching topics
Legacy dotted aliases are also accepted: Legacy dotted aliases are also accepted:
+2
View File
@@ -221,6 +221,8 @@ Notes:
- while `mqtt.iata` is `UNSET`, enabled MQTT brokers do not attempt to connect - while `mqtt.iata` is `UNSET`, enabled MQTT brokers do not attempt to connect
- the current MQTT server states are loaded when the page opens - the current MQTT server states are loaded when the page opens
- you can toggle each MQTT server on or off from this panel - you can toggle each MQTT server on or off from this panel
- turning off a connected MQTT server publishes retained offline status before the client disconnects
- changing `mqtt.iata` away from a configured value publishes retained offline status to the old status topic before the new topic is used
- if all three servers are enabled at once, the panel shows a warning recommending two at most - if all three servers are enabled at once, the panel shows a warning recommending two at most
## `/stats` Overview ## `/stats` Overview
+18
View File
@@ -557,3 +557,21 @@ releases:
area: web-panel area: web-panel
text: "Documented no-PSRAM `/stats` history behavior, stale browser-token recovery, and the lightweight `/api/session` endpoint used by the web panel." text: "Documented no-PSRAM `/stats` history behavior, stale browser-token recovery, and the lightweight `/api/session` endpoint used by the web panel."
breaking_changes: [] breaking_changes: []
- track: repeater-mqtt
version: "1.3.15"
tag: "repeater-mqtt-eastmesh-v1.3.15"
date: "2026-04-30"
previous_version: "1.3.14"
summary: "Publishes retained offline status for clean MQTT broker disables and IATA topic changes so operator-driven disconnects do not leave stale online status behind."
changes:
- type: fixed
area: mqtt
text: "Published explicit retained offline status before a connected broker is toggled off, matching operator expectations without relying on last-will behavior that only fires on unclean disconnects."
- type: fixed
area: mqtt
text: "Published retained offline status to the old status topic before `mqtt.iata` changes, preventing stale online status from remaining under the previous IATA path."
- type: docs
area: mqtt
text: "Updated the CLI and web-panel docs to describe retained offline status publishing for broker toggles and IATA changes."
breaking_changes: []
+12 -3
View File
@@ -695,12 +695,12 @@ int MQTTUplink::buildRawJson(char* buffer, size_t buffer_size, const mesh::Packe
return len; return len;
} }
void MQTTUplink::publishOnlineStatus(BrokerState& broker) { void MQTTUplink::publishBrokerStatus(BrokerState& broker, bool online) {
char* payload = allocScratchBuffer(768); char* payload = allocScratchBuffer(768);
if (payload == nullptr) { if (payload == nullptr) {
return; return;
} }
int len = buildStatusJson(payload, 768, true); int len = buildStatusJson(payload, 768, online);
if (len > 0 && static_cast<size_t>(len) < 768) { if (len > 0 && static_cast<size_t>(len) < 768) {
queuePublish(broker, broker.status_topic, payload, true); queuePublish(broker, broker.status_topic, payload, true);
} }
@@ -999,7 +999,7 @@ void MQTTUplink::loop(const MQTTStatusSnapshot& snapshot) {
connect_started = true; connect_started = true;
} }
if (broker.connected && !broker.connect_announced) { if (broker.connected && !broker.connect_announced) {
publishOnlineStatus(broker); publishBrokerStatus(broker, true);
broker.connect_announced = true; broker.connect_announced = true;
_last_status_publish = millis(); _last_status_publish = millis();
} else if (!broker.connected) { } else if (!broker.connected) {
@@ -1116,6 +1116,12 @@ bool MQTTUplink::setEndpointEnabled(uint8_t bit, bool enabled) {
return false; return false;
} }
} else { } else {
for (BrokerState& broker : _brokers) {
if (broker.spec != nullptr && broker.spec->bit == bit && broker.connected && broker.client != nullptr) {
publishBrokerStatus(broker, false);
break;
}
}
next_mask &= ~bit; next_mask &= ~bit;
} }
_prefs.enabled_mask = next_mask; _prefs.enabled_mask = next_mask;
@@ -1158,6 +1164,9 @@ bool MQTTUplink::setIata(const char* iata) {
for (size_t i = 0; cleaned[i] != 0; ++i) { for (size_t i = 0; cleaned[i] != 0; ++i) {
cleaned[i] = toupper(static_cast<unsigned char>(cleaned[i])); cleaned[i] = toupper(static_cast<unsigned char>(cleaned[i]));
} }
if (strcmp(cleaned, _prefs.iata) != 0 && !isUnsetIataValue(_prefs.iata)) {
publishStatus(false);
}
if (strcmp(cleaned, MQTT_UNSET_IATA) == 0) { if (strcmp(cleaned, MQTT_UNSET_IATA) == 0) {
StrHelper::strncpy(_prefs.iata, MQTT_UNSET_IATA, sizeof(_prefs.iata)); StrHelper::strncpy(_prefs.iata, MQTT_UNSET_IATA, sizeof(_prefs.iata));
refreshIdentityStrings(); refreshIdentityStrings();
+1 -1
View File
@@ -138,7 +138,7 @@ private:
void destroyBroker(BrokerState& broker, bool reset_retry_state = true); void destroyBroker(BrokerState& broker, bool reset_retry_state = true);
bool refreshToken(BrokerState& broker); bool refreshToken(BrokerState& broker);
void publishStatus(bool online); void publishStatus(bool online);
void publishOnlineStatus(BrokerState& broker); void publishBrokerStatus(BrokerState& broker, bool online);
void queuePublish(BrokerState& broker, const char* topic, const char* payload, bool retain); void queuePublish(BrokerState& broker, const char* topic, const char* payload, bool retain);
int buildStatusJson(char* buffer, size_t buffer_size, bool online) const; int buildStatusJson(char* buffer, size_t buffer_size, bool online) const;
int buildPacketJson(char* buffer, size_t buffer_size, const mesh::Packet& packet, bool is_tx, int rssi, float snr, int buildPacketJson(char* buffer, size_t buffer_size, const mesh::Packet& packet, bool is_tx, int rssi, float snr,