Newer
Older
smart-home-server / devices / relay / relay_esp8266 / REST_API.h
#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