Example: Availability & Command Confirmation

A complete, ready-to-flash sketch showing device availability tracking and command confirmation working together — copy the code for your board below.

Last Will (LWT)
Instant online/offline detection
Birth message
Clears the retained "offline" on reconnect
Heartbeat
Catches a stuck-but-connected device
Command confirmation
Echoes the applied value back, Tasmota-style
Concepts explained in full: see MQTT Confirmed for what each of these does and why, and the MQTT guide for topic structure and connection settings. Requires the PubSubClient library.

Before you flash

  1. Set ssid / wifiPass to your Wi-Fi credentials.
  2. Set mqttUser to your Sub-account Key, mqttPass to your MQTT password (Console → API & Connections → MQTT Credentials).
  3. Create a Device named esp8266 (or esp32) and two Fields, temperature and relay1, in Console → Devices — names must match the sketch exactly.

1Sketch

ESP8266
ESP32
// ============================================================================
// Virtuino Cloud — MQTT Availability & Command Confirmation (ESP8266)
//
// Demonstrates, all wired up together:
//   - Bare MQTT topics (no account key, no /in//out/, no "device/" segment)
//   - Last Will (LWT) + birth message for instant online/offline detection
//   - Heartbeat, so a stuck-but-connected device is also detected
//   - Command confirmation (Tasmota-style echo) for a relay
//
// See /docs/mqtt_docs.html for full topic reference and connection
// settings, and /docs/mqtt_confirmed.html for the concepts this sketch
// implements.
//
// Library: PubSubClient by Nick O'Leary
// (Arduino IDE -> Sketch -> Include Library -> Manage Libraries -> "PubSubClient")
// ============================================================================

#include <ESP8266WiFi.h>
#include <PubSubClient.h>

// ── Wi-Fi ────────────────────────────────────────────────────────────────
const char* ssid     = "YOUR_WIFI_SSID";
const char* wifiPass = "YOUR_WIFI_PASSWORD";

// ── Virtuino Cloud MQTT ─────────────────────────────────────────────────
// mqttUser is only ever used to LOG IN — it is never part of a topic.
const char* mqttHost = "cloud.virtuino.com";
const int   mqttPort = 1883;                    // use 8883 + WiFiClientSecure for TLS
const char* mqttUser = "vr-abcd1234";            // your Sub-account Key
const char* mqttPass = "YOUR_MQTT_PASSWORD";
String      clientId;                           // built in setup(): "device_" + chip ID

// ── Topics ──────────────────────────────────────────────────────────────
// Bare topics only — no account key, no "device/" segment, no in/out.
// Must match the Device and Field names you created in Console -> Devices.
String deviceName = "esp8266";
String topicRelay = deviceName + "/relay1";   // used for BOTH subscribe (command) and publish (confirmation)
String topicTemp  = deviceName + "/temperature";
String topicLWT   = deviceName + "/LWT";      // reserved field name — connection state, not a data field

const unsigned long HEARTBEAT_INTERVAL_MS = 60000;   // publish something at least this often
unsigned long lastHeartbeat = 0;
unsigned long lastTempPublish = 0;

WiFiClient   wifiClient;
PubSubClient mqtt(wifiClient);

// ── Incoming message handler ───────────────────────────────────────────
void onMessage(char* topic, byte* payload, unsigned int len) {
  String msg = "";
  for (unsigned int i = 0; i < len; i++) msg += (char)payload[i];
  Serial.println("[" + String(topic) + "] " + msg);

  if (String(topic) == topicRelay) {
    bool relayOn = (msg == "1");
    digitalWrite(D1, relayOn ? HIGH : LOW);

    // Confirmation: echo the value we just applied back on the SAME bare
    // topic. The broker keeps the "receive command" and "send confirmation"
    // directions on separate real topics automatically — no risk of the
    // device looping on its own message.
    mqtt.publish(topicRelay.c_str(), msg.c_str());
  }
}

void connectMQTT() {
  while (!mqtt.connected()) {
    Serial.print("Connecting to MQTT...");
    // Last Will registered at connect time: topic, QoS, retain, message.
    // Bare topic, no account key, no /in/ or /out/ — see mqtt_confirmed.html.
    if (mqtt.connect(clientId.c_str(), mqttUser, mqttPass,
                      topicLWT.c_str(), 1, true, "offline")) {
      Serial.println(" connected!");
      mqtt.subscribe(topicRelay.c_str());

      // Birth message — clears the retained "offline" from last time.
      mqtt.publish(topicLWT.c_str(), "online", true);

      // Publish current relay state once, so the dashboard starts in sync.
      mqtt.publish(topicRelay.c_str(), digitalRead(D1) ? "1" : "0");
    } else {
      Serial.print(" failed, rc="); Serial.println(mqtt.state());
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  clientId = "device_" + String(ESP.getChipId(), HEX);   // unique per chip — no account key needed
  pinMode(D1, OUTPUT);

  WiFi.begin(ssid, wifiPass);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.println("\nWi-Fi connected: " + WiFi.localIP().toString());

  mqtt.setServer(mqttHost, mqttPort);
  mqtt.setCallback(onMessage);
  mqtt.setKeepAlive(30);   // broker declares the device dead after ~1.5x this interval
}

void loop() {
  if (!mqtt.connected()) connectMQTT();
  mqtt.loop();

  unsigned long now = millis();

  // Publish temperature every 10 seconds (replace with a real sensor reading).
  if (now - lastTempPublish > 10000) {
    lastTempPublish = now;
    float temp = random(200, 300) / 10.0;
    mqtt.publish(topicTemp.c_str(), String(temp, 2).c_str());
  }

  // Heartbeat: even if nothing else changes, publish something at least
  // this often, so "last seen" stays fresh for devices that would
  // otherwise stay quiet for long periods (Device Monitor still catches a
  // stuck-but-connected device — the LWT alone can't, since the TCP
  // connection is still alive).
  if (now - lastHeartbeat > HEARTBEAT_INTERVAL_MS) {
    lastHeartbeat = now;
    mqtt.publish(topicLWT.c_str(), "online", true);
  }
}
// ============================================================================
// Virtuino Cloud — MQTT Availability & Command Confirmation (ESP32)
//
// Demonstrates, all wired up together:
//   - Bare MQTT topics (no account key, no /in//out/, no "device/" segment)
//   - Last Will (LWT) + birth message for instant online/offline detection
//   - Heartbeat, so a stuck-but-connected device is also detected
//   - Command confirmation (Tasmota-style echo) for a relay
//
// See /docs/mqtt_docs.html for full topic reference and connection
// settings, and /docs/mqtt_confirmed.html for the concepts this sketch
// implements.
//
// Library: PubSubClient by Nick O'Leary
// (Arduino IDE -> Sketch -> Include Library -> Manage Libraries -> "PubSubClient")
// ============================================================================

#include <WiFi.h>
#include <PubSubClient.h>

// ── Wi-Fi ────────────────────────────────────────────────────────────────
const char* ssid     = "YOUR_WIFI_SSID";
const char* wifiPass = "YOUR_WIFI_PASSWORD";

// ── Virtuino Cloud MQTT ─────────────────────────────────────────────────
// mqttUser is only ever used to LOG IN — it is never part of a topic.
const char* mqttHost = "cloud.virtuino.com";
const int   mqttPort = 1883;                    // use 8883 + WiFiClientSecure for TLS
const char* mqttUser = "vr-abcd1234";            // your Sub-account Key
const char* mqttPass = "YOUR_MQTT_PASSWORD";
String      clientId;                           // built in setup(): "device_" + chip ID

const int RELAY_PIN = 26;

// ── Topics ──────────────────────────────────────────────────────────────
// Bare topics only — no account key, no "device/" segment, no in/out.
// Must match the Device and Field names you created in Console -> Devices.
String deviceName = "esp32";
String topicRelay = deviceName + "/relay1";   // used for BOTH subscribe (command) and publish (confirmation)
String topicTemp  = deviceName + "/temperature";
String topicLWT   = deviceName + "/LWT";      // reserved field name — connection state, not a data field

const unsigned long HEARTBEAT_INTERVAL_MS = 60000;   // publish something at least this often
unsigned long lastHeartbeat = 0;
unsigned long lastTempPublish = 0;

WiFiClient   wifiClient;
PubSubClient mqtt(wifiClient);

// ── Incoming message handler ───────────────────────────────────────────
void onMessage(char* topic, byte* payload, unsigned int len) {
  String msg = "";
  for (unsigned int i = 0; i < len; i++) msg += (char)payload[i];
  Serial.println("[" + String(topic) + "] " + msg);

  if (String(topic) == topicRelay) {
    bool relayOn = (msg == "1");
    digitalWrite(RELAY_PIN, relayOn ? HIGH : LOW);

    // Confirmation: echo the value we just applied back on the SAME bare
    // topic. The broker keeps the "receive command" and "send confirmation"
    // directions on separate real topics automatically — no risk of the
    // device looping on its own message.
    mqtt.publish(topicRelay.c_str(), msg.c_str());
  }
}

void connectMQTT() {
  while (!mqtt.connected()) {
    Serial.print("Connecting to MQTT...");
    // Last Will registered at connect time: topic, QoS, retain, message.
    // Bare topic, no account key, no /in/ or /out/ — see mqtt_confirmed.html.
    if (mqtt.connect(clientId.c_str(), mqttUser, mqttPass,
                      topicLWT.c_str(), 1, true, "offline")) {
      Serial.println(" connected!");
      mqtt.subscribe(topicRelay.c_str());

      // Birth message — clears the retained "offline" from last time.
      mqtt.publish(topicLWT.c_str(), "online", true);

      // Publish current relay state once, so the dashboard starts in sync.
      mqtt.publish(topicRelay.c_str(), digitalRead(RELAY_PIN) ? "1" : "0");
    } else {
      Serial.print(" failed, rc="); Serial.println(mqtt.state());
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  clientId = "device_" + String((uint32_t)ESP.getEfuseMac(), HEX);   // unique per chip — no account key needed
  pinMode(RELAY_PIN, OUTPUT);

  WiFi.begin(ssid, wifiPass);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.println("\nWi-Fi connected: " + WiFi.localIP().toString());

  mqtt.setServer(mqttHost, mqttPort);
  mqtt.setCallback(onMessage);
  mqtt.setKeepAlive(30);   // broker declares the device dead after ~1.5x this interval
}

void loop() {
  if (!mqtt.connected()) connectMQTT();
  mqtt.loop();

  unsigned long now = millis();

  // Publish temperature every 10 seconds (replace with a real sensor reading).
  if (now - lastTempPublish > 10000) {
    lastTempPublish = now;
    float temp = random(200, 300) / 10.0;
    mqtt.publish(topicTemp.c_str(), String(temp, 2).c_str());
  }

  // Heartbeat: even if nothing else changes, publish something at least
  // this often, so "last seen" stays fresh for devices that would
  // otherwise stay quiet for long periods (Device Monitor still catches a
  // stuck-but-connected device — the LWT alone can't, since the TCP
  // connection is still alive).
  if (now - lastHeartbeat > HEARTBEAT_INTERVAL_MS) {
    lastHeartbeat = now;
    mqtt.publish(topicLWT.c_str(), "online", true);
  }
}