Knowing that a command actually reached the device, and knowing when a device stops responding. Two related problems that MQTT does not solve on its own.
PUBACK of QoS 1
means the broker accepted the message — never that the device received it, and certainly not that
it applied it. Raising QoS to 2 changes nothing here. This is the single most common misunderstanding.
Virtuino Cloud uses the same convention as Tasmota and other established firmwares: the platform sends a command on one topic, and the device publishes the value back on the matching topic in the opposite direction.
| Direction | Topic your device uses |
|---|---|
| Cloud → device (command) | <device>/<field> — subscribe |
| Device → cloud (confirmation) | <device>/<field> — publish |
Both directions map to the same field — in fact the same bare topic string — so nothing extra has to be configured. You subscribe to it to receive the command, and publish back to the same topic string to confirm; the broker keeps the two directions separate automatically.
# cloud sends the command (you receive it on your subscription) esp32/relay1 "1" # device applies it, then confirms by publishing the same value # to the SAME topic string esp32/relay1 "1"
Turn on “command confirmation” for the field in Console → Fields → Edit and set how long to wait. The default is 5 seconds, which is generous — a device that answers at all answers in milliseconds.
Normally, a command from a dashboard, scheduler, rule or script is written to the database immediately, the widgets update, and any scripts or rules that watch that field are triggered — all before the device has done anything.
With confirmation enabled, the command is only sent to the device. Nothing is stored, no widget is updated from the command itself, and no scripts or rules run. When the device publishes the value back, that is what gets stored, updates the dashboard and triggers everything downstream.
| Without confirmation | With confirmation | |
|---|---|---|
| Database | written immediately | written when the device answers |
| Widgets | updated immediately | updated when the device answers |
| Scripts / rules / HTTP triggers | run immediately | run when the device answers |
| If the device never answers | value stored anyway | nothing stored, you are alerted |
The result is that your history reflects what the equipment actually did, not what was requested. A rule such as “when the pump turns on, start a timer” no longer fires for a pump that never started.
While waiting, a small spinner appears on the widget and an indicator lights up orange next to the zoom controls. On confirmation it flashes green and disappears. If the timeout expires it turns red and stays, and the widget is reset to its previous value.
Failures are always recorded in the dashboard logs, whether or not you asked for email or push notifications. To also be notified, tick “Alert me if a command is not confirmed” in the dashboard settings. That option only appears if at least one of your fields has confirmation enabled.
Publish the value back on the same bare topic after applying it — the broker keeps your subscription (command in) and your publish (confirmation out) on separate real topics automatically, so one variable is all you need.
// ── Topic ───────────────────────────────────────── // One bare topic, used for BOTH subscribe (receive command) // and publish (send confirmation) — no account key needed. String deviceName = "esp32"; String topicRelay = deviceName + "/relay1"; void onMessage(char* topic, byte* payload, unsigned int len) { String msg = ""; for (int i = 0; i < len; i++) msg += (char)payload[i]; if (String(topic) == topicRelay) { digitalWrite(RELAY_PIN, msg == "1" ? HIGH : LOW); // Confirmation — publish the same value back to the same topic string mqtt.publish(topicRelay.c_str(), msg.c_str()); } }
digitalRead() returns 0 or 1 and never matches the value
that was sent, so every command would be reported as unconfirmed.
On connect it is worth publishing the current state once, so the dashboard starts in sync:
if (mqtt.connect(clientId.c_str(), mqttUser, mqttPass)) { mqtt.subscribe(topicRelay.c_str()); mqtt.publish(topicRelay.c_str(), digitalRead(RELAY_PIN) ? "1" : "0"); }
Confirmation tells you that one particular command failed. It cannot tell you that a device died at three in the morning while nobody was sending anything. For that, add a Device Monitor widget to your dashboard: choose the device, set how long of a silence means offline, and it shows the live state and alerts you when it drops.
The widget monitors the device, not a value — leave the field selector empty. It works with any transport, MQTT or HTTP polling, because it measures the time since the last incoming value from any source.
Without help from the device, the only signal is silence: the device is considered offline once the timeout expires. A device that only answers commands and never reports on its own will therefore look online until that timeout passes.
The Last Will and Testament fixes this. You register a message with the broker when you connect, and the broker publishes it for you if you disappear — power loss, network drop or crash. Detection becomes immediate instead of waiting for the timeout.
| Setting | Value |
|---|---|
| Will topic | <device>/LWT (bare — no account key, no in/out) |
| Will payload | offline |
| Birth message | publish online to the same topic after connecting |
| QoS / Retain | QoS 1, Retain ON |
LWT is a reserved field name..../<device>/LWT are read as connection state and are not stored as a field
value. Do not create a data field called LWT.
String topicStatus = deviceName + "/LWT"; // bare — broker scopes it to your account automatically void connectMQTT() { while (!mqtt.connected()) { // The last four arguments register the Last Will: // topic, QoS, retain, message if (mqtt.connect(clientId.c_str(), mqttUser, mqttPass, topicStatus.c_str(), 1, true, "offline")) { // Birth message — clears the retained "offline" from last time mqtt.publish(topicStatus.c_str(), "online", true); mqtt.subscribe(topicRelay.c_str()); } else { delay(5000); } } } void setup() { // The broker declares the device dead after about 1.5x this interval mqtt.setKeepAlive(30); }
| Symptom | Cause |
|---|---|
| Always “not confirmed”, but the output works | The confirmation is published to a different field name than the command — or, if you're using the older fully-qualified topic form, to the wrong one of the two topics. |
| The output switches on and off endlessly | Only possible with the older fully-qualified topic form: the device published its confirmation to its own command topic, so it received its own message back. Not possible with the bare topic form, since subscribe and publish are automatically kept on separate real topics. |
| Works for a relay, always fails for a slider | The sketch confirms with digitalRead(), which can only return
0 or 1. Echo the received value instead. |
| Device Monitor says online although the device is unplugged | No Last Will is configured, so detection waits for the silence timeout. Lower the timeout, or add LWT for instant detection. |
| The widget does not react at all | Confirmation is enabled but the device never answers, so nothing is stored. Check the dashboard logs. |
| A setting change seems to have no effect | Field settings are cached for up to one minute. Wait, then retry. |
For topic structure, broker settings and connection reference, see the MQTT Broker Setup guide.