Scheduled commands are executed server-side and sent to your device via MQTT at the scheduled time. From the device's perspective, scheduled commands look exactly like manual commands.
- Server schedules command - User schedules via web dashboard
- Background processor executes - At scheduled time, server creates a regular command
- MQTT delivery - Command is published to
gansys/{hardwareId}/commands - Device receives - Your Arduino/ESP32 receives the command
- Device executes - Device performs the action (turn on/off actuator)
- Device acknowledges - Device publishes acknowledgement to
gansys/{hardwareId}/acks
Your existing device code should work without modifications because:
- Scheduled commands become regular commands when executed
- They use the same MQTT topics
- They have the same JSON structure
- They require the same acknowledgement format
Commands arrive on topic gansys/{hardwareId}/commands:
{
"commands": [
{
"id": "cmd_abc123",
"channelKey": "spray_pump",
"commandType": "set_state",
"desiredBooleanState": true,
"note": "Scheduled: Spray pesticide for 15 minutes",
"overrideUntil": "2026-04-27T15:30:00Z"
}
],
"pestControlSchedule": {
"enabled": true,
"sprayEntries": [...],
"uvStartTime": "18:00",
"uvEndTime": "06:00"
}
}Publish to gansys/{hardwareId}/acks:
{
"acknowledgements": [
{
"commandId": "cmd_abc123",
"status": "acknowledged",
"executedAt": "2026-04-27T15:20:05Z",
"deviceMessage": "Spray pump activated"
}
]
}Your RTC module can be used for:
#include <RTClib.h>
RTC_DS3231 rtc;
void setup() {
if (!rtc.begin()) {
Serial.println("RTC not found!");
}
// Sync with NTP or set manually
if (rtc.lostPower()) {
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
}
void loop() {
DateTime now = rtc.now();
// Use for logging
Serial.print("Current time: ");
Serial.println(now.timestamp());
}void handleCommand(JsonObject cmd) {
DateTime now = rtc.now();
String overrideUntil = cmd["overrideUntil"];
// Parse ISO 8601 timestamp
DateTime expiry = parseISO8601(overrideUntil);
if (now > expiry) {
Serial.println("Command expired, ignoring");
return;
}
// Execute command
executeCommand(cmd);
}void acknowledgeCommand(String commandId) {
DateTime now = rtc.now();
JsonDocument doc;
JsonArray acks = doc["acknowledgements"].to<JsonArray>();
JsonObject ack = acks.add<JsonObject>();
ack["commandId"] = commandId;
ack["status"] = "acknowledged";
ack["executedAt"] = now.timestamp(DateTime::TIMESTAMP_FULL);
ack["deviceMessage"] = "Executed at local time";
publishAcknowledgement(doc);
}struct QueuedCommand {
String commandId;
String channelKey;
bool desiredState;
DateTime scheduledFor;
};
std::vector<QueuedCommand> commandQueue;
void queueCommand(JsonObject cmd) {
QueuedCommand queued;
queued.commandId = cmd["id"].as<String>();
queued.channelKey = cmd["channelKey"].as<String>();
queued.desiredState = cmd["desiredBooleanState"];
queued.scheduledFor = parseISO8601(cmd["overrideUntil"]);
commandQueue.push_back(queued);
}
void processCommandQueue() {
DateTime now = rtc.now();
for (auto it = commandQueue.begin(); it != commandQueue.end();) {
if (now >= it->scheduledFor) {
executeQueuedCommand(*it);
it = commandQueue.erase(it);
} else {
++it;
}
}
}#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <RTClib.h>
RTC_DS3231 rtc;
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
const char* HARDWARE_ID = "ESP32_001";
void setup() {
Serial.begin(115200);
// Initialize RTC
if (!rtc.begin()) {
Serial.println("RTC not found!");
}
// Connect to WiFi
WiFi.begin("SSID", "PASSWORD");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
// Connect to MQTT
mqtt.setServer("mqtt.example.com", 1883);
mqtt.setCallback(onMqttMessage);
connectMqtt();
}
void loop() {
if (!mqtt.connected()) {
connectMqtt();
}
mqtt.loop();
// Your existing code...
}
void connectMqtt() {
String clientId = String("gansys_") + HARDWARE_ID;
if (mqtt.connect(clientId.c_str(), "username", "password")) {
String commandTopic = String("gansys/") + HARDWARE_ID + "/commands";
mqtt.subscribe(commandTopic.c_str());
Serial.println("MQTT connected");
}
}
void onMqttMessage(char* topic, byte* payload, unsigned int length) {
JsonDocument doc;
deserializeJson(doc, payload, length);
if (doc.containsKey("commands")) {
JsonArray commands = doc["commands"];
for (JsonObject cmd : commands) {
handleCommand(cmd);
}
}
}
void handleCommand(JsonObject cmd) {
String commandId = cmd["id"];
String channelKey = cmd["channelKey"];
bool desiredState = cmd["desiredBooleanState"];
Serial.print("Executing command: ");
Serial.println(commandId);
// Execute the command
if (channelKey == "spray_pump") {
digitalWrite(SPRAY_PUMP_PIN, desiredState ? HIGH : LOW);
}
// Acknowledge
acknowledgeCommand(commandId, "acknowledged", "Command executed successfully");
}
void acknowledgeCommand(String commandId, String status, String message) {
DateTime now = rtc.now();
JsonDocument doc;
JsonArray acks = doc["acknowledgements"].to<JsonArray>();
JsonObject ack = acks.add<JsonObject>();
ack["commandId"] = commandId;
ack["status"] = status;
ack["executedAt"] = now.timestamp(DateTime::TIMESTAMP_FULL);
ack["deviceMessage"] = message;
String payload;
serializeJson(doc, payload);
String ackTopic = String("gansys/") + HARDWARE_ID + "/acks";
mqtt.publish(ackTopic.c_str(), payload.c_str());
}- Schedule a command via web dashboard for 2 minutes in the future
- Monitor serial output on your device
- Verify command received at the scheduled time
- Check acknowledgement is sent back to server
- Confirm in dashboard that command status shows "acknowledged"
- Check MQTT connection is active
- Verify subscription to
gansys/{hardwareId}/commands - Check server logs for scheduler activity
- Sync RTC with NTP periodically
- Use DS3231 (more accurate than DS1307)
- Check battery backup
- Ensure device clock is synchronized
- Check
overrideUntiltimestamp parsing - Verify time zone handling
- Scheduled commands have a 2-minute override window by default
- Device should execute commands immediately when received
- RTC is optional but recommended for accurate logging
- All timestamps use ISO 8601 format with UTC timezone