Knowing that a command actually reached and was applied by the device — not just that it was sent.
Say a user taps a light switch on a dashboard. Here's exactly what happens, step by step:
Two outcomes: either the device answers in time and everything — automations, both dashboards, and any third-party app — updates together; or it doesn't, and nothing changes anywhere. Never a half-applied, out-of-sync state.
| Without confirmation | With confirmation | |
|---|---|---|
| Database & widgets | updated immediately | updated when the device answers |
| Rules / scripts / HTTP triggers | run immediately | run when the device answers |
| If the device never answers | value stored anyway | nothing stored — you're alerted |
Console → Devices → open the device → Edit → set Command confirmation to Enabled, choose a timeout (default 5s).
Replay the same message on every incoming message.
Do this only with confirmation enabled (step 1). The echo is what confirms the command. If your device replays messages while confirmation is switched off, the platform treats the replay as a brand-new value and sends it back to the device, which replays again — an endless loop.
In the dashboard's settings, tick "Alert me if a command is not confirmed" to get an email/push, not just a log entry.
Two fields shown below (relay1, relay2) to make the point that
every command field needs its own echo — there's no per-field switch anymore, so a
field you forget here will simply always time out.
digitalRead() only works for a simple on/off relay. For a slider or any numeric field,
echo the value you actually parsed from the command — a pin reading will never match it.
// ── Topics ────────────────────────────────────────────── // Bare topics — no account key needed. Each one is used for // BOTH subscribe (receive command) and publish (echo back); // the broker keeps the two directions separate automatically. String deviceName = "esp32"; String topicRelay1 = deviceName + "/relay1"; String topicRelay2 = deviceName + "/relay2"; void onMessage(char* topic, byte* payload, unsigned int len) { String msg = ""; for (unsigned int i = 0; i < len; i++) msg += (char)payload[i]; if (String(topic) == topicRelay1) { digitalWrite(RELAY1_PIN, msg == "1" ? HIGH : LOW); mqtt.publish(topicRelay1.c_str(), msg.c_str()); // echo — confirms relay1 } if (String(topic) == topicRelay2) { digitalWrite(RELAY2_PIN, msg == "1" ? HIGH : LOW); mqtt.publish(topicRelay2.c_str(), msg.c_str()); // echo — confirms relay2 } } void setup() { // ... WiFi.begin(...), mqtt.setServer(...), mqtt.setCallback(onMessage) ... mqtt.subscribe(topicRelay1.c_str()); mqtt.subscribe(topicRelay2.c_str()); }
This is the whole program, ready to flash on an ESP32. It controls a green and a red LED from the dashboard, confirms every command, and publishes a virtual sensor value every 5 seconds.
Change only the four settings at the top — your Wi-Fi, and the MQTT credentials from
Console → API & Connections → MQTT Credentials. The device
(esp32) and its fields (green_led, red_led,
sensor) must already exist in your account.
deviceName + "-" + chipId. That is what lets Virtuino Cloud
show this device as online or offline without a single extra line of code, and it keeps two boards
running the same sketch from knocking each other off the broker.KEEP_ALIVE_SEC decides how quickly a power cut is noticed — about 1.5× its
value, so 20 seconds means roughly 30. See
Device Online/Offline Detection.
// ==================================================== // Virtuino Cloud - ESP32 example // // Shows two features: // // 1. COMMAND CONFIRMATION // The device echoes back every command it applies, // so the dashboard shows the REAL state of the // device, not just the state you asked for. // // 2. ONLINE / OFFLINE STATUS // Nothing to do here. Virtuino Cloud is told by // the broker when this device connects and when // it disconnects, so there is no extra code. // All it needs is the client ID below. // ==================================================== #include <WiFi.h> #include <PubSubClient.h> // ---------------------------------------------------- // WiFi settings // ---------------------------------------------------- const char* WIFI_SSID = "YOUR_WIFI_SSID"; const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; // ---------------------------------------------------- // Virtuino Cloud MQTT settings // Console -> API & Connections -> MQTT Credentials // ---------------------------------------------------- const char* MQTT_HOST = "cloud.virtuino.com"; const int MQTT_PORT = 1883; const char* MQTT_USER = "YOUR_SUB_ACCOUNT_KEY"; const char* MQTT_PASS = "YOUR_MQTT_PASSWORD"; // ---------------------------------------------------- // Device // This device must already exist in Virtuino Cloud // ---------------------------------------------------- String deviceName = "esp32"; // ---------------------------------------------------- // How often the device tells the broker it is alive. // // This also decides how fast Virtuino Cloud notices a // power cut: about 1.5 x this value. // 20 -> offline within ~30 seconds // 60 -> offline within ~90 seconds (less traffic) // ---------------------------------------------------- const uint16_t KEEP_ALIVE_SEC = 20; // ---------------------------------------------------- // GPIO // ---------------------------------------------------- #define GREEN_LED_PIN 13 #define RED_LED_PIN 12 // ---------------------------------------------------- // Virtuino Cloud fields // These fields must already exist under device "esp32" // ---------------------------------------------------- String topicGreen = deviceName + "/green_led"; String topicRed = deviceName + "/red_led"; String topicSensor = deviceName + "/sensor"; // ---------------------------------------------------- WiFiClient wifiClient; PubSubClient mqtt(wifiClient); unsigned long lastSensorTime = 0; const unsigned long SENSOR_INTERVAL = 5000; // ==================================================== // WiFi connection // ==================================================== void connectWiFi() { if (WiFi.status() == WL_CONNECTED) return; Serial.print("Connecting to WiFi"); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("WiFi connected. IP: "); Serial.println(WiFi.localIP()); } // ==================================================== // MQTT callback // ==================================================== void mqttCallback(char* topic, byte* payload, unsigned int length) { String message; for (unsigned int i = 0; i < length; i++) { message += (char)payload[i]; } String receivedTopic = String(topic); Serial.print("MQTT received ["); Serial.print(receivedTopic); Serial.print("] "); Serial.println(message); // -------------------------------------------------- // GREEN LED // -------------------------------------------------- if (receivedTopic == topicGreen) { if (message == "1") { digitalWrite(GREEN_LED_PIN, HIGH); } else if (message == "0") { digitalWrite(GREEN_LED_PIN, LOW); } else { return; // Ignore invalid command } // ----------------------------------------------- // VIRTUINO CLOUD COMMAND CONFIRMATION // // Publish the value back to the SAME topic, but // only AFTER the command has actually been applied. // // Virtuino Cloud treats this message as proof that // the device did the job, and marks the command as // confirmed. Until it arrives, the dashboard keeps // showing the command as pending. // // Send it here, at the end of the command, and NOT // when the message first arrives - otherwise you // would be confirming something you have not done. // ----------------------------------------------- mqtt.publish(topicGreen.c_str(), message.c_str()); Serial.println("Green LED command confirmed."); } // -------------------------------------------------- // RED LED // -------------------------------------------------- else if (receivedTopic == topicRed) { if (message == "1") { digitalWrite(RED_LED_PIN, HIGH); } else if (message == "0") { digitalWrite(RED_LED_PIN, LOW); } else { return; } // Virtuino Cloud confirmation - same as above mqtt.publish(topicRed.c_str(), message.c_str()); Serial.println("Red LED command confirmed."); } } // ==================================================== // MQTT connection // ==================================================== void connectMQTT() { while (!mqtt.connected()) { if (WiFi.status() != WL_CONNECTED) { connectWiFi(); } Serial.print("Connecting to Virtuino Cloud MQTT..."); // ------------------------------------------------ // Client ID // // Virtuino Cloud recognises this device when the // client ID is the device name, optionally followed // by a dash and anything you like: // // esp32 // esp32-a1b2c3 // // The chip ID is added here so that several boards // running this same sketch never use the same // client ID. Devices sharing a client ID would keep // disconnecting each other. // ------------------------------------------------ uint64_t chipId = ESP.getEfuseMac(); String clientId = deviceName + "-" + String((uint32_t)(chipId >> 32), HEX) + String((uint32_t)chipId, HEX); // ------------------------------------------------ // Connect // // Nothing else to pass. Virtuino Cloud is told by // the broker when this client connects and when it // disconnects, including after a power cut. // ------------------------------------------------ if (mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS)) { Serial.print(" connected as "); Serial.println(clientId); // Subscribe to commands mqtt.subscribe(topicGreen.c_str()); mqtt.subscribe(topicRed.c_str()); Serial.println("Subscribed to LED commands."); } else { Serial.print(" failed, rc="); Serial.println(mqtt.state()); delay(4000); } } } // ==================================================== // Setup // ==================================================== void setup() { Serial.begin(115200); pinMode(GREEN_LED_PIN, OUTPUT); pinMode(RED_LED_PIN, OUTPUT); digitalWrite(GREEN_LED_PIN, LOW); digitalWrite(RED_LED_PIN, LOW); // Random generator, used by the virtual sensor below randomSeed(esp_random()); connectWiFi(); mqtt.setServer(MQTT_HOST, MQTT_PORT); mqtt.setCallback(mqttCallback); mqtt.setKeepAlive(KEEP_ALIVE_SEC); mqtt.setBufferSize(512); } // ==================================================== // Main loop // ==================================================== void loop() { if (WiFi.status() != WL_CONNECTED) { connectWiFi(); } if (!mqtt.connected()) { connectMQTT(); } mqtt.loop(); // -------------------------------------------------- // Virtual sensor every 5 seconds // -------------------------------------------------- if (millis() - lastSensorTime >= SENSOR_INTERVAL) { lastSensorTime = millis(); // Example random value: 0.0 - 100.0 float sensorValue = random(0, 1001) / 10.0; String value = String(sensorValue, 1); mqtt.publish(topicSensor.c_str(), value.c_str()); Serial.print("Virtual sensor: "); Serial.println(value); } }