diff --git a/docs/custom-cli.md b/docs/custom-cli.md index 0da69ef0..054994e2 100644 --- a/docs/custom-cli.md +++ b/docs/custom-cli.md @@ -50,6 +50,8 @@ Notes: - new repeater MQTT installs default `mqtt.iata` to `UNSET` - `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 +- 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: diff --git a/docs/web-panel.md b/docs/web-panel.md index dea1ac2d..25e45bcc 100644 --- a/docs/web-panel.md +++ b/docs/web-panel.md @@ -221,6 +221,8 @@ Notes: - while `mqtt.iata` is `UNSET`, enabled MQTT brokers do not attempt to connect - the current MQTT server states are loaded when the page opens - 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 ## `/stats` Overview diff --git a/release-notes.yml b/release-notes.yml index 854b0504..8c78a239 100644 --- a/release-notes.yml +++ b/release-notes.yml @@ -557,3 +557,21 @@ releases: 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." 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: [] diff --git a/src/helpers/mqtt/MQTTUplink.cpp b/src/helpers/mqtt/MQTTUplink.cpp index f7a37ae4..fee542b5 100644 --- a/src/helpers/mqtt/MQTTUplink.cpp +++ b/src/helpers/mqtt/MQTTUplink.cpp @@ -695,12 +695,12 @@ int MQTTUplink::buildRawJson(char* buffer, size_t buffer_size, const mesh::Packe return len; } -void MQTTUplink::publishOnlineStatus(BrokerState& broker) { +void MQTTUplink::publishBrokerStatus(BrokerState& broker, bool online) { char* payload = allocScratchBuffer(768); if (payload == nullptr) { return; } - int len = buildStatusJson(payload, 768, true); + int len = buildStatusJson(payload, 768, online); if (len > 0 && static_cast(len) < 768) { queuePublish(broker, broker.status_topic, payload, true); } @@ -999,7 +999,7 @@ void MQTTUplink::loop(const MQTTStatusSnapshot& snapshot) { connect_started = true; } if (broker.connected && !broker.connect_announced) { - publishOnlineStatus(broker); + publishBrokerStatus(broker, true); broker.connect_announced = true; _last_status_publish = millis(); } else if (!broker.connected) { @@ -1116,6 +1116,12 @@ bool MQTTUplink::setEndpointEnabled(uint8_t bit, bool enabled) { return false; } } 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; } _prefs.enabled_mask = next_mask; @@ -1158,6 +1164,9 @@ bool MQTTUplink::setIata(const char* iata) { for (size_t i = 0; cleaned[i] != 0; ++i) { cleaned[i] = toupper(static_cast(cleaned[i])); } + if (strcmp(cleaned, _prefs.iata) != 0 && !isUnsetIataValue(_prefs.iata)) { + publishStatus(false); + } if (strcmp(cleaned, MQTT_UNSET_IATA) == 0) { StrHelper::strncpy(_prefs.iata, MQTT_UNSET_IATA, sizeof(_prefs.iata)); refreshIdentityStrings(); diff --git a/src/helpers/mqtt/MQTTUplink.h b/src/helpers/mqtt/MQTTUplink.h index 33a1ff56..915726f0 100644 --- a/src/helpers/mqtt/MQTTUplink.h +++ b/src/helpers/mqtt/MQTTUplink.h @@ -138,7 +138,7 @@ private: void destroyBroker(BrokerState& broker, bool reset_retry_state = true); bool refreshToken(BrokerState& broker); 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); 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,