Platform-Agnostic API
The Smart Dashboard API is hardware-agnostic and works with any device capable of making HTTP requests.
While this documentation includes a NodeMCU (ESP8266/ESP32) firmware example as a reference implementation,
the API can also be integrated with:
- 🐍 Python applications on Raspberry Pi, Linux servers, or desktop
- 📱 Mobile applications (iOS/Android) with native HTTP clients
- 🌐 Web frontends using JavaScript fetch() or Axios
- 🔧 Other WiFi-enabled devices (Arduino with WiFi shields, ESP32, etc.)
Note: The provided firmware is only a reference implementation, not a limitation of supported platforms.
Complete firmware example with auto‑reconnect, state sync, and command queue processing.
/*
* NodeMCU Smart Bulb Controller (4 Devices) - State Persistence
* ESP8266 / NodeMCU
* Full integration with Smart Dashboard API
*
* Features:
* - 4 independently controlled relay outputs
* - HTTPS communication with ODIVORA Smart Dashboard API
* - Persistent relay state using LittleFS
* - Device heartbeat with retry on failure
* - Remote ON / OFF / TOGGLE commands
* - State confirmation
* - Device reconciliation after boot
* - Automatic WiFi reconnection
* - HTTP/JSON error handling
* - Request timeouts
*/
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
// WiFi & Server config
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* SERVER_URL = "https://odivora.com";
const char* WIFI_BOARD_ID = "ESP8266";
const char* API_KEY = "YOUR_API_KEY";
const char* API_SECRET = "YOUR_API_SECRET";
#define DEVICE_COUNT 4
const char* DEVICE_IDS[DEVICE_COUNT] = {
"SD-A42EACCBB767", "SD-64316F62789F",
"SD-5D22DC343C9B", "SD-E2C60A2E5EDD"
};
const int RELAY_PINS[DEVICE_COUNT] = { D1, D2, D5, D6 };
const bool RELAY_ACTIVE_LOW = false;
// Timing - heartbeats every 4s, retries every 2s
const unsigned long HEARTBEAT_INTERVAL = 4000;
const unsigned long COMMAND_INTERVAL = 1500;
const unsigned long WIFI_RETRY_INTERVAL = 5000;
const unsigned long HEARTBEAT_RETRY_INTERVAL = 2000;
const unsigned long HTTP_TIMEOUT = 10000;
// Persistent state
const char* STATE_FILE = "/relay_state.json";
// Runtime state
bool bulbState[DEVICE_COUNT] = { false, false, false, false };
String lastCommand[DEVICE_COUNT];
unsigned long lastHeartbeat = 0, lastCmdCheck = 0;
unsigned long lastWiFiRetry = 0, lastHeartbeatRetry = 0;
bool heartbeatFailed[DEVICE_COUNT] = { false, false, false, false };
// HTTP clients
WiFiClientSecure secureClient;
WiFiClient plainClient;
bool useTLS = false;
// Forward declarations
void connectWiFi();
void reconnectWiFi();
void setRelay(int idx, bool on);
void executeCommand(int idx, const String& cmd);
void sendAllHeartbeats();
void retryFailedHeartbeats();
void checkAllCommands();
void confirmState(int idx);
void reconcileAllDevices();
bool loadPersistentState();
bool savePersistentState();
bool beginHttp(HTTPClient& http, const String& path);
String urlEncode(const String& value);
void setup() {
Serial.begin(115200);
delay(200);
Serial.println();
Serial.println("================================================");
Serial.println(" ODIVORA NODEMCU SMART BULB CONTROLLER");
Serial.println("================================================");
useTLS = String(SERVER_URL).startsWith("https://");
if (useTLS) {
secureClient.setInsecure();
secureClient.setTimeout(HTTP_TIMEOUT / 1000);
} else {
plainClient.setTimeout(HTTP_TIMEOUT / 1000);
}
// Initialize LittleFS
if (!LittleFS.begin()) {
LittleFS.format();
LittleFS.begin();
}
// Load saved relay states
if (!loadPersistentState()) {
for (int i = 0; i < DEVICE_COUNT; i++) bulbState[i] = false;
}
// Configure relay pins
for (int i = 0; i < DEVICE_COUNT; i++) {
pinMode(RELAY_PINS[i], OUTPUT);
setRelay(i, bulbState[i]);
}
connectWiFi();
if (WiFi.status() == WL_CONNECTED) reconcileAllDevices();
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
reconnectWiFi();
delay(10);
return;
}
// Send heartbeats every 4 seconds
if (millis() - lastHeartbeat >= HEARTBEAT_INTERVAL) {
lastHeartbeat = millis();
sendAllHeartbeats();
}
// Retry failed heartbeats every 2 seconds
if (millis() - lastHeartbeatRetry >= HEARTBEAT_RETRY_INTERVAL) {
lastHeartbeatRetry = millis();
retryFailedHeartbeats();
}
// Check commands every 1.5 seconds
if (millis() - lastCmdCheck >= COMMAND_INTERVAL) {
lastCmdCheck = millis();
checkAllCommands();
}
yield();
}
bool beginHttp(HTTPClient& http, const String& path) {
String url = String(SERVER_URL) + path;
http.setTimeout(HTTP_TIMEOUT);
if (useTLS) return http.begin(secureClient, url);
return http.begin(plainClient, url);
}
void sendAllHeartbeats() {
for (int i = 0; i < DEVICE_COUNT; i++) {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
DynamicJsonDocument stateDoc(256);
stateDoc["power"] = bulbState[i];
stateDoc["timestamp"] = millis();
String stateJson;
serializeJson(stateDoc, stateJson);
String path = "/api/v1/smart_dashboard/device/" + String(DEVICE_IDS[i]) +
"/heartbeat?status=online&wifi_board_id=" + urlEncode(WIFI_BOARD_ID) +
"&initial_state=" + urlEncode(stateJson);
if (!beginHttp(http, path)) {
http.end();
heartbeatFailed[i] = true;
continue;
}
http.addHeader("X-API-Key", API_KEY);
http.addHeader("X-API-Secret", API_SECRET);
http.addHeader("Accept", "application/json");
int code = http.GET();
if (code >= 200 && code < 300) {
heartbeatFailed[i] = false;
} else {
heartbeatFailed[i] = true;
}
http.end();
delay(30);
yield();
}
}
void retryFailedHeartbeats() {
bool anyFailed = false;
for (int i = 0; i < DEVICE_COUNT; i++) {
if (heartbeatFailed[i]) { anyFailed = true; break; }
}
if (!anyFailed) return;
for (int i = 0; i < DEVICE_COUNT; i++) {
if (!heartbeatFailed[i]) continue;
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
DynamicJsonDocument stateDoc(256);
stateDoc["power"] = bulbState[i];
stateDoc["timestamp"] = millis();
String stateJson;
serializeJson(stateDoc, stateJson);
String path = "/api/v1/smart_dashboard/device/" + String(DEVICE_IDS[i]) +
"/heartbeat?status=online&wifi_board_id=" + urlEncode(WIFI_BOARD_ID) +
"&initial_state=" + urlEncode(stateJson);
if (!beginHttp(http, path)) { http.end(); continue; }
http.addHeader("X-API-Key", API_KEY);
http.addHeader("X-API-Secret", API_SECRET);
http.addHeader("Accept", "application/json");
int code = http.GET();
if (code >= 200 && code < 300) heartbeatFailed[i] = false;
http.end();
delay(30);
yield();
}
}
void checkAllCommands() {
for (int i = 0; i < DEVICE_COUNT; i++) {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
String path = "/api/v1/smart_dashboard/device/" + String(DEVICE_IDS[i]) +
"/status?wifi_board_id=" + urlEncode(WIFI_BOARD_ID);
if (!beginHttp(http, path)) { http.end(); continue; }
http.addHeader("X-API-Key", API_KEY);
http.addHeader("X-API-Secret", API_SECRET);
http.addHeader("Accept", "application/json");
int code = http.GET();
if (code == 200) {
DynamicJsonDocument doc(1024);
if (!deserializeJson(doc, http.getString())) {
if (doc.containsKey("command") && !doc["command"].isNull()) {
String cmd = doc["command"].as<String>();
cmd.trim();
cmd.toLowerCase();
if (cmd.length() > 0 && cmd != lastCommand[i]) {
lastCommand[i] = cmd;
executeCommand(i, cmd);
}
}
}
}
http.end();
delay(50);
yield();
}
}
void executeCommand(int idx, const String& cmd) {
if (idx < 0 || idx >= DEVICE_COUNT) return;
bool newState;
if (cmd == "on") newState = true;
else if (cmd == "off") newState = false;
else if (cmd == "toggle") newState = !bulbState[idx];
else return;
bulbState[idx] = newState;
setRelay(idx, newState);
savePersistentState();
confirmState(idx);
}
void confirmState(int idx) {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
String path = "/api/v1/smart_dashboard/device/" + String(DEVICE_IDS[idx]) + "/confirm_state";
if (!beginHttp(http, path)) { http.end(); return; }
DynamicJsonDocument doc(256);
doc["power"] = bulbState[idx];
doc["wifi_board_id"] = WIFI_BOARD_ID;
String payload;
serializeJson(doc, payload);
http.addHeader("Content-Type", "application/json");
http.addHeader("X-API-Key", API_KEY);
http.addHeader("X-API-Secret", API_SECRET);
http.POST(payload);
http.end();
}
void reconcileAllDevices() {
for (int i = 0; i < DEVICE_COUNT; i++) {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
String path = "/api/v1/smart_dashboard/device/" + String(DEVICE_IDS[i]) + "/reconcile";
if (!beginHttp(http, path)) { http.end(); continue; }
DynamicJsonDocument doc(256);
doc["power"] = bulbState[i];
doc["wifi_board_id"] = WIFI_BOARD_ID;
String payload;
serializeJson(doc, payload);
http.addHeader("Content-Type", "application/json");
http.addHeader("X-API-Key", API_KEY);
http.addHeader("X-API-Secret", API_SECRET);
http.POST(payload);
http.end();
delay(100);
yield();
}
}
void setRelay(int idx, bool on) {
if (idx < 0 || idx >= DEVICE_COUNT) return;
int outputState = RELAY_ACTIVE_LOW ? (on ? LOW : HIGH) : (on ? HIGH : LOW);
digitalWrite(RELAY_PINS[idx], outputState);
}
bool savePersistentState() {
if (!LittleFS.begin()) return false;
DynamicJsonDocument doc(512);
JsonArray states = doc.createNestedArray("states");
for (int i = 0; i < DEVICE_COUNT; i++) states.add(bulbState[i]);
File file = LittleFS.open(STATE_FILE, "w");
if (!file) return false;
size_t bytes = serializeJson(doc, file);
file.close();
return bytes > 0;
}
bool loadPersistentState() {
if (!LittleFS.begin()) return false;
if (!LittleFS.exists(STATE_FILE)) return false;
File file = LittleFS.open(STATE_FILE, "r");
if (!file) return false;
DynamicJsonDocument doc(512);
if (deserializeJson(doc, file)) { file.close(); return false; }
file.close();
if (!doc.containsKey("states")) return false;
JsonArray states = doc["states"].as<JsonArray>();
if (states.size() != DEVICE_COUNT) return false;
for (int i = 0; i < DEVICE_COUNT; i++) bulbState[i] = states[i].as<bool>();
return true;
}
void connectWiFi() {
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 30000) {
delay(500);
yield();
}
}
void reconnectWiFi() {
if (millis() - lastWiFiRetry < WIFI_RETRY_INTERVAL) return;
lastWiFiRetry = millis();
WiFi.disconnect();
delay(100);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
String urlEncode(const String& value) {
String encoded;
const char* hex = "0123456789ABCDEF";
for (size_t i = 0; i < value.length(); i++) {
unsigned char c = static_cast<unsigned char>(value[i]);
if (isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
encoded += static_cast<char>(c);
} else {
encoded += '%';
encoded += hex[(c >> 4) & 0x0F];
encoded += hex[c & 0x0F];
}
}
return encoded;
}