mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
feat!: remove the old API, ports 8081 and 8080 (step 4a)
Step 4a of docs/SECURITY_PLAN.md. Port 80 keeps the eleven HTTP routes of ADR 0006 and the socket at /ws; everything else moved to protocol 2. - Port 8081 (ApiRoutes) and the socket on 8080 (SocketEmitter) are gone. Protocol 2 owns its frame buffer and loop-task check. - Port 80 drops every route the old web UI called: reads, edits, settings, scans, reboot, /shades.cfg, /discovery. HttpRoutes.cpp holds what stays. - ModelSnapshot, SnapshotSlots and the read documents are gone. The item snapshot types stay, as ItemSnapshot, for events, MQTT and the command replies. - Model edits reply only to their protocol 2 op: requestResult goes. - The settings' toJSON writers, used only by the settings routes, go. - The _espsomfy_rts._tcp mDNS record moves to port 80, with path=/ws. - The memory event reports the loop-rate window it closes. - A deleted room no longer re-publishes its retained MQTT topics. - e2e: the heap check reads the sync's status; the sync is checked on its own instead of against /controller. - README gets an API and ports section. BREAKING CHANGE: ports 8081 and 8080 and the old HTTP routes are gone. Home Assistant needs integration 3.0.0 or newer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c91c20d444
commit
2a954aa158
53 changed files with 613 additions and 3549 deletions
|
|
@ -1,7 +1,4 @@
|
|||
#include <WiFi.h>
|
||||
#include <LittleFS.h>
|
||||
#include <Update.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include <esp_timer.h>
|
||||
#include "esp_log.h"
|
||||
#include "mbedtls/md.h"
|
||||
|
|
@ -9,52 +6,36 @@
|
|||
#include <esp_random.h>
|
||||
#include <bootloader_random.h>
|
||||
#include "ConfigSettings.h"
|
||||
#include "ConfigFile.h"
|
||||
#include "Utils.h"
|
||||
#include "SSDP.h"
|
||||
#include "Somfy.h"
|
||||
#include "WResp.h"
|
||||
#include "LoopQueue.h"
|
||||
#include "ModelSnapshot.h"
|
||||
#include "ItemSnapshot.h"
|
||||
#include "SomfySnapshot.h"
|
||||
#include "SomfyEditor.h"
|
||||
#include "Web.h"
|
||||
#include "WebRoutes.h"
|
||||
#include "outputs/ProtoSocket.h"
|
||||
#include "MqttConnection.h"
|
||||
#include "ESPNetwork.h"
|
||||
#include "Timing.h"
|
||||
#include "LoginThrottle.h"
|
||||
#include <AsyncTCP.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncJson.h>
|
||||
#include <memory>
|
||||
|
||||
extern ConfigSettings settings;
|
||||
extern SSDPClass SSDP;
|
||||
extern rebootDelay_t rebootDelay;
|
||||
extern LoopStats loopStats;
|
||||
extern SomfyShadeController somfy;
|
||||
extern Web webServer;
|
||||
extern MqttConnection mqtt;
|
||||
extern ESPNetwork net;
|
||||
extern LoopQueue loopQueue;
|
||||
extern SnapshotSlots snapshotSlots;
|
||||
|
||||
char g_async_content[WEB_MAX_RESPONSE];
|
||||
|
||||
|
||||
// General responses
|
||||
static const char _response_404[] = "404: Service Not Found";
|
||||
|
||||
|
||||
// Encodings
|
||||
const char _encoding_text[] = "text/plain";
|
||||
const char _encoding_json[] = "application/json";
|
||||
|
||||
static const char *TAG = "Web";
|
||||
|
||||
// Failed logins per client IP, on both web servers.
|
||||
// Failed logins per client IP.
|
||||
static LoginThrottle loginThrottle;
|
||||
// The throttle's key for a client: its IPv4 address. The firmware doesn't
|
||||
// enable IPv6; an IPv6 client would share key 0 with every other one.
|
||||
|
|
@ -65,7 +46,7 @@ static uint64_t loginNow() { return (uint64_t)esp_timer_get_time() / 1000; }
|
|||
// The tokens' HMAC key. It has its own NVS namespace, which no settings
|
||||
// save clears and no backup holds, so only erasing NVS replaces it.
|
||||
static uint8_t tokenKey[API_TOKEN_KEY_SIZE];
|
||||
// Reads the token key, or makes one on first boot. Runs before either web
|
||||
// Reads the token key, or makes one on first boot. Runs before the web
|
||||
// server starts, so no token is made with an empty key, and before WiFi, so
|
||||
// the hardware RNG takes its entropy from the bootloader's source: calling it
|
||||
// once the radio is on would disturb WiFi.
|
||||
|
|
@ -82,6 +63,14 @@ static void loadTokenKey() {
|
|||
pref.end();
|
||||
}
|
||||
|
||||
// Sent when a command can't be handed to the main loop.
|
||||
static void sendQueueFull(AsyncWebServerRequest *request) {
|
||||
request->send(503, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Command queue full\"}"));
|
||||
}
|
||||
// Sent when the radio can't take a frame (suspended or queue full).
|
||||
static void sendRadioUnavailable(AsyncWebServerRequest *request) {
|
||||
request->send(503, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Radio TX unavailable\"}"));
|
||||
}
|
||||
// Hands an item to loop(), which owns the shades. When it can't be the
|
||||
// request is answered with the reason and false is returned.
|
||||
bool queueLoopItem(AsyncWebServerRequest *request, const loop_item_t &item) {
|
||||
|
|
@ -91,27 +80,15 @@ bool queueLoopItem(AsyncWebServerRequest *request, const loop_item_t &item) {
|
|||
if(!loopQueue.submit(item)) { sendQueueFull(request); return false; }
|
||||
return true;
|
||||
}
|
||||
bool queueShadeRequest(AsyncWebServerRequest *request, const shade_request_t &req) {
|
||||
static bool queueShadeRequest(AsyncWebServerRequest *request, const shade_request_t &req) {
|
||||
loop_item_t item;
|
||||
item.request = req;
|
||||
return queueLoopItem(request, item);
|
||||
}
|
||||
// Sent when a command can't be handed to the main loop.
|
||||
void sendQueueFull(AsyncWebServerRequest *request) {
|
||||
request->send(503, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Command queue full\"}"));
|
||||
}
|
||||
// Sent when the radio can't take a frame (suspended or queue full).
|
||||
void sendRadioUnavailable(AsyncWebServerRequest *request) {
|
||||
request->send(503, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Radio TX unavailable\"}"));
|
||||
}
|
||||
|
||||
AsyncWebServer asyncServer(80);
|
||||
AsyncWebServer asyncApiServer(8081);
|
||||
void Web::startup() {
|
||||
ESP_LOGI(TAG, "Launching web server...");
|
||||
loadTokenKey();
|
||||
asyncApiServer.begin();
|
||||
ESP_LOGI(TAG, "Async API server started on port 8081");
|
||||
}
|
||||
void Web::loop() {
|
||||
delay(1);
|
||||
|
|
@ -187,183 +164,20 @@ bool asyncHasParam(AsyncWebServerRequest *request, const char *name) {
|
|||
}
|
||||
|
||||
// -- Serialization helpers --
|
||||
// Replies that still read the live shade model on this task (command and edit
|
||||
// replies) copy one item into the ModelSnapshot types and write it the way the
|
||||
// read routes do. The read routes themselves write from a ModelSnapshot that
|
||||
// loop() fills (sendSnapshot).
|
||||
void serializeShadeRef(SomfyShade *shade, JsonFormatter &json) {
|
||||
// The command replies read the live shade model on this task: they copy the
|
||||
// one item into the snapshot types and write its reference.
|
||||
static void serializeShadeRef(SomfyShade *shade, JsonFormatter &json) {
|
||||
shade_snapshot_t s;
|
||||
snapshotShade(*shade, s);
|
||||
writeShadeRef(json, s);
|
||||
}
|
||||
// paired and lastRollingCode are passed in for a response that reports the
|
||||
// change the main loop is about to make. An unpair is held until it goes on
|
||||
// air (holdLoopItem), so the answer comes true.
|
||||
void serializeShade(SomfyShade *shade, JsonFormatter &json, bool paired, uint16_t lastRollingCode) {
|
||||
shade_snapshot_t s;
|
||||
snapshotShade(*shade, s);
|
||||
s.paired = paired;
|
||||
s.lastRollingCode = lastRollingCode;
|
||||
writeShade(json, s);
|
||||
}
|
||||
void serializeShade(SomfyShade *shade, JsonFormatter &json) { serializeShade(shade, json, shade->paired, shade->lastRollingCode); }
|
||||
static void serializeGroupRef(SomfyGroup *group, JsonFormatter &json) {
|
||||
group_snapshot_t s;
|
||||
snapshotGroup(*group, s);
|
||||
writeGroupRef(json, s);
|
||||
}
|
||||
static void serializeGroup(SomfyGroup *group, JsonFormatter &json) {
|
||||
group_snapshot_t s;
|
||||
snapshotGroup(*group, s);
|
||||
shade_snapshot_t linked;
|
||||
writeGroup(json, s, liveShade, &linked);
|
||||
}
|
||||
void serializeTransceiverConfig(JsonFormatter &json) {
|
||||
auto &cfg = somfy.transceiver.config;
|
||||
json.addElem("type", cfg.type);
|
||||
json.addElem("TXPin", cfg.TXPin);
|
||||
json.addElem("RXPin", cfg.RXPin);
|
||||
json.addElem("SCKPin", cfg.SCKPin);
|
||||
json.addElem("MOSIPin", cfg.MOSIPin);
|
||||
json.addElem("MISOPin", cfg.MISOPin);
|
||||
json.addElem("CSNPin", cfg.CSNPin);
|
||||
json.addElem("rxBandwidth", cfg.rxBandwidth);
|
||||
json.addElem("frequency", cfg.frequency);
|
||||
json.addElem("deviation", cfg.deviation);
|
||||
json.addElem("txPower", cfg.txPower);
|
||||
json.addElem("proto", static_cast<uint8_t>(cfg.proto));
|
||||
json.addElem("enabled", cfg.enabled);
|
||||
json.addElem("noiseDetection", cfg.noiseDetection);
|
||||
json.addElem("radioInit", cfg.radioInit);
|
||||
}
|
||||
void serializeAppVersion(JsonFormatter &json, appver_t &ver) {
|
||||
json.addElem("name", ver.name);
|
||||
json.addElem("major", ver.major);
|
||||
json.addElem("minor", ver.minor);
|
||||
json.addElem("build", ver.build);
|
||||
json.addElem("suffix", ver.suffix);
|
||||
}
|
||||
void serializeVersion(JsonFormatter &json) {
|
||||
json.beginObject("fwVersion");
|
||||
serializeAppVersion(json, settings.fwVersion);
|
||||
json.endObject();
|
||||
json.beginObject("appVersion");
|
||||
serializeAppVersion(json, settings.appVersion);
|
||||
json.endObject();
|
||||
}
|
||||
|
||||
// The parts of /controller that aren't in the shade model.
|
||||
static void controllerHeader(JsonFormatter &json, const model_snapshot_t &snap) {
|
||||
json.addElem("maxRooms", (uint8_t)SOMFY_MAX_ROOMS);
|
||||
json.addElem("maxShades", (uint8_t)SOMFY_MAX_SHADES);
|
||||
json.addElem("maxGroups", (uint8_t)SOMFY_MAX_GROUPS);
|
||||
json.addElem("maxGroupedShades", (uint8_t)SOMFY_MAX_GROUPED_SHADES);
|
||||
json.addElem("maxLinkedRemotes", (uint8_t)SOMFY_MAX_LINKED_REMOTES);
|
||||
json.addElem("startingAddress", snap.startingAddress);
|
||||
json.beginObject("transceiver");
|
||||
json.beginObject("config");
|
||||
serializeTransceiverConfig(json);
|
||||
json.endObject();
|
||||
json.endObject();
|
||||
json.beginObject("version");
|
||||
serializeVersion(json);
|
||||
json.endObject();
|
||||
}
|
||||
// The parts of /discovery that aren't in the shade model.
|
||||
static void discoveryHeader(JsonFormatter &json, const model_snapshot_t &snap) {
|
||||
(void)snap;
|
||||
json.addElem("serverId", settings.serverId);
|
||||
json.addElem("version", settings.fwVersion.name);
|
||||
json.addElem("model", "ESPSomfyRTS");
|
||||
json.addElem("hostname", settings.hostname);
|
||||
json.addElem("authType", static_cast<uint8_t>(settings.Security.type));
|
||||
json.addElem("permissions", settings.Security.permissions);
|
||||
json.addElem("chipModel", settings.chipModel);
|
||||
json.addElem("connType", net.connTypeName());
|
||||
json.beginObject("memory");
|
||||
json.addElem("max", ESP.getMaxAllocHeap());
|
||||
json.addElem("free", ESP.getFreeHeap());
|
||||
json.addElem("min", ESP.getMinFreeHeap());
|
||||
json.addElem("total", ESP.getHeapSize());
|
||||
json.addElem("uptime", (uint64_t)millis());
|
||||
// This runs on the web server's task, so it reports the last window the
|
||||
// loop task closed rather than reading the one it is still filling.
|
||||
json.addElem("loopRate", loopStats.publishedRate());
|
||||
json.addElem("loopMaxUs", loopStats.publishedMaxUs());
|
||||
json.endObject();
|
||||
}
|
||||
|
||||
// A read served from a ModelSnapshot that loop() fills, so the document
|
||||
// describes one moment (docs/adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md).
|
||||
// Each item is formatted into a fixed 3 KB unit and streamed as the TCP send
|
||||
// window allows, so there is no growing response buffer either.
|
||||
struct SnapshotRead {
|
||||
uint8_t slot;
|
||||
char unit[SNAPSHOT_UNIT_SIZE];
|
||||
SnapshotWriter writer;
|
||||
SnapshotRead(uint8_t slot, snapshot_doc_t doc, uint8_t id, snapshot_header_t header)
|
||||
: slot(slot), writer(snapshotSlots.snapshot(slot), doc, id, header, unit, sizeof(unit)) {}
|
||||
// The response is gone, whether it finished or the client went away.
|
||||
~SnapshotRead() { snapshotSlots.release(this->slot); }
|
||||
};
|
||||
void sendSnapshot(AsyncWebServerRequest *request, snapshot_doc_t doc, uint8_t id, snapshot_header_t header) {
|
||||
const int8_t slot = snapshotSlots.acquire();
|
||||
if(slot < 0) {
|
||||
request->send(503, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Busy, try the read again\"}"));
|
||||
return;
|
||||
}
|
||||
loop_item_t item;
|
||||
item.type = loop_item_type_t::snapshot_fill;
|
||||
item.slot = static_cast<uint8_t>(slot);
|
||||
if(!loopQueue.submit(item)) {
|
||||
snapshotSlots.cancel(slot);
|
||||
sendQueueFull(request);
|
||||
return;
|
||||
}
|
||||
auto read = std::make_shared<SnapshotRead>(static_cast<uint8_t>(slot), doc, id, header);
|
||||
// The headers go now; the body waits until loop() has filled the snapshot.
|
||||
// The library asks again on the next ACK or poll.
|
||||
AsyncWebServerResponse *response = request->beginChunkedResponse(_encoding_json,
|
||||
[read](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
|
||||
(void)index;
|
||||
if(!snapshotSlots.filled(read->slot)) return RESPONSE_TRY_AGAIN;
|
||||
return read->writer.generate(buffer, maxLen);
|
||||
});
|
||||
request->send(response);
|
||||
}
|
||||
|
||||
// -- Async handler implementations --
|
||||
void Web::handleDiscovery(AsyncWebServerRequest *request) {
|
||||
if(request->method() == HTTP_POST || request->method() == HTTP_GET) {
|
||||
ESP_LOGD(TAG, "Async Discovery Requested");
|
||||
sendSnapshot(request, snapshot_doc_t::discovery, 0, discoveryHeader);
|
||||
}
|
||||
else
|
||||
request->send(500, _encoding_text, "Invalid http method");
|
||||
}
|
||||
void Web::handleGetRooms(AsyncWebServerRequest *request) {
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
if(request->method() == HTTP_POST || request->method() == HTTP_GET) sendSnapshot(request, snapshot_doc_t::rooms);
|
||||
else request->send(404, _encoding_text, _response_404);
|
||||
}
|
||||
void Web::handleGetShades(AsyncWebServerRequest *request) {
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
if(request->method() == HTTP_POST || request->method() == HTTP_GET) sendSnapshot(request, snapshot_doc_t::shades);
|
||||
else request->send(404, _encoding_text, _response_404);
|
||||
}
|
||||
void Web::handleGetGroups(AsyncWebServerRequest *request) {
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
if(request->method() == HTTP_POST || request->method() == HTTP_GET) sendSnapshot(request, snapshot_doc_t::groups);
|
||||
else request->send(404, _encoding_text, _response_404);
|
||||
}
|
||||
void Web::handleController(AsyncWebServerRequest *request) {
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
if(request->method() == HTTP_POST || request->method() == HTTP_GET) {
|
||||
settings.printAvailHeap();
|
||||
sendSnapshot(request, snapshot_doc_t::controller, 0, controllerHeader);
|
||||
}
|
||||
else request->send(404, _encoding_text, _response_404);
|
||||
}
|
||||
void Web::handleLogin(AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
char token[API_TOKEN_SIZE];
|
||||
|
|
@ -534,188 +348,6 @@ void Web::handleTiltCommand(AsyncWebServerRequest *request, JsonVariant &json) {
|
|||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}"));
|
||||
}
|
||||
}
|
||||
void Web::handleRepeatCommand(AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
uint8_t shadeId = 255;
|
||||
uint8_t groupId = 255;
|
||||
shade_request_t req;
|
||||
if(asyncHasParam(request, "shadeId")) shadeId = asyncParam(request, "shadeId").toInt();
|
||||
else if(asyncHasParam(request, "groupId")) groupId = asyncParam(request, "groupId").toInt();
|
||||
if(asyncHasParam(request, "command")) req.command = translateSomfyCommand(asyncParam(request, "command"));
|
||||
if(asyncHasParam(request, "repeat")) req.repeat = requestRepeat(asyncParam(request, "repeat").toInt());
|
||||
if(asyncHasParam(request, "stepSize")) req.stepSize = asyncParam(request, "stepSize").toInt();
|
||||
if(shadeId == 255 && groupId == 255 && !json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) shadeId = obj["shadeId"];
|
||||
if(!obj["groupId"].isNull()) groupId = obj["groupId"];
|
||||
if(!obj["stepSize"].isNull()) req.stepSize = obj["stepSize"];
|
||||
if(!obj["command"].isNull()) { String scmd = obj["command"]; req.command = translateSomfyCommand(scmd); }
|
||||
if(!obj["repeat"].isNull()) req.repeat = requestRepeat(obj["repeat"].as<int>());
|
||||
}
|
||||
if(shadeId != 255) {
|
||||
ESP_LOGI(TAG, "handleRepeatCommand shade=%u command=%s", shadeId, translateSomfyCommand(req.command).c_str());
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade reference could not be found.\"}")); return; }
|
||||
req.type = shade_request_type_t::shade_repeat;
|
||||
req.id = shadeId;
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShadeRef(shade, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else if(groupId != 255) {
|
||||
ESP_LOGI(TAG, "handleRepeatCommand group=%u command=%s", groupId, translateSomfyCommand(req.command).c_str());
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
if(!group) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group reference could not be found.\"}")); return; }
|
||||
req.type = shade_request_type_t::group_repeat;
|
||||
req.id = groupId;
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeGroupRef(group, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}"));
|
||||
}
|
||||
void Web::handleRoom(AsyncWebServerRequest *request) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
if(request->method() == HTTP_GET) {
|
||||
if(asyncHasParam(request, "roomId")) {
|
||||
int roomId = asyncParam(request, "roomId").toInt();
|
||||
// The status goes out before the snapshot is filled, so whether the room
|
||||
// exists is checked here, on the live model.
|
||||
if(somfy.getRoomById(roomId)) sendSnapshot(request, snapshot_doc_t::room, roomId);
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Room Id not found.\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid room id.\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}"));
|
||||
}
|
||||
void Web::handleShade(AsyncWebServerRequest *request) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
if(request->method() == HTTP_GET) {
|
||||
if(asyncHasParam(request, "shadeId")) {
|
||||
int shadeId = asyncParam(request, "shadeId").toInt();
|
||||
// As for /room, existence is checked on the live model.
|
||||
if(somfy.getShadeById(shadeId)) sendSnapshot(request, snapshot_doc_t::shade, shadeId);
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid shade id.\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}"));
|
||||
}
|
||||
void Web::handleGroup(AsyncWebServerRequest *request) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
if(request->method() == HTTP_GET) {
|
||||
if(asyncHasParam(request, "groupId")) {
|
||||
int groupId = asyncParam(request, "groupId").toInt();
|
||||
// As for /room, existence is checked on the live model.
|
||||
if(somfy.getGroupById(groupId)) sendSnapshot(request, snapshot_doc_t::group, groupId);
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid group id.\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}"));
|
||||
}
|
||||
void Web::handleSetPositions(AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
shade_request_t req;
|
||||
req.type = shade_request_type_t::set_positions;
|
||||
if(asyncHasParam(request, "shadeId")) req.id = asyncParam(request, "shadeId").toInt();
|
||||
if(asyncHasParam(request, "position")) req.position = requestTarget(asyncParam(request, "position").toInt());
|
||||
if(asyncHasParam(request, "tiltPosition")) req.tiltPosition = requestTarget(asyncParam(request, "tiltPosition").toInt());
|
||||
if(!json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) req.id = obj["shadeId"];
|
||||
if(!obj["position"].isNull()) req.position = requestTarget(obj["position"].as<int>());
|
||||
if(!obj["tiltPosition"].isNull()) req.tiltPosition = requestTarget(obj["tiltPosition"].as<int>());
|
||||
}
|
||||
if(req.id != 255) {
|
||||
ESP_LOGI(TAG, "handleSetPositions shade=%u pos=%d tiltPos=%d", req.id, req.position, req.tiltPosition);
|
||||
SomfyShade *shade = somfy.getShadeById(req.id);
|
||||
if(shade) {
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
// loop() applies the request after we answer so this is the state before
|
||||
// it. Clients get the result from the shadeState socket event.
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShade(shade, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"An invalid shadeId was provided\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"shadeId was not provided\"}"));
|
||||
}
|
||||
void Web::handleSetSensor(AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
uint8_t shadeId = asyncHasParam(request, "shadeId") ? asyncParam(request, "shadeId").toInt() : 255;
|
||||
uint8_t groupId = asyncHasParam(request, "groupId") ? asyncParam(request, "groupId").toInt() : 255;
|
||||
shade_request_t req;
|
||||
if(asyncHasParam(request, "sunny")) req.sunny = toBoolean(asyncParam(request, "sunny").c_str(), false) ? 1 : 0;
|
||||
if(asyncHasParam(request, "windy")) req.windy = asyncParam(request, "windy").toInt();
|
||||
if(asyncHasParam(request, "repeat")) req.repeat = requestRepeat(asyncParam(request, "repeat").toInt());
|
||||
if(!json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) shadeId = obj["shadeId"].as<uint8_t>();
|
||||
if(!obj["groupId"].isNull()) groupId = obj["groupId"].as<uint8_t>();
|
||||
if(!obj["sunny"].isNull()) {
|
||||
if(obj["sunny"].is<bool>()) req.sunny = obj["sunny"].as<bool>() ? 1 : 0;
|
||||
else req.sunny = obj["sunny"].as<int8_t>();
|
||||
}
|
||||
if(!obj["windy"].isNull()) {
|
||||
if(obj["windy"].is<bool>()) req.windy = obj["windy"].as<bool>() ? 1 : 0;
|
||||
else req.windy = obj["windy"].as<int8_t>();
|
||||
}
|
||||
if(!obj["repeat"].isNull()) req.repeat = requestRepeat(obj["repeat"].as<int>());
|
||||
}
|
||||
if(shadeId != 255) {
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
if(shade) {
|
||||
req.type = shade_request_type_t::shade_sensor;
|
||||
req.id = shadeId;
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
// The main loop applies the command after we answer so this is the state
|
||||
// before it. Clients get the result from the shadeState socket event.
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShade(shade, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"An invalid shadeId was provided\"}"));
|
||||
}
|
||||
else if(groupId != 255) {
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
if(group) {
|
||||
req.type = shade_request_type_t::group_sensor;
|
||||
req.id = groupId;
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
// As above, this is the state before the command and groupState follows.
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeGroup(group, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"An invalid groupId was provided\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"shadeId was not provided\"}"));
|
||||
}
|
||||
void Web::handleBackup(AsyncWebServerRequest *request) {
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
bool attach = false;
|
||||
|
|
@ -744,17 +376,6 @@ void Web::handleBackup(AsyncWebServerRequest *request) {
|
|||
request->send(200, _encoding_text, somfy.backupData);
|
||||
}
|
||||
}
|
||||
void Web::handleReboot(AsyncWebServerRequest *request) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
if(request->method() == HTTP_POST || request->method() == HTTP_PUT) {
|
||||
ESP_LOGI(TAG, "Async Rebooting ESP...");
|
||||
rebootDelay.reboot = true;
|
||||
rebootDelay.rebootTime = millis() + 500;
|
||||
request->send(200, _encoding_json, "{\"status\":\"OK\",\"desc\":\"Successfully started reboot\"}");
|
||||
}
|
||||
else request->send(201, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method\"}");
|
||||
}
|
||||
void Web::handleNotFound(AsyncWebServerRequest *request) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
snprintf(g_async_content, sizeof(g_async_content), "404 Service Not Found: %s", request->url().c_str());
|
||||
|
|
@ -762,41 +383,37 @@ void Web::handleNotFound(AsyncWebServerRequest *request) {
|
|||
}
|
||||
|
||||
// Logs how long part of a request took, when it is worth logging.
|
||||
static void logRequestTime(const char *server, AsyncWebServerRequest *request, const IPAddress &ip, const char *part, uint32_t us) {
|
||||
static void logRequestTime(AsyncWebServerRequest *request, const IPAddress &ip, const char *part, uint32_t us) {
|
||||
if(!worthLogging(us)) return;
|
||||
char what[112];
|
||||
snprintf(what, sizeof(what), "%s %s %s from %u.%u.%u.%u %s", server, request->methodToString(), request->url().c_str(),
|
||||
snprintf(what, sizeof(what), "web %s %s from %u.%u.%u.%u %s", request->methodToString(), request->url().c_str(),
|
||||
ip[0], ip[1], ip[2], ip[3], part);
|
||||
logElapsed(TAG, what, us);
|
||||
}
|
||||
// Logs how long a request takes to handle and, once the connection closes, how
|
||||
// long the whole exchange took. The total includes chunked bodies like
|
||||
// /controller that are built while they are sent, after the handler returns.
|
||||
// The server name must be a literal as the disconnect handler keeps it.
|
||||
static void timeRequest(AsyncWebServerRequest *request, ArMiddlewareNext next, const char *server) {
|
||||
// long the whole exchange took. The total includes bodies sent after the
|
||||
// handler returns, such as a web UI file.
|
||||
static void timeRequest(AsyncWebServerRequest *request, ArMiddlewareNext next) {
|
||||
ElapsedTimer timer;
|
||||
// Taken now as the client may already be gone when the disconnect runs.
|
||||
IPAddress ip = request->client() ? request->client()->remoteIP() : IPAddress();
|
||||
// Uploads set their own disconnect handler while the body arrives, before
|
||||
// this runs, and replacing it would stop the reboot after an update.
|
||||
if(!request->multipart()) {
|
||||
request->onDisconnect([request, timer, server, ip]() { logRequestTime(server, request, ip, "total", timer.us()); });
|
||||
request->onDisconnect([request, timer, ip]() { logRequestTime(request, ip, "total", timer.us()); });
|
||||
}
|
||||
next();
|
||||
logRequestTime(server, request, ip, "handler", timer.us());
|
||||
logRequestTime(request, ip, "handler", timer.us());
|
||||
}
|
||||
|
||||
void Web::begin() {
|
||||
ESP_LOGI(TAG, "Creating Web MicroServices...");
|
||||
asyncApiServer.addMiddleware([](AsyncWebServerRequest *r, ArMiddlewareNext next) { timeRequest(r, next, "api"); });
|
||||
asyncServer.addMiddleware([](AsyncWebServerRequest *r, ArMiddlewareNext next) { timeRequest(r, next, "web"); });
|
||||
// Async API Server (port 8081)
|
||||
asyncServer.addMiddleware([](AsyncWebServerRequest *r, ArMiddlewareNext next) { timeRequest(r, next); });
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "PUT,POST,GET,OPTIONS");
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "*");
|
||||
registerApiRoutes(asyncApiServer);
|
||||
// Before the routes, so serveStatic never answers /ws.
|
||||
protoSocket.begin(asyncServer);
|
||||
registerWebUiRoutes(asyncServer);
|
||||
registerHttpRoutes(asyncServer);
|
||||
asyncServer.begin();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue