#ifndef REST_API_H
#define REST_API_H
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include "WebPages.h"
// ---------------------- внешние сущности из .ino / других .h ----------------------
extern ESP8266WebServer server;
extern String savedSSID;
extern String savedPASS;
extern bool isOn;
void saveWiFiConfig(const String &ssid, const String &pass);
void saveIsOn(bool on);
void setOn(bool on);
String getUniqueID();
String getMAC();
// ---------------------- утилиты ----------------------
inline String deviceModeToString(DeviceMode m) {
switch (m) {
case DEVICE_MODE_SETUP: return F("setup");
case DEVICE_MODE_NORMAL: return F("normal");
case DEVICE_MODE_ERROR: return F("error");
case DEVICE_MODE_UPDATING: return F("updating");
}
return F("error");
}
inline void sendJson(int code, const String &body) {
server.send(code, F("application/json; charset=utf-8"), body);
}
// Простенький парсер "ключ":"значение" из JSON-строки (без вложенных кавычек и т.п.)
inline String extractJsonStringValue(const String &body, const String &key) {
String pattern = "\"" + key + "\"";
int keyIndex = body.indexOf(pattern);
if (keyIndex < 0) return "";
int colonIndex = body.indexOf(':', keyIndex);
if (colonIndex < 0) return "";
int firstQuote = body.indexOf('"', colonIndex + 1);
if (firstQuote < 0) return "";
int secondQuote = body.indexOf('"', firstQuote + 1);
if (secondQuote < 0) return "";
return body.substring(firstQuote + 1, secondQuote);
}
// Проверка токена из заголовка Authorization: Bearer <token>
inline bool hasValidToken() {
if (authToken.length() == 0) return false;
String header = server.header(F("Authorization"));
if (!header.startsWith(F("Bearer "))) return false;
String token = header.substring(7); // после "Bearer "
token.trim();
return token == authToken;
}
inline bool requireAuth() {
if (!hasValidToken()) {
String json = F("{\"status\":\"error\",\"error\":\"Unauthorized\",\"message\":\"Missing or invalid token\"}");
sendJson(401, json);
return false;
}
return true;
}
// Быстрый ответ "NotAvailable" для неподходящего режима
inline void sendNotAvailable() {
String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Setup mode is not active\"}");
sendJson(403, json);
}
// ---------------------- 1. GET /about ----------------------
inline void handleAbout() {
IPAddress ip = WiFi.localIP();
String json = "{";
json += "\"device_name\":\"" + deviceName + "\",";
json += "\"device_type\":\"" + String(DEVICE_TYPE) + "\",";
json += "\"firmware_version\":\"" + String(FW_VERSION) + "\",";
json += "\"device_id\":\"" + getUniqueID() + "\",";
json += "\"server\":\"" + serverBaseUrl + "\",";
json += "\"status\":\"" + deviceModeToString(deviceMode) + "\",";
json += "\"ip_address\":\"" + ip.toString() + "\",";
json += "\"mac_address\":\"" + getMAC() + "\",";
json += "\"uptime\":" + String(millis() / 1000);
json += "}";
sendJson(200, json);
}
// ---------------------- 2. GET /status ----------------------
// Для реле: {"state": "on" | "off" }
inline void handleStatus() {
// В режиме setup можем отдавать статус и без токена (по спецификации разрешено ограниченное поведение)
if (deviceMode == DEVICE_MODE_NORMAL) {
if (!requireAuth()) return;
}
String json = "{";
json += "\"state\":\"";
json += (isOn ? "on" : "off");
json += "\"}";
sendJson(200, json);
}
// ---------------------- 3. POST /action ----------------------
// {
// "action": "set_state",
// "params": { "state": "on" | "off" }
// }
inline void handleAction() {
if (deviceMode != DEVICE_MODE_NORMAL) {
// В режиме setup /action недоступен
String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Action not available in this mode\"}");
sendJson(403, json);
return;
}
if (!requireAuth()) return;
String body = server.arg("plain");
body.trim();
String action = extractJsonStringValue(body, F("action"));
if (action != F("set_state")) {
String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Device does not support this action or params\"}");
sendJson(400, json);
return;
}
String state = extractJsonStringValue(body, F("state"));
state.toLowerCase();
bool newState;
if (state == F("on")) {
newState = true;
} else if (state == F("off")) {
newState = false;
} else {
String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Unknown state value\"}");
sendJson(400, json);
return;
}
setOn(newState);
saveIsOn(newState);
String json = F("{\"status\":\"ok\",\"message\":\"State changed\"}");
sendJson(200, json);
}
// ---------------------- 4. POST /set_token ----------------------
// setup: без токена, первый раз устанавливает токен и переводит в normal
// normal: требует токен, меняет его
inline void handleSetToken() {
String body = server.arg("plain");
body.trim();
String newToken = extractJsonStringValue(body, F("token"));
if (newToken.length() == 0) {
String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"Token is required\"}");
sendJson(400, json);
return;
}
if (deviceMode == DEVICE_MODE_SETUP) {
if (authToken.length() > 0) {
// Устройство уже было провиженено
String json = F("{\"status\":\"error\",\"error\":\"AlreadyProvisioned\",\"message\":\"Device already provisioned\"}");
sendJson(409, json);
return;
}
authToken = newToken;
deviceMode = DEVICE_MODE_NORMAL;
saveDeviceConfig();
String json = F("{\"status\":\"ok\",\"message\":\"Token set. Device mode: normal\"}");
sendJson(200, json);
return;
}
// DEVICE_MODE_NORMAL — смена токена только с действующим токеном
if (deviceMode == DEVICE_MODE_NORMAL) {
if (!requireAuth()) return;
authToken = newToken;
saveDeviceConfig();
String json = F("{\"status\":\"ok\",\"message\":\"Token updated\"}");
sendJson(200, json);
return;
}
// Прочие режимы
String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Cannot set token in current mode\"}");
sendJson(403, json);
}
// ---------------------- 5. GET /setup ----------------------
// В режиме setup отдаём HTML-страницу настройки Wi-Fi
inline void handleSetupGet() {
if (deviceMode != DEVICE_MODE_SETUP) {
sendNotAvailable();
return;
}
// Используем уже готовую HTML-страницу
server.send(200, F("text/html; charset=utf-8"), wifiSetupPage);
}
// ---------------------- 5. POST /setup ----------------------
// JSON: {"ssid":"...","password":"...","server":"http://..."}
// или form-data, как в текущей веб-форме
inline void handleSetupPost() {
if (deviceMode != DEVICE_MODE_SETUP) {
sendNotAvailable();
return;
}
String ssid;
String pass;
String serverUrl;
// 1) Попытка прочитать как форму (из существующей веб-страницы)
if (server.hasArg(F("ssid"))) {
ssid = server.arg(F("ssid"));
pass = server.arg(F("pass"));
if (server.hasArg(F("server"))) {
serverUrl = server.arg(F("server"));
}
} else {
// 2) Попытка прочитать JSON
String body = server.arg("plain");
body.trim();
ssid = extractJsonStringValue(body, F("ssid"));
pass = extractJsonStringValue(body, F("password"));
serverUrl = extractJsonStringValue(body, F("server"));
}
if (ssid.length() == 0) {
String json = F("{\"status\":\"error\",\"error\":\"IllegalActionOrParams\",\"message\":\"SSID is required\"}");
sendJson(400, json);
return;
}
savedSSID = ssid;
savedPASS = pass;
saveWiFiConfig(savedSSID, savedPASS);
if (serverUrl.length() > 0) {
serverBaseUrl = serverUrl;
}
String json = F("{\"status\":\"ok\",\"message\":\"Wi-Fi configured. Connecting...\"}");
sendJson(200, json);
// Дальше просто перезапустимся — при старте прошивка сама попробует подключиться к Wi-Fi
delay(800);
ESP.restart();
}
// ---------------------- 6. POST /reboot ----------------------
inline void handleRebootApi() {
if (deviceMode != DEVICE_MODE_NORMAL) {
String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Reboot available only in normal mode\"}");
sendJson(403, json);
return;
}
if (!requireAuth()) return;
String json = F("{\"status\":\"ok\",\"message\":\"Device will reboot now\"}");
sendJson(200, json);
Serial.println(F("Reboot requested via REST API"));
delay(500);
ESP.restart();
}
// ---------------------- 7. POST /reset ----------------------
// Сброс всех настроек к заводским, переход в setup
inline void handleResetApi() {
if (deviceMode != DEVICE_MODE_NORMAL) {
String json = F("{\"status\":\"error\",\"error\":\"NotAvailable\",\"message\":\"Reset available only in normal mode\"}");
sendJson(403, json);
return;
}
if (!requireAuth()) return;
// Сбрасываем Wi-Fi
savedSSID = "";
savedPASS = "";
saveWiFiConfig(savedSSID, savedPASS);
// Выключаем реле
setOn(false);
saveIsOn(false);
// Сбрасываем токен и режим
authToken = "";
serverBaseUrl = "";
deviceMode = DEVICE_MODE_SETUP;
saveDeviceConfig();
String json = F("{\"status\":\"ok\",\"message\":\"Device reset to factory settings. Entering setup mode.\"}");
sendJson(200, json);
delay(800);
ESP.restart();
}
// ---------------------- Регистрация роутов ----------------------
inline void registerRestApiRoutes() {
server.on(F("/about"), HTTP_GET, handleAbout);
server.on(F("/status"), HTTP_GET, handleStatus);
server.on(F("/action"), HTTP_POST, handleAction);
server.on(F("/set_token"), HTTP_POST, handleSetToken);
server.on(F("/setup"), HTTP_GET, handleSetupGet);
server.on(F("/setup"), HTTP_POST, handleSetupPost);
server.on(F("/reboot"), HTTP_POST, handleRebootApi);
server.on(F("/reset"), HTTP_POST, handleResetApi);
}
#endif // REST_API_H