mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
refactor: split the transports into the modules GLOSSARY.md names
Code moves unchanged; only files and names change. - entryPoints/WebUiRoutes.cpp: registerWebUiRoutes(), the port 80 routes and the UI's files, with the helpers only they use. - entryPoints/ApiRoutes.cpp: registerApiRoutes(), the port 8081 routes. - entryPoints/Web.cpp keeps the servers' lifecycle, auth and the handlers both route sets call; entryPoints/WebRoutes.h is what the three share. - entryPoints/MQTT.* -> MqttConnection.*, class MQTTClass -> MqttConnection. - outputs/SomfyMqtt.cpp -> outputs/MqttPublisher.cpp. - outputs/Sockets.* -> outputs/SocketEmitter.*. The subscribe and unsubscribe lists (the latter repeated groups/+/direction/set) become MqttCommands' MQTT_COMMAND_TOPICS, and a native test checks the parser takes every topic in it. GLOSSARY.md gains MqttConnection and Change fan-out. 328 native tests. Flash: esp32devdbg +3,112 bytes (the routes are a second ArduinoJson file), esp32dev -2,336 bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4c5bc01fe5
commit
a0ad301779
20 changed files with 1186 additions and 1066 deletions
|
|
@ -86,6 +86,13 @@ _Avoid_: MQTTController
|
|||
What pushes state events to WebSocket clients on port 8080. A client can only join or leave a room; it sends no commands.
|
||||
_Avoid_: WebSocketController
|
||||
|
||||
**MqttConnection**
|
||||
The link to the MQTT broker that MqttCommands and MqttPublisher share: connecting, the root topic and the subscriptions.
|
||||
_Avoid_: MQTTClass
|
||||
|
||||
**Change fan-out**
|
||||
What sends everything that changed about a shade or group in one pass of `loop()` once: its changed MQTT topics, then one state event.
|
||||
|
||||
## Domain modules
|
||||
|
||||
**Domain module**
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ A shade with every linked remote populated is about 1.5 KB today, so nothing ove
|
|||
## Large websocket events are dropped
|
||||
|
||||
**Status:** open
|
||||
**Location:** [`JsonSockEvent` in src/WResp.cpp](../src/WResp.cpp), buffer `g_response[MAX_SOCK_RESPONSE]` (2048 bytes) in [src/outputs/Sockets.cpp](../src/outputs/Sockets.cpp).
|
||||
**Location:** [`JsonSockEvent` in src/WResp.cpp](../src/WResp.cpp), buffer `g_response[MAX_SOCK_RESPONSE]` (2048 bytes) in [src/outputs/SocketEmitter.cpp](../src/outputs/SocketEmitter.cpp).
|
||||
|
||||
### Summary
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ Unlike the `/controller` HTTP crash, this path does **not** abort — there is n
|
|||
|
||||
Serializing a group for an HTTP reply first calls `group->updateFlags()`. When the group's flags no longer match its shades', that writes `group->flags`, sends a `groupState` socket event and publishes the group's MQTT topics, all on the async_tcp task, while `loop()` sends events and publishes too.
|
||||
|
||||
Every socket event is formatted into the one static buffer `g_response` in [src/outputs/Sockets.cpp](../src/outputs/Sockets.cpp) through the one `JsonSockEvent` in `sockEmit`. Two tasks emitting at once write over each other's event. The MQTT client (PubSubClient) isn't safe to use from two tasks either.
|
||||
Every socket event is formatted into the one static buffer `g_response` in [src/outputs/SocketEmitter.cpp](../src/outputs/SocketEmitter.cpp) through the one `JsonSockEvent` in `sockEmit`. Two tasks emitting at once write over each other's event. The MQTT client (PubSubClient) isn't safe to use from two tasks either.
|
||||
|
||||
Reached from every route that serializes a group: `/controller`, `/discovery`, `/groups`, `/group`, `/groupCommand`, `/addGroup`, `/saveGroup`, `/linkToGroup`, `/unlinkFromGroup` and others.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Plan: domain modules behind the LoopQueue
|
||||
|
||||
**Status:** steps 0–4 built (PRs #39 to #43); step 5 planned.
|
||||
**Status:** all steps built (PRs #39 to #44).
|
||||
**Written:** 2026-10-15, against commit `e624a41` on `refactor/entry-points-outputs`. Terms are defined in [GLOSSARY.md](../GLOSSARY.md); the threading rule is [ADR-0002](adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md).
|
||||
|
||||
## Why
|
||||
|
|
@ -168,6 +168,22 @@ Flash: `esp32devdbg` 1,547,821 → 1,536,337 bytes, `esp32dev` 1,400,401 → 1,3
|
|||
|
||||
8 new native tests, 327 in all. Flash: `esp32devdbg` 1,536,337 → 1,536,533 bytes, `esp32dev` 1,393,221 → 1,393,449 bytes.
|
||||
|
||||
### Step 5: the file split
|
||||
Code moved unchanged; only files and names changed.
|
||||
|
||||
| File | Holds |
|
||||
|---|---|
|
||||
| `entryPoints/WebUiRoutes.cpp` | `registerWebUiRoutes()`: the port 80 routes and the UI's files, with the helpers only they use |
|
||||
| `entryPoints/ApiRoutes.cpp` | `registerApiRoutes()`: the port 8081 routes |
|
||||
| `entryPoints/Web.cpp` | the web servers' lifecycle, auth, and the handlers both route sets call |
|
||||
| `entryPoints/WebRoutes.h` | what those three share |
|
||||
| `entryPoints/MqttCommands.*` | + `MQTT_COMMAND_TOPICS`, the topics the connection subscribes to |
|
||||
| `MqttConnection.*` | was `entryPoints/MQTT.*`; class `MQTTClass` → `MqttConnection` |
|
||||
| `outputs/MqttPublisher.cpp` | was `outputs/SomfyMqtt.cpp` |
|
||||
| `outputs/SocketEmitter.*` | was `outputs/Sockets.*` |
|
||||
|
||||
The subscribe and unsubscribe lists, which repeated `groups/+/direction/set`, became the one `MQTT_COMMAND_TOPICS` list, and a native test checks that the parser takes every topic in it. Flash: `esp32devdbg` 1,536,533 → 1,539,645 bytes (+3.1 KB, since the routes are a second file that uses ArduinoJson, as SOMFY_SPLIT_PLAN.md warned), `esp32dev` 1,393,449 → 1,391,113 bytes (−2.3 KB, where `-Os` inlines less).
|
||||
|
||||
### Step 0 on a device
|
||||
NVS stays at `0x9000`, so the rolling codes survive. The file system partition shrinks from `0xE0000` to `0xD0000`, and the existing LittleFS won't mount, because `LittleFS.begin()` doesn't format. So:
|
||||
1. Take a backup through `/backup`.
|
||||
|
|
|
|||
|
|
@ -14,17 +14,17 @@ static inline void safe_wdt_reset() {
|
|||
#include "ConfigSettings.h"
|
||||
#include "ESPNetwork.h"
|
||||
#include "entryPoints/Web.h"
|
||||
#include "outputs/Sockets.h"
|
||||
#include "outputs/SocketEmitter.h"
|
||||
#include "Utils.h"
|
||||
#include "SSDP.h"
|
||||
#include "entryPoints/MQTT.h"
|
||||
#include "MqttConnection.h"
|
||||
|
||||
static const char *TAG = "Network";
|
||||
|
||||
extern ConfigSettings settings;
|
||||
extern Web webServer;
|
||||
extern SocketEmitter sockEmit;
|
||||
extern MQTTClass mqtt;
|
||||
extern MqttConnection mqtt;
|
||||
extern rebootDelay_t rebootDelay;
|
||||
extern ESPNetwork net;
|
||||
extern SomfyShadeController somfy;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
#include "ConfigSettings.h"
|
||||
#include "GitOTA.h"
|
||||
#include "Utils.h"
|
||||
#include "outputs/Sockets.h"
|
||||
#include "outputs/SocketEmitter.h"
|
||||
#include "Somfy.h"
|
||||
#include "entryPoints/Web.h"
|
||||
#include "WResp.h"
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
#include <esp_task_wdt.h>
|
||||
#include "esp_log.h"
|
||||
#include "ConfigSettings.h"
|
||||
#include "MQTT.h"
|
||||
#include "MqttCommands.h"
|
||||
#include "MqttConnection.h"
|
||||
#include "entryPoints/MqttCommands.h"
|
||||
#include "Somfy.h"
|
||||
#include "LoopQueue.h"
|
||||
#include "ESPNetwork.h"
|
||||
|
|
@ -26,21 +26,21 @@ extern rebootDelay_t rebootDelay;
|
|||
extern LoopQueue loopQueue;
|
||||
|
||||
|
||||
bool MQTTClass::begin() {
|
||||
bool MqttConnection::begin() {
|
||||
this->suspended = false;
|
||||
return true;
|
||||
}
|
||||
bool MQTTClass::end() {
|
||||
bool MqttConnection::end() {
|
||||
this->suspended = true;
|
||||
this->disconnect();
|
||||
return true;
|
||||
}
|
||||
void MQTTClass::reset() {
|
||||
void MqttConnection::reset() {
|
||||
this->disconnect();
|
||||
this->lastConnect = 0;
|
||||
this->connect();
|
||||
}
|
||||
bool MQTTClass::loop() {
|
||||
bool MqttConnection::loop() {
|
||||
if(settings.MQTT.enabled && !rebootDelay.reboot && !this->suspended && !mqttClient.connected()) {
|
||||
esp_task_wdt_reset();
|
||||
if(!this->connected() && net.connected()) this->connect();
|
||||
|
|
@ -49,7 +49,7 @@ bool MQTTClass::loop() {
|
|||
if(settings.MQTT.enabled) mqttClient.loop();
|
||||
return true;
|
||||
}
|
||||
void MQTTClass::receive(const char *topic, byte*payload, uint32_t length) {
|
||||
void MqttConnection::receive(const char *topic, byte*payload, uint32_t length) {
|
||||
esp_task_wdt_reset(); // Make sure we do not reboot here.
|
||||
ESP_LOGD(TAG, "MQTT Topic:%s payload:%.*s", topic, (int)length, (const char *)payload);
|
||||
// This already runs on loop(), but going through the loop queue keeps MQTT
|
||||
|
|
@ -61,7 +61,7 @@ void MQTTClass::receive(const char *topic, byte*payload, uint32_t length) {
|
|||
}
|
||||
loopQueue.submit(item);
|
||||
}
|
||||
bool MQTTClass::connect() {
|
||||
bool MqttConnection::connect() {
|
||||
esp_task_wdt_reset(); // Make sure we do not reboot here.
|
||||
if(mqttClient.connected()) {
|
||||
if(!settings.MQTT.enabled || this->suspended)
|
||||
|
|
@ -88,21 +88,8 @@ bool MQTTClass::connect() {
|
|||
this->publish("serverId", settings.serverId, true);
|
||||
this->publish("mac", net.mac.c_str());
|
||||
somfy.publish();
|
||||
this->subscribe("shades/+/target/set");
|
||||
this->subscribe("shades/+/tiltTarget/set");
|
||||
this->subscribe("shades/+/direction/set");
|
||||
this->subscribe("shades/+/mypos/set");
|
||||
this->subscribe("shades/+/myTiltPos/set");
|
||||
this->subscribe("shades/+/sunFlag/set");
|
||||
this->subscribe("shades/+/sunny/set");
|
||||
this->subscribe("shades/+/windy/set");
|
||||
this->subscribe("shades/+/position/set");
|
||||
this->subscribe("shades/+/tiltPosition/set");
|
||||
this->subscribe("groups/+/direction/set");
|
||||
this->subscribe("groups/+/sunFlag/set");
|
||||
this->subscribe("groups/+/sunny/set");
|
||||
this->subscribe("groups/+/windy/set");
|
||||
mqttClient.setCallback(MQTTClass::receive);
|
||||
for(const char *topic : MQTT_COMMAND_TOPICS) this->subscribe(topic);
|
||||
mqttClient.setCallback(MqttConnection::receive);
|
||||
ESP_LOGI(TAG, "MQTT Startup Completed");
|
||||
esp_task_wdt_reset();
|
||||
this->lastConnect = millis();
|
||||
|
|
@ -119,28 +106,14 @@ bool MQTTClass::connect() {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
bool MQTTClass::disconnect() {
|
||||
bool MqttConnection::disconnect() {
|
||||
if(mqttClient.connected()) {
|
||||
this->unsubscribe("shades/+/target/set");
|
||||
this->unsubscribe("shades/+/direction/set");
|
||||
this->unsubscribe("shades/+/tiltTarget/set");
|
||||
this->unsubscribe("shades/+/mypos/set");
|
||||
this->unsubscribe("shades/+/myTiltPos/set");
|
||||
this->unsubscribe("shades/+/sunFlag/set");
|
||||
this->unsubscribe("groups/+/direction/set");
|
||||
this->unsubscribe("shades/+/sunny/set");
|
||||
this->unsubscribe("shades/+/windy/set");
|
||||
this->unsubscribe("shades/+/position/set");
|
||||
this->unsubscribe("shades/+/tiltPosition/set");
|
||||
this->unsubscribe("groups/+/direction/set");
|
||||
this->unsubscribe("groups/+/sunFlag/set");
|
||||
this->unsubscribe("groups/+/sunny/set");
|
||||
this->unsubscribe("groups/+/windy/set");
|
||||
for(const char *topic : MQTT_COMMAND_TOPICS) this->unsubscribe(topic);
|
||||
mqttClient.disconnect();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool MQTTClass::unsubscribe(const char *topic) {
|
||||
bool MqttConnection::unsubscribe(const char *topic) {
|
||||
if(mqttClient.connected()) {
|
||||
char top[128];
|
||||
if(strlen(settings.MQTT.rootTopic) > 0)
|
||||
|
|
@ -152,7 +125,7 @@ bool MQTTClass::unsubscribe(const char *topic) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
bool MQTTClass::subscribe(const char *topic) {
|
||||
bool MqttConnection::subscribe(const char *topic) {
|
||||
if(mqttClient.connected()) {
|
||||
esp_task_wdt_reset(); // Make sure we do not reboot here.
|
||||
char top[128];
|
||||
|
|
@ -165,7 +138,7 @@ bool MQTTClass::subscribe(const char *topic) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
bool MQTTClass::publish(const char *topic, const char *payload, bool retain) {
|
||||
bool MqttConnection::publish(const char *topic, const char *payload, bool retain) {
|
||||
if(mqttClient.connected()) {
|
||||
char top[128];
|
||||
if(strlen(settings.MQTT.rootTopic) > 0)
|
||||
|
|
@ -178,11 +151,11 @@ bool MQTTClass::publish(const char *topic, const char *payload, bool retain) {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
bool MQTTClass::publish(const char *topic, uint32_t val, bool retain) {
|
||||
bool MqttConnection::publish(const char *topic, uint32_t val, bool retain) {
|
||||
snprintf(g_content, sizeof(g_content), "%u", val);
|
||||
return this->publish(topic, g_content, retain);
|
||||
}
|
||||
bool MQTTClass::unpublish(const char *topic) {
|
||||
bool MqttConnection::unpublish(const char *topic) {
|
||||
if(mqttClient.connected()) {
|
||||
char top[128];
|
||||
if(strlen(settings.MQTT.rootTopic) > 0)
|
||||
|
|
@ -199,7 +172,7 @@ bool MQTTClass::unpublish(const char *topic) {
|
|||
// mqttClient.endPublish();
|
||||
}
|
||||
|
||||
bool MQTTClass::publishBuffer(const char *topic, uint8_t *data, uint16_t len, bool retain) {
|
||||
bool MqttConnection::publishBuffer(const char *topic, uint8_t *data, uint16_t len, bool retain) {
|
||||
size_t res;
|
||||
uint16_t offset = 0;
|
||||
uint16_t to_write = len;
|
||||
|
|
@ -216,28 +189,28 @@ bool MQTTClass::publishBuffer(const char *topic, uint8_t *data, uint16_t len, bo
|
|||
mqttClient.endPublish();
|
||||
return true;
|
||||
}
|
||||
bool MQTTClass::publishDisco(const char *topic, JsonObject &obj, bool retain) {
|
||||
bool MqttConnection::publishDisco(const char *topic, JsonObject &obj, bool retain) {
|
||||
serializeJson(obj, g_content, sizeof(g_content));
|
||||
this->publishBuffer(topic, (uint8_t *)g_content, strlen(g_content), retain);
|
||||
return true;
|
||||
}
|
||||
bool MQTTClass::publish(const char *topic, int8_t val, bool retain) {
|
||||
bool MqttConnection::publish(const char *topic, int8_t val, bool retain) {
|
||||
snprintf(g_content, sizeof(g_content), "%d", val);
|
||||
return this->publish(topic, g_content, retain);
|
||||
}
|
||||
bool MQTTClass::publish(const char *topic, uint8_t val, bool retain) {
|
||||
bool MqttConnection::publish(const char *topic, uint8_t val, bool retain) {
|
||||
snprintf(g_content, sizeof(g_content), "%u", val);
|
||||
return this->publish(topic, g_content, retain);
|
||||
}
|
||||
bool MQTTClass::publish(const char *topic, uint16_t val, bool retain) {
|
||||
bool MqttConnection::publish(const char *topic, uint16_t val, bool retain) {
|
||||
snprintf(g_content, sizeof(g_content), "%u", val);
|
||||
return this->publish(topic, g_content, retain);
|
||||
}
|
||||
bool MQTTClass::publish(const char *topic, bool val, bool retain) {
|
||||
bool MqttConnection::publish(const char *topic, bool val, bool retain) {
|
||||
snprintf(g_content, sizeof(g_content), "%s", val ? "true" : "false");
|
||||
return this->publish(topic, g_content, retain);
|
||||
}
|
||||
bool MQTTClass::connected() {
|
||||
bool MqttConnection::connected() {
|
||||
if(settings.MQTT.enabled) return mqttClient.connected();
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
#ifndef MQTT_H
|
||||
#define MQTT_H
|
||||
#ifndef MQTT_CONNECTION_H
|
||||
#define MQTT_CONNECTION_H
|
||||
#include <Arduino.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
class MQTTClass {
|
||||
class MqttConnection {
|
||||
public:
|
||||
uint64_t lastConnect = 0;
|
||||
bool suspended = false;
|
||||
|
|
@ -5,10 +5,10 @@
|
|||
#include "ConfigSettings.h"
|
||||
#include "ESPNetwork.h"
|
||||
#include "entryPoints/Web.h"
|
||||
#include "outputs/Sockets.h"
|
||||
#include "outputs/SocketEmitter.h"
|
||||
#include "Utils.h"
|
||||
#include "Somfy.h"
|
||||
#include "entryPoints/MQTT.h"
|
||||
#include "MqttConnection.h"
|
||||
#include "LoopQueue.h"
|
||||
#include "ShadeRequests.h"
|
||||
#include "SomfyShadeOps.h"
|
||||
|
|
@ -35,7 +35,7 @@ SocketEmitter sockEmit;
|
|||
ESPNetwork net;
|
||||
rebootDelay_t rebootDelay;
|
||||
SomfyShadeController somfy;
|
||||
MQTTClass mqtt;
|
||||
MqttConnection mqtt;
|
||||
GitUpdater git;
|
||||
SomfyShadeOps shadeOps;
|
||||
ShadeRequests shadeRequests(shadeOps);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
#include "Utils.h"
|
||||
#include "ConfigSettings.h"
|
||||
#include "Somfy.h"
|
||||
#include "outputs/Sockets.h"
|
||||
#include "outputs/SocketEmitter.h"
|
||||
|
||||
static const char *TAG = "Somfy";
|
||||
|
||||
|
|
|
|||
52
src/entryPoints/ApiRoutes.cpp
Normal file
52
src/entryPoints/ApiRoutes.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncJson.h>
|
||||
#include "Web.h"
|
||||
#include "WebRoutes.h"
|
||||
|
||||
// ApiRoutes: the routes integrations such as Home Assistant call on port 8081.
|
||||
// The handlers are the ones the config web UI calls too (Web.cpp).
|
||||
|
||||
extern Web webServer;
|
||||
|
||||
void registerApiRoutes(AsyncWebServer &server) {
|
||||
// GET endpoints
|
||||
server.on("/discovery", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleDiscovery(r); });
|
||||
server.on("/rooms", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleGetRooms(r); });
|
||||
server.on("/shades", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleGetShades(r); });
|
||||
server.on("/groups", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleGetGroups(r); });
|
||||
server.on("/controller", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleController(r); });
|
||||
server.on("/room", HTTP_GET, [](AsyncWebServerRequest *r) { webServer.handleRoom(r); });
|
||||
server.on("/shade", HTTP_GET, [](AsyncWebServerRequest *r) { webServer.handleShade(r); });
|
||||
server.on("/group", HTTP_GET, [](AsyncWebServerRequest *r) { webServer.handleGroup(r); });
|
||||
server.on("/downloadFirmware", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleDownloadFirmware(r); });
|
||||
server.on("/backup", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleBackup(r); });
|
||||
server.on("/reboot", WebRequestMethodComposite(HTTP_POST) | HTTP_PUT, [](AsyncWebServerRequest *r) { webServer.handleReboot(r); });
|
||||
// JSON body endpoints
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/shadeCommand",
|
||||
[](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleShadeCommand(r, j); }));
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/groupCommand",
|
||||
[](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleGroupCommand(r, j); }));
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/tiltCommand",
|
||||
[](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleTiltCommand(r, j); }));
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/repeatCommand",
|
||||
[](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleRepeatCommand(r, j); }));
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/setPositions",
|
||||
[](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleSetPositions(r, j); }));
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/setSensor",
|
||||
[](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleSetSensor(r, j); }));
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/login",
|
||||
[](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleLogin(r, j); }));
|
||||
// GET fallback for command endpoints (query params)
|
||||
server.on("/shadeCommand", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleShadeCommand(r, v); });
|
||||
server.on("/groupCommand", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleGroupCommand(r, v); });
|
||||
server.on("/tiltCommand", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleTiltCommand(r, v); });
|
||||
server.on("/repeatCommand", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleRepeatCommand(r, v); });
|
||||
server.on("/setPositions", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleSetPositions(r, v); });
|
||||
server.on("/setSensor", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleSetSensor(r, v); });
|
||||
server.on("/login", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleLogin(r, v); });
|
||||
// OPTIONS preflight + not found
|
||||
server.onNotFound([](AsyncWebServerRequest *r) {
|
||||
if(r->method() == HTTP_OPTIONS) { r->send(200); return; }
|
||||
webServer.handleNotFound(r);
|
||||
});
|
||||
}
|
||||
|
|
@ -2,6 +2,23 @@
|
|||
#include <string.h>
|
||||
#include "MqttCommands.h"
|
||||
|
||||
const char *const MQTT_COMMAND_TOPICS[MQTT_COMMAND_TOPIC_COUNT] = {
|
||||
"shades/+/target/set",
|
||||
"shades/+/tiltTarget/set",
|
||||
"shades/+/direction/set",
|
||||
"shades/+/mypos/set",
|
||||
"shades/+/myTiltPos/set",
|
||||
"shades/+/sunFlag/set",
|
||||
"shades/+/sunny/set",
|
||||
"shades/+/windy/set",
|
||||
"shades/+/position/set",
|
||||
"shades/+/tiltPosition/set",
|
||||
"groups/+/direction/set",
|
||||
"groups/+/sunFlag/set",
|
||||
"groups/+/sunny/set",
|
||||
"groups/+/windy/set"
|
||||
};
|
||||
|
||||
namespace {
|
||||
struct segment_t {
|
||||
const char *s = "";
|
||||
|
|
|
|||
|
|
@ -11,4 +11,9 @@
|
|||
// Turns a message on a command topic into a shade request. False for a topic
|
||||
// or value it doesn't take, which is dropped.
|
||||
bool parseMqttCommand(const char *topic, const uint8_t *payload, size_t length, shade_request_t &req);
|
||||
|
||||
// The command topics under the root topic, + standing for any id. The MQTT
|
||||
// connection subscribes to these.
|
||||
#define MQTT_COMMAND_TOPIC_COUNT 14
|
||||
extern const char *const MQTT_COMMAND_TOPICS[MQTT_COMMAND_TOPIC_COUNT];
|
||||
#endif
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
45
src/entryPoints/WebRoutes.h
Normal file
45
src/entryPoints/WebRoutes.h
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#ifndef WEB_ROUTES_H
|
||||
#define WEB_ROUTES_H
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include "Somfy.h"
|
||||
#include "LoopQueue.h"
|
||||
#include "ModelSnapshot.h"
|
||||
#include "GitOTA.h"
|
||||
|
||||
// What WebUiRoutes, ApiRoutes and the handlers both of them call (Web.cpp)
|
||||
// share. Internal to the web servers.
|
||||
|
||||
// Registers the routes the config web UI calls on port 80, and its files.
|
||||
void registerWebUiRoutes(AsyncWebServer &server);
|
||||
// Registers the routes integrations such as Home Assistant call on port 8081.
|
||||
void registerApiRoutes(AsyncWebServer &server);
|
||||
|
||||
#define WEB_MAX_RESPONSE 4096
|
||||
// Where replies are formatted before they are sent. async_tcp only.
|
||||
extern char g_async_content[WEB_MAX_RESPONSE];
|
||||
extern const char _encoding_text[];
|
||||
extern const char _encoding_json[];
|
||||
|
||||
// Sent when a request can't be handed to loop().
|
||||
void sendQueueFull(AsyncWebServerRequest *request);
|
||||
// Sent when the radio can't take a frame (suspended or queue full).
|
||||
void sendRadioUnavailable(AsyncWebServerRequest *request);
|
||||
// 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);
|
||||
bool queueShadeRequest(AsyncWebServerRequest *request, const shade_request_t &req);
|
||||
// A query parameter, or an empty string when it is missing.
|
||||
String asyncParam(AsyncWebServerRequest *request, const char *name);
|
||||
bool asyncHasParam(AsyncWebServerRequest *request, const char *name);
|
||||
// Serves a read from a ModelSnapshot that loop() fills.
|
||||
void sendSnapshot(AsyncWebServerRequest *request, snapshot_doc_t doc, uint8_t id = 0, snapshot_header_t header = nullptr);
|
||||
|
||||
// One item of a reply, copied from the live shade model.
|
||||
void serializeShadeRef(SomfyShade *shade, JsonFormatter &json);
|
||||
void serializeShade(SomfyShade *shade, JsonFormatter &json);
|
||||
void serializeShade(SomfyShade *shade, JsonFormatter &json, bool paired, uint16_t lastRollingCode);
|
||||
void serializeTransceiverConfig(JsonFormatter &json);
|
||||
void serializeAppVersion(JsonFormatter &json, appver_t &ver);
|
||||
void serializeGitVersion(JsonFormatter &json);
|
||||
void serializeGitRelease(GitRelease *rel, JsonFormatter &json);
|
||||
#endif
|
||||
967
src/entryPoints/WebUiRoutes.cpp
Normal file
967
src/entryPoints/WebUiRoutes.cpp
Normal file
|
|
@ -0,0 +1,967 @@
|
|||
#include <WiFi.h>
|
||||
#include <LittleFS.h>
|
||||
#include <Update.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include "esp_log.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 "SomfyEditor.h"
|
||||
#include "Web.h"
|
||||
#include "WebRoutes.h"
|
||||
#include "MqttConnection.h"
|
||||
#include "GitOTA.h"
|
||||
#include "ESPNetwork.h"
|
||||
#include <AsyncTCP.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncJson.h>
|
||||
|
||||
// WebUiRoutes: the routes the config web UI calls on port 80, and the UI's own
|
||||
// files.
|
||||
|
||||
extern ConfigSettings settings;
|
||||
extern SSDPClass SSDP;
|
||||
extern rebootDelay_t rebootDelay;
|
||||
extern SomfyShadeController somfy;
|
||||
extern Web webServer;
|
||||
extern MqttConnection mqtt;
|
||||
extern GitUpdater git;
|
||||
extern ESPNetwork net;
|
||||
extern LoopQueue loopQueue;
|
||||
|
||||
static const char *TAG = "Web";
|
||||
static const char _encoding_html[] = "text/html";
|
||||
|
||||
static bool queueSetMyPosition(AsyncWebServerRequest *request, SomfyShade *shade, int8_t pos, int8_t tilt) {
|
||||
shade_request_t req;
|
||||
req.type = shade_request_type_t::set_favorite;
|
||||
req.id = shade->getShadeId();
|
||||
req.position = pos;
|
||||
req.tiltPosition = tilt;
|
||||
return queueShadeRequest(request, req);
|
||||
}
|
||||
// loop() clears paired and saves it once the Prog is queued. The shade
|
||||
// belongs to loop() so it isn't changed here.
|
||||
static bool queueUnpairShade(AsyncWebServerRequest *request, SomfyShade *shade) {
|
||||
shade_request_t req;
|
||||
req.type = shade_request_type_t::unpair;
|
||||
req.id = shade->getShadeId();
|
||||
return queueShadeRequest(request, req);
|
||||
}
|
||||
// Sending a command reads and commits the rolling code on loop(), so a new
|
||||
// code is set there too.
|
||||
static bool queueSetRollingCode(AsyncWebServerRequest *request, SomfyShade *shade, uint16_t rollingCode) {
|
||||
shade_request_t req;
|
||||
req.type = shade_request_type_t::set_rolling_code;
|
||||
req.id = shade->getShadeId();
|
||||
req.rollingCode = rollingCode;
|
||||
return queueShadeRequest(request, req);
|
||||
}
|
||||
// A restore's options travel through the loop queue as RESTORE_* bits.
|
||||
static uint8_t restoreParts(const restore_options_t &opts) {
|
||||
return (opts.settings ? RESTORE_SETTINGS : 0) | (opts.shades ? RESTORE_SHADES : 0) |
|
||||
(opts.network ? RESTORE_NETWORK : 0) | (opts.transceiver ? RESTORE_TRANSCEIVER : 0) |
|
||||
(opts.repeaters ? RESTORE_REPEATERS : 0) | (opts.mqtt ? RESTORE_MQTT : 0);
|
||||
}
|
||||
// The radio and MQTT are shut down once an upload starts and only a reboot
|
||||
// brings them back. The completion handler schedules it but never runs if the
|
||||
// client goes away mid-upload, so make sure the device reboots then too.
|
||||
static void onUploadDisconnect() {
|
||||
if(webServer.uploadSuccess || rebootDelay.reboot) return;
|
||||
ESP_LOGW(TAG, "Upload abandoned, rebooting");
|
||||
if(Update.isRunning()) Update.abort();
|
||||
rebootDelay.reboot = true;
|
||||
rebootDelay.rebootTime = millis() + 500;
|
||||
}
|
||||
// Hands an edit of the shade model to loop() (docs/adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md).
|
||||
// The reply only says it was accepted; the result reaches every client as a
|
||||
// requestResult event carrying the same requestId once loop() has applied it.
|
||||
static void queueModelEdit(AsyncWebServerRequest *request, model_edit_type_t type, JsonVariantConst body) {
|
||||
if(!beginModelEdit()) {
|
||||
request->send(503, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Busy, try the change again\"}"));
|
||||
return;
|
||||
}
|
||||
model_edit_t *edit = new (std::nothrow) model_edit_t();
|
||||
if(!edit) {
|
||||
cancelModelEdit(nullptr);
|
||||
sendQueueFull(request);
|
||||
return;
|
||||
}
|
||||
edit->type = type;
|
||||
edit->requestId = nextEditRequestId();
|
||||
edit->doc.set(body);
|
||||
const uint32_t requestId = edit->requestId;
|
||||
loop_item_t item;
|
||||
item.type = loop_item_type_t::model_edit;
|
||||
item.edit = edit;
|
||||
if(edit->doc.overflowed() || !loopQueue.submit(item)) {
|
||||
cancelModelEdit(edit);
|
||||
sendQueueFull(request);
|
||||
return;
|
||||
}
|
||||
char reply[64];
|
||||
snprintf(reply, sizeof(reply), "{\"status\":\"accepted\",\"requestId\":%lu}", (unsigned long)requestId);
|
||||
request->send(200, _encoding_json, reply);
|
||||
}
|
||||
// The JSON (PUT/POST) form of an edit route. Bodies over 4 KB are refused with
|
||||
// 413 before they reach the heap.
|
||||
static AsyncCallbackJsonWebHandler *editHandler(const char *uri, model_edit_type_t type) {
|
||||
auto *handler = new AsyncCallbackJsonWebHandler(uri, [type](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No object supplied.\"}")); return; }
|
||||
queueModelEdit(request, type, json);
|
||||
});
|
||||
handler->setMaxContentLength(4096);
|
||||
return handler;
|
||||
}
|
||||
// The GET form of an edit route: its query parameters become the request.
|
||||
static void queueModelEditFromParams(AsyncWebServerRequest *request, model_edit_type_t type, std::initializer_list<const char *> names) {
|
||||
JsonDocument body;
|
||||
for(const char *name : names) {
|
||||
if(!asyncHasParam(request, name)) continue;
|
||||
const String value = asyncParam(request, name);
|
||||
if(strcmp(name, "paired") == 0) body[name] = toBoolean(value.c_str(), false);
|
||||
else body[name] = strtoul(value.c_str(), nullptr, 10);
|
||||
}
|
||||
queueModelEdit(request, type, body.as<JsonVariantConst>());
|
||||
}
|
||||
|
||||
void registerWebUiRoutes(AsyncWebServer &server) {
|
||||
server.on("/loginContext", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
AsyncJsonResponse *response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot().to<JsonObject>();
|
||||
root["type"] = static_cast<uint8_t>(settings.Security.type);
|
||||
root["permissions"] = settings.Security.permissions;
|
||||
root["serverId"] = settings.serverId;
|
||||
root["version"] = settings.fwVersion.name;
|
||||
root["model"] = "ESPSomfyRTS";
|
||||
root["hostname"] = settings.hostname;
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
});
|
||||
// Web Interface
|
||||
// Command endpoints - delegate to async handler methods
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/tiltCommand",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleTiltCommand(request, json); }));
|
||||
server.on("/tiltCommand", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleTiltCommand(request, v); });
|
||||
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/repeatCommand",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleRepeatCommand(request, json); }));
|
||||
server.on("/repeatCommand", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleRepeatCommand(request, v); });
|
||||
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/shadeCommand",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleShadeCommand(request, json); }));
|
||||
server.on("/shadeCommand", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleShadeCommand(request, v); });
|
||||
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/groupCommand",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleGroupCommand(request, json); }));
|
||||
server.on("/groupCommand", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleGroupCommand(request, v); });
|
||||
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/setPositions",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleSetPositions(request, json); }));
|
||||
server.on("/setPositions", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleSetPositions(request, v); });
|
||||
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/setSensor",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleSetSensor(request, json); }));
|
||||
server.on("/setSensor", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleSetSensor(request, v); });
|
||||
|
||||
server.on("/upnp.xml", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
AsyncResponseStream *response = request->beginResponseStream("text/xml");
|
||||
SSDP.schema(*response);
|
||||
request->send(response);
|
||||
});
|
||||
|
||||
// / and /loginContext are already handled by serveStatic and startup()
|
||||
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/login",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleLogin(request, json); }));
|
||||
server.on("/login", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleLogin(request, v); });
|
||||
|
||||
server.on("/shades.cfg", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(LittleFS, "/shades.cfg", _encoding_text);
|
||||
});
|
||||
server.on("/shades.tmp", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(LittleFS, "/shades.tmp", _encoding_text);
|
||||
});
|
||||
|
||||
server.on("/getReleases", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
GitRepo repo;
|
||||
repo.getReleases();
|
||||
git.setCurrentRelease(repo);
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
// Inline GitRepo::toJSON
|
||||
resp.beginObject("fwVersion");
|
||||
serializeAppVersion(resp, settings.fwVersion);
|
||||
resp.endObject();
|
||||
resp.beginObject("appVersion");
|
||||
serializeAppVersion(resp, settings.appVersion);
|
||||
resp.endObject();
|
||||
resp.beginArray("releases");
|
||||
for(uint8_t i = 0; i < GIT_MAX_RELEASES + 1; i++) {
|
||||
if(repo.releases[i].id == 0) continue;
|
||||
resp.beginObject();
|
||||
serializeGitRelease(&repo.releases[i], resp);
|
||||
resp.endObject();
|
||||
}
|
||||
resp.endArray();
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
});
|
||||
|
||||
server.on("/downloadFirmware", HTTP_GET, [](AsyncWebServerRequest *request) { webServer.handleDownloadFirmware(request); });
|
||||
|
||||
server.on("/cancelFirmware", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
if(!git.lockFS) {
|
||||
git.status = GIT_UPDATE_CANCELLING;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeGitVersion(resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
git.cancelled = true;
|
||||
}
|
||||
else {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Cannot cancel during filesystem update.\"}"));
|
||||
}
|
||||
});
|
||||
|
||||
server.on("/backup", HTTP_GET, [](AsyncWebServerRequest *request) { webServer.handleBackup(request); });
|
||||
|
||||
server.on("/restore", HTTP_POST,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
if(webServer.uploadSuccess) {
|
||||
restore_options_t opts;
|
||||
if(asyncHasParam(request, "data")) {
|
||||
String dataStr = asyncParam(request, "data");
|
||||
ESP_LOGD(TAG, "%s", dataStr.c_str());
|
||||
JsonDocument doc;
|
||||
DeserializationError err = deserializeJson(doc, dataStr);
|
||||
if(err) {
|
||||
request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}");
|
||||
return;
|
||||
}
|
||||
else {
|
||||
JsonObject obj = doc.as<JsonObject>();
|
||||
opts.fromJSON(obj);
|
||||
}
|
||||
}
|
||||
else {
|
||||
ESP_LOGD(TAG, "No restore options sent. Using defaults...");
|
||||
opts.shades = true;
|
||||
}
|
||||
// The main loop restores, since it owns the shades and their rolling
|
||||
// codes, and then reboots.
|
||||
loop_item_t item;
|
||||
item.type = loop_item_type_t::restore_shades;
|
||||
item.restoreParts = restoreParts(opts);
|
||||
if(!queueLoopItem(request, item)) return;
|
||||
request->send(200, _encoding_json, "{\"status\":\"Success\",\"desc\":\"Restoring Shade settings\"}");
|
||||
}
|
||||
},
|
||||
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
esp_task_wdt_reset();
|
||||
if(index == 0) {
|
||||
webServer.uploadSuccess = false;
|
||||
ESP_LOGD(TAG, "Restore: %s", filename.c_str());
|
||||
File fup = LittleFS.open("/shades.tmp", "w");
|
||||
fup.close();
|
||||
}
|
||||
if(len > 0) {
|
||||
File fup = LittleFS.open("/shades.tmp", "a");
|
||||
fup.write(data, len);
|
||||
fup.close();
|
||||
}
|
||||
if(final) {
|
||||
webServer.uploadSuccess = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Static file routes removed - handled by serveStatic in startup()
|
||||
|
||||
server.on("/controller", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleController(request); });
|
||||
server.on("/rooms", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleGetRooms(request); });
|
||||
server.on("/shades", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleGetShades(request); });
|
||||
server.on("/groups", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleGetGroups(request); });
|
||||
server.on("/room", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleRoom(request); });
|
||||
server.on("/shade", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleShade(request); });
|
||||
server.on("/group", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleGroup(request); });
|
||||
|
||||
// What a new room, shade or group would get, from a snapshot.
|
||||
server.on("/getNextRoom", HTTP_GET, [](AsyncWebServerRequest *request) { sendSnapshot(request, snapshot_doc_t::nextRoom); });
|
||||
server.on("/getNextShade", HTTP_GET, [](AsyncWebServerRequest *request) { sendSnapshot(request, snapshot_doc_t::nextShade); });
|
||||
server.on("/getNextGroup", HTTP_GET, [](AsyncWebServerRequest *request) { sendSnapshot(request, snapshot_doc_t::nextGroup); });
|
||||
server.on("/groupOptions", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
if(!asyncHasParam(request, "groupId")) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid group id.\"}"));
|
||||
return;
|
||||
}
|
||||
const int groupId = atoi(asyncParam(request, "groupId").c_str());
|
||||
// As for /group, existence is checked on the live model.
|
||||
if(somfy.getGroupById(groupId)) sendSnapshot(request, snapshot_doc_t::groupOptions, groupId);
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}"));
|
||||
});
|
||||
// Adding and saving rooms, shades and groups. Edits of the shade model:
|
||||
// loop() applies them and sends requestResult.
|
||||
server.addHandler(editHandler("/addRoom", model_edit_type_t::add_room));
|
||||
server.addHandler(editHandler("/addShade", model_edit_type_t::add_shade));
|
||||
server.addHandler(editHandler("/addGroup", model_edit_type_t::add_group));
|
||||
server.addHandler(editHandler("/saveRoom", model_edit_type_t::save_room));
|
||||
server.addHandler(editHandler("/saveShade", model_edit_type_t::save_shade));
|
||||
server.addHandler(editHandler("/saveGroup", model_edit_type_t::save_group));
|
||||
|
||||
|
||||
// setMyPosition - supports both GET with query params and POST/PUT with JSON body
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/setMyPosition",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
uint8_t shadeId = 255;
|
||||
int8_t pos = -1;
|
||||
int8_t tilt = -1;
|
||||
if(!json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) shadeId = obj["shadeId"];
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); return; }
|
||||
if(!obj["pos"].isNull()) pos = obj["pos"].as<int8_t>();
|
||||
if(!obj["tilt"].isNull()) tilt = obj["tilt"].as<int8_t>();
|
||||
}
|
||||
SomfyShade* shade = somfy.getShadeById(shadeId);
|
||||
if(shade) {
|
||||
if(pos >= 0 && pos <= 100)
|
||||
if(!queueSetMyPosition(request, shade, pos, requestTarget(tilt))) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShadeRef(shade, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}"));
|
||||
}
|
||||
}));
|
||||
server.on("/setMyPosition", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint8_t shadeId = 255;
|
||||
int8_t pos = -1;
|
||||
int8_t tilt = -1;
|
||||
if(asyncHasParam(request, "shadeId")) {
|
||||
shadeId = atoi(asyncParam(request, "shadeId").c_str());
|
||||
if(asyncHasParam(request, "pos")) pos = atoi(asyncParam(request, "pos").c_str());
|
||||
if(asyncHasParam(request, "tilt")) tilt = atoi(asyncParam(request, "tilt").c_str());
|
||||
}
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; }
|
||||
SomfyShade* shade = somfy.getShadeById(shadeId);
|
||||
if(shade) {
|
||||
if(pos >= 0 && pos <= 100)
|
||||
if(!queueSetMyPosition(request, shade, pos, requestTarget(tilt))) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShadeRef(shade, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}"));
|
||||
}
|
||||
});
|
||||
|
||||
// setRollingCode - supports both query params and JSON body
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/setRollingCode",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
uint8_t shadeId = 255;
|
||||
uint16_t rollingCode = 0;
|
||||
if(!json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) shadeId = obj["shadeId"];
|
||||
if(!obj["rollingCode"].isNull()) rollingCode = obj["rollingCode"];
|
||||
}
|
||||
SomfyShade* shade = nullptr;
|
||||
if(shadeId != 255) shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to set rolling code\"}"));
|
||||
}
|
||||
else {
|
||||
if(!queueSetRollingCode(request, shade, rollingCode)) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShade(shade, resp, shade->paired, rollingCode);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
}));
|
||||
server.on("/setRollingCode", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint8_t shadeId = 255;
|
||||
uint16_t rollingCode = 0;
|
||||
if(asyncHasParam(request, "shadeId")) {
|
||||
shadeId = atoi(asyncParam(request, "shadeId").c_str());
|
||||
rollingCode = atoi(asyncParam(request, "rollingCode").c_str());
|
||||
}
|
||||
SomfyShade* shade = nullptr;
|
||||
if(shadeId != 255) shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to set rolling code\"}"));
|
||||
}
|
||||
else {
|
||||
if(!queueSetRollingCode(request, shade, rollingCode)) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShade(shade, resp, shade->paired, rollingCode);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
});
|
||||
|
||||
server.addHandler(editHandler("/setPaired", model_edit_type_t::set_paired));
|
||||
server.on("/setPaired", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
queueModelEditFromParams(request, model_edit_type_t::set_paired, { "shadeId", "paired" });
|
||||
});
|
||||
|
||||
// unpairShade
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/unpairShade",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
uint8_t shadeId = 255;
|
||||
if(!json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) shadeId = obj["shadeId"];
|
||||
}
|
||||
SomfyShade* shade = nullptr;
|
||||
if(shadeId != 255) shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to unpair\"}"));
|
||||
}
|
||||
else {
|
||||
if(!queueUnpairShade(request, shade)) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShade(shade, resp, false, shade->lastRollingCode);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
}));
|
||||
server.on("/unpairShade", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint8_t shadeId = 255;
|
||||
if(asyncHasParam(request, "shadeId"))
|
||||
shadeId = atoi(asyncParam(request, "shadeId").c_str());
|
||||
SomfyShade* shade = nullptr;
|
||||
if(shadeId != 255) shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to unpair\"}"));
|
||||
}
|
||||
else {
|
||||
if(!queueUnpairShade(request, shade)) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShade(shade, resp, false, shade->lastRollingCode);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
});
|
||||
|
||||
// Linking repeaters, remotes and group members, and deleting. Edits of the
|
||||
// shade model: loop() applies them and sends requestResult.
|
||||
server.addHandler(editHandler("/linkRepeater", model_edit_type_t::link_repeater));
|
||||
server.on("/linkRepeater", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
queueModelEditFromParams(request, model_edit_type_t::link_repeater, { "address" });
|
||||
});
|
||||
server.addHandler(editHandler("/unlinkRepeater", model_edit_type_t::unlink_repeater));
|
||||
server.on("/unlinkRepeater", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
queueModelEditFromParams(request, model_edit_type_t::unlink_repeater, { "address" });
|
||||
});
|
||||
server.addHandler(editHandler("/linkRemote", model_edit_type_t::link_remote));
|
||||
server.addHandler(editHandler("/unlinkRemote", model_edit_type_t::unlink_remote));
|
||||
server.addHandler(editHandler("/linkToGroup", model_edit_type_t::link_to_group));
|
||||
server.addHandler(editHandler("/unlinkFromGroup", model_edit_type_t::unlink_from_group));
|
||||
server.addHandler(editHandler("/deleteRoom", model_edit_type_t::delete_room));
|
||||
server.on("/deleteRoom", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
queueModelEditFromParams(request, model_edit_type_t::delete_room, { "roomId" });
|
||||
});
|
||||
server.addHandler(editHandler("/deleteShade", model_edit_type_t::delete_shade));
|
||||
server.on("/deleteShade", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
queueModelEditFromParams(request, model_edit_type_t::delete_shade, { "shadeId" });
|
||||
});
|
||||
server.addHandler(editHandler("/deleteGroup", model_edit_type_t::delete_group));
|
||||
server.on("/deleteGroup", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
queueModelEditFromParams(request, model_edit_type_t::delete_group, { "groupId" });
|
||||
});
|
||||
// The repeaters, from a snapshot like the other reads.
|
||||
server.on("/repeaters", HTTP_GET, [](AsyncWebServerRequest *request) { sendSnapshot(request, snapshot_doc_t::repeaters); });
|
||||
|
||||
|
||||
// updateFirmware - file upload
|
||||
server.on("/updateFirmware", HTTP_POST,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
if(Update.hasError())
|
||||
request->send(500, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Error updating firmware: \"}");
|
||||
else
|
||||
request->send(200, _encoding_json, "{\"status\":\"SUCCESS\",\"desc\":\"Successfully updated firmware\"}");
|
||||
rebootDelay.reboot = true;
|
||||
rebootDelay.rebootTime = millis() + 500;
|
||||
},
|
||||
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
if(index == 0) {
|
||||
webServer.uploadSuccess = false;
|
||||
ESP_LOGI(TAG, "Update: %s", filename.c_str());
|
||||
if(!Update.begin(UPDATE_SIZE_UNKNOWN)) {
|
||||
ESP_LOGE(TAG, "Update begin failed");
|
||||
}
|
||||
else {
|
||||
somfy.transceiver.end();
|
||||
mqtt.end();
|
||||
request->onDisconnect(onUploadDisconnect);
|
||||
}
|
||||
}
|
||||
if(len > 0) {
|
||||
if(Update.write(data, len) != len) {
|
||||
ESP_LOGE(TAG, "Upload of %s aborted invalid size %d", filename.c_str(), len);
|
||||
Update.abort();
|
||||
}
|
||||
}
|
||||
if(final) {
|
||||
if(Update.end(true)) {
|
||||
ESP_LOGI(TAG, "Update Success: %u Rebooting...", index + len);
|
||||
webServer.uploadSuccess = true;
|
||||
}
|
||||
else {
|
||||
ESP_LOGE(TAG, "Update end failed");
|
||||
}
|
||||
}
|
||||
esp_task_wdt_reset();
|
||||
});
|
||||
|
||||
// updateShadeConfig - file upload
|
||||
server.on("/updateShadeConfig", HTTP_POST,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
if(git.lockFS) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Filesystem update in progress\"}"));
|
||||
return;
|
||||
}
|
||||
request->send(200, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Updating Shade Config: \"}");
|
||||
},
|
||||
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
if(index == 0) {
|
||||
ESP_LOGI(TAG, "Update: shades.cfg");
|
||||
File fup = LittleFS.open("/shades.tmp", "w");
|
||||
fup.close();
|
||||
}
|
||||
if(len > 0) {
|
||||
File fup = LittleFS.open("/shades.tmp", "a");
|
||||
fup.write(data, len);
|
||||
fup.close();
|
||||
}
|
||||
if(final) {
|
||||
// The main loop loads it, since it owns the shades.
|
||||
loop_item_t item;
|
||||
item.type = loop_item_type_t::load_shades;
|
||||
loopQueue.submit(item);
|
||||
}
|
||||
});
|
||||
|
||||
// updateApplication - file upload
|
||||
server.on("/updateApplication", HTTP_POST,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
if(Update.hasError())
|
||||
request->send(500, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Error updating application: \"}");
|
||||
else
|
||||
request->send(200, _encoding_json, "{\"status\":\"SUCCESS\",\"desc\":\"Successfully updated application\"}");
|
||||
rebootDelay.reboot = true;
|
||||
rebootDelay.rebootTime = millis() + 500;
|
||||
},
|
||||
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
if(index == 0) {
|
||||
webServer.uploadSuccess = false;
|
||||
ESP_LOGI(TAG, "Update: %s", filename.c_str());
|
||||
if(!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) {
|
||||
ESP_LOGE(TAG, "Update begin failed");
|
||||
}
|
||||
else {
|
||||
somfy.transceiver.end();
|
||||
mqtt.end();
|
||||
request->onDisconnect(onUploadDisconnect);
|
||||
}
|
||||
}
|
||||
if(len > 0) {
|
||||
if(Update.write(data, len) != len) {
|
||||
ESP_LOGE(TAG, "Upload of %s aborted invalid size %d", filename.c_str(), len);
|
||||
Update.abort();
|
||||
}
|
||||
}
|
||||
if(final) {
|
||||
if(Update.end(true)) {
|
||||
webServer.uploadSuccess = true;
|
||||
ESP_LOGI(TAG, "Update Success: %u Rebooting...", index + len);
|
||||
somfy.commit();
|
||||
}
|
||||
else {
|
||||
somfy.commit();
|
||||
ESP_LOGE(TAG, "Update end failed");
|
||||
}
|
||||
}
|
||||
esp_task_wdt_reset();
|
||||
});
|
||||
|
||||
server.on("/scanaps", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
esp_task_wdt_reset();
|
||||
esp_task_wdt_delete(NULL);
|
||||
if(net.softAPOpened) WiFi.disconnect(false);
|
||||
int n = WiFi.scanNetworks(false, true);
|
||||
esp_task_wdt_add(NULL);
|
||||
ESP_LOGI(TAG, "Scanned %d networks", n);
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
resp.beginObject("connected");
|
||||
resp.addElem("name", settings.WIFI.ssid);
|
||||
resp.addElem("passphrase", settings.WIFI.passphrase);
|
||||
resp.addElem("strength", (int32_t)WiFi.RSSI());
|
||||
resp.addElem("channel", (int32_t)WiFi.channel());
|
||||
resp.endObject();
|
||||
resp.beginArray("accessPoints");
|
||||
for(int i = 0; i < n; ++i) {
|
||||
if(WiFi.SSID(i).length() == 0 || WiFi.RSSI(i) < -95) continue;
|
||||
resp.beginObject();
|
||||
resp.addElem("name", WiFi.SSID(i).c_str());
|
||||
resp.addElem("channel", (int32_t)WiFi.channel(i));
|
||||
resp.addElem("strength", (int32_t)WiFi.RSSI(i));
|
||||
resp.addElem("macAddress", WiFi.BSSIDstr(i).c_str());
|
||||
resp.endObject();
|
||||
}
|
||||
resp.endArray();
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
});
|
||||
|
||||
server.on("/reboot", WebRequestMethodComposite(HTTP_POST) | HTTP_PUT, [](AsyncWebServerRequest *request) { webServer.handleReboot(request); });
|
||||
|
||||
// saveSecurity
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/saveSecurity",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) {
|
||||
request->send(400, _encoding_html, "Error parsing JSON body");
|
||||
return;
|
||||
}
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
settings.Security.fromJSON(obj);
|
||||
settings.Security.save();
|
||||
char token[65];
|
||||
webServer.createAPIToken(request->client()->remoteIP(), token);
|
||||
JsonDocument sdoc;
|
||||
JsonObject sobj = sdoc.to<JsonObject>();
|
||||
settings.Security.toJSON(sobj);
|
||||
sobj["apiKey"] = token;
|
||||
serializeJson(sdoc, g_async_content, sizeof(g_async_content));
|
||||
request->send(200, _encoding_json, g_async_content);
|
||||
}));
|
||||
|
||||
// getSecurity
|
||||
server.on("/getSecurity", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
JsonDocument doc;
|
||||
JsonObject obj = doc.to<JsonObject>();
|
||||
settings.Security.toJSON(obj);
|
||||
serializeJson(doc, g_async_content, sizeof(g_async_content));
|
||||
request->send(200, _encoding_json, g_async_content);
|
||||
});
|
||||
|
||||
// saveRadio
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/saveRadio",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) {
|
||||
request->send(400, _encoding_html, "Error parsing JSON body");
|
||||
return;
|
||||
}
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
somfy.transceiver.fromJSON(obj);
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
resp.beginObject("config");
|
||||
serializeTransceiverConfig(resp);
|
||||
resp.endObject();
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}));
|
||||
|
||||
// getRadio
|
||||
server.on("/getRadio", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeTransceiverConfig(resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
});
|
||||
|
||||
// sendRemoteCommand - supports GET with query params and POST/PUT with JSON body
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/sendRemoteCommand",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
somfy_frame_t frame;
|
||||
uint8_t repeats = 0;
|
||||
if(!json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
String scmd;
|
||||
if(!obj["address"].isNull()) frame.remoteAddress = obj["address"];
|
||||
if(!obj["command"].isNull()) scmd = obj["command"].as<String>();
|
||||
if(!obj["repeats"].isNull()) repeats = obj["repeats"];
|
||||
if(!obj["rcode"].isNull()) frame.rollingCode = obj["rcode"];
|
||||
if(!obj["encKey"].isNull()) frame.encKey = obj["encKey"];
|
||||
frame.cmd = translateSomfyCommand(scmd.c_str());
|
||||
}
|
||||
if(frame.remoteAddress > 0 && frame.rollingCode > 0) {
|
||||
if(somfy.sendFrame(frame, repeats))
|
||||
request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Command Sent\"}"));
|
||||
else
|
||||
sendRadioUnavailable(request);
|
||||
}
|
||||
else
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No address or rolling code provided\"}"));
|
||||
}));
|
||||
server.on("/sendRemoteCommand", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
somfy_frame_t frame;
|
||||
uint8_t repeats = 0;
|
||||
if(asyncHasParam(request, "address")) {
|
||||
frame.remoteAddress = atoi(asyncParam(request, "address").c_str());
|
||||
if(asyncHasParam(request, "encKey")) frame.encKey = atoi(asyncParam(request, "encKey").c_str());
|
||||
if(asyncHasParam(request, "command")) frame.cmd = translateSomfyCommand(asyncParam(request, "command"));
|
||||
if(asyncHasParam(request, "rcode")) frame.rollingCode = atoi(asyncParam(request, "rcode").c_str());
|
||||
if(asyncHasParam(request, "repeats")) repeats = atoi(asyncParam(request, "repeats").c_str());
|
||||
}
|
||||
if(frame.remoteAddress > 0 && frame.rollingCode > 0) {
|
||||
if(somfy.sendFrame(frame, repeats))
|
||||
request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Command Sent\"}"));
|
||||
else
|
||||
sendRadioUnavailable(request);
|
||||
}
|
||||
else
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No address or rolling code provided\"}"));
|
||||
});
|
||||
|
||||
// setgeneral
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/setgeneral",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) {
|
||||
request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}");
|
||||
return;
|
||||
}
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["hostname"].isNull() || !obj["ssdpBroadcast"].isNull() || !obj["checkForUpdate"].isNull()) {
|
||||
bool checkForUpdate = settings.checkForUpdate;
|
||||
settings.fromJSON(obj);
|
||||
settings.save();
|
||||
if(settings.checkForUpdate != checkForUpdate) git.emitUpdateCheck();
|
||||
if(!obj["hostname"].isNull()) net.updateHostname();
|
||||
}
|
||||
if(!obj["ntpServer"].isNull() || !obj["ntpServer"].isNull()) {
|
||||
settings.NTP.fromJSON(obj);
|
||||
settings.NTP.save();
|
||||
}
|
||||
request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set General Settings\"}");
|
||||
}));
|
||||
|
||||
// setNetwork
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/setNetwork",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) {
|
||||
request->send(400, _encoding_html, "Error parsing JSON body");
|
||||
return;
|
||||
}
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
bool reboot = false;
|
||||
if(!obj["connType"].isNull() && obj["connType"].as<uint8_t>() != static_cast<uint8_t>(settings.connType)) {
|
||||
settings.connType = static_cast<conn_types_t>(obj["connType"].as<uint8_t>());
|
||||
settings.save();
|
||||
reboot = true;
|
||||
}
|
||||
if(!obj["wifi"].isNull()) {
|
||||
JsonObject objWifi = obj["wifi"];
|
||||
if(settings.connType == conn_types_t::wifi) {
|
||||
if(!objWifi["ssid"].isNull() && objWifi["ssid"].as<String>().compareTo(settings.WIFI.ssid) != 0) {
|
||||
if(WiFi.softAPgetStationNum() == 0) reboot = true;
|
||||
}
|
||||
if(!objWifi["passphrase"].isNull() && objWifi["passphrase"].as<String>().compareTo(settings.WIFI.passphrase) != 0) {
|
||||
if(WiFi.softAPgetStationNum() == 0) reboot = true;
|
||||
}
|
||||
}
|
||||
settings.WIFI.fromJSON(objWifi);
|
||||
settings.WIFI.save();
|
||||
}
|
||||
if(!obj["ethernet"].isNull()) {
|
||||
JsonObject objEth = obj["ethernet"];
|
||||
if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref)
|
||||
reboot = true;
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C6
|
||||
settings.Ethernet.fromJSON(objEth);
|
||||
settings.Ethernet.save();
|
||||
#endif
|
||||
}
|
||||
if(reboot) {
|
||||
ESP_LOGI(TAG, "Rebooting ESP for new Network settings...");
|
||||
rebootDelay.reboot = true;
|
||||
rebootDelay.rebootTime = millis() + 1000;
|
||||
}
|
||||
request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set Network Settings\"}");
|
||||
}));
|
||||
|
||||
// setIP
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/setIP",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) {
|
||||
request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Setting IP...");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
settings.IP.fromJSON(obj);
|
||||
settings.IP.save();
|
||||
request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set Network Settings\"}");
|
||||
}));
|
||||
|
||||
// connectwifi
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/connectwifi",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) {
|
||||
request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}");
|
||||
return;
|
||||
}
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
ESP_LOGD(TAG, "Settings WIFI connection...");
|
||||
String ssid = "";
|
||||
String passphrase = "";
|
||||
if(!obj["ssid"].isNull()) ssid = obj["ssid"].as<String>();
|
||||
if(!obj["passphrase"].isNull()) passphrase = obj["passphrase"].as<String>();
|
||||
bool reboot = false;
|
||||
if(ssid.compareTo(settings.WIFI.ssid) != 0) reboot = true;
|
||||
if(passphrase.compareTo(settings.WIFI.passphrase) != 0) reboot = true;
|
||||
if(!settings.WIFI.ssidExists(ssid.c_str()) && ssid.length() > 0) {
|
||||
request->send(400, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"WiFi Network Does not exist\"}");
|
||||
}
|
||||
else {
|
||||
SETCHARPROP(settings.WIFI.ssid, ssid.c_str(), sizeof(settings.WIFI.ssid));
|
||||
SETCHARPROP(settings.WIFI.passphrase, passphrase.c_str(), sizeof(settings.WIFI.passphrase));
|
||||
settings.WIFI.save();
|
||||
settings.WIFI.print();
|
||||
request->send(201, _encoding_json, "{\"status\":\"OK\",\"desc\":\"Successfully set server connection\"}");
|
||||
if(reboot) {
|
||||
ESP_LOGI(TAG, "Rebooting ESP for new WiFi settings...");
|
||||
rebootDelay.reboot = true;
|
||||
rebootDelay.rebootTime = millis() + 1000;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// modulesettings
|
||||
server.on("/modulesettings", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
resp.addElem("fwVersion", settings.fwVersion.name);
|
||||
resp.addElem("ssdpBroadcast", settings.ssdpBroadcast);
|
||||
resp.addElem("hostname", settings.hostname);
|
||||
resp.addElem("connType", static_cast<uint8_t>(settings.connType));
|
||||
resp.addElem("chipModel", settings.chipModel);
|
||||
resp.addElem("checkForUpdate", settings.checkForUpdate);
|
||||
resp.addElem("ntpServer", settings.NTP.ntpServer);
|
||||
resp.addElem("posixZone", settings.NTP.posixZone);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
});
|
||||
|
||||
// networksettings
|
||||
server.on("/networksettings", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
JsonDocument doc;
|
||||
JsonObject obj = doc.to<JsonObject>();
|
||||
settings.toJSON(obj);
|
||||
obj["fwVersion"] = settings.fwVersion.name;
|
||||
JsonObject eth = obj.createNestedObject("ethernet");
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C6
|
||||
settings.Ethernet.toJSON(eth);
|
||||
#endif
|
||||
JsonObject wifi = obj.createNestedObject("wifi");
|
||||
settings.WIFI.toJSON(wifi);
|
||||
JsonObject ip = obj.createNestedObject("ip");
|
||||
settings.IP.toJSON(ip);
|
||||
serializeJson(doc, g_async_content, sizeof(g_async_content));
|
||||
request->send(200, _encoding_json, g_async_content);
|
||||
});
|
||||
|
||||
// connectmqtt
|
||||
server.addHandler(new AsyncCallbackJsonWebHandler("/connectmqtt",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) {
|
||||
request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}");
|
||||
return;
|
||||
}
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
ESP_LOGD(TAG, "Saving MQTT");
|
||||
mqtt.disconnect();
|
||||
settings.MQTT.fromJSON(obj);
|
||||
settings.MQTT.save();
|
||||
JsonDocument sdoc;
|
||||
JsonObject sobj = sdoc.to<JsonObject>();
|
||||
settings.MQTT.toJSON(sobj);
|
||||
serializeJson(sdoc, g_async_content, sizeof(g_async_content));
|
||||
request->send(200, _encoding_json, g_async_content);
|
||||
}));
|
||||
|
||||
// mqttsettings
|
||||
server.on("/mqttsettings", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
JsonDocument doc;
|
||||
JsonObject obj = doc.to<JsonObject>();
|
||||
settings.MQTT.toJSON(obj);
|
||||
serializeJson(doc, g_async_content, sizeof(g_async_content));
|
||||
request->send(200, _encoding_json, g_async_content);
|
||||
});
|
||||
|
||||
// Sort orders: the ids in the order the client lists them.
|
||||
server.addHandler(editHandler("/roomSortOrder", model_edit_type_t::room_order));
|
||||
server.addHandler(editHandler("/shadeSortOrder", model_edit_type_t::shade_order));
|
||||
server.addHandler(editHandler("/groupSortOrder", model_edit_type_t::group_order));
|
||||
|
||||
server.on("/beginFrequencyScan", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
somfy.transceiver.beginFrequencyScan();
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeTransceiverConfig(resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
});
|
||||
|
||||
server.on("/endFrequencyScan", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
somfy.transceiver.endFrequencyScan();
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeTransceiverConfig(resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
});
|
||||
|
||||
server.on("/recoverFilesystem", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||
if(git.status == GIT_UPDATING)
|
||||
request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Filesystem is updating. Please wait!!!\"}");
|
||||
else if(git.status != GIT_STATUS_READY)
|
||||
request->send(200, "application/json", "{\"status\":\"ERROR\",\"desc\":\"Cannot recover file system at this time.\"}");
|
||||
else {
|
||||
git.recoverFilesystem();
|
||||
request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Recovering filesystem from github please wait!!!\"}");
|
||||
}
|
||||
});
|
||||
|
||||
server.onNotFound([](AsyncWebServerRequest *r) {
|
||||
if(r->method() == HTTP_OPTIONS) { r->send(200); return; }
|
||||
webServer.handleNotFound(r);
|
||||
});
|
||||
|
||||
// serveStatic MUST be registered AFTER all route handlers so it doesn't shadow them
|
||||
server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
|
||||
|
||||
}
|
||||
|
|
@ -2,13 +2,13 @@
|
|||
#include "ConfigSettings.h"
|
||||
#include "Somfy.h"
|
||||
#include "SomfySnapshot.h"
|
||||
#include "Sockets.h"
|
||||
#include "entryPoints/MQTT.h"
|
||||
#include "SocketEmitter.h"
|
||||
#include "MqttConnection.h"
|
||||
|
||||
extern SomfyShadeController somfy;
|
||||
extern SocketEmitter sockEmit;
|
||||
extern ConfigSettings settings;
|
||||
extern MQTTClass mqtt;
|
||||
extern MqttConnection mqtt;
|
||||
|
||||
void SomfyRoom::publish() {
|
||||
if(mqtt.connected()) {
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
#include <ArduinoJson.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include "esp_log.h"
|
||||
#include "Sockets.h"
|
||||
#include "SocketEmitter.h"
|
||||
#include "ConfigSettings.h"
|
||||
#include "Somfy.h"
|
||||
#include "ESPNetwork.h"
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
#include <esp_task_wdt.h>
|
||||
#include "Somfy.h"
|
||||
#include "SomfyEditor.h"
|
||||
#include "Sockets.h"
|
||||
#include "entryPoints/MQTT.h"
|
||||
#include "SocketEmitter.h"
|
||||
#include "MqttConnection.h"
|
||||
|
||||
extern SomfyShadeController somfy;
|
||||
extern SocketEmitter sockEmit;
|
||||
extern MQTTClass mqtt;
|
||||
extern MqttConnection mqtt;
|
||||
|
||||
void SomfyShade::emitState() { this->changed |= SHADE_CHANGED_STATE; }
|
||||
void SomfyShade::emitState(const char *evt) { this->emitState(255, evt); }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include <unity.h>
|
||||
#include <string.h>
|
||||
#include <string>
|
||||
#include "entryPoints/MqttCommands.h"
|
||||
|
||||
static shade_request_t req;
|
||||
|
|
@ -137,6 +138,14 @@ static void test_group_sunny_and_windy() {
|
|||
TEST_ASSERT_TRUE(parse("espsomfy/groups/2/windy/set", "1"));
|
||||
TEST_ASSERT_EQUAL_INT8(1, req.windy);
|
||||
}
|
||||
static void test_every_subscribed_topic_is_understood() {
|
||||
// What the connection subscribes to and what the parser takes can't drift.
|
||||
for(const char *pattern : MQTT_COMMAND_TOPICS) {
|
||||
std::string topic = std::string("espsomfy/") + pattern;
|
||||
topic.replace(topic.find('+'), 1, "3");
|
||||
TEST_ASSERT_TRUE_MESSAGE(parse(topic.c_str(), "1"), topic.c_str());
|
||||
}
|
||||
}
|
||||
static void test_groups_only_take_their_commands() {
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/groups/2/target/set", "30"));
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/groups/2/mypos/set", "30"));
|
||||
|
|
@ -161,5 +170,6 @@ int main(int argc, char **argv) {
|
|||
RUN_TEST(test_group_direction_and_sun_flag);
|
||||
RUN_TEST(test_group_sunny_and_windy);
|
||||
RUN_TEST(test_groups_only_take_their_commands);
|
||||
RUN_TEST(test_every_subscribed_topic_is_understood);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue