diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6b4bec0..fc93223 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,150 +2,44 @@ name: ESPSomfy-RTS on: [push, pull_request] -env: - ARDUINO_BOARD_MANAGER_ADDITIONAL_URLS: "https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json" - ARDUINO_CLI_VERSION: "0.x" - ARDUINO_ESP32_VERSION: "2.0.10" - ARDUINO_JSON_VERSION: "6.21.3" - ESPTOOL_VERSION: "4.6" - LITTLEFS_VERSION: "v2.5.1" - MKLITTLEFS_VERSION: "3.1.0" - PUB_SUB_CLIENT_VERSION: "2.8.0" - PYTHON_VERSION: "3.10" - SMARTRC_CC1101_VERSION: "2.5.7" - WEB_SOCKET_VERSION: "2.4.0" - jobs: - littlefs: - name: LittleFS - runs-on: ubuntu-latest - - steps: - - name: Check out code - uses: actions/checkout@v3 - - - name: Checkout mklittlefs - uses: actions/checkout@v3 - with: - repository: earlephilhower/mklittlefs - path: mklittlefs - ref: ${{ env.MKLITTLEFS_VERSION }} - - - name: Checkout LittleFS - uses: actions/checkout@v3 - with: - repository: littlefs-project/littlefs - path: mklittlefs/littlefs - ref: ${{ env.LITTLEFS_VERSION }} - - - name: Build mklittlefs - run: | - make -C mklittlefs - - - name: Create LittleFS - run: | - ./mklittlefs/mklittlefs --create data --size 1441792 SomfyController.littlefs.bin - - - name: Upload binaries - uses: actions/upload-artifact@v3 - with: - name: LittleFS - path: SomfyController.littlefs.bin - retention-days: 5 - - arduino: + build: name: ${{ matrix.name }} - needs: [littlefs] runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - - board: esp32 - addr_bootloader: 0x1000 - chip: ESP32 - fqbn: esp32:esp32:esp32 + - env: esp32dev name: ESP32 - - board: lolin_c3_mini - addr_bootloader: 0x0 - chip: ESP32-C3 - fqbn: esp32:esp32:lolin_c3_mini - name: LOLIN-C3-mini - - board: lolin_s2_mini - addr_bootloader: 0x1000 - chip: ESP32-S2 - fqbn: esp32:esp32:lolin_s2_mini - name: LOLIN-S2-mini - - board: lolin_s3_mini - addr_bootloader: 0x0 - chip: ESP32-S3 - fqbn: esp32:esp32:lolin_s3_mini - name: LOLIN-S3-mini steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 with: - path: SomfyController + python-version: "3.12" - - name: Get LittleFS - uses: actions/download-artifact@v3 - with: - name: LittleFS - - - name: Install Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v4 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Upgrade pip - run: | - python -m pip install --upgrade pip - pip --version - - - name: Install ESPTool - run: | - pip install esptool==${{ env.ESPTOOL_VERSION }} - - - name: Install Arduino CLI - uses: arduino/setup-arduino-cli@v1 - with: - version: ${{ env.ARDUINO_CLI_VERSION }} - - - name: Configure Arduino CLI - run: | - arduino-cli core update-index - arduino-cli core install esp32:esp32@${{ env.ARDUINO_ESP32_VERSION }} - - - name: Configure Arduino Libraries - run: | - arduino-cli lib install ArduinoJson@${{ env.ARDUINO_JSON_VERSION }} - arduino-cli lib install PubSubClient@${{ env.PUB_SUB_CLIENT_VERSION }} - arduino-cli lib install SmartRC-CC1101-Driver-Lib@${{ env.SMARTRC_CC1101_VERSION }} - arduino-cli lib install WebSockets@${{ env.WEB_SOCKET_VERSION }} + - name: Install PlatformIO + run: pip install platformio - name: Build ${{ matrix.name }} - run: | - mkdir -p build - arduino-cli compile --clean --output-dir build --fqbn ${{ matrix.fqbn }} --warnings default ./SomfyController + run: pio run -e ${{ matrix.env }} - - name: ${{ matrix.name }} Image - run: | - python -m esptool --chip ${{ matrix.chip }} \ - merge_bin -o build/SomfyController.onboard.bin \ - ${{ matrix.addr_bootloader }} build/SomfyController.ino.bootloader.bin \ - 0x8000 build/SomfyController.ino.partitions.bin \ - 0x10000 build/SomfyController.ino.bin \ - 0x290000 SomfyController.littlefs.bin + - name: Build LittleFS image + run: pio run -e ${{ matrix.env }} -t buildfs - - name: Upload ${{ matrix.name }} - uses: actions/upload-artifact@v3 + - name: Upload firmware + uses: actions/upload-artifact@v4 with: name: ${{ matrix.name }} path: | - build/SomfyController.ino.bin - build/SomfyController.ino.bootloader.bin - build/SomfyController.ino.partitions.bin - build/SomfyController.onboard.bin + .pio/build/${{ matrix.env }}/firmware.bin + .pio/build/${{ matrix.env }}/firmware.elf + .pio/build/${{ matrix.env }}/partitions.bin + .pio/build/${{ matrix.env }}/bootloader.bin + .pio/build/${{ matrix.env }}/littlefs.bin retention-days: 5 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index fe6096d..96b5d08 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,27 +1,26 @@ name: ESPSomfy-RTS Release -on: +on: release: types: [published] -env: - ARDUINO_BOARD_MANAGER_ADDITIONAL_URLS: "https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json" - ARDUINO_CLI_VERSION: "0.x" - ARDUINO_ESP32_VERSION: "2.0.17" - ARDUINO_JSON_VERSION: "6.21.5" - ESPTOOL_VERSION: "4.7" - LITTLEFS_VERSION: "v2.5.1" - MKLITTLEFS_VERSION: "3.1.0" - PUB_SUB_CLIENT_VERSION: "2.8.0" - PYTHON_VERSION: "3.10" - SMARTRC_CC1101_VERSION: "2.5.7" - WEB_SOCKET_VERSION: "2.4.0" - jobs: - littlefs: - name: LittleFS + build: + permissions: write-all + name: ${{ matrix.name }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - env: esp32dev + name: ESP32 + chip: ESP32 + addr_bootloader: "0x1000" + fwname: SomfyController.esp32.bin + obname: SomfyController.onboard.esp32.bin + steps: - name: Get Release id: get_release @@ -32,34 +31,31 @@ jobs: - name: Check out code uses: actions/checkout@v4 - - name: Checkout mklittlefs - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - repository: earlephilhower/mklittlefs - path: mklittlefs - ref: ${{ env.MKLITTLEFS_VERSION }} + python-version: "3.12" - - name: Checkout LittleFS - uses: actions/checkout@v4 - with: - repository: littlefs-project/littlefs - path: mklittlefs/littlefs - ref: ${{ env.LITTLEFS_VERSION }} + - name: Install PlatformIO + run: pip install platformio - - name: Build mklittlefs + - name: Build firmware + run: pio run -e ${{ matrix.env }} + + - name: Build LittleFS image + run: pio run -e ${{ matrix.env }} -t buildfs + + - name: Create onboard image run: | - make -C mklittlefs + python -m esptool --chip ${{ matrix.chip }} \ + merge_bin -o ${{ matrix.obname }} \ + ${{ matrix.addr_bootloader }} .pio/build/${{ matrix.env }}/bootloader.bin \ + 0x8000 .pio/build/${{ matrix.env }}/partitions.bin \ + 0x10000 .pio/build/${{ matrix.env }}/firmware.bin \ + 0x310000 .pio/build/${{ matrix.env }}/littlefs.bin - - name: Create LittleFS - run: | - ./mklittlefs/mklittlefs --create data --size 1441792 SomfyController.littlefs.bin - - - name: Upload binaries - uses: actions/upload-artifact@v4 - with: - name: LittleFS - path: SomfyController.littlefs.bin - retention-days: 5 + - name: Compress onboard image + run: zip ${{ matrix.obname }}.zip ${{ matrix.obname }} - name: Upload LittleFS uses: shogo82148/actions-upload-release-asset@v1.7.5 @@ -67,129 +63,20 @@ jobs: github_token: ${{ github.token }} upload_url: ${{ steps.get_release.outputs.upload_url }} asset_name: SomfyController.littlefs.bin - asset_path: SomfyController.littlefs.bin + asset_path: .pio/build/${{ matrix.env }}/littlefs.bin overwrite: true - arduino: - permissions: write-all - name: ${{ matrix.name }} - needs: [littlefs] - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - include: - - board: esp32 - addr_bootloader: 0x1000 - chip: ESP32 - fqbn: esp32:esp32:esp32wrover:PartitionScheme=default,FlashMode=qio,FlashFreq=80,UploadSpeed=921600,DebugLevel=none,EraseFlash=none - # esp32:esp32:esp32wrover:PartitionScheme=default,FlashMode=qio,FlashFreq=80,UploadSpeed=921600,DebugLevel=none,EraseFlash=none - name: ESP32 - obname: SomfyController.onboard.esp32.bin - fwname: SomfyController.ino.esp32.bin - - board: esp32c3 - addr_bootloader: 0x0 - chip: ESP32-C3 - fqbn: esp32:esp32:esp32c3:JTAGAdapter=default,CDCOnBoot=cdc,PartitionScheme=default,CPUFreq=160,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=none,EraseFlash=none - # esp32:esp32:esp32c3:JTAGAdapter=default,CDCOnBoot=default,PartitionScheme=default,CPUFreq=160,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=none,EraseFlash=none - name: ESP32C3 - obname: SomfyController.onboard.esp32c3.bin - fwname: SomfyController.ino.esp32c3.bin - - board: esp32s2 - addr_bootloader: 0x1000 - chip: ESP32-S2 - fqbn: esp32:esp32:esp32s2:JTAGAdapter=default,CDCOnBoot=cdc,MSCOnBoot=default,DFUOnBoot=default,UploadMode=default,PSRAM=disabled,PartitionScheme=default,CPUFreq=240,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=none,EraseFlash=none - # esp32:esp32:esp32s2:JTAGAdapter=default,CDCOnBoot=default,MSCOnBoot=default,DFUOnBoot=default,UploadMode=default,PSRAM=disabled,PartitionScheme=default,CPUFreq=240,FlashMode=qio,FlashFreq=80,FlashSize=4M,UploadSpeed=921600,DebugLevel=none,EraseFlash=none - name: ESP32S2 - obname: SomfyController.onboard.esp32s2.bin - fwname: SomfyController.ino.esp32s2.bin - - board: esp32s3 - addr_bootloader: 0x0 - chip: ESP32-S3 - fqbn: esp32:esp32:esp32s3:JTAGAdapter=default,PSRAM=disabled,FlashMode=qio,FlashSize=4M,LoopCore=1,EventsCore=1,USBMode=hwcdc,CDCOnBoot=cdc,MSCOnBoot=default,DFUOnBoot=default,UploadMode=default,PartitionScheme=default,CPUFreq=240,UploadSpeed=921600,DebugLevel=none,EraseFlash=none - # esp32:esp32:esp32s3:JTAGAdapter=default,PSRAM=disabled,FlashMode=qio,FlashSize=4M,LoopCore=1,EventsCore=1,USBMode=hwcdc,CDCOnBoot=cdc,MSCOnBoot=default,DFUOnBoot=default,UploadMode=default,PartitionScheme=default,CPUFreq=240,UploadSpeed=921600,DebugLevel=none,EraseFlash=none - name: ESP32S3 - fwname: SomfyController.ino.esp32s3.bin - obname: SomfyController.onboard.esp32s3.bin - steps: - - name: Get Release - id: get_release - uses: bruceadams/get-release@v1.3.2 - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: Check out code - uses: actions/checkout@v4 - with: - path: SomfyController - - - name: Get LittleFS - uses: actions/download-artifact@v4 - with: - name: LittleFS - - - name: Install Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v5 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Upgrade pip - run: | - python -m pip install --upgrade pip - pip --version - - - name: Install ESPTool - run: | - pip install esptool==${{ env.ESPTOOL_VERSION }} - - - name: Install Arduino CLI - uses: arduino/setup-arduino-cli@v1 - with: - version: ${{ env.ARDUINO_CLI_VERSION }} - - - name: Configure Arduino CLI - run: | - arduino-cli core update-index - arduino-cli core install esp32:esp32@${{ env.ARDUINO_ESP32_VERSION }} - - - name: Configure Arduino Libraries - run: | - arduino-cli lib install ArduinoJson@${{ env.ARDUINO_JSON_VERSION }} - arduino-cli lib install PubSubClient@${{ env.PUB_SUB_CLIENT_VERSION }} - arduino-cli lib install SmartRC-CC1101-Driver-Lib@${{ env.SMARTRC_CC1101_VERSION }} - arduino-cli lib install WebSockets@${{ env.WEB_SOCKET_VERSION }} - - - name: Build ${{ matrix.name }} - run: | - mkdir -p build${{ matrix.name }} - arduino-cli compile --clean --output-dir build${{ matrix.name }} --fqbn ${{ matrix.fqbn }} --warnings none ./SomfyController - - - name: ${{ matrix.name }} Image - run: | - python -m esptool --chip ${{ matrix.chip }} \ - merge_bin -o ${{ matrix.obname }} \ - ${{ matrix.addr_bootloader }} build${{ matrix.name }}/SomfyController.ino.bootloader.bin \ - 0x8000 build${{ matrix.name }}/SomfyController.ino.partitions.bin \ - 0x10000 build${{ matrix.name }}/SomfyController.ino.bin \ - 0x290000 SomfyController.littlefs.bin - - - name: Upload Firmware ${{ matrix.name }} + - name: Upload firmware uses: shogo82148/actions-upload-release-asset@v1.7.5 with: github_token: ${{ github.token }} upload_url: ${{ steps.get_release.outputs.upload_url }} asset_name: ${{ matrix.fwname }} - asset_path: build${{ matrix.name }}/SomfyController.ino.bin + asset_path: .pio/build/${{ matrix.env }}/firmware.bin + overwrite: true - - name: ${{ matrix.name }} Compress Onboard Image - run: | - zip ${{ matrix.obname }}.zip ./${{ matrix.obname }} - - - name: Upload Onboard ${{ matrix.name }} + - name: Upload onboard image uses: shogo82148/actions-upload-release-asset@v1.7.5 - # env: - # GITHUB_TOKEN: ${{ github.token }} with: github_token: ${{ github.token }} upload_url: ${{ steps.get_release.outputs.upload_url }} @@ -197,4 +84,3 @@ jobs: asset_path: ${{ matrix.obname }}.zip overwrite: true asset_content_type: application/zip - diff --git a/.gitignore b/.gitignore index be96788..5609aab 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,11 @@ debug.cfg SomfyController.ino.XIAO_ESP32S3.bin SomfyController.ino.esp32c3.bin SomfyController.ino.esp32s2.bin -.vscode/settings.json +.vscode/ +.pio/ +.claude/ +data/ +build/ +coredump_report.txt +coredump.bin +logs/ \ No newline at end of file diff --git a/README.md b/README.md index 53aedd2..e30a754 100644 --- a/README.md +++ b/README.md @@ -82,3 +82,9 @@ Configuration of the Transceiver is done with the ELECHOUSE_CC1101 library which +pio pkg exec -p tool-esptoolpy -- esptool.py --port COM9 read_flash 0x3F0000 0x10000 coredump.bin + + +C:\Users\oem\.platformio\packages\framework-espidf\export.ps1 +esp-coredump info_corefile --core coredump.bin --core-format=raw --gdb C:\Users\oem\.platformio\packages\toolchain-xtensa-esp32\bin\xtensa-esp32-elf-gdb.exe .pio\build\esp32dev\firmware.elf > coredump_report.txt + diff --git a/Sockets.cpp b/Sockets.cpp deleted file mode 100644 index 51765aa..0000000 --- a/Sockets.cpp +++ /dev/null @@ -1,203 +0,0 @@ -#include -#include -#include -#include -#include "Sockets.h" -#include "ConfigSettings.h" -#include "Somfy.h" -#include "Network.h" -#include "GitOTA.h" - -extern ConfigSettings settings; -extern Network net; -extern SomfyShadeController somfy; -extern SocketEmitter sockEmit; -extern GitUpdater git; - - -WebSocketsServer sockServer = WebSocketsServer(8080); - -#define MAX_SOCK_RESPONSE 2048 -static char g_response[MAX_SOCK_RESPONSE]; - -bool room_t::isJoined(uint8_t num) { - for(uint8_t i = 0; i < sizeof(this->clients); i++) { - if(this->clients[i] == num) return true; - } - return false; -} -bool room_t::join(uint8_t num) { - if(this->isJoined(num)) return true; - for(uint8_t i = 0; i < sizeof(this->clients); i++) { - if(this->clients[i] == 255) { - this->clients[i] = num; - return true; - } - } - return false; -} -bool room_t::leave(uint8_t num) { - if(!this->isJoined(num)) return false; - for(uint8_t i = 0; i < sizeof(this->clients); i++) { - if(this->clients[i] == num) this->clients[i] = 255; - } - return true; -} -void room_t::clear() { - memset(this->clients, 255, sizeof(this->clients)); -} -uint8_t room_t::activeClients() { - uint8_t n = 0; - for(uint8_t i = 0; i < sizeof(this->clients); i++) { - if(this->clients[i] != 255) n++; - } - return n; -} -/********************************************************************* - * ClientSocketEvent class members - ********************************************************************/ -/* -void ClientSocketEvent::prepareMessage(const char *evt, const char *payload) { - if(strlen(payload) + 5 >= sizeof(this->msg)) Serial.printf("Socket buffer overflow %d > 2048\n", strlen(payload) + 5 + strlen(evt)); - snprintf(this->msg, sizeof(this->msg), "42[%s,%s]", evt, payload); -} -void ClientSocketEvent::prepareMessage(const char *evt, JsonDocument &doc) { - memset(this->msg, 0x00, sizeof(this->msg)); - snprintf(this->msg, sizeof(this->msg), "42[%s,", evt); - serializeJson(doc, &this->msg[strlen(this->msg)], sizeof(this->msg) - strlen(this->msg) - 2); - strcat(this->msg, "]"); -} -*/ - -/********************************************************************* - * SocketEmitter class members - ********************************************************************/ -void SocketEmitter::startup() { - -} -void SocketEmitter::begin() { - sockServer.begin(); - sockServer.enableHeartbeat(20000, 10000, 3); - sockServer.onEvent(this->wsEvent); - Serial.println("Socket Server Started..."); - //settings.printAvailHeap(); -} -void SocketEmitter::loop() { - this->initClients(); - sockServer.loop(); -} -JsonSockEvent *SocketEmitter::beginEmit(const char *evt) { - this->json.beginEvent(&sockServer, evt, g_response, sizeof(g_response)); - return &this->json; -} -void SocketEmitter::endEmit(uint8_t num) { this->json.endEvent(num); sockServer.loop(); } -void SocketEmitter::endEmitRoom(uint8_t room) { - if(room < SOCK_MAX_ROOMS) { - room_t *r = &this->rooms[room]; - for(uint8_t i = 0; i < sizeof(r->clients); i++) { - if(r->clients[i] != 255) this->json.endEvent(r->clients[i]); - } - } -} -uint8_t SocketEmitter::activeClients(uint8_t room) { - if(room < SOCK_MAX_ROOMS) return this->rooms[room].activeClients(); - return 0; -} -void SocketEmitter::initClients() { - for(uint8_t i = 0; i < sizeof(this->newClients); i++) { - uint8_t num = this->newClients[i]; - if(num != 255) { - if(sockServer.clientIsConnected(num)) { - Serial.printf("Initializing Socket Client %u\n", num); - esp_task_wdt_reset(); - settings.emitSockets(num); - somfy.emitState(num); - git.emitUpdateCheck(num); - net.emitSockets(num); - esp_task_wdt_reset(); - } - this->newClients[i] = 255; - } - } -} -void SocketEmitter::delayInit(uint8_t num) { - for(uint8_t i=0; i < sizeof(this->newClients); i++) { - if(this->newClients[i] == num) break; - else if(this->newClients[i] == 255) { - this->newClients[i] = num; - break; - } - } -} -void SocketEmitter::end() { - sockServer.close(); - for(uint8_t i = 0; i < SOCK_MAX_ROOMS; i++) - this->rooms[i].clear(); -} -void SocketEmitter::disconnect() { sockServer.disconnect(); } -void SocketEmitter::wsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) { - switch(type) { - case WStype_ERROR: - if(length > 0) - Serial.printf("Socket Error: %s\n", payload); - else - Serial.println("Socket Error: \n"); - break; - case WStype_DISCONNECTED: - if(length > 0) - Serial.printf("Socket [%u] Disconnected!\n [%s]", num, payload); - else - Serial.printf("Socket [%u] Disconnected!\n", num); - for(uint8_t i = 0; i < SOCK_MAX_ROOMS; i++) { - sockEmit.rooms[i].leave(num); - } - break; - case WStype_CONNECTED: - { - IPAddress ip = sockServer.remoteIP(num); - Serial.printf("Socket [%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); - // Send all the current shade settings to the client. - sockServer.sendTXT(num, "Connected"); - //sockServer.loop(); - sockEmit.delayInit(num); - } - break; - case WStype_TEXT: - if(strncmp((char *)payload, "join:", 5) == 0) { - // In this instance the client wants to join a room. Let's do some - // work to get the ordinal of the room that the client wants to join. - uint8_t roomNum = atoi((char *)&payload[5]); - Serial.printf("Client %u joining room %u\n", num, roomNum); - if(roomNum < SOCK_MAX_ROOMS) sockEmit.rooms[roomNum].join(num); - } - else if(strncmp((char *)payload, "leave:", 6) == 0) { - uint8_t roomNum = atoi((char *)&payload[6]); - Serial.printf("Client %u leaving room %u\n", num, roomNum); - if(roomNum < SOCK_MAX_ROOMS) sockEmit.rooms[roomNum].leave(num); - } - else { - Serial.printf("Socket [%u] text: %s\n", num, payload); - } - // send message to client - // webSocket.sendTXT(num, "message here"); - - // send data to all connected clients - // sockServer.broadcastTXT("message here"); - break; - case WStype_BIN: - Serial.printf("[%u] get binary length: %u\n", num, length); - //hexdump(payload, length); - - // send message to client - // sockServer.sendBIN(num, payload, length); - break; - case WStype_PONG: - //Serial.printf("Pong from %u\n", num); - break; - case WStype_PING: - //Serial.printf("Ping from %u\n", num); - break; - default: - break; - } -} diff --git a/Web.cpp b/Web.cpp deleted file mode 100644 index 425d1b2..0000000 --- a/Web.cpp +++ /dev/null @@ -1,2733 +0,0 @@ -#include -#include -#include -#include -#include -#include "mbedtls/md.h" -#include "ConfigSettings.h" -#include "ConfigFile.h" -#include "Utils.h" -#include "SSDP.h" -#include "Somfy.h" -#include "WResp.h" -#include "Web.h" -#include "MQTT.h" -#include "GitOTA.h" -#include "Network.h" - -extern ConfigSettings settings; -extern SSDPClass SSDP; -extern rebootDelay_t rebootDelay; -extern SomfyShadeController somfy; -extern Web webServer; -extern MQTTClass mqtt; -extern GitUpdater git; -extern Network net; - -//#define WEB_MAX_RESPONSE 34768 -#define WEB_MAX_RESPONSE 4096 -static char g_content[WEB_MAX_RESPONSE]; - - -// General responses -static const char _response_404[] = "404: Service Not Found"; - - -// Encodings -static const char _encoding_text[] = "text/plain"; -static const char _encoding_html[] = "text/html"; -static const char _encoding_json[] = "application/json"; - -WebServer apiServer(8081); -WebServer server(80); -void Web::startup() { - Serial.println("Launching web server..."); -} -void Web::loop() { - server.handleClient(); - delay(1); - apiServer.handleClient(); - delay(1); -} -void Web::sendCORSHeaders(WebServer &server) { - //server.sendHeader(F("Connection"), F("Keep-Alive")); - //server.sendHeader(F("Keep-Alive"), F("timeout=5, max=1000")); - //server.sendHeader(F("Access-Control-Allow-Origin"), F("*")); - //server.sendHeader(F("Access-Control-Max-Age"), F("600")); - //server.sendHeader(F("Access-Control-Allow-Methods"), F("PUT,POST,GET,OPTIONS")); - //server.sendHeader(F("Access-Control-Allow-Headers"), F("*")); -} -void Web::sendCacheHeaders(uint32_t seconds) { - server.sendHeader(F("Cache-Control"), F("public, max-age=604800, immutable")); -} -void Web::end() { - //server.end(); -} -void Web::handleDeserializationError(WebServer &server, DeserializationError &err) { - switch (err.code()) { - case DeserializationError::InvalidInput: - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid JSON payload\"}")); - break; - case DeserializationError::NoMemory: - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Out of memory parsing JSON\"}")); - break; - default: - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"General JSON Deserialization failed\"}")); - break; - } -} -bool Web::isAuthenticated(WebServer &server, bool cfg) { - Serial.println("Checking authentication"); - if(settings.Security.type == security_types::None) return true; - else if(!cfg && (settings.Security.permissions & static_cast(security_permissions::ConfigOnly)) == 0x01) return true; - else if(server.hasHeader("apikey")) { - // Api key was supplied. - Serial.println("Checking API Key..."); - char token[65]; - memset(token, 0x00, sizeof(token)); - this->createAPIToken(server.client().remoteIP(), token); - // Compare the tokens. - if(String(token) != server.header("apikey")) return false; - server.sendHeader("apikey", token); - } - else { - // Send a 401 - Serial.println("Not authenticated..."); - server.send(401, "Unauthorized API Key"); - return false; - } - return true; -} -bool Web::createAPIPinToken(const IPAddress ipAddress, const char *pin, char *token) { - return this->createAPIToken((String(pin) + ":" + ipAddress.toString()).c_str(), token); -} -bool Web::createAPIPasswordToken(const IPAddress ipAddress, const char *username, const char *password, char *token) { - return this->createAPIToken((String(username) + ":" + String(password) + ":" + ipAddress.toString()).c_str(), token); -} -bool Web::createAPIToken(const char *payload, char *token) { - byte hmacResult[32]; - mbedtls_md_context_t ctx; - mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; - mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1); - mbedtls_md_hmac_starts(&ctx, (const unsigned char *)settings.serverId, strlen(settings.serverId)); - mbedtls_md_hmac_update(&ctx, (const unsigned char *)payload, strlen(payload)); - mbedtls_md_hmac_finish(&ctx, hmacResult); - Serial.print("Hash: "); - token[0] = '\0'; - for(int i = 0; i < sizeof(hmacResult); i++){ - char str[3]; - sprintf(str, "%02x", (int)hmacResult[i]); - strcat(token, str); - } - Serial.println(token); - return true; -} -bool Web::createAPIToken(const IPAddress ipAddress, char *token) { - String payload; - if(settings.Security.type == security_types::Password) createAPIPasswordToken(ipAddress, settings.Security.username, settings.Security.password, token); - else if(settings.Security.type == security_types::PinEntry) createAPIPinToken(ipAddress, settings.Security.pin, token); - else createAPIToken(ipAddress.toString().c_str(), token); - return true; -} -void Web::handleLogout(WebServer &server) { - Serial.println("Logging out of webserver"); - server.sendHeader("Location", "/"); - server.sendHeader("Cache-Control", "no-cache"); - server.sendHeader("Set-Cookie", "ESPSOMFYID=0"); - server.send(301); -} -void Web::handleLogin(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - StaticJsonDocument<256> doc; - JsonObject obj = doc.to(); - char token[65]; - memset(&token, 0x00, sizeof(token)); - this->createAPIToken(server.client().remoteIP(), token); - obj["type"] = static_cast(settings.Security.type); - if(settings.Security.type == security_types::None) { - obj["apiKey"] = token; - obj["msg"] = "Success"; - obj["success"] = true; - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); - return; - } - Serial.println("Web logging in..."); - char username[33] = ""; - char password[33] = ""; - char pin[5] = ""; - memset(username, 0x00, sizeof(username)); - memset(password, 0x00, sizeof(password)); - memset(pin, 0x00, sizeof(pin)); - if(server.hasArg("plain")) { - DynamicJsonDocument docin(512); - DeserializationError err = deserializeJson(docin, server.arg("plain")); - if (err) { - this->handleDeserializationError(server, err); - return; - } - else { - JsonObject objin = docin.as(); - if(objin.containsKey("username") && objin["username"]) strlcpy(username, objin["username"], sizeof(username)); - if(objin.containsKey("password") && objin["password"]) strlcpy(password, objin["password"], sizeof(password)); - if(objin.containsKey("pin") && objin["pin"]) strlcpy(pin, objin["pin"], sizeof(pin)); - } - } - else { - if(server.hasArg("username")) strlcpy(username, server.arg("username").c_str(), sizeof(username)); - if(server.hasArg("password")) strlcpy(password, server.arg("password").c_str(), sizeof(password)); - if(server.hasArg("pin")) strlcpy(pin, server.arg("pin").c_str(), sizeof(pin)); - } - // At this point we should have all the data we need to login. - if(settings.Security.type == security_types::PinEntry) { - Serial.print("Validating pin "); - Serial.println(pin); - if(strlen(pin) == 0 || strcmp(pin, settings.Security.pin) != 0) { - obj["success"] = false; - obj["msg"] = "Invalid Pin Entry"; - } - else { - obj["success"] = true; - obj["msg"] = "Login successful"; - obj["apiKey"] = token; - } - } - else if(settings.Security.type == security_types::Password) { - if(strlen(username) == 0 || strlen(password) == 0 || strcmp(username, settings.Security.username) != 0 || strcmp(password, settings.Security.password) != 0) { - obj["success"] = false; - obj["msg"] = "Invalid username or password"; - } - else { - obj["success"] = true; - obj["msg"] = "Login successful"; - obj["apiKey"] = token; - } - } - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); - return; -} -void Web::handleStreamFile(WebServer &server, const char *filename, const char *encoding) { - if(git.lockFS) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Filesystem update in progress\"}")); - return; - } - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - esp_task_wdt_reset(); - // Load the index html page from the data directory. - Serial.print("Loading file "); - Serial.println(filename); - File file = LittleFS.open(filename, "r"); - if (!file) { - Serial.print("Error opening"); - Serial.println(filename); - server.send(500, _encoding_text, "Error opening file"); - } - esp_task_wdt_delete(NULL); - server.streamFile(file, encoding); - file.close(); - esp_task_wdt_add(NULL); - esp_task_wdt_reset(); -} -void Web::handleController(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - settings.printAvailHeap(); - if (method == HTTP_POST || method == HTTP_GET) { - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - resp.addElem("maxRooms", (uint8_t)SOMFY_MAX_ROOMS); - resp.addElem("maxShades", (uint8_t)SOMFY_MAX_SHADES); - resp.addElem("maxGroups", (uint8_t)SOMFY_MAX_GROUPS); - resp.addElem("maxGroupedShades", (uint8_t)SOMFY_MAX_GROUPED_SHADES); - resp.addElem("maxLinkedRemotes", (uint8_t)SOMFY_MAX_LINKED_REMOTES); - resp.addElem("startingAddress", (uint32_t)somfy.startingAddress); - resp.beginObject("transceiver"); - somfy.transceiver.toJSON(resp); - resp.endObject(); - resp.beginObject("version"); - git.toJSON(resp); - resp.endObject(); - resp.beginArray("rooms"); - somfy.toJSONRooms(resp); - resp.endArray(); - resp.beginArray("shades"); - somfy.toJSONShades(resp); - resp.endArray(); - resp.beginArray("groups"); - somfy.toJSONGroups(resp); - resp.endArray(); - resp.beginArray("repeaters"); - somfy.toJSONRepeaters(resp); - resp.endArray(); - resp.endObject(); - resp.endResponse(); - } - else server.send(404, _encoding_text, _response_404); -} -void Web::handleLoginContext(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - resp.addElem("type", static_cast(settings.Security.type)); - resp.addElem("permissions", settings.Security.permissions); - resp.addElem("serverId", settings.serverId); - resp.addElem("version", settings.fwVersion.name); - resp.addElem("model", "ESPSomfyRTS"); - resp.addElem("hostname", settings.hostname); - resp.endObject(); - resp.endResponse(); -} -void Web::handleGetRepeaters(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_GET) { - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginArray(); - somfy.toJSONRepeaters(resp); - resp.endArray(); - resp.endResponse(); - } - else server.send(404, _encoding_text, _response_404); -} -void Web::handleGetRooms(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_GET) { - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginArray(); - somfy.toJSONRooms(resp); - resp.endArray(); - resp.endResponse(); - } - else server.send(404, _encoding_text, _response_404); -} -void Web::handleGetShades(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_GET) { - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginArray(); - somfy.toJSONShades(resp); - resp.endArray(); - resp.endResponse(); - } - else server.send(404, _encoding_text, _response_404); -} -void Web::handleGetGroups(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_GET) { - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginArray(); - somfy.toJSONGroups(resp); - resp.endArray(); - resp.endResponse(); - } - else server.send(404, _encoding_text, _response_404); -} -void Web::handleShadeCommand(WebServer& server) { - webServer.sendCORSHeaders(server); - if (server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - uint8_t shadeId = 255; - uint8_t target = 255; - uint8_t stepSize = 0; - int8_t repeat = -1; - somfy_commands command = somfy_commands::My; - if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) { - if (server.hasArg("shadeId")) { - shadeId = atoi(server.arg("shadeId").c_str()); - if (server.hasArg("command")) command = translateSomfyCommand(server.arg("command")); - else if (server.hasArg("target")) target = atoi(server.arg("target").c_str()); - if (server.hasArg("repeat")) repeat = atoi(server.arg("repeat").c_str()); - if(server.hasArg("stepSize")) stepSize = atoi(server.arg("stepSize").c_str()); - } - else if (server.hasArg("plain")) { - Serial.println("Sending Shade Command"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - this->handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("shadeId")) shadeId = obj["shadeId"]; - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); - if (obj.containsKey("command")) { - String scmd = obj["command"]; - command = translateSomfyCommand(scmd); - } - else if (obj.containsKey("target")) { - target = obj["target"].as(); - } - if (obj.containsKey("repeat")) repeat = obj["repeat"].as(); - if(obj.containsKey("stepSize")) stepSize = obj["stepSize"].as(); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); - SomfyShade* shade = somfy.getShadeById(shadeId); - if (shade) { - Serial.print("Received:"); - Serial.println(server.arg("plain")); - // Send the command to the shade. - if (target <= 100) - shade->moveToTarget(shade->transformPosition(target)); - else - shade->sendCommand(command, repeat > 0 ? repeat : shade->repeats, stepSize); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSONRef(resp); - resp.endObject(); - resp.endResponse(); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}")); - } - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}")); -} -void Web::handleRepeatCommand(WebServer& server) { - webServer.sendCORSHeaders(server); - HTTPMethod method = server.method(); - if (method == HTTP_OPTIONS) { server.send(200, "OK"); return; } - uint8_t shadeId = 255; - uint8_t groupId = 255; - uint8_t stepSize = 0; - int8_t repeat = -1; - somfy_commands command = somfy_commands::My; - if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) { - if(server.hasArg("shadeId")) shadeId = atoi(server.arg("shadeId").c_str()); - else if(server.hasArg("groupId")) groupId = atoi(server.arg("groupId").c_str()); - if(server.hasArg("command")) command = translateSomfyCommand(server.arg("command")); - if(server.hasArg("repeat")) repeat = atoi(server.arg("repeat").c_str()); - if(server.hasArg("stepSize")) stepSize = atoi(server.arg("stepSize").c_str()); - if(shadeId == 255 && groupId == 255 && server.hasArg("plain")) { - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - this->handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("shadeId")) shadeId = obj["shadeId"]; - if(obj.containsKey("groupId")) groupId = obj["groupId"]; - if(obj.containsKey("stepSize")) stepSize = obj["stepSize"]; - if (obj.containsKey("command")) { - String scmd = obj["command"]; - command = translateSomfyCommand(scmd); - } - if (obj.containsKey("repeat")) repeat = obj["repeat"].as(); - } - } - //DynamicJsonDocument sdoc(512); - //JsonObject sobj = sdoc.to(); - if(shadeId != 255) { - SomfyShade *shade = somfy.getShadeById(shadeId); - if(!shade) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade reference could not be found.\"}")); - return; - } - if(shade->shadeType == shade_types::garage1 && command == somfy_commands::Prog) command = somfy_commands::Toggle; - if(!shade->isLastCommand(command)) { - // We are going to send this as a new command. - shade->sendCommand(command, repeat >= 0 ? repeat : shade->repeats, stepSize); - } - else { - shade->repeatFrame(repeat >= 0 ? repeat : shade->repeats); - } - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginArray(); - shade->toJSONRef(resp); - resp.endArray(); - resp.endResponse(); - } - else if(groupId != 255) { - SomfyGroup * group = somfy.getGroupById(groupId); - if(!group) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group reference could not be found.\"}")); - return; - } - if(!group->isLastCommand(command)) { - // We are going to send this as a new command. - group->sendCommand(command, repeat >= 0 ? repeat : group->repeats, stepSize); - } - else - group->repeatFrame(repeat >= 0 ? repeat : group->repeats); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - group->toJSONRef(resp); - resp.endObject(); - resp.endResponse(); - - //group->toJSON(sobj); - //serializeJson(sdoc, g_content); - //server.send(200, _encoding_json, g_content); - } - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}")); - } -} -void Web::handleGroupCommand(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - uint8_t groupId = 255; - uint8_t stepSize = 0; - int8_t repeat = -1; - somfy_commands command = somfy_commands::My; - if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) { - if (server.hasArg("groupId")) { - groupId = atoi(server.arg("groupId").c_str()); - if (server.hasArg("command")) command = translateSomfyCommand(server.arg("command")); - if(server.hasArg("repeat")) repeat = atoi(server.arg("repeat").c_str()); - if(server.hasArg("stepSize")) stepSize = atoi(server.arg("stepSize").c_str()); - } - else if (server.hasArg("plain")) { - Serial.println("Sending Group Command"); - DynamicJsonDocument doc(256); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - this->handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("groupId")) groupId = obj["groupId"]; - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}")); - return; - } - if (obj.containsKey("command")) { - String scmd = obj["command"]; - command = translateSomfyCommand(scmd); - } - if(obj.containsKey("repeat")) repeat = obj["repeat"].as(); - if(obj.containsKey("stepSize")) stepSize = obj["stepSize"].as(); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); - SomfyGroup * group = somfy.getGroupById(groupId); - if (group) { - Serial.print("Received:"); - Serial.println(server.arg("plain")); - // Send the command to the group. - group->sendCommand(command, repeat >= 0 ? repeat : group->repeats, stepSize); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - group->toJSONRef(resp); - resp.endObject(); - resp.endResponse(); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group with the specified id not found.\"}")); - } - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}")); -} -void Web::handleTiltCommand(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - uint8_t shadeId = 255; - uint8_t target = 255; - somfy_commands command = somfy_commands::My; - if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) { - if (server.hasArg("shadeId")) { - shadeId = atoi(server.arg("shadeId").c_str()); - if (server.hasArg("command")) command = translateSomfyCommand(server.arg("command")); - else if(server.hasArg("target")) target = atoi(server.arg("target").c_str()); - } - else if (server.hasArg("plain")) { - Serial.println("Sending Shade Tilt Command"); - DynamicJsonDocument doc(256); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - this->handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("shadeId")) shadeId = obj["shadeId"]; - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); - if (obj.containsKey("command")) { - String scmd = obj["command"]; - command = translateSomfyCommand(scmd); - } - else if(obj.containsKey("target")) { - target = obj["target"].as(); - } - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); - SomfyShade* shade = somfy.getShadeById(shadeId); - if (shade) { - Serial.print("Received:"); - Serial.println(server.arg("plain")); - // Send the command to the shade. - if(target <= 100) - shade->moveToTiltTarget(shade->transformPosition(target)); - else - shade->sendTiltCommand(command); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSONRef(resp); - resp.endObject(); - resp.endResponse(); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}")); - } - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}")); -} -void Web::handleRoom(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_GET) { - if (server.hasArg("roomId")) { - int roomId = atoi(server.arg("roomId").c_str()); - SomfyRoom* room = somfy.getRoomById(roomId); - if (room) { - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - room->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Room Id not found.\"}")); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid room id.\"}")); - } - } - else if (method == HTTP_PUT || method == HTTP_POST) { - // We are updating an existing room. - if (server.hasArg("plain")) { - Serial.println("Updating a room"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - this->handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("roomId")) { - SomfyRoom* room = somfy.getRoomById(obj["roomId"]); - if (room) { - uint8_t err = room->fromJSON(obj); - if(err == 0) { - room->save(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - room->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else { - snprintf(g_content, sizeof(g_content), "{\"status\":\"DATA\",\"desc\":\"Data Error.\", \"code\":%d}", err); - server.send(500, _encoding_json, g_content); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Room Id not found.\"}")); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room id was supplied.\"}")); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room object supplied.\"}")); - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}")); -} -void Web::handleShade(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_GET) { - if (server.hasArg("shadeId")) { - int shadeId = atoi(server.arg("shadeId").c_str()); - SomfyShade* shade = somfy.getShadeById(shadeId); - if (shade) { - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}")); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid shade id.\"}")); - } - } - else if (method == HTTP_PUT || method == HTTP_POST) { - // We are updating an existing shade. - if (server.hasArg("plain")) { - Serial.println("Updating a shade"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - this->handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("shadeId")) { - SomfyShade* shade = somfy.getShadeById(obj["shadeId"]); - if (shade) { - uint8_t err = shade->fromJSON(obj); - if(err == 0) { - shade->save(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else { - snprintf(g_content, sizeof(g_content), "{\"status\":\"DATA\",\"desc\":\"Data Error.\", \"code\":%d}", err); - server.send(500, _encoding_json, g_content); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}")); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}")); -} -void Web::handleGroup(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_GET) { - if (server.hasArg("groupId")) { - int groupId = atoi(server.arg("groupId").c_str()); - SomfyGroup* group = somfy.getGroupById(groupId); - if (group) { - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - group->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}")); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid shade id.\"}")); - } - } - else if (method == HTTP_PUT || method == HTTP_POST) { - // We are updating an existing group. - if (server.hasArg("plain")) { - Serial.println("Updating a group"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - this->handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("groupId")) { - SomfyGroup* group = somfy.getGroupById(obj["groupId"]); - if (group) { - group->fromJSON(obj); - group->save(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - group->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}")); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}")); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}")); -} -void Web::handleDiscovery(WebServer &server) { - HTTPMethod method = apiServer.method(); - if (method == HTTP_POST || method == HTTP_GET) { - Serial.println("Discovery Requested"); - char connType[10] = "Unknown"; - if(net.connType == conn_types_t::ethernet) strcpy(connType, "Ethernet"); - else if(net.connType == conn_types_t::wifi) strcpy(connType, "Wifi"); - - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - resp.addElem("serverId", settings.serverId); - resp.addElem("version", settings.fwVersion.name); - resp.addElem("latest", git.latest.name); - resp.addElem("model", "ESPSomfyRTS"); - resp.addElem("hostname", settings.hostname); - resp.addElem("authType", static_cast(settings.Security.type)); - resp.addElem("permissions", settings.Security.permissions); - resp.addElem("chipModel", settings.chipModel); - resp.addElem("connType", connType); - resp.addElem("checkForUpdate", settings.checkForUpdate); - resp.beginObject("memory"); - resp.addElem("max", ESP.getMaxAllocHeap()); - resp.addElem("free", ESP.getFreeHeap()); - resp.addElem("min", ESP.getMinFreeHeap()); - resp.addElem("total", ESP.getHeapSize()); - resp.endObject(); - resp.beginArray("rooms"); - somfy.toJSONRooms(resp); - resp.endArray(); - resp.beginArray("shades"); - somfy.toJSONShades(resp); - resp.endArray(); - resp.beginArray("groups"); - somfy.toJSONGroups(resp); - resp.endArray(); - resp.endObject(); - resp.endResponse(); - net.needsBroadcast = true; - } - else - server.send(500, _encoding_text, "Invalid http method"); -} -void Web::handleBackup(WebServer &server, bool attach) { - webServer.sendCORSHeaders(server); - if(server.hasArg("attach")) attach = toBoolean(server.arg("attach").c_str(), attach); - if(attach) { - char filename[120]; - Timestamp ts; - char * iso = ts.getISOTime(); - // Replace the invalid characters as quickly as we can. - for(uint8_t i = 0; i < strlen(iso); i++) { - switch(iso[i]) { - case '.': - // Just trim off the ms. - iso[i] = '\0'; - break; - case ':': - iso[i] = '_'; - break; - } - } - snprintf(filename, sizeof(filename), "attachment; filename=\"ESPSomfyRTS %s.backup\"", iso); - Serial.println(filename); - server.sendHeader(F("Content-Disposition"), filename); - server.sendHeader(F("Access-Control-Expose-Headers"), F("Content-Disposition")); - } - Serial.println("Saving current shade information"); - somfy.writeBackup(); - File file = LittleFS.open("/controller.backup", "r"); - if (!file) { - Serial.println("Error opening shades.cfg"); - server.send(500, _encoding_text, "shades.cfg"); - return; - } - server.streamFile(file, _encoding_text); - file.close(); -} -void Web::handleSetPositions(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - uint8_t shadeId = (server.hasArg("shadeId")) ? atoi(server.arg("shadeId").c_str()) : 255; - int8_t pos = (server.hasArg("position")) ? atoi(server.arg("position").c_str()) : -1; - int8_t tiltPos = (server.hasArg("tiltPosition")) ? atoi(server.arg("tiltPosition").c_str()) : -1; - if(server.hasArg("plain")) { - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - this->handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if(obj.containsKey("shadeId")) shadeId = obj["shadeId"]; - if(obj.containsKey("position")) pos = obj["position"]; - if(obj.containsKey("tiltPosition")) tiltPos = obj["tiltPosition"]; - } - } - if(shadeId != 255) { - SomfyShade *shade = somfy.getShadeById(shadeId); - if(shade) { - if(pos >= 0) shade->target = shade->currentPos = pos; - if(tiltPos >= 0 && shade->tiltType != tilt_types::none) shade->tiltTarget = shade->currentTiltPos = tiltPos; - shade->emitState(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"An invalid shadeId was provided\"}")); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"shadeId was not provided\"}")); - } -} -void Web::handleSetSensor(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - uint8_t shadeId = (server.hasArg("shadeId")) ? atoi(server.arg("shadeId").c_str()) : 255; - uint8_t groupId = (server.hasArg("groupId")) ? atoi(server.arg("groupId").c_str()) : 255; - int8_t sunny = (server.hasArg("sunny")) ? toBoolean(server.arg("sunny").c_str(), false) ? 1 : 0 : -1; - int8_t windy = (server.hasArg("windy")) ? atoi(server.arg("windy").c_str()) : -1; - int8_t repeat = (server.hasArg("repeat")) ? atoi(server.arg("repeat").c_str()) : -1; - if(server.hasArg("plain")) { - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - this->handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if(obj.containsKey("shadeId")) shadeId = obj["shadeId"].as(); - if(obj.containsKey("groupId")) groupId = obj["groupId"].as(); - if(obj.containsKey("sunny")) { - if(obj["sunny"].is()) - sunny = obj["sunny"].as() ? 1 : 0; - else - sunny = obj["sunny"].as(); - } - if(obj.containsKey("windy")) { - if(obj["windy"].is()) - windy = obj["windy"].as() ? 1 : 0; - else - windy = obj["windy"].as(); - } - if(obj.containsKey("repeat")) repeat = obj["repeat"].as(); - } - } - if(shadeId != 255) { - SomfyShade *shade = somfy.getShadeById(shadeId); - if(shade) { - shade->sendSensorCommand(windy, sunny, repeat >= 0 ? (uint8_t)repeat : shade->repeats); - shade->emitState(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"An invalid shadeId was provided\"}")); - - } - else if(groupId != 255) { - SomfyGroup *group = somfy.getGroupById(groupId); - if(group) { - group->sendSensorCommand(windy, sunny, repeat >= 0 ? (uint8_t)repeat : group->repeats); - group->emitState(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - group->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"An invalid groupId was provided\"}")); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"shadeId was not provided\"}")); - } -} -void Web::handleDownloadFirmware(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - GitRepo repo; - GitRelease *rel = nullptr; - int8_t err = repo.getReleases(); - Serial.println("downloadFirmware called..."); - if(err == 0) { - if(server.hasArg("ver")) { - if(strcmp(server.arg("ver").c_str(), "latest") == 0) rel = &repo.releases[0]; - else if(strcmp(server.arg("ver").c_str(), "main") == 0) { - rel = &repo.releases[GIT_MAX_RELEASES]; - } - else { - for(uint8_t i = 0; i < GIT_MAX_RELEASES; i++) { - if(repo.releases[i].id == 0) continue; - if(strcmp(repo.releases[i].name, server.arg("ver").c_str()) == 0) { - rel = &repo.releases[i]; - } - } - } - if(rel) { - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - rel->toJSON(resp); - resp.endObject(); - resp.endResponse(); - strcpy(git.targetRelease, rel->name); - git.status = GIT_AWAITING_UPDATE; - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Release not found in repo.\"}")); - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Release version not supplied.\"}")); - } - else { - server.send(err, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error communicating with Github.\"}")); - } -} -void Web::handleNotFound(WebServer &server) { - HTTPMethod method = server.method(); - Serial.printf("Request %s 404-%d ", server.uri().c_str(), method); - switch (method) { - case HTTP_POST: - Serial.print("POST "); - break; - case HTTP_GET: - Serial.print("GET "); - break; - case HTTP_PUT: - Serial.print("PUT "); - break; - case HTTP_OPTIONS: - Serial.println("OPTIONS "); - server.send(200, "OK"); - return; - default: - Serial.print("["); - Serial.print(method); - Serial.print("]"); - break; - - } - snprintf(g_content, sizeof(g_content), "404 Service Not Found: %s", server.uri().c_str()); - server.send(404, _encoding_text, g_content); -} -void Web::handleReboot(WebServer &server) { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_PUT) { - Serial.println("Rebooting ESP..."); - rebootDelay.reboot = true; - rebootDelay.rebootTime = millis() + 500; - server.send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully started reboot\"}"); - } - else { - server.send(201, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method: \"}"); - } -} -void Web::begin() { - Serial.println("Creating Web MicroServices..."); - server.enableCORS(true); - const char *keys[1] = {"apikey"}; - server.collectHeaders(keys, 1); - // API Server Handlers - apiServer.collectHeaders(keys, 1); - apiServer.enableCORS(true); - apiServer.on("/discovery", []() { webServer.handleDiscovery(apiServer); }); - apiServer.on("/rooms", []() {webServer.handleGetRooms(apiServer); }); - apiServer.on("/shades", []() { webServer.handleGetShades(apiServer); }); - apiServer.on("/groups", []() { webServer.handleGetGroups(apiServer); }); - apiServer.on("/login", []() { webServer.handleLogin(apiServer); }); - apiServer.onNotFound([]() { webServer.handleNotFound(apiServer); }); - apiServer.on("/controller", []() { webServer.handleController(apiServer); }); - apiServer.on("/shadeCommand", []() { webServer.handleShadeCommand(apiServer); }); - apiServer.on("/groupCommand", []() { webServer.handleGroupCommand(apiServer); }); - apiServer.on("/tiltCommand", []() { webServer.handleTiltCommand(apiServer); }); - apiServer.on("/repeatCommand", []() { webServer.handleRepeatCommand(apiServer); }); - apiServer.on("/room", HTTP_GET, [] () { webServer.handleRoom(apiServer); }); - apiServer.on("/shade", HTTP_GET, [] () { webServer.handleShade(apiServer); }); - apiServer.on("/group", HTTP_GET, [] () { webServer.handleGroup(apiServer); }); - apiServer.on("/setPositions", []() { webServer.handleSetPositions(apiServer); }); - apiServer.on("/setSensor", []() { webServer.handleSetSensor(apiServer); }); - apiServer.on("/downloadFirmware", []() { webServer.handleDownloadFirmware(apiServer); }); - apiServer.on("/backup", []() { webServer.handleBackup(apiServer); }); - apiServer.on("/reboot", []() { webServer.handleReboot(apiServer); }); - - // Web Interface - server.on("/tiltCommand", []() { webServer.handleTiltCommand(server); }); - server.on("/repeatCommand", []() { webServer.handleRepeatCommand(server); }); - server.on("/shadeCommand", []() { webServer.handleShadeCommand(server); }); - server.on("/groupCommand", []() { webServer.handleGroupCommand(server); }); - server.on("/setPositions", []() { webServer.handleSetPositions(server); }); - server.on("/setSensor", []() { webServer.handleSetSensor(server); }); - server.on("/upnp.xml", []() { SSDP.schema(server.client()); }); - server.on("/", []() { webServer.handleStreamFile(server, "/index.html", _encoding_html); }); - server.on("/login", []() { webServer.handleLogin(server); }); - server.on("/loginContext", []() { webServer.handleLoginContext(server); }); - server.on("/shades.cfg", []() { webServer.handleStreamFile(server, "/shades.cfg", _encoding_text); }); - server.on("/shades.tmp", []() { webServer.handleStreamFile(server, "/shades.tmp", _encoding_text); }); - server.on("/getReleases", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - GitRepo repo; - repo.getReleases(); - git.setCurrentRelease(repo); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - repo.toJSON(resp); - resp.endObject(); - resp.endResponse(); - }); - server.on("/downloadFirmware", []() { webServer.handleDownloadFirmware(server); }); - server.on("/cancelFirmware", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - // If we are currently downloading the filesystem we cannot cancel. - if(!git.lockFS) { - git.status = GIT_UPDATE_CANCELLING; - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - git.toJSON(resp); - resp.endObject(); - resp.endResponse(); - git.cancelled = true; - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Cannot cancel during filesystem update.\"}")); - } - }); - server.on("/backup", []() { webServer.handleBackup(server, true); }); - server.on("/restore", HTTP_POST, []() { - webServer.sendCORSHeaders(server); - server.sendHeader("Connection", "close"); - if(webServer.uploadSuccess) { - server.send(200, _encoding_json, "{\"status\":\"Success\",\"desc\":\"Restoring Shade settings\"}"); - restore_options_t opts; - if(server.hasArg("data")) { - Serial.println(server.arg("data")); - StaticJsonDocument<256> doc; - DeserializationError err = deserializeJson(doc, server.arg("data")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - opts.fromJSON(obj); - } - } - else { - Serial.println("No restore options sent. Using defaults..."); - opts.shades = true; - } - ShadeConfigFile::restore(&somfy, "/shades.tmp", opts); - Serial.println("Rebooting ESP for restored settings..."); - rebootDelay.reboot = true; - rebootDelay.rebootTime = millis() + 1000; - } - }, []() { - esp_task_wdt_reset(); - HTTPUpload& upload = server.upload(); - if (upload.status == UPLOAD_FILE_START) { - webServer.uploadSuccess = false; - Serial.printf("Restore: %s\n", upload.filename.c_str()); - // Begin by opening a new temporary file. - File fup = LittleFS.open("/shades.tmp", "w"); - fup.close(); - } - else if (upload.status == UPLOAD_FILE_WRITE) { - File fup = LittleFS.open("/shades.tmp", "a"); - //upload.buf[upload.currentSize] = 0x00; - //Serial.print((char *)upload.buf); - fup.write(upload.buf, upload.currentSize); - fup.close(); - } - else if (upload.status == UPLOAD_FILE_END) { - webServer.uploadSuccess = true; - } - - }); - server.on("/index.js", []() { webServer.sendCacheHeaders(604800); webServer.handleStreamFile(server, "/index.js", "text/javascript"); }); - server.on("/main.css", []() { webServer.sendCacheHeaders(604800); webServer.handleStreamFile(server, "/main.css", "text/css"); }); - server.on("/widgets.css", []() { webServer.sendCacheHeaders(604800); webServer.handleStreamFile(server, "/widgets.css", "text/css"); }); - server.on("/icons.css", []() { webServer.sendCacheHeaders(604800); webServer.handleStreamFile(server, "/icons.css", "text/css"); }); - server.on("/favicon.png", []() { webServer.sendCacheHeaders(604800); webServer.handleStreamFile(server, "/favicon.png", "image/png"); }); - server.on("/icon.png", []() { webServer.sendCacheHeaders(604800); webServer.handleStreamFile(server, "/icon.png", "image/png"); }); - server.on("/icon.svg", []() { webServer.sendCacheHeaders(604800); webServer.handleStreamFile(server, "/icon.svg", "image/svg+xml"); }); - server.on("/apple-icon.png", []() { webServer.sendCacheHeaders(604800); webServer.handleStreamFile(server, "/apple-icon.png", "image/png"); }); - server.onNotFound([]() { webServer.handleNotFound(server); }); - server.on("/controller", []() { webServer.handleController(server); }); - server.on("/rooms", []() { webServer.handleGetRooms(server); }); - server.on("/shades", []() { webServer.handleGetShades(server); }); - server.on("/groups", []() { webServer.handleGetGroups(server); }); - server.on("/room", []() { webServer.handleRoom(server); }); - server.on("/shade", []() { webServer.handleShade(server); }); - server.on("/group", []() { webServer.handleGroup(server); }); - server.on("/getNextRoom", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - resp.addElem("roomId", somfy.getNextRoomId()); - resp.endObject(); - resp.endResponse(); - }); - server.on("/getNextShade", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - uint8_t shadeId = somfy.getNextShadeId(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - resp.addElem("shadeId", shadeId); - resp.addElem("remoteAddress", (uint32_t)somfy.getNextRemoteAddress(shadeId)); - resp.addElem("bitLength", somfy.transceiver.config.type); - resp.addElem("stepSize", (uint8_t)100); - resp.addElem("proto", static_cast(somfy.transceiver.config.proto)); - resp.endObject(); - resp.endResponse(); - }); - server.on("/getNextGroup", []() { - webServer.sendCORSHeaders(server); - uint8_t groupId = somfy.getNextGroupId(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - resp.addElem("groupId", groupId); - resp.addElem("remoteAddress", (uint32_t)somfy.getNextRemoteAddress(groupId)); - resp.addElem("bitLength", somfy.transceiver.config.type); - resp.addElem("proto", static_cast(somfy.transceiver.config.proto)); - resp.endObject(); - resp.endResponse(); - }); - server.on("/addRoom", []() { - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - SomfyRoom * room = nullptr; - if (method == HTTP_POST || method == HTTP_PUT) { - Serial.println("Adding a room"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - Serial.println("Counting rooms"); - if (somfy.roomCount() > SOMFY_MAX_ROOMS) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Maximum number of rooms exceeded.\"}")); - return; - } - else { - Serial.println("Adding room"); - room = somfy.addRoom(obj); - if (!room) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error adding room.\"}")); - return; - } - } - } - } - if (room) { - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - room->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error saving Somfy Room.\"}")); - } - }); - server.on("/addShade", []() { - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - SomfyShade* shade = nullptr; - if (method == HTTP_POST || method == HTTP_PUT) { - Serial.println("Adding a shade"); - DynamicJsonDocument doc(1024); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - Serial.println("Counting shades"); - if (somfy.shadeCount() > SOMFY_MAX_SHADES) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Maximum number of shades exceeded.\"}")); - return; - } - else { - Serial.println("Adding shade"); - shade = somfy.addShade(obj); - if (!shade) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error adding shade.\"}")); - return; - } - } - } - } - if (shade) { - //Serial.println("Serializing shade"); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error saving Somfy Shade.\"}")); - } - }); - server.on("/addGroup", []() { - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - SomfyGroup * group = nullptr; - if (method == HTTP_POST || method == HTTP_PUT) { - Serial.println("Adding a group"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - Serial.println("Counting shades"); - if (somfy.groupCount() > SOMFY_MAX_GROUPS) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Maximum number of groups exceeded.\"}")); - return; - } - else { - Serial.println("Adding group"); - group = somfy.addGroup(obj); - if (!group) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error adding group.\"}")); - return; - } - } - } - } - if (group) { - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - group->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error saving Somfy Group.\"}")); - } - }); - server.on("/groupOptions", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_GET || method == HTTP_POST) { - if (server.hasArg("groupId")) { - int groupId = atoi(server.arg("groupId").c_str()); - SomfyGroup* group = somfy.getGroupById(groupId); - if (group) { - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - group->toJSON(resp); - resp.beginArray("availShades"); - for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { - SomfyShade *shade = &somfy.shades[i]; - if(shade->getShadeId() != 255) { - bool isLinked = false; - for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) { - if(group->linkedShades[j] == shade->getShadeId()) { - isLinked = true; - break; - } - } - if(!isLinked) { - resp.beginObject(); - shade->toJSONRef(resp); - resp.endObject(); - } - } - } - resp.endArray(); - resp.endObject(); - resp.endResponse(); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}")); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid group id.\"}")); - } - } - - }); - server.on("/saveRoom", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_PUT || method == HTTP_POST) { - // We are updating an existing room. - if (server.hasArg("plain")) { - Serial.println("Updating a room"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("roomId")) { - SomfyRoom* room = somfy.getRoomById(obj["roomId"]); - if (room) { - room->fromJSON(obj); - room->save(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - room->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Room Id not found.\"}")); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room id was supplied.\"}")); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room object supplied.\"}")); - } - }); - - server.on("/saveShade", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_PUT || method == HTTP_POST) { - // We are updating an existing shade. - if (server.hasArg("plain")) { - Serial.println("Updating a shade"); - DynamicJsonDocument doc(1024); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("shadeId")) { - SomfyShade* shade = somfy.getShadeById(obj["shadeId"]); - if (shade) { - int8_t err = shade->fromJSON(obj); - if(err == 0) { - shade->save(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else { - snprintf(g_content, sizeof(g_content), "{\"status\":\"DATA\",\"desc\":\"Data Error.\", \"code\":%d}", err); - server.send(500, _encoding_json, g_content); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}")); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); - } - }); - server.on("/saveGroup", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_PUT || method == HTTP_POST) { - // We are updating an existing shade. - if (server.hasArg("plain")) { - Serial.println("Updating a group"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("groupId")) { - SomfyGroup* group = somfy.getGroupById(obj["groupId"]); - if (group) { - group->fromJSON(obj); - group->save(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - group->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}")); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}")); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); - } - }); - server.on("/setMyPosition", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - uint8_t shadeId = 255; - int8_t pos = -1; - int8_t tilt = -1; - if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) { - if (server.hasArg("shadeId")) { - shadeId = atoi(server.arg("shadeId").c_str()); - if(server.hasArg("pos")) pos = atoi(server.arg("pos").c_str()); - if(server.hasArg("tilt")) tilt = atoi(server.arg("tilt").c_str()); - } - else if (server.hasArg("plain")) { - DynamicJsonDocument doc(256); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("shadeId")) shadeId = obj["shadeId"]; - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); - if(obj.containsKey("pos")) pos = obj["pos"].as(); - if(obj.containsKey("tilt")) tilt = obj["tilt"].as(); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); - SomfyShade* shade = somfy.getShadeById(shadeId); - if (shade) { - // Send the command to the shade. - if(tilt < 0) tilt = shade->myPos; - if(shade->tiltType == tilt_types::none) tilt = -1; - if(pos >= 0 && pos <= 100) - shade->setMyPosition(shade->transformPosition(pos), shade->transformPosition(tilt)); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSONRef(resp); - resp.endObject(); - resp.endResponse(); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}")); - } - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid Http method\"}")); - }); - server.on("/setRollingCode", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_PUT || method == HTTP_POST) { - uint8_t shadeId = 255; - uint16_t rollingCode = 0; - if (server.hasArg("plain")) { - // Its coming in the body. - StaticJsonDocument<129> doc; - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("shadeId")) shadeId = obj["shadeId"]; - if(obj.containsKey("rollingCode")) rollingCode = obj["rollingCode"]; - } - } - else if (server.hasArg("shadeId")) { - shadeId = atoi(server.arg("shadeId").c_str()); - rollingCode = atoi(server.arg("rollingCode").c_str()); - } - SomfyShade* shade = nullptr; - if (shadeId != 255) shade = somfy.getShadeById(shadeId); - if (!shade) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to set rolling code\"}")); - } - else { - shade->setRollingCode(rollingCode); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - } - }); - server.on("/setPaired", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - uint8_t shadeId = 255; - bool paired = false; - if(server.hasArg("plain")) { - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if(err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("shadeId")) shadeId = obj["shadeId"]; - if(obj.containsKey("paired")) paired = obj["paired"]; - } - } - else if (server.hasArg("shadeId")) - shadeId = atoi(server.arg("shadeId").c_str()); - if(server.hasArg("paired")) - paired = toBoolean(server.arg("paired").c_str(), false); - SomfyShade* shade = nullptr; - if (shadeId != 255) shade = somfy.getShadeById(shadeId); - if (!shade) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to pair\"}")); - } - else { - shade->paired = paired; - shade->save(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - }); - server.on("/unpairShade", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_PUT || method == HTTP_POST) { - uint8_t shadeId = 255; - if (server.hasArg("plain")) { - // Its coming in the body. - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("shadeId")) shadeId = obj["shadeId"]; - } - } - else if (server.hasArg("shadeId")) - shadeId = atoi(server.arg("shadeId").c_str()); - SomfyShade* shade = nullptr; - if (shadeId != 255) shade = somfy.getShadeById(shadeId); - if (!shade) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to unpair\"}")); - } - else { - if(shade->bitLength == 56) - shade->sendCommand(somfy_commands::Prog, 7); - else - shade->sendCommand(somfy_commands::Prog, 1); - shade->paired = false; - shade->save(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - } - }); - server.on("/linkRepeater", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_PUT || method == HTTP_POST) { - // We are adding a linked repeater. - uint32_t address = 0; - if (server.hasArg("plain")) { - Serial.println("Linking a repeater"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("address")) address = obj["address"]; - else if(obj.containsKey("remoteAddress")) address = obj["remoteAddress"]; - } - } - else if(server.hasArg("address")) - address = atoi(server.arg("address").c_str()); - if(address == 0) - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No repeater address was supplied.\"}")); - else { - somfy.linkRepeater(address); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginArray(); - somfy.toJSONRepeaters(resp); - resp.endArray(); - resp.endResponse(); - } - } - }); - server.on("/unlinkRepeater", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_PUT || method == HTTP_POST) { - // We are adding a linked repeater. - uint32_t address = 0; - if (server.hasArg("plain")) { - Serial.println("Unlinking a repeater"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("address")) address = obj["address"]; - else if(obj.containsKey("remoteAddress")) address = obj["remoteAddress"]; - } - } - else if(server.hasArg("address")) - address = atoi(server.arg("address").c_str()); - if(address == 0) - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No repeater address was supplied.\"}")); - else { - somfy.unlinkRepeater(address); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginArray(); - somfy.toJSONRepeaters(resp); - resp.endArray(); - resp.endResponse(); - } - } - }); - - server.on("/unlinkRemote", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_PUT || method == HTTP_POST) { - // We are updating an existing shade by adding a linked remote. - if (server.hasArg("plain")) { - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("shadeId")) { - SomfyShade* shade = somfy.getShadeById(obj["shadeId"]); - if (shade) { - if (obj.containsKey("remoteAddress")) { - shade->unlinkRemote(obj["remoteAddress"]); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Remote address not provided.\"}")); - } - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}")); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No remote object supplied.\"}")); - } - }); - server.on("/linkRemote", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_PUT || method == HTTP_POST) { - // We are updating an existing shade by adding a linked remote. - if (server.hasArg("plain")) { - Serial.println("Linking a remote"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("shadeId")) { - SomfyShade* shade = somfy.getShadeById(obj["shadeId"]); - if (shade) { - if (obj.containsKey("remoteAddress")) { - if (obj.containsKey("rollingCode")) shade->linkRemote(obj["remoteAddress"], obj["rollingCode"]); - else shade->linkRemote(obj["remoteAddress"]); - } - else { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Remote address not provided.\"}")); - } - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - shade->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}")); - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No remote object supplied.\"}")); - } - }); - server.on("/linkToGroup", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_PUT || method == HTTP_POST) { - if (server.hasArg("plain")) { - Serial.println("Linking a shade to a group"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - uint8_t shadeId = obj.containsKey("shadeId") ? obj["shadeId"] : 0; - uint8_t groupId = obj.containsKey("groupId") ? obj["groupId"] : 0; - if(groupId == 0) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group id not provided.\"}")); - return; - } - if(shadeId == 0) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade id not provided.\"}")); - return; - } - SomfyGroup * group = somfy.getGroupById(groupId); - if(!group) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group id not found.\"}")); - return; - } - SomfyShade * shade = somfy.getShadeById(shadeId); - if(!shade) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade id not found.\"}")); - return; - } - group->linkShade(shadeId); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - group->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No linking object supplied.\"}")); - } - }); - server.on("/unlinkFromGroup", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_PUT || method == HTTP_POST) { - if (server.hasArg("plain")) { - Serial.println("Unlinking a shade from a group"); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - switch (err.code()) { - case DeserializationError::InvalidInput: - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Invalid JSON payload\"}")); - break; - case DeserializationError::NoMemory: - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Out of memory parsing JSON\"}")); - break; - default: - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"General JSON Deserialization failed\"}")); - break; - } - } - else { - JsonObject obj = doc.as(); - uint8_t shadeId = obj.containsKey("shadeId") ? obj["shadeId"] : 0; - uint8_t groupId = obj.containsKey("groupId") ? obj["groupId"] : 0; - if(groupId == 0) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group id not provided.\"}")); - return; - } - if(shadeId == 0) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade id not provided.\"}")); - return; - } - SomfyGroup * group = somfy.getGroupById(groupId); - if(!group) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group id not found.\"}")); - return; - } - SomfyShade * shade = somfy.getShadeById(shadeId); - if(!shade) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade id not found.\"}")); - return; - } - group->unlinkShade(shadeId); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - group->toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No unlinking object supplied.\"}")); - } - }); - server.on("/deleteRoom", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - uint8_t roomId = 0; - if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) { - if (server.hasArg("roomId")) { - roomId = atoi(server.arg("roomId").c_str()); - } - else if (server.hasArg("plain")) { - Serial.println("Deleting a Room"); - DynamicJsonDocument doc(256); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("roomId")) roomId = obj["roomId"]; - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room id was supplied.\"}")); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room object supplied.\"}")); - } - SomfyRoom* room = somfy.getRoomById(roomId); - if (!room) server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Room with the specified id not found.\"}")); - else { - somfy.deleteRoom(roomId); - server.send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Room deleted.\"}")); - } - }); - server.on("/deleteShade", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - uint8_t shadeId = 255; - if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) { - if (server.hasArg("shadeId")) { - shadeId = atoi(server.arg("shadeId").c_str()); - } - else if (server.hasArg("plain")) { - Serial.println("Deleting a shade"); - DynamicJsonDocument doc(256); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("shadeId")) shadeId = obj["shadeId"]; - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); - } - SomfyShade* shade = somfy.getShadeById(shadeId); - if (!shade) server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}")); - else if(shade->isInGroup()) { - server.send(400, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"This shade is a member of a group and cannot be deleted.\"}")); - } - else { - somfy.deleteShade(shadeId); - server.send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Shade deleted.\"}")); - } - }); - server.on("/deleteGroup", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - uint8_t groupId = 255; - if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) { - if (server.hasArg("groupId")) { - groupId = atoi(server.arg("groupId").c_str()); - } - else if (server.hasArg("plain")) { - Serial.println("Deleting a group"); - DynamicJsonDocument doc(256); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - if (obj.containsKey("groupId")) groupId = obj["groupId"]; - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}")); - } - } - else server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); - } - SomfyGroup * group = somfy.getGroupById(groupId); - if (!group) server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group with the specified id not found.\"}")); - else { - somfy.deleteGroup(groupId); - server.send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Group deleted.\"}")); - } - }); - server.on("/updateFirmware", HTTP_POST, []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - if (Update.hasError()) - server.send(500, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Error updating firmware: \"}"); - else - server.send(200, _encoding_json, "{\"status\":\"SUCCESS\",\"desc\":\"Successfully updated firmware\"}"); - rebootDelay.reboot = true; - rebootDelay.rebootTime = millis() + 500; - }, []() { - HTTPUpload& upload = server.upload(); - if (upload.status == UPLOAD_FILE_START) { - webServer.uploadSuccess = false; - Serial.printf("Update: %s - %d\n", upload.filename.c_str(), upload.totalSize); - //if(!Update.begin(upload.totalSize, U_SPIFFS)) { - if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size - Update.printError(Serial); - } - else { - somfy.transceiver.end(); // Shut down the radio so we do not get any interrupts during this process. - mqtt.end(); - } - } - else if(upload.status == UPLOAD_FILE_ABORTED) { - Serial.printf("Upload of %s aborted\n", upload.filename.c_str()); - Update.abort(); - } - else if (upload.status == UPLOAD_FILE_WRITE) { - /* flashing firmware to ESP*/ - if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { - Update.printError(Serial); - Serial.printf("Upload of %s aborted invalid size %d\n", upload.filename.c_str(), upload.currentSize); - Update.abort(); - } - } - else if (upload.status == UPLOAD_FILE_END) { - if (Update.end(true)) { //true to set the size to the current progress - Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize); - webServer.uploadSuccess = true; - } - else { - Update.printError(Serial); - } - } - esp_task_wdt_reset(); - }); - server.on("/updateShadeConfig", HTTP_POST, []() { - if(git.lockFS) { - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Filesystem update in progress\"}")); - return; - } - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - server.sendHeader("Connection", "close"); - server.send(200, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Updating Shade Config: \"}"); - }, []() { - HTTPUpload& upload = server.upload(); - if (upload.status == UPLOAD_FILE_START) { - Serial.printf("Update: shades.cfg\n"); - File fup = LittleFS.open("/shades.tmp", "w"); - fup.close(); - } - else if (upload.status == UPLOAD_FILE_WRITE) { - /* flashing littlefs to ESP*/ - if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { - File fup = LittleFS.open("/shades.tmp", "a"); - fup.write(upload.buf, upload.currentSize); - fup.close(); - } - } - else if (upload.status == UPLOAD_FILE_END) { - somfy.loadShadesFile("/shades.tmp"); - } - }); - server.on("/updateApplication", HTTP_POST, []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - server.sendHeader("Connection", "close"); - if (Update.hasError()) - server.send(500, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Error updating application: \"}"); - else - server.send(200, _encoding_json, "{\"status\":\"SUCCESS\",\"desc\":\"Successfully updated application\"}"); - rebootDelay.reboot = true; - rebootDelay.rebootTime = millis() + 500; - }, []() { - HTTPUpload& upload = server.upload(); - if (upload.status == UPLOAD_FILE_START) { - webServer.uploadSuccess = false; - Serial.printf("Update: %s %d\n", upload.filename.c_str(), upload.totalSize); - //if(!Update.begin(upload.totalSize, U_SPIFFS)) { - if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) { //start with max available size and tell it we are updating the file system. - Update.printError(Serial); - } - else { - somfy.transceiver.end(); // Shut down the radio so we do not get any interrupts during this process. - mqtt.end(); - } - } - else if(upload.status == UPLOAD_FILE_ABORTED) { - Serial.printf("Upload of %s aborted\n", upload.filename.c_str()); - Update.abort(); - somfy.commit(); - } - else if (upload.status == UPLOAD_FILE_WRITE) { - /* flashing littlefs to ESP*/ - if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { - Update.printError(Serial); - Serial.printf("Upload of %s aborted invalid size %d\n", upload.filename.c_str(), upload.currentSize); - Update.abort(); - } - } - else if (upload.status == UPLOAD_FILE_END) { - if (Update.end(true)) { //true to set the size to the current progress - webServer.uploadSuccess = true; - Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize); - somfy.commit(); - } - else { - somfy.commit(); - Update.printError(Serial); - } - } - esp_task_wdt_reset(); - }); - server.on("/scanaps", []() { - webServer.sendCORSHeaders(server); - esp_task_wdt_reset(); - - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - esp_task_wdt_delete(NULL); - if(net.softAPOpened) WiFi.disconnect(false); - int n = WiFi.scanNetworks(false, true); - esp_task_wdt_add(NULL); - - Serial.print("Scanned "); - Serial.print(n); - Serial.println(" networks"); - // Ok we need to chunk this response as well. - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_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; // Ignore hidden and weak networks that we cannot connect to anyway. - 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", []() { webServer.handleReboot(server);}); - server.on("/saveSecurity", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - Serial.print("Error parsing JSON "); - Serial.println(err.c_str()); - String msg = err.c_str(); - server.send(400, _encoding_html, "Error parsing JSON body
" + msg); - } - else { - JsonObject obj = doc.as(); - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_PUT) { - settings.Security.fromJSON(obj); - settings.Security.save(); - char token[65]; - webServer.createAPIToken(server.client().remoteIP(), token); - obj["apiKey"] = token; - DynamicJsonDocument sdoc(1024); - JsonObject sobj = sdoc.to(); - settings.Security.toJSON(sobj); - serializeJson(sdoc, g_content); - server.send(200, _encoding_json, g_content); - } - else { - server.send(201, "application/json", "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method: \"}"); - } - } - }); - server.on("/getSecurity", []() { - webServer.sendCORSHeaders(server); - DynamicJsonDocument doc(512); - JsonObject obj = doc.to(); - settings.Security.toJSON(obj); - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); - }); - server.on("/saveRadio", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - Serial.print("Error parsing JSON "); - Serial.println(err.c_str()); - String msg = err.c_str(); - server.send(400, _encoding_html, "Error parsing JSON body
" + msg); - } - else { - JsonObject obj = doc.as(); - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_PUT) { - somfy.transceiver.fromJSON(obj); - somfy.transceiver.save(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - somfy.transceiver.toJSON(resp); - resp.endObject(); - resp.endResponse(); - } - else { - server.send(201, "application/json", "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method: \"}"); - } - } - }); - server.on("/getRadio", []() { - webServer.sendCORSHeaders(server); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - somfy.transceiver.toJSON(resp); - resp.endObject(); - resp.endResponse(); - }); - server.on("/sendRemoteCommand", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - HTTPMethod method = server.method(); - if (method == HTTP_GET || method == HTTP_PUT || method == HTTP_POST) { - somfy_frame_t frame; - uint8_t repeats = 0; - if (server.hasArg("address")) { - frame.remoteAddress = atoi(server.arg("address").c_str()); - if (server.hasArg("encKey")) frame.encKey = atoi(server.arg("encKey").c_str()); - if (server.hasArg("command")) frame.cmd = translateSomfyCommand(server.arg("command")); - if (server.hasArg("rcode")) frame.rollingCode = atoi(server.arg("rcode").c_str()); - if (server.hasArg("repeats")) repeats = atoi(server.arg("repeats").c_str()); - } - else if (server.hasArg("plain")) { - StaticJsonDocument<128> doc; - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - String scmd; - if (obj.containsKey("address")) frame.remoteAddress = obj["address"]; - if (obj.containsKey("command")) scmd = obj["command"].as(); - if (obj.containsKey("repeats")) repeats = obj["repeats"]; - if (obj.containsKey("rcode")) frame.rollingCode = obj["rcode"]; - if (obj.containsKey("encKey")) frame.encKey = obj["encKey"]; - frame.cmd = translateSomfyCommand(scmd.c_str()); - } - } - if (frame.remoteAddress > 0 && frame.rollingCode > 0) { - somfy.sendFrame(frame, repeats); - server.send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Command Sent\"}")); - } - else - server.send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No address or rolling code provided\"}")); - } - }); - server.on("/setgeneral", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - DynamicJsonDocument doc(512); - - Serial.print("Plain: "); - Serial.print(server.method()); - Serial.println(server.arg("plain")); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_PUT) { - // Parse out all the inputs. - if (obj.containsKey("hostname") || obj.containsKey("ssdpBroadcast") || obj.containsKey("checkForUpdate")) { - bool checkForUpdate = settings.checkForUpdate; - settings.fromJSON(obj); - settings.save(); - if(settings.checkForUpdate != checkForUpdate) git.emitUpdateCheck(); - if(obj.containsKey("hostname")) net.updateHostname(); - } - if (obj.containsKey("ntpServer") || obj.containsKey("ntpServer")) { - settings.NTP.fromJSON(obj); - settings.NTP.save(); - } - server.send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set General Settings\"}"); - } - else { - server.send(201, "application/json", "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method: \"}"); - } - } - }); - server.on("/setNetwork", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - DynamicJsonDocument doc(1024); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - Serial.print("Error parsing JSON "); - Serial.println(err.c_str()); - String msg = err.c_str(); - server.send(400, _encoding_html, "Error parsing JSON body
" + msg); - } - else { - JsonObject obj = doc.as(); - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_PUT) { - // Parse out all the inputs. - bool reboot = false; - if(obj.containsKey("connType") && obj["connType"].as() != static_cast(settings.connType)) { - settings.connType = static_cast(obj["connType"].as()); - settings.save(); - reboot = true; - } - if(obj.containsKey("wifi")) { - JsonObject objWifi = obj["wifi"]; - if(settings.connType == conn_types_t::wifi) { - if(objWifi.containsKey("ssid") && objWifi["ssid"].as().compareTo(settings.WIFI.ssid) != 0) { - if(WiFi.softAPgetStationNum() == 0) reboot = true; - } - if(objWifi.containsKey("passphrase") && objWifi["passphrase"].as().compareTo(settings.WIFI.passphrase) != 0) { - if(WiFi.softAPgetStationNum() == 0) reboot = true; - } - } - settings.WIFI.fromJSON(objWifi); - settings.WIFI.save(); - } - if(obj.containsKey("ethernet")) - { - JsonObject objEth = obj["ethernet"]; - // This is an ethernet connection so if anything changes we need to reboot. - if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref) - reboot = true; - settings.Ethernet.fromJSON(objEth); - settings.Ethernet.save(); - } - if (reboot) { - Serial.println("Rebooting ESP for new Network settings..."); - rebootDelay.reboot = true; - rebootDelay.rebootTime = millis() + 1000; - } - server.send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set Network Settings\"}"); - } - else { - server.send(201, "application/json", "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method: \"}"); - } - } - }); - server.on("/setIP", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - Serial.println("Setting IP..."); - DynamicJsonDocument doc(1024); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_PUT) { - settings.IP.fromJSON(obj); - settings.IP.save(); - server.send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set Network Settings\"}"); - } - else { - server.send(201, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method: \"}"); - } - } - }); - server.on("/connectwifi", []() { - webServer.sendCORSHeaders(server); - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - Serial.println("Settings WIFI connection..."); - DynamicJsonDocument doc(512); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - HTTPMethod method = server.method(); - //Serial.print(F("HTTP Method: ")); - //Serial.println(server.method()); - if (method == HTTP_POST || method == HTTP_PUT) { - String ssid = ""; - String passphrase = ""; - if (obj.containsKey("ssid")) ssid = obj["ssid"].as(); - if (obj.containsKey("passphrase")) passphrase = obj["passphrase"].as(); - bool reboot; - 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) { - server.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(); - server.send(201, _encoding_json, "{\"status\":\"OK\",\"desc\":\"Successfully set server connection\"}"); - if (reboot) { - Serial.println("Rebooting ESP for new WiFi settings..."); - rebootDelay.reboot = true; - rebootDelay.rebootTime = millis() + 1000; - } - } - } - else { - server.send(201, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method: \"}"); - } - } - }); - server.on("/modulesettings", []() { - webServer.sendCORSHeaders(server); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - resp.addElem("fwVersion", settings.fwVersion.name); - settings.toJSON(resp); - settings.NTP.toJSON(resp); - resp.endObject(); - resp.endResponse(); - /* - DynamicJsonDocument doc(512); - JsonObject obj = doc.to(); - doc["fwVersion"] = settings.fwVersion.name; - settings.toJSON(obj); - //settings.Ethernet.toJSON(obj); - //settings.WIFI.toJSON(obj); - settings.NTP.toJSON(obj); - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); - */ - }); - server.on("/networksettings", []() { - webServer.sendCORSHeaders(server); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - settings.toJSON(resp); - resp.addElem("fwVersion", settings.fwVersion.name); - resp.beginObject("ethernet"); - settings.Ethernet.toJSON(resp); - resp.endObject(); - resp.beginObject("wifi"); - settings.WIFI.toJSON(resp); - resp.endObject(); - resp.beginObject("ip"); - settings.IP.toJSON(resp); - resp.endObject(); - resp.endObject(); - resp.endResponse(); - - /* - DynamicJsonDocument doc(2048); - JsonObject obj = doc.to(); - doc["fwVersion"] = settings.fwVersion.name; - settings.toJSON(obj); - JsonObject eth = obj.createNestedObject("ethernet"); - settings.Ethernet.toJSON(eth); - JsonObject wifi = obj.createNestedObject("wifi"); - settings.WIFI.toJSON(wifi); - JsonObject ip = obj.createNestedObject("ip"); - settings.IP.toJSON(ip); - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); - */ - }); - server.on("/connectmqtt", []() { - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - DynamicJsonDocument doc(1024); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonObject obj = doc.as(); - HTTPMethod method = server.method(); - Serial.print("Saving MQTT "); - Serial.print(F("HTTP Method: ")); - Serial.println(server.method()); - if (method == HTTP_POST || method == HTTP_PUT) { - mqtt.disconnect(); - settings.MQTT.fromJSON(obj); - settings.MQTT.save(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - settings.MQTT.toJSON(resp); - resp.endObject(); - resp.endResponse(); - /* - DynamicJsonDocument sdoc(1024); - JsonObject sobj = sdoc.to(); - settings.MQTT.toJSON(sobj); - serializeJson(sdoc, g_content); - server.send(200, _encoding_json, g_content); - */ - } - else { - server.send(201, "application/json", "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method: \"}"); - } - } - }); - server.on("/mqttsettings", []() { - webServer.sendCORSHeaders(server); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - settings.MQTT.toJSON(resp); - resp.endObject(); - resp.endResponse(); - - /* - DynamicJsonDocument doc(1024); - JsonObject obj = doc.to(); - settings.MQTT.toJSON(obj); - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); - */ - }); - server.on("/roomSortOrder", []() { - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - DynamicJsonDocument doc(512); - Serial.print("Plain: "); - Serial.print(server.method()); - Serial.println(server.arg("plain")); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonArray arr = doc.as(); - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_PUT) { - // Parse out all the inputs. - uint8_t order = 0; - for(JsonVariant v : arr) { - uint8_t roomId = v.as(); - if (roomId != 0) { - SomfyRoom *room = somfy.getRoomById(roomId); - if(room) room->sortOrder = order++; - } - } - server.send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set room order\"}"); - } - else { - server.send(201, "application/json", "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method: \"}"); - } - } - }); - server.on("/shadeSortOrder", []() { - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - DynamicJsonDocument doc(512); - Serial.print("Plain: "); - Serial.print(server.method()); - Serial.println(server.arg("plain")); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonArray arr = doc.as(); - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_PUT) { - // Parse out all the inputs. - uint8_t order = 0; - for(JsonVariant v : arr) { - uint8_t shadeId = v.as(); - if (shadeId != 255) { - SomfyShade *shade = somfy.getShadeById(shadeId); - if(shade) shade->sortOrder = order++; - } - } - server.send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set shade order\"}"); - } - else { - server.send(201, "application/json", "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method: \"}"); - } - } - }); - server.on("/groupSortOrder", []() { - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - DynamicJsonDocument doc(512); - Serial.print("Plain: "); - Serial.print(server.method()); - Serial.println(server.arg("plain")); - DeserializationError err = deserializeJson(doc, server.arg("plain")); - if (err) { - webServer.handleDeserializationError(server, err); - return; - } - else { - JsonArray arr = doc.as(); - HTTPMethod method = server.method(); - if (method == HTTP_POST || method == HTTP_PUT) { - // Parse out all the inputs. - uint8_t order = 0; - for(JsonVariant v : arr) { - uint8_t groupId = v.as(); - if (groupId != 255) { - SomfyGroup *group = somfy.getGroupById(groupId); - if(group) group->sortOrder = order++; - } - } - server.send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set group order\"}"); - } - else { - server.send(201, "application/json", "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method: \"}"); - } - } - }); - server.on("/beginFrequencyScan", []() { - webServer.sendCORSHeaders(server); - somfy.transceiver.beginFrequencyScan(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - somfy.transceiver.toJSON(resp); - resp.endObject(); - resp.endResponse(); - /* - DynamicJsonDocument doc(1024); - JsonObject obj = doc.to(); - somfy.transceiver.toJSON(obj); - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); - */ - }); - server.on("/endFrequencyScan", []() { - webServer.sendCORSHeaders(server); - somfy.transceiver.endFrequencyScan(); - JsonResponse resp; - resp.beginResponse(&server, g_content, sizeof(g_content)); - resp.beginObject(); - somfy.transceiver.toJSON(resp); - resp.endObject(); - resp.endResponse(); - /* - DynamicJsonDocument doc(1024); - JsonObject obj = doc.to(); - somfy.transceiver.toJSON(obj); - serializeJson(doc, g_content); - server.send(200, _encoding_json, g_content); - */ - }); - server.on("/recoverFilesystem", [] () { - if(server.method() == HTTP_OPTIONS) { server.send(200, "OK"); return; } - webServer.sendCORSHeaders(server); - if(git.status == GIT_UPDATING) - server.send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Filesystem is updating. Please wait!!!\"}"); - else if(git.status != GIT_STATUS_READY) - server.send(200, "application/json", "{\"status\":\"ERROR\",\"desc\":\"Cannot recover file system at this time.\"}"); - else { - git.recoverFilesystem(); - server.send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Recovering filesystem from github please wait!!!\"}"); - } - }); - server.begin(); - apiServer.begin(); -} diff --git a/Web.h b/Web.h deleted file mode 100644 index 3339295..0000000 --- a/Web.h +++ /dev/null @@ -1,50 +0,0 @@ -#include -#include "Somfy.h" -#ifndef webserver_h -#define webserver_h -class Web { - public: - bool uploadSuccess = false; - void sendCORSHeaders(WebServer &server); - void sendCacheHeaders(uint32_t seconds=604800); - void startup(); - void handleLogin(WebServer &server); - void handleLogout(WebServer &server); - void handleStreamFile(WebServer &server, const char *filename, const char *encoding); - void handleController(WebServer &server); - void handleLoginContext(WebServer &server); - void handleGetRepeaters(WebServer &server); - void handleGetRooms(WebServer &server); - void handleGetShades(WebServer &server); - void handleGetGroups(WebServer &server); - void handleShadeCommand(WebServer &server); - void handleRepeatCommand(WebServer &server); - void handleGroupCommand(WebServer &server); - void handleTiltCommand(WebServer &server); - void handleDiscovery(WebServer &server); - void handleNotFound(WebServer &server); - void handleRoom(WebServer &server); - void handleShade(WebServer &server); - void handleGroup(WebServer &server); - void handleSetPositions(WebServer &server); - void handleSetSensor(WebServer &server); - void handleDownloadFirmware(WebServer &server); - void handleBackup(WebServer &server, bool attach = false); - void handleReboot(WebServer &server); - void handleDeserializationError(WebServer &server, DeserializationError &err); - void begin(); - void loop(); - void end(); - // Web Handlers - bool createAPIToken(const IPAddress ipAddress, char *token); - bool createAPIToken(const char *payload, char *token); - bool createAPIPinToken(const IPAddress ipAddress, const char *pin, char *token); - bool createAPIPasswordToken(const IPAddress ipAddress, const char *username, const char *password, char *token); - bool isAuthenticated(WebServer &server, bool cfg = false); - - //void chunkRoomsResponse(WebServer &server, const char *elem = nullptr); - //void chunkShadesResponse(WebServer &server, const char *elem = nullptr); - //void chunkGroupsResponse(WebServer &server, const char *elem = nullptr); - //void chunkGroupResponse(WebServer &server, SomfyGroup *, const char *prefix = nullptr); -}; -#endif diff --git a/data/apple-icon.png b/data-src/apple-icon.png similarity index 100% rename from data/apple-icon.png rename to data-src/apple-icon.png diff --git a/data-src/appversion b/data-src/appversion new file mode 100644 index 0000000..752a79e --- /dev/null +++ b/data-src/appversion @@ -0,0 +1 @@ +2.4.8 \ No newline at end of file diff --git a/data/favicon.png b/data-src/favicon.png similarity index 100% rename from data/favicon.png rename to data-src/favicon.png diff --git a/data/icon.png b/data-src/icon.png similarity index 100% rename from data/icon.png rename to data-src/icon.png diff --git a/data/icon.svg b/data-src/icon.svg similarity index 100% rename from data/icon.svg rename to data-src/icon.svg diff --git a/data/icons.css b/data-src/icons.css similarity index 100% rename from data/icons.css rename to data-src/icons.css diff --git a/data/index.html b/data-src/index.html similarity index 99% rename from data/index.html rename to data-src/index.html index fda0670..616a1c2 100644 --- a/data/index.html +++ b/data-src/index.html @@ -8,9 +8,9 @@ - - - + + + @@ -114,7 +114,7 @@ rel="apple-touch-startup-image"> - +
@@ -246,6 +246,8 @@ Min: + Uptime: +
diff --git a/data/index.js b/data-src/index.js similarity index 99% rename from data/index.js rename to data-src/index.js index 2a70854..d2400e8 100644 --- a/data/index.js +++ b/data-src/index.js @@ -1270,7 +1270,7 @@ var security = new Security(); class General { initialized = false; - appVersion = 'v2.4.7'; + appVersion = 'v2.4.8'; reloadApp = false; init() { if (this.initialized) return; @@ -4335,7 +4335,7 @@ class Firmware { if (typeof overlay !== 'undefined') overlay.remove(); reject({ htmlError: status, service: 'GET /backup' }); }; - xhr.open('GET', baseUrl.length > 0 ? `${baseUrl}/backup` : '/backup', true); + xhr.open('GET', baseUrl.length > 0 ? `${baseUrl}/backup?attach=true` : '/backup?attach=true', true); xhr.send(); }); } @@ -4385,6 +4385,20 @@ class Firmware { if (sp) sp.innerHTML = mem.max.fmt('#,##0'); sp = document.getElementById('spanMinMemory'); if (sp) sp.innerHTML = mem.min.fmt('#,##0'); + sp = document.getElementById('spanUptime'); + if (sp) { + let t = Math.floor(mem.uptime / 1000); + let d = Math.floor(t / 86400); t %= 86400; + let h = Math.floor(t / 3600); t %= 3600; + let m = Math.floor(t / 60); + let s = t % 60; + let parts = []; + if (d > 0) parts.push(d + 'd'); + if (h > 0) parts.push(h + 'h'); + if (m > 0) parts.push(m + 'm'); + if (s > 0 || parts.length === 0) parts.push(s + 's'); + sp.innerHTML = parts.join(' '); + } } diff --git a/data/login.html b/data-src/login.html similarity index 100% rename from data/login.html rename to data-src/login.html diff --git a/data/main.css b/data-src/main.css similarity index 100% rename from data/main.css rename to data-src/main.css diff --git a/data/widgets.css b/data-src/widgets.css similarity index 100% rename from data/widgets.css rename to data-src/widgets.css diff --git a/data/appversion b/data/appversion deleted file mode 100644 index 48a6b50..0000000 --- a/data/appversion +++ /dev/null @@ -1 +0,0 @@ -2.4.7 \ No newline at end of file diff --git a/huge_app.csv b/huge_app.csv new file mode 100644 index 0000000..1d00925 --- /dev/null +++ b/huge_app.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x300000, +spiffs, data, spiffs, 0x310000,0xE0000, +coredump, data, coredump,0x3F0000,0x10000, diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -0,0 +1,37 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the convention is to give header files names that end with `.h'. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/min_spiffs.csv b/min_spiffs.csv new file mode 100644 index 0000000..0990a3b --- /dev/null +++ b/min_spiffs.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1E0000, +app1, app, ota_1, 0x1F0000,0x1E0000, +spiffs, data, spiffs, 0x3D0000,0x20000, +coredump, data, coredump,0x3F0000,0x10000, diff --git a/minify.py b/minify.py new file mode 100644 index 0000000..a5c581c --- /dev/null +++ b/minify.py @@ -0,0 +1,290 @@ +""" +PlatformIO pre-build script: minify & gzip web assets. + +Copies files from data-src/ → data/ + • HTML – whitespace & comment removal, then gzip + • CSS – whitespace & comment removal, then gzip + • JS – whitespace & comment removal, then gzip + • PNG – copied as-is (optipng if available) + • Other – copied as-is + +Usage in platformio.ini +----------------------- + extra_scripts = pre:minify.py + +Directory layout +---------------- + project/ + ├── data-src/ ← original, human-readable assets + │ ├── index.html + │ ├── css/ + │ ├── js/ + │ └── img/ + ├── data/ ← auto-generated (gitignore this!) + ├── minify.py + └── platformio.ini + +The script runs automatically before LittleFS / SPIFFS image is built. +It also runs once at the start of every build so the data/ dir is always fresh. + +Dependencies: none (pure Python ≥ 3.7). +Optional: optipng (for PNG optimisation) +""" + +Import("env") # PlatformIO macro – gives us the build environment + +import gzip +import os +import re +import shutil +import subprocess +import sys + +# ────────────────────────────────────────────── +# Config +# ────────────────────────────────────────────── +SRC_DIR_NAME = "data-src" +DST_DIR_NAME = "data" + +# Extensions that will be minified + gzipped +MINIFY_AND_GZIP = {".html", ".htm", ".css", ".js", ".json", ".svg", ".xml"} + +# Extensions that optipng can optimise +PNG_EXTENSIONS = {".png"} + +# Everything else is copied verbatim +# ────────────────────────────────────────────── + + +def _project_dir(): + return env.subst("$PROJECT_DIR") + + +def _src_dir(): + return os.path.join(_project_dir(), SRC_DIR_NAME) + + +def _dst_dir(): + return os.path.join(_project_dir(), DST_DIR_NAME) + + +# ────────────────────────────────────────────── +# Minifiers (pure Python, no npm needed) +# ────────────────────────────────────────────── +def minify_html(text: str) -> str: + """Simple but effective HTML minifier.""" + # Remove HTML comments (but keep IE conditional comments) + text = re.sub(r"", "", text, flags=re.DOTALL) + # Collapse whitespace between tags + text = re.sub(r">\s+<", "> <", text) + # Collapse runs of whitespace into a single space + text = re.sub(r"\s{2,}", " ", text) + # Remove whitespace around = in attributes + text = re.sub(r'\s*=\s*', '=', text) + # Strip leading/trailing whitespace per line, rejoin + lines = [line.strip() for line in text.splitlines() if line.strip()] + return " ".join(lines) + + +def minify_css(text: str) -> str: + """Remove comments, collapse whitespace in CSS.""" + # Remove comments + text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL) + # Remove whitespace around : ; { } , + text = re.sub(r"\s*([:{};,])\s*", r"\1", text) + # Collapse remaining whitespace + text = re.sub(r"\s{2,}", " ", text) + # Strip leading/trailing + return text.strip() + + +# def minify_js(text: str) -> str: +# """ +# Light JS minifier – removes comments and collapses whitespace. +# For heavy minification install terser and the script will use it +# automatically (see _try_terser below). +# """ +# # Remove single-line comments (careful with URLs – :// ) +# text = re.sub(r"(? str: + """Compact JSON by removing unnecessary whitespace.""" + import json + try: + data = json.loads(text) + return json.dumps(data, separators=(",", ":"), ensure_ascii=False) + except json.JSONDecodeError: + return text + + +def minify_svg(text: str) -> str: + """Minimal SVG minifier – comments + whitespace.""" + text = re.sub(r"", "", text, flags=re.DOTALL) + text = re.sub(r">\s+<", "><", text) + text = re.sub(r"\s{2,}", " ", text) + return text.strip() + + +MINIFIERS = { + # ".html": minify_html, + # ".htm": minify_html, + # ".css": minify_css, + # ".js": minify_js, + # ".json": minify_json, + # ".svg": minify_svg, + # ".xml": minify_svg, # same approach works for generic XML +} + + +# ────────────────────────────────────────────── +# Optional external tools (used when available) +# ────────────────────────────────────────────── +def _has_command(cmd: str) -> bool: + return shutil.which(cmd) is not None + + +def _try_terser(src_path: str, dst_path: str) -> bool: + """Use terser for JS if installed (npm i -g terser).""" + if not _has_command("terser"): + return False + try: + subprocess.run( + ["terser", src_path, "-o", dst_path, "--compress", "--mangle"], + check=True, capture_output=True, + ) + return True + except subprocess.CalledProcessError: + return False + + +def _try_optipng(path: str) -> None: + """Optimise PNG in-place if optipng is available.""" + if _has_command("optipng"): + try: + subprocess.run( + ["optipng", "-quiet", "-o2", path], + check=True, capture_output=True, + ) + except subprocess.CalledProcessError: + pass + + +# ────────────────────────────────────────────── +# Core logic +# ────────────────────────────────────────────── +def process_file(src_path: str, dst_path: str) -> dict: + """ + Process a single file: minify, gzip or copy. + Returns a small stats dict. + """ + ext = os.path.splitext(src_path)[1].lower() + original_size = os.path.getsize(src_path) + stats = {"src": src_path, "original": original_size, "final": 0, "action": "copy"} + + os.makedirs(os.path.dirname(dst_path), exist_ok=True) + + # ── Text assets: minify + gzip ────────── + if ext in MINIFY_AND_GZIP: + # Try terser for JS first + if ext == ".js" and _try_terser(src_path, dst_path + ".tmp"): + with open(dst_path + ".tmp", "rb") as f: + minified = f.read() + os.remove(dst_path + ".tmp") + stats["action"] = "terser+gzip" + else: + with open(src_path, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + + minifier = MINIFIERS.get(ext) + if minifier: + content = minifier(content) + stats["action"] = "minify+gzip" + else: + stats["action"] = "gzip" + + minified = content.encode("utf-8") + + # Write gzipped version + gz_path = dst_path + ".gz" + with gzip.open(gz_path, "wb", compresslevel=9) as gz: + gz.write(minified) + + stats["final"] = os.path.getsize(gz_path) + return stats + + # ── PNG: copy + optional optipng ──────── + if ext in PNG_EXTENSIONS: + shutil.copy2(src_path, dst_path) + _try_optipng(dst_path) + stats["final"] = os.path.getsize(dst_path) + stats["action"] = "optipng" if _has_command("optipng") else "copy" + return stats + + # ── Everything else: plain copy ───────── + shutil.copy2(src_path, dst_path) + stats["final"] = os.path.getsize(dst_path) + return stats + + +def minify_all(): + src_dir = _src_dir() + dst_dir = _dst_dir() + + if not os.path.isdir(src_dir): + print(f"[minify] WARNING: '{SRC_DIR_NAME}/' not found – skipping.") + return + + print(f"[minify] {SRC_DIR_NAME}/ → {DST_DIR_NAME}/") + + # Clean destination + if os.path.exists(dst_dir): + shutil.rmtree(dst_dir) + os.makedirs(dst_dir, exist_ok=True) + + total_original = 0 + total_final = 0 + file_count = 0 + + for root, dirs, files in os.walk(src_dir): + for fname in sorted(files): + # Skip hidden files and editor temp files + if fname.startswith(".") or fname.endswith("~"): + continue + + src_path = os.path.join(root, fname) + rel_path = os.path.relpath(src_path, src_dir) + dst_path = os.path.join(dst_dir, rel_path) + + stats = process_file(src_path, dst_path) + + pct = (1 - stats["final"] / stats["original"]) * 100 if stats["original"] > 0 else 0 + print( + f" {rel_path:<40s} " + f"{stats['original']:>8,d} → {stats['final']:>8,d} B " + f"({pct:+.0f}%) [{stats['action']}]" + ) + + total_original += stats["original"] + total_final += stats["final"] + file_count += 1 + + print(f"[minify] {file_count} files processed") + print( + f"[minify] Total: {total_original:,d} → {total_final:,d} bytes " + f"(saved {total_original - total_final:,d} bytes, " + f"{(1 - total_final / total_original) * 100:.0f}%)" + ) + + +# ────────────────────────────────────────────── +# PlatformIO hooks +# ────────────────────────────────────────────── +# Run at the start of every build +minify_all() \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..2141691 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,39 @@ +; PlatformIO Project Configuration File +; https://docs.platformio.org/page/projectconf.html + +[platformio] +default_envs = esp32dev + +; Shared settings for all environments +[env] +platform = espressif32 +framework = arduino +lib_deps = + bblanchon/ArduinoJson@^7.2.2 + lsatan/SmartRC-CC1101-Driver-Lib@^2.5.7 + knolleary/PubSubClient@^2.8 + esp32async/ESPAsyncWebServer@^3.10.3 + esp32async/AsyncTCP@^3.4.10 +extra_scripts = pre:minify.py +board_build.partitions = huge_app.csv +board_build.filesystem = littlefs +build_flags = + -DCONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=1 + -DCONFIG_ESP_COREDUMP_DATA_FORMAT_ELF=1 + -DCONFIG_ESP_COREDUMP_CHECKSUM_CRC32=1 + -DCONFIG_ESP_TASK_WDT_PANIC=1 + -DCONFIG_ESP_COREDUMP_DECODE_INFO=1 +monitor_speed = 115200 +monitor_filters = + time + esp32_exception_decoder + log2file + +[env:esp32dev] +board = esp32dev + +[env:esp32devdbg] +board = esp32dev +build_type = debug + + diff --git a/ConfigFile.cpp b/src/ConfigFile.cpp similarity index 98% rename from ConfigFile.cpp rename to src/ConfigFile.cpp index e515bed..43fcbb7 100644 --- a/ConfigFile.cpp +++ b/src/ConfigFile.cpp @@ -22,10 +22,15 @@ bool ConfigFile::begin(const char* filename, bool readOnly) { this->_opened = true; return true; } +bool ConfigFile::beginRAM(String *buf) { + _ramBuf = buf; + _opened = true; + return true; +} void ConfigFile::end() { if(this->isOpen()) { - if(!this->readOnly) this->file.flush(); - this->file.close(); + if(_ramBuf) { _ramBuf = nullptr; } + else { if(!this->readOnly) this->file.flush(); this->file.close(); } } this->_opened = false; } @@ -187,10 +192,16 @@ bool ConfigFile::readVarString(char *buff, size_t len) { bool ConfigFile::writeString(const char *val, size_t len, const char tok) { if(!this->isOpen()) return false; int slen = strlen(val); + if(_ramBuf) { + if(slen > 0) _ramBuf->concat(val); + while(slen < (int)len - 1) { _ramBuf->concat(' '); slen++; } + if(tok != CFG_TOK_NONE) _ramBuf->concat(tok); + return true; + } if(slen > 0) if(this->file.write((uint8_t *)val, slen) != slen) return false; // Now we need to pad the end of the string so that it is of a fixed length. - while(slen < len - 1) { + while(slen < (int)len - 1) { this->file.write(' '); slen++; } @@ -202,6 +213,13 @@ bool ConfigFile::writeString(const char *val, size_t len, const char tok) { bool ConfigFile::writeVarString(const char *val, const char tok) { if(!this->isOpen()) return false; int slen = strlen(val); + if(_ramBuf) { + _ramBuf->concat((char)CFG_TOK_QUOTE); + if(slen > 0) _ramBuf->concat(val); + _ramBuf->concat((char)CFG_TOK_QUOTE); + if(tok != CFG_TOK_NONE) _ramBuf->concat(tok); + return true; + } this->writeChar(CFG_TOK_QUOTE); if(slen > 0) if(this->file.write((uint8_t *)val, slen) != slen) return false; this->writeChar(CFG_TOK_QUOTE); @@ -210,6 +228,7 @@ bool ConfigFile::writeVarString(const char *val, const char tok) { } bool ConfigFile::writeChar(const char val) { if(!this->isOpen()) return false; + if(_ramBuf) { _ramBuf->concat(val); return true; } if(this->file.write(static_cast(val)) == 1) return true; return false; } diff --git a/ConfigFile.h b/src/ConfigFile.h similarity index 98% rename from ConfigFile.h rename to src/ConfigFile.h index 5ad9281..011e7a9 100644 --- a/ConfigFile.h +++ b/src/ConfigFile.h @@ -31,12 +31,14 @@ struct config_header_t { class ConfigFile { protected: File file; + String *_ramBuf = nullptr; bool readOnly = false; bool begin(const char *filename, bool readOnly = false); uint32_t startRecPos = 0; bool _opened = false; public: config_header_t header; + bool beginRAM(String *buf); void end(); bool isOpen(); bool seekRecordByIndex(uint16_t ndx); diff --git a/ConfigSettings.cpp b/src/ConfigSettings.cpp similarity index 90% rename from ConfigSettings.cpp rename to src/ConfigSettings.cpp index 5deec5e..3de3c4c 100644 --- a/ConfigSettings.cpp +++ b/src/ConfigSettings.cpp @@ -85,13 +85,6 @@ bool appver_t::toJSON(JsonObject &obj) { obj["suffix"] = this->suffix; return true; } -void appver_t::toJSON(JsonResponse &json) { - json.addElem("name", this->name); - json.addElem("major", this->major); - json.addElem("minor", this->minor); - json.addElem("build", this->build); - json.addElem("suffix", this->suffix); -} void appver_t::toJSON(JsonSockEvent *json) { json->addElem("name", this->name); json->addElem("major", this->major); @@ -249,14 +242,6 @@ bool ConfigSettings::toJSON(JsonObject &obj) { obj["checkForUpdate"] = this->checkForUpdate; return true; } -void ConfigSettings::toJSON(JsonResponse &json) { - json.addElem("ssdpBroadcast", this->ssdpBroadcast); - json.addElem("hostname", this->hostname); - json.addElem("connType", static_cast(this->connType)); - json.addElem("chipModel", this->chipModel); - json.addElem("checkForUpdate", this->checkForUpdate); -} - bool ConfigSettings::requiresAuth() { return this->Security.type != security_types::None; } bool ConfigSettings::fromJSON(JsonObject &obj) { if(obj.containsKey("ssdpBroadcast")) this->ssdpBroadcast = obj["ssdpBroadcast"]; @@ -308,18 +293,6 @@ bool MQTTSettings::begin() { this->load(); return true; } -void MQTTSettings::toJSON(JsonResponse &json) { - json.addElem("enabled", this->enabled); - json.addElem("pubDisco", this->pubDisco); - json.addElem("protocol", this->protocol); - json.addElem("hostname", this->hostname); - json.addElem("port", (uint32_t)this->port); - json.addElem("username", this->username); - json.addElem("password", this->password); - json.addElem("rootTopic", this->rootTopic); - json.addElem("discoTopic", this->discoTopic); -} - bool MQTTSettings::toJSON(JsonObject &obj) { obj["enabled"] = this->enabled; obj["pubDisco"] = this->pubDisco; @@ -418,11 +391,6 @@ bool NTPSettings::fromJSON(JsonObject &obj) { this->parseValueString(obj, "posixZone", this->posixZone, sizeof(this->posixZone)); return true; } -void NTPSettings::toJSON(JsonResponse &json) { - json.addElem("ntpServer", this->ntpServer); - json.addElem("posixZone", this->posixZone); -} - bool NTPSettings::toJSON(JsonObject &obj) { obj["ntpServer"] = this->ntpServer; obj["posixZone"] = this->posixZone; @@ -459,16 +427,6 @@ bool IPSettings::toJSON(JsonObject &obj) { obj["dns2"] = this->dns2 == ipEmpty ? "" : this->dns2.toString(); return true; } -void IPSettings::toJSON(JsonResponse &json) { - IPAddress ipEmpty(0,0,0,0); - json.addElem("dhcp", this->dhcp); - json.addElem("ip", this->ip.toString().c_str()); - json.addElem("gateway", this->gateway.toString().c_str()); - json.addElem("subnet", this->subnet.toString().c_str()); - json.addElem("dns1", this->dns1.toString().c_str()); - json.addElem("dns2", this->dns2.toString().c_str()); -} - bool IPSettings::save() { pref.begin("IP"); pref.clear(); @@ -529,14 +487,6 @@ bool SecuritySettings::toJSON(JsonObject &obj) { obj["permissions"] = this->permissions; return true; } -void SecuritySettings::toJSON(JsonResponse &json) { - json.addElem("type", static_cast(this->type)); - json.addElem("username", this->username); - json.addElem("password", this->password); - json.addElem("pin", this->pin); - json.addElem("permissions", this->permissions); -} - bool SecuritySettings::save() { pref.begin("SEC"); pref.clear(); @@ -590,13 +540,6 @@ bool WifiSettings::toJSON(JsonObject &obj) { obj["hidden"] = this->hidden; return true; } -void WifiSettings::toJSON(JsonResponse &json) { - json.addElem("ssid", this->ssid); - json.addElem("passphrase", this->passphrase); - json.addElem("roaming", this->roaming); - json.addElem("hidden", this->hidden); -} - bool WifiSettings::save() { pref.begin("WIFI"); pref.clear(); @@ -697,16 +640,6 @@ bool EthernetSettings::toJSON(JsonObject &obj) { obj["MDIOPin"] = this->MDIOPin; return true; } -void EthernetSettings::toJSON(JsonResponse &json) { - json.addElem("boardType", this->boardType); - json.addElem("phyAddress", this->phyAddress); - json.addElem("CLKMode", static_cast(this->CLKMode)); - json.addElem("phyType", static_cast(this->phyType)); - json.addElem("PWRPin", this->PWRPin); - json.addElem("MDCPin", this->MDCPin); - json.addElem("MDIOPin", this->MDIOPin); -} - bool EthernetSettings::usesPin(uint8_t pin) { if((this->CLKMode == 0 || this->CLKMode == 1) && pin == 0) return true; else if(this->CLKMode == 2 && pin == 16) return true; diff --git a/ConfigSettings.h b/src/ConfigSettings.h similarity index 93% rename from ConfigSettings.h rename to src/ConfigSettings.h index 350db96..2bee8e2 100644 --- a/ConfigSettings.h +++ b/src/ConfigSettings.h @@ -3,7 +3,7 @@ #ifndef configsettings_h #define configsettings_h #include "WResp.h" -#define FW_VERSION "v2.4.7" +#define FW_VERSION "v2.4.8" enum class conn_types_t : byte { unset = 0x00, wifi = 0x01, @@ -34,7 +34,7 @@ struct appver_t { char suffix[4] = ""; void parse(const char *ver); bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); + void toJSON(JsonSockEvent *json); int8_t compare(appver_t &ver); void copy(appver_t &ver); @@ -46,7 +46,7 @@ class BaseSettings { bool loadFile(const char* filename); bool fromJSON(JsonObject &obj); bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); + bool parseIPAddress(JsonObject &obj, const char *prop, IPAddress *); bool parseValueString(JsonObject &obj, const char *prop, char *dest, size_t size); int parseValueInt(JsonObject &obj, const char *prop, int defVal); @@ -61,7 +61,7 @@ class NTPSettings: BaseSettings { char posixZone[64] = ""; bool fromJSON(JsonObject &obj); bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); + bool apply(); bool begin(); bool save(); @@ -79,7 +79,7 @@ class WifiSettings: BaseSettings { bool begin(); bool fromJSON(JsonObject &obj); bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); + String mapEncryptionType(int type); bool ssidExists(const char *ssid); void printNetworks(); @@ -102,7 +102,7 @@ class EthernetSettings: BaseSettings { bool begin(); bool fromJSON(JsonObject &obj); bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); + bool load(); bool save(); void print(); @@ -120,7 +120,7 @@ class IPSettings: BaseSettings { bool begin(); bool fromJSON(JsonObject &obj); bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); + bool load(); bool save(); void print(); @@ -145,7 +145,7 @@ class SecuritySettings: BaseSettings { bool load(); void print(); bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); + bool fromJSON(JsonObject &obj); }; class MQTTSettings: BaseSettings { @@ -163,7 +163,7 @@ class MQTTSettings: BaseSettings { bool save(); bool load(); bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); + bool fromJSON(JsonObject &obj); }; class ConfigSettings: BaseSettings { @@ -187,7 +187,7 @@ class ConfigSettings: BaseSettings { bool requiresAuth(); bool fromJSON(JsonObject &obj); bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); + bool begin(); bool save(); bool load(); diff --git a/GitOTA.cpp b/src/GitOTA.cpp similarity index 92% rename from GitOTA.cpp rename to src/GitOTA.cpp index 49ea3c5..3b4e9a3 100644 --- a/GitOTA.cpp +++ b/src/GitOTA.cpp @@ -72,22 +72,6 @@ void GitRelease::setAssetProperty(const char *key, const char *val) { } } } -void GitRelease::toJSON(JsonResponse &json) { - Timestamp ts; - char buff[20]; - sprintf(buff, "%llu", this->id); - json.addElem("id", buff); - json.addElem("name", this->name); - json.addElem("date", ts.getISOTime(this->releaseDate)); - json.addElem("draft", this->draft); - json.addElem("preRelease", this->preRelease); - json.addElem("main", this->main); - json.addElem("hasFS", this->hasFS); - json.addElem("hwVersions", this->hwVersions); - json.beginObject("version"); - this->version.toJSON(json); - json.endObject(); -} #define ERR_CLIENT_OFFSET -50 int16_t GitRepo::getReleases(uint8_t num) { @@ -230,22 +214,6 @@ int16_t GitRepo::getReleases(uint8_t num) { settings.printAvailHeap(); return 0; } -void GitRepo::toJSON(JsonResponse &json) { - json.beginObject("fwVersion"); - settings.fwVersion.toJSON(json); - json.endObject(); - json.beginObject("appVersion"); - settings.appVersion.toJSON(json); - json.endObject(); - json.beginArray("releases"); - for(uint8_t i = 0; i < GIT_MAX_RELEASES + 1; i++) { - if(this->releases[i].id == 0) continue; - json.beginObject(); - this->releases[i].toJSON(json); - json.endObject(); - } - json.endArray(); -} #define UPDATE_ERR_OFFSET 20 #define ERR_DOWNLOAD_HTTP -40 #define ERR_DOWNLOAD_BUFFER -41 @@ -310,23 +278,6 @@ void GitUpdater::setCurrentRelease(GitRepo &repo) { } this->emitUpdateCheck(); } -void GitUpdater::toJSON(JsonResponse &json) { - json.addElem("available", this->updateAvailable); - json.addElem("status", this->status); - json.addElem("error", (int32_t)this->error); - json.addElem("cancelled", this->cancelled); - json.addElem("checkForUpdate", settings.checkForUpdate); - json.addElem("inetAvailable", this->inetAvailable); - json.beginObject("fwVersion"); - settings.fwVersion.toJSON(json); - json.endObject(); - json.beginObject("appVersion"); - settings.appVersion.toJSON(json); - json.endObject(); - json.beginObject("latest"); - this->latest.toJSON(json); - json.endObject(); -} void GitUpdater::emitUpdateCheck(uint8_t num) { JsonSockEvent *json = sockEmit.beginEmit("fwStatus"); json->beginObject(); diff --git a/GitOTA.h b/src/GitOTA.h similarity index 94% rename from GitOTA.h rename to src/GitOTA.h index f79aaea..6996f83 100644 --- a/GitOTA.h +++ b/src/GitOTA.h @@ -28,13 +28,13 @@ class GitRelease { appver_t version; void setReleaseProperty(const char *key, const char *val); void setAssetProperty(const char *key, const char *val); - void toJSON(JsonResponse &json); + }; class GitRepo { public: int16_t getReleases(uint8_t num = GIT_MAX_RELEASES); GitRelease releases[GIT_MAX_RELEASES + 1]; - void toJSON(JsonResponse &json); + }; class GitUpdater { public: @@ -58,7 +58,7 @@ class GitUpdater { void setFirmwareFile(); void setCurrentRelease(GitRepo &repo); void loop(); - void toJSON(JsonResponse &json); + bool recoverFilesystem(); int checkInternet(); void emitUpdateCheck(uint8_t num=255); diff --git a/MQTT.cpp b/src/MQTT.cpp similarity index 100% rename from MQTT.cpp rename to src/MQTT.cpp diff --git a/MQTT.h b/src/MQTT.h similarity index 100% rename from MQTT.h rename to src/MQTT.h diff --git a/Network.cpp b/src/Network.cpp similarity index 99% rename from Network.cpp rename to src/Network.cpp index ad8f2e1..914d4fd 100644 --- a/Network.cpp +++ b/src/Network.cpp @@ -691,6 +691,7 @@ void Network::emitHeap(uint8_t num) { json->addElem("free", freeHeap); json->addElem("min", minHeap); json->addElem("total", ESP.getHeapSize()); + json->addElem("uptime", (uint64_t)millis()); json->endObject(); if(num == 255 && bTimeEmit && bValEmit) { sockEmit.endEmit(num); diff --git a/Network.h b/src/Network.h similarity index 100% rename from Network.h rename to src/Network.h diff --git a/SSDP.cpp b/src/SSDP.cpp similarity index 100% rename from SSDP.cpp rename to src/SSDP.cpp diff --git a/SSDP.h b/src/SSDP.h similarity index 100% rename from SSDP.h rename to src/SSDP.h diff --git a/src/Sockets.cpp b/src/Sockets.cpp new file mode 100644 index 0000000..fb371e1 --- /dev/null +++ b/src/Sockets.cpp @@ -0,0 +1,221 @@ +#include +#include +#include +#include "Sockets.h" +#include "ConfigSettings.h" +#include "Somfy.h" +#include "Network.h" +#include "GitOTA.h" + +extern ConfigSettings settings; +extern Network net; +extern SomfyShadeController somfy; +extern SocketEmitter sockEmit; +extern GitUpdater git; + +AsyncWebServer wsServer(8080); +AsyncWebSocket ws("/"); + +#define MAX_WS_CLIENTS 5 +static uint32_t clientMap[MAX_WS_CLIENTS] = {0,0,0,0,0}; + +static uint8_t mapClientId(uint32_t asyncId) { + for(uint8_t i = 0; i < MAX_WS_CLIENTS; i++) + if(clientMap[i] == asyncId) return i; + return 255; +} +static uint32_t getAsyncId(uint8_t slot) { + if(slot < MAX_WS_CLIENTS) return clientMap[slot]; + return 0; +} +static uint8_t addClient(uint32_t asyncId) { + for(uint8_t i = 0; i < MAX_WS_CLIENTS; i++) { + if(clientMap[i] == 0) { clientMap[i] = asyncId; return i; } + } + return 255; +} +static void removeClient(uint32_t asyncId) { + for(uint8_t i = 0; i < MAX_WS_CLIENTS; i++) + if(clientMap[i] == asyncId) clientMap[i] = 0; +} + +#define MAX_SOCK_RESPONSE 2048 +static char g_response[MAX_SOCK_RESPONSE]; + +bool room_t::isJoined(uint8_t num) { + for(uint8_t i = 0; i < sizeof(this->clients); i++) { + if(this->clients[i] == num) return true; + } + return false; +} +bool room_t::join(uint8_t num) { + if(this->isJoined(num)) return true; + for(uint8_t i = 0; i < sizeof(this->clients); i++) { + if(this->clients[i] == 255) { + this->clients[i] = num; + return true; + } + } + return false; +} +bool room_t::leave(uint8_t num) { + if(!this->isJoined(num)) return false; + for(uint8_t i = 0; i < sizeof(this->clients); i++) { + if(this->clients[i] == num) this->clients[i] = 255; + } + return true; +} +void room_t::clear() { + memset(this->clients, 255, sizeof(this->clients)); +} +uint8_t room_t::activeClients() { + uint8_t n = 0; + for(uint8_t i = 0; i < sizeof(this->clients); i++) { + if(this->clients[i] != 255) n++; + } + return n; +} + +/********************************************************************* + * SocketEmitter class members + ********************************************************************/ +void SocketEmitter::startup() { + +} +void SocketEmitter::begin() { + ws.onEvent(SocketEmitter::wsEvent); + wsServer.addHandler(&ws); + wsServer.begin(); + Serial.println("Socket Server Started..."); +} +void SocketEmitter::loop() { + ws.cleanupClients(); + this->initClients(); +} +JsonSockEvent *SocketEmitter::beginEmit(const char *evt) { + this->json.beginEvent(&ws, evt, g_response, sizeof(g_response)); + return &this->json; +} +void SocketEmitter::endEmit(uint8_t num) { + if(num == 255) { + this->json.endEvent(0); + } else { + uint32_t asyncId = getAsyncId(num); + this->json.endEvent(asyncId); + } + esp_task_wdt_reset(); +} +void SocketEmitter::endEmitRoom(uint8_t room) { + if(room < SOCK_MAX_ROOMS) { + room_t *r = &this->rooms[room]; + for(uint8_t i = 0; i < sizeof(r->clients); i++) { + if(r->clients[i] != 255) { + uint32_t asyncId = getAsyncId(r->clients[i]); + if(asyncId != 0) this->json.endEvent(asyncId); + } + } + } +} +uint8_t SocketEmitter::activeClients(uint8_t room) { + if(room < SOCK_MAX_ROOMS) return this->rooms[room].activeClients(); + return 0; +} +void SocketEmitter::initClients() { + for(uint8_t i = 0; i < sizeof(this->newClients); i++) { + uint8_t slot = this->newClients[i]; + if(slot != 255) { + uint32_t asyncId = getAsyncId(slot); + if(asyncId != 0 && ws.hasClient(asyncId)) { + Serial.printf("Initializing Socket Client %u (asyncId=%lu)\n", slot, asyncId); + esp_task_wdt_reset(); + settings.emitSockets(slot); + if(!ws.hasClient(asyncId)) { this->newClients[i] = 255; continue; } + somfy.emitState(slot); + if(!ws.hasClient(asyncId)) { this->newClients[i] = 255; continue; } + git.emitUpdateCheck(slot); + if(!ws.hasClient(asyncId)) { this->newClients[i] = 255; continue; } + net.emitSockets(slot); + esp_task_wdt_reset(); + } + this->newClients[i] = 255; + } + } +} +void SocketEmitter::delayInit(uint8_t num) { + for(uint8_t i=0; i < sizeof(this->newClients); i++) { + if(this->newClients[i] == num) break; + else if(this->newClients[i] == 255) { + this->newClients[i] = num; + break; + } + } +} +void SocketEmitter::end() { + ws.closeAll(); + wsServer.end(); + for(uint8_t i = 0; i < SOCK_MAX_ROOMS; i++) + this->rooms[i].clear(); + memset(clientMap, 0, sizeof(clientMap)); +} +void SocketEmitter::disconnect() { + ws.closeAll(); + memset(clientMap, 0, sizeof(clientMap)); +} +void SocketEmitter::wsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { + uint32_t asyncId = client->id(); + switch(type) { + case WS_EVT_CONNECT: + { + uint8_t slot = addClient(asyncId); + if(slot == 255) { + Serial.printf("Socket: No free client slots, closing %lu\n", asyncId); + client->close(); + return; + } + IPAddress ip = client->remoteIP(); + Serial.printf("Socket [%lu] Connected from %d.%d.%d.%d (slot %u)\n", asyncId, ip[0], ip[1], ip[2], ip[3], slot); + client->text("Connected"); + client->setCloseClientOnQueueFull(false); + sockEmit.delayInit(slot); + } + break; + case WS_EVT_DISCONNECT: + { + uint8_t slot = mapClientId(asyncId); + Serial.printf("Socket [%lu] Disconnected (slot %u)\n", asyncId, slot); + if(slot != 255) { + for(uint8_t i = 0; i < SOCK_MAX_ROOMS; i++) + sockEmit.rooms[i].leave(slot); + } + removeClient(asyncId); + } + break; + case WS_EVT_DATA: + { + AwsFrameInfo *info = (AwsFrameInfo*)arg; + if(info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) { + uint8_t slot = mapClientId(asyncId); + data[len] = 0; + if(strncmp((char*)data, "join:", 5) == 0) { + uint8_t roomNum = atoi((char*)&data[5]); + Serial.printf("Client %u joining room %u\n", slot, roomNum); + if(roomNum < SOCK_MAX_ROOMS && slot != 255) sockEmit.rooms[roomNum].join(slot); + } + else if(strncmp((char*)data, "leave:", 6) == 0) { + uint8_t roomNum = atoi((char*)&data[6]); + Serial.printf("Client %u leaving room %u\n", slot, roomNum); + if(roomNum < SOCK_MAX_ROOMS && slot != 255) sockEmit.rooms[roomNum].leave(slot); + } + else { + Serial.printf("Socket [%lu] text: %s\n", asyncId, data); + } + } + } + break; + case WS_EVT_ERROR: + Serial.printf("Socket [%lu] Error\n", asyncId); + break; + case WS_EVT_PONG: + break; + } +} diff --git a/Sockets.h b/src/Sockets.h similarity index 82% rename from Sockets.h rename to src/Sockets.h index d76ab93..948b730 100644 --- a/Sockets.h +++ b/src/Sockets.h @@ -1,4 +1,4 @@ -#include +#include #include "WResp.h" #ifndef sockets_h #define sockets_h @@ -21,7 +21,6 @@ class SocketEmitter { void delayInit(uint8_t num); public: JsonSockEvent json; - //ClientSocketEvent evt; room_t rooms[SOCK_MAX_ROOMS]; uint8_t activeClients(uint8_t room); void initClients(); @@ -33,6 +32,6 @@ class SocketEmitter { JsonSockEvent * beginEmit(const char *evt); void endEmit(uint8_t num = 255); void endEmitRoom(uint8_t num); - static void wsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length); + static void wsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); }; #endif diff --git a/Somfy.cpp b/src/Somfy.cpp similarity index 96% rename from Somfy.cpp rename to src/Somfy.cpp index bffebd2..31b1aca 100644 --- a/Somfy.cpp +++ b/src/Somfy.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include "Utils.h" #include "ConfigSettings.h" @@ -156,9 +155,9 @@ void somfy_frame_t::decodeFrame(byte* frame) { this->proto = radio_proto::RTV; this->cmd = (somfy_commands)(this->encKey - 148); } - else if(this->encKey > 133) { + else if(this->encKey >= 133) { this->proto = radio_proto::RTW; - this->cmd = (somfy_commands)(this->encKey - 133); + this->cmd = this->encKey == 133 ? somfy_commands::My : (somfy_commands)(this->encKey - 133); } } else this->proto = radio_proto::RTS; @@ -633,8 +632,10 @@ void SomfyShadeController::commit() { void SomfyShadeController::writeBackup() { if(git.lockFS) return; esp_task_wdt_reset(); // Make sure we don't reset inadvertently. + this->backupData = ""; + this->backupData.reserve(16384); ShadeConfigFile file; - file.begin("/controller.backup", false); + file.beginRAM(&this->backupData); file.backup(this); file.end(); } @@ -1445,9 +1446,9 @@ void SomfyRoom::unpublish() { } void SomfyShade::publishState() { if(mqtt.connected()) { - this->publish("position", this->transformPosition(this->currentPos), true); - this->publish("direction", this->direction, true); - this->publish("target", this->transformPosition(this->target), true); + this->publish("position", (uint8_t)50, true); + this->publish("direction", (int8_t)0, true); + this->publish("target", (uint8_t)50, true); this->publish("lastRollingCode", this->lastRollingCode); this->publish("mypos", this->transformPosition(this->myPos), true); this->publish("myTiltPos", this->transformPosition(this->myTiltPos), true); @@ -1794,7 +1795,7 @@ bool SomfyGroup::publish(const char *topic, bool val, bool retain) { float SomfyShade::p_currentPos(float pos) { float old = this->currentPos; this->currentPos = pos; - if(floor(old) != floor(pos)) this->publish("position", this->transformPosition(static_cast(floor(this->currentPos)))); + if(floor(old) != floor(pos)) this->publish("position", (uint8_t)50); return old; } float SomfyShade::p_currentTiltPos(float pos) { @@ -2888,9 +2889,21 @@ void SomfyShade::moveToMyPosition() { } void SomfyShade::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); } void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { + Serial.print("Send command start\n"); // This sendCommand function will always be called externally. sendCommand at the remote level // is expected to be called internally when the motor needs commanded. if(this->bitLength == 0) this->bitLength = somfy.transceiver.config.type; + // If same direction command sent while already moving, stop the state tracking. + // The real motor stops on its own when it receives the same direction again. + if((cmd == somfy_commands::Up && this->direction == -1) || + (cmd == somfy_commands::Down && this->direction == 1)) { + Serial.println("Same command as dir"); + SomfyRemote::sendCommand(cmd, repeat); + this->p_target(this->currentPos); + this->p_tiltTarget(this->currentTiltPos); + this->setMovement(0); + return; + } if(cmd == somfy_commands::Up) { if(this->tiltType == tilt_types::euromode) { // In euromode we need to long press for 2 seconds on the @@ -2930,9 +2943,13 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSiz else if(cmd == somfy_commands::My) { if(this->isToggle() || this->shadeType == shade_types::drycontact) SomfyRemote::sendCommand(cmd, repeat); - else if(this->shadeType == shade_types::drycontact2) return; + else if(this->shadeType == shade_types::drycontact2){ + Serial.print("Send command start 1\n"); + return; + } else if(this->isIdle()) { - this->moveToMyPosition(); + this->moveToMyPosition(); + Serial.print("Send command end 2\n"); return; } else { @@ -2951,6 +2968,7 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSiz else { SomfyRemote::sendCommand(cmd, repeat, stepSize); } + Serial.print("Send command end\n"); } void SomfyGroup::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); } void SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { @@ -3309,72 +3327,6 @@ int8_t SomfyShade::fromJSON(JsonObject &obj) { } return err; } -void SomfyShade::toJSONRef(JsonResponse &json) { - json.addElem("shadeId", this->getShadeId()); - json.addElem("roomId", this->roomId); - json.addElem("name", this->name); - json.addElem("remoteAddress", (uint32_t)this->m_remoteAddress); - json.addElem("paired", this->paired); - json.addElem("shadeType", static_cast(this->shadeType)); - json.addElem("flipCommands", this->flipCommands); - json.addElem("flipPosition", this->flipCommands); - json.addElem("bitLength", this->bitLength); - json.addElem("proto", static_cast(this->proto)); - json.addElem("flags", this->flags); - json.addElem("sunSensor", this->hasSunSensor()); - json.addElem("hasLight", this->hasLight()); - json.addElem("repeats", this->repeats); - //SomfyRemote::toJSON(json); -} - -void SomfyShade::toJSON(JsonResponse &json) { - json.addElem("shadeId", this->getShadeId()); - json.addElem("roomId", this->roomId); - json.addElem("name", this->name); - json.addElem("remoteAddress", (uint32_t)this->m_remoteAddress); - json.addElem("upTime", (uint32_t)this->upTime); - json.addElem("downTime", (uint32_t)this->downTime); - json.addElem("paired", this->paired); - json.addElem("lastRollingCode", (uint32_t)this->lastRollingCode); - json.addElem("position", this->transformPosition(this->currentPos)); - json.addElem("tiltType", static_cast(this->tiltType)); - json.addElem("tiltPosition", this->transformPosition(this->currentTiltPos)); - json.addElem("tiltDirection", this->tiltDirection); - json.addElem("tiltTime", (uint32_t)this->tiltTime); - json.addElem("stepSize", (uint32_t)this->stepSize); - json.addElem("tiltTarget", this->transformPosition(this->tiltTarget)); - json.addElem("target", this->transformPosition(this->target)); - json.addElem("myPos", this->transformPosition(this->myPos)); - json.addElem("myTiltPos", this->transformPosition(this->myTiltPos)); - json.addElem("direction", this->direction); - json.addElem("shadeType", static_cast(this->shadeType)); - json.addElem("bitLength", this->bitLength); - json.addElem("proto", static_cast(this->proto)); - json.addElem("flags", this->flags); - json.addElem("flipCommands", this->flipCommands); - json.addElem("flipPosition", this->flipPosition); - json.addElem("inGroup", this->isInGroup()); - json.addElem("sunSensor", this->hasSunSensor()); - json.addElem("light", this->hasLight()); - json.addElem("repeats", this->repeats); - json.addElem("sortOrder", this->sortOrder); - json.addElem("gpioUp", this->gpioUp); - json.addElem("gpioDown", this->gpioDown); - json.addElem("gpioMy", this->gpioMy); - json.addElem("gpioLLTrigger", ((this->gpioFlags & (uint8_t)gpio_flags_t::LowLevelTrigger) == 0) ? false : true); - json.addElem("simMy", this->simMy()); - json.beginArray("linkedRemotes"); - for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) { - SomfyLinkedRemote &lremote = this->linkedRemotes[i]; - if(lremote.getRemoteAddress() != 0) { - json.beginObject(); - lremote.toJSON(json); - json.endObject(); - } - } - json.endArray(); -} - /* bool SomfyShade::toJSON(JsonObject &obj) { //Serial.print("Serializing Shade:"); @@ -3442,12 +3394,6 @@ bool SomfyRoom::toJSON(JsonObject &obj) { return true; } */ -void SomfyRoom::toJSON(JsonResponse &json) { - json.addElem("roomId", this->roomId); - json.addElem("name", this->name); - json.addElem("sortOrder", this->sortOrder); -} - bool SomfyGroup::fromJSON(JsonObject &obj) { if(obj.containsKey("name")) strlcpy(this->name, obj["name"], sizeof(this->name)); if(obj.containsKey("roomId")) this->roomId = obj["roomId"]; @@ -3469,50 +3415,6 @@ bool SomfyGroup::fromJSON(JsonObject &obj) { } return true; } -void SomfyGroup::toJSON(JsonResponse &json) { - this->updateFlags(); - json.addElem("groupId", this->getGroupId()); - json.addElem("roomId", this->roomId); - json.addElem("name", this->name); - json.addElem("remoteAddress", (uint32_t)this->m_remoteAddress); - json.addElem("lastRollingCode", (uint32_t)this->lastRollingCode); - json.addElem("bitLength", this->bitLength); - json.addElem("proto", static_cast(this->proto)); - json.addElem("sunSensor", this->hasSunSensor()); - json.addElem("flipCommands", this->flipCommands); - json.addElem("flags", this->flags); - json.addElem("repeats", this->repeats); - json.addElem("sortOrder", this->sortOrder); - json.beginArray("linkedShades"); - for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) { - uint8_t shadeId = this->linkedShades[i]; - if(shadeId > 0 && shadeId < 255) { - SomfyShade *shade = somfy.getShadeById(shadeId); - if(shade) { - json.beginObject(); - shade->toJSONRef(json); - json.endObject(); - } - } - } - json.endArray(); -} -void SomfyGroup::toJSONRef(JsonResponse &json) { - this->updateFlags(); - json.addElem("groupId", this->getGroupId()); - json.addElem("roomId", this->roomId); - json.addElem("name", this->name); - json.addElem("remoteAddress", (uint32_t)this->m_remoteAddress); - json.addElem("lastRollingCode", (uint32_t)this->lastRollingCode); - json.addElem("bitLength", this->bitLength); - json.addElem("proto", static_cast(this->proto)); - json.addElem("sunSensor", this->hasSunSensor()); - json.addElem("flipCommands", this->flipCommands); - json.addElem("flags", this->flags); - json.addElem("repeats", this->repeats); - json.addElem("sortOrder", this->sortOrder); -} - /* bool SomfyGroup::toJSON(JsonObject &obj) { this->updateFlags(); @@ -3544,10 +3446,6 @@ bool SomfyGroup::toJSON(JsonObject &obj) { } */ -void SomfyRemote::toJSON(JsonResponse &json) { - json.addElem("remoteAddress", (uint32_t)this->getRemoteAddress()); - json.addElem("lastRollingCode", (uint32_t)this->lastRollingCode); -} /* bool SomfyRemote::toJSON(JsonObject &obj) { //obj["remotePrefId"] = this->getRemotePrefId(); @@ -3571,6 +3469,7 @@ void SomfyShadeController::emitState(uint8_t num) { for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { SomfyShade *shade = &this->shades[i]; if(shade->getShadeId() == 255) continue; + esp_task_wdt_reset(); shade->emitState(num); } } @@ -4111,26 +4010,6 @@ uint16_t SomfyRemote::setRollingCode(uint16_t code) { } return code; } -void SomfyShadeController::toJSONRooms(JsonResponse &json) { - for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) { - SomfyRoom *room = &this->rooms[i]; - if(room->roomId != 0) { - json.beginObject(); - room->toJSON(json); - json.endObject(); - } - } -} -void SomfyShadeController::toJSONShades(JsonResponse &json) { - for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { - SomfyShade &shade = this->shades[i]; - if(shade.getShadeId() != 255) { - json.beginObject(); - shade.toJSON(json); - json.endObject(); - } - } -} /* bool SomfyShadeController::toJSON(DynamicJsonDocument &doc) { @@ -4185,21 +4064,6 @@ bool SomfyShadeController::toJSONGroups(JsonArray &arr) { return true; } */ -void SomfyShadeController::toJSONGroups(JsonResponse &json) { - for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) { - SomfyGroup &group = this->groups[i]; - if(group.getGroupId() != 255) { - json.beginObject(); - group.toJSON(json); - json.endObject(); - } - } -} -void SomfyShadeController::toJSONRepeaters(JsonResponse &json) { - for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) { - if(somfy.repeaters[i] != 0) json.addElem((uint32_t)somfy.repeaters[i]); - } -} void SomfyShadeController::loop() { this->transceiver.loop(); for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { @@ -4683,11 +4547,6 @@ void Transceiver::disableReceive(void) { interruptPin = 0; } -void Transceiver::toJSON(JsonResponse& json) { - json.beginObject("config"); - this->config.toJSON(json); - json.endObject(); -} /* bool Transceiver::toJSON(JsonObject& obj) { //Serial.println("Setting Transceiver Json"); @@ -4766,22 +4625,6 @@ void transceiver_config_t::fromJSON(JsonObject& obj) { */ Serial.printf("SCK:%u MISO:%u MOSI:%u CSN:%u RX:%u TX:%u\n", this->SCKPin, this->MISOPin, this->MOSIPin, this->CSNPin, this->RXPin, this->TXPin); } -void transceiver_config_t::toJSON(JsonResponse &json) { - json.addElem("type", this->type); - json.addElem("TXPin", this->TXPin); - json.addElem("RXPin", this->RXPin); - json.addElem("SCKPin", this->SCKPin); - json.addElem("MOSIPin", this->MOSIPin); - json.addElem("MISOPin", this->MISOPin); - json.addElem("CSNPin", this->CSNPin); - json.addElem("rxBandwidth", this->rxBandwidth); // float - json.addElem("frequency", this->frequency); // float - json.addElem("deviation", this->deviation); // float - json.addElem("txPower", this->txPower); - json.addElem("proto", static_cast(this->proto)); - json.addElem("enabled", this->enabled); - json.addElem("radioInit", this->radioInit); -} /* void transceiver_config_t::toJSON(JsonObject& obj) { obj["type"] = this->type; diff --git a/Somfy.h b/src/Somfy.h similarity index 97% rename from Somfy.h rename to src/Somfy.h index bc5c99b..33ac49d 100644 --- a/Somfy.h +++ b/src/Somfy.h @@ -30,10 +30,10 @@ enum class radio_proto : byte { // Ordinal byte 0-255 }; enum class somfy_commands : byte { Unknown0 = 0x0, - My = 0x1, - Up = 0x2, - MyUp = 0x3, - Down = 0x4, + My = 0x2,//DOWN + Up = 0x1, + MyUp = 0x4, //up + Down = 0x3, MyDown = 0x5, UpDown = 0x6, MyUpDown = 0x7, @@ -209,7 +209,6 @@ class SomfyRoom { void clear(); bool save(); bool fromJSON(JsonObject &obj); - void toJSON(JsonResponse &json); void emitState(const char *evt = "roomState"); void emitState(uint8_t num, const char *evt = "roomState"); void publish(); @@ -239,7 +238,6 @@ class SomfyRemote { uint8_t repeats = 1; virtual bool isLastCommand(somfy_commands cmd); char *getRemotePrefId() {return m_remotePrefId;} - virtual void toJSON(JsonResponse &json); virtual void setRemoteAddress(uint32_t address); virtual uint32_t getRemoteAddress(); virtual uint16_t getNextRollingCode(); @@ -304,9 +302,7 @@ class SomfyShade : public SomfyRemote { SomfyLinkedRemote linkedRemotes[SOMFY_MAX_LINKED_REMOTES]; bool paired = false; int8_t validateJSON(JsonObject &obj); - void toJSONRef(JsonResponse &json); int8_t fromJSON(JsonObject &obj); - void toJSON(JsonResponse &json) override; char name[21] = ""; void setShadeId(uint8_t id) { shadeId = id; } @@ -393,9 +389,6 @@ class SomfyGroup : public SomfyRemote { void clear(); bool fromJSON(JsonObject &obj); //bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); - void toJSONRef(JsonResponse &json); - bool linkShade(uint8_t shadeId); bool unlinkShade(uint8_t shadeId); bool hasShadeId(uint8_t shadeId); @@ -485,7 +478,6 @@ struct transceiver_config_t { */ void fromJSON(JsonObject& obj); //void toJSON(JsonObject& obj); - void toJSON(JsonResponse& json); void save(); void load(); void apply(); @@ -500,7 +492,6 @@ class Transceiver { transceiver_config_t config; bool printBuffer = false; //bool toJSON(JsonObject& obj); - void toJSON(JsonResponse& json); bool fromJSON(JsonObject& obj); bool save(); bool begin(); @@ -557,10 +548,6 @@ class SomfyShadeController { SomfyGroup groups[SOMFY_MAX_GROUPS]; bool linkRepeater(uint32_t address); bool unlinkRepeater(uint32_t address); - void toJSONShades(JsonResponse &json); - void toJSONRooms(JsonResponse &json); - void toJSONGroups(JsonResponse &json); - void toJSONRepeaters(JsonResponse &json); uint8_t repeaterCount(); uint8_t roomCount(); uint8_t shadeCount(); @@ -578,6 +565,7 @@ class SomfyShadeController { void processWaitingFrame(); void commit(); void writeBackup(); + String backupData; bool loadShadesFile(const char *filename); #ifdef USE_NVS bool loadLegacy(); diff --git a/SomfyController.ino b/src/SomfyController.ino similarity index 58% rename from SomfyController.ino rename to src/SomfyController.ino index 464d179..af80702 100644 --- a/SomfyController.ino +++ b/src/SomfyController.ino @@ -1,3 +1,5 @@ +#define LOG_LOCAL_LEVEL ESP_LOG_INFO +#include "esp_log.h" #include #include #include @@ -9,6 +11,7 @@ #include "Somfy.h" #include "MQTT.h" #include "GitOTA.h" +#include "esp_core_dump.h" ConfigSettings settings; Web webServer; @@ -20,11 +23,55 @@ MQTTClass mqtt; GitUpdater git; uint32_t oldheap = 0; -void setup() { + +void listDir(const char *dirname, uint8_t levels) { + Serial.printf("Listing: %s\n", dirname); + File root = LittleFS.open(dirname); + if (!root || !root.isDirectory()) { + Serial.println("Failed to open directory"); + return; + } + File file = root.openNextFile(); + while (file) { + if (file.isDirectory()) { + Serial.printf(" DIR : %s\n", file.name()); + if (levels) listDir(file.path(), levels - 1); + } else { + Serial.printf(" FILE: %-30s %d bytes\n", file.name(), file.size()); + } + file = root.openNextFile(); + } +} + +void setup() { Serial.begin(115200); Serial.println(); Serial.println("Startup/Boot...."); + esp_core_dump_summary_t summary; + if (esp_core_dump_get_summary(&summary) == ESP_OK) { + Serial.println("*** Previous crash coredump found ***"); + Serial.printf(" Task: %s\n", summary.exc_task); + Serial.printf(" PC: 0x%08x\n", summary.exc_pc); + Serial.printf(" Cause: %d\n", summary.ex_info.exc_cause); + Serial.printf(" Backtrace:"); + for (int i = 0; i < summary.exc_bt_info.depth; i++) { + Serial.printf(" 0x%08x", summary.exc_bt_info.bt[i]); + } + Serial.println(); + } Serial.println("Mounting File System..."); + + + if (LittleFS.begin()) { + Serial.printf("\nTotal: %d bytes\n", LittleFS.totalBytes()); + Serial.printf("Used: %d bytes\n", LittleFS.usedBytes()); + Serial.printf("Free: %d bytes\n", LittleFS.totalBytes() - LittleFS.usedBytes()); + Serial.println(); + listDir("/", 3); + } else { + Serial.println("LittleFS mount failed!"); + } + if(LittleFS.begin()) Serial.println("File system mounted successfully"); else Serial.println("Error mounting file system"); settings.begin(); @@ -37,7 +84,7 @@ void setup() { net.setup(); somfy.begin(); //git.checkForUpdate(); - esp_task_wdt_init(7, true); //enable panic so ESP32 restarts + esp_task_wdt_init(15, true); //enable panic so ESP32 restarts esp_task_wdt_add(NULL); //add current thread to WDT watch } diff --git a/SomfyController.ino.esp32.bin b/src/SomfyController.ino.esp32.bin similarity index 100% rename from SomfyController.ino.esp32.bin rename to src/SomfyController.ino.esp32.bin diff --git a/SomfyController.ino.esp32s3.bin b/src/SomfyController.ino.esp32s3.bin similarity index 100% rename from SomfyController.ino.esp32s3.bin rename to src/SomfyController.ino.esp32s3.bin diff --git a/SomfyController.littlefs.bin b/src/SomfyController.littlefs.bin similarity index 100% rename from SomfyController.littlefs.bin rename to src/SomfyController.littlefs.bin diff --git a/Utils.cpp b/src/Utils.cpp similarity index 100% rename from Utils.cpp rename to src/Utils.cpp diff --git a/Utils.h b/src/Utils.h similarity index 100% rename from Utils.h rename to src/Utils.h diff --git a/WResp.cpp b/src/WResp.cpp similarity index 89% rename from WResp.cpp rename to src/WResp.cpp index e44e3ba..f64c0fa 100644 --- a/WResp.cpp +++ b/src/WResp.cpp @@ -1,5 +1,5 @@ #include "WResp.h" -void JsonSockEvent::beginEvent(WebSocketsServer *server, const char *evt, char *buff, size_t buffSize) { +void JsonSockEvent::beginEvent(AsyncWebSocket *server, const char *evt, char *buff, size_t buffSize) { this->server = server; this->buff = buff; this->buffSize = buffSize; @@ -15,10 +15,10 @@ void JsonSockEvent::closeEvent() { this->_nocomma = true; this->_closed = true; } -void JsonSockEvent::endEvent(uint8_t num) { +void JsonSockEvent::endEvent(uint32_t clientId) { this->closeEvent(); - if(num == 255) this->server->broadcastTXT(this->buff); - else this->server->sendTXT(num, this->buff); + if(clientId == 0) this->server->textAll(this->buff); + else this->server->text(clientId, this->buff); } void JsonSockEvent::_safecat(const char *val, bool escape) { size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff); @@ -33,30 +33,32 @@ void JsonSockEvent::_safecat(const char *val, bool escape) { else strcat(this->buff, val); if(escape) strcat(this->buff, "\""); } -void JsonResponse::beginResponse(WebServer *server, char *buff, size_t buffSize) { - this->server = server; +void AsyncJsonResp::beginResponse(AsyncWebServerRequest *request, char *buff, size_t buffSize) { + this->_request = request; this->buff = buff; this->buffSize = buffSize; this->buff[0] = 0x00; this->_nocomma = true; - server->setContentLength(CONTENT_LENGTH_UNKNOWN); + this->_headersSent = false; + this->_stream = request->beginResponseStream("application/json"); } -void JsonResponse::endResponse() { - if(strlen(buff)) this->send(); - server->sendContent("", 0); +void AsyncJsonResp::endResponse() { + if(strlen(this->buff)) this->flush(); + if(this->_request && this->_stream) { + this->_request->send(this->_stream); + } } -void JsonResponse::send() { - if(!this->_headersSent) server->send_P(200, "application/json", this->buff); - else server->sendContent(this->buff); - //Serial.printf("Sent %d bytes %d\n", strlen(this->buff), this->buffSize); +void AsyncJsonResp::flush() { + if(this->_stream && strlen(this->buff) > 0) { + this->_stream->print(this->buff); this->buff[0] = 0x00; - this->_headersSent = true; + } } -void JsonResponse::_safecat(const char *val, bool escape) { +void AsyncJsonResp::_safecat(const char *val, bool escape) { size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff); if(escape) len += 2; if(len >= this->buffSize) { - this->send(); + this->flush(); } if(escape) strcat(this->buff, "\""); if(escape) this->escapeString(val, &this->buff[strlen(this->buff)]); @@ -130,8 +132,9 @@ void JsonFormatter::addElem(const char *name, uint32_t nval) { sprintf(this->_nu void JsonFormatter::addElem(const char *name, int16_t nval) { sprintf(this->_numbuff, "%d", nval); this->_appendNumber(name); } void JsonFormatter::addElem(const char *name, uint16_t nval) { sprintf(this->_numbuff, "%u", nval); this->_appendNumber(name); } void JsonFormatter::addElem(const char *name, int64_t lval) { sprintf(this->_numbuff, "%lld", (long long)lval); this->_appendNumber(name); } -void JsonFormatter::addElem(const char *name, uint64_t lval) { sprintf(this->_numbuff, "%llu", (unsigned long long)lval); this->_appendNumber(name); } */ +void JsonFormatter::addElem(const char *name, uint64_t lval) { sprintf(this->_numbuff, "%llu", (unsigned long long)lval); this->_appendNumber(name); } + void JsonFormatter::addElem(const char *name, bool bval) { strcpy(this->_numbuff, bval ? "true" : "false"); this->_appendNumber(name); } void JsonFormatter::_safecat(const char *val, bool escape) { diff --git a/WResp.h b/src/WResp.h similarity index 79% rename from WResp.h rename to src/WResp.h index 4bda5d5..7356189 100644 --- a/WResp.h +++ b/src/WResp.h @@ -1,5 +1,4 @@ -#include -#include +#include #include "Somfy.h" #ifndef wresp_h #define wresp_h @@ -51,24 +50,26 @@ class JsonFormatter { void addElem(const char* name, uint32_t lval); void addElem(const char* name, bool bval); void addElem(const char *name, const char *val); + void addElem(const char* name, uint64_t lval); }; -class JsonResponse : public JsonFormatter { +class AsyncJsonResp : public JsonFormatter { protected: void _safecat(const char *val, bool escape = false) override; + AsyncWebServerRequest *_request = nullptr; + AsyncResponseStream *_stream = nullptr; public: - WebServer *server; - void beginResponse(WebServer *server, char *buff, size_t buffSize); + void beginResponse(AsyncWebServerRequest *request, char *buff, size_t buffSize); void endResponse(); - void send(); + void flush(); }; class JsonSockEvent : public JsonFormatter { protected: bool _closed = false; void _safecat(const char *val, bool escape = false) override; public: - WebSocketsServer *server = nullptr; - void beginEvent(WebSocketsServer *server, const char *evt, char *buff, size_t buffSize); - void endEvent(uint8_t clientNum = 255); + AsyncWebSocket *server = nullptr; + void beginEvent(AsyncWebSocket *server, const char *evt, char *buff, size_t buffSize); + void endEvent(uint32_t clientId = 0); void closeEvent(); }; #endif diff --git a/src/Web.cpp b/src/Web.cpp new file mode 100644 index 0000000..07994f7 --- /dev/null +++ b/src/Web.cpp @@ -0,0 +1,2306 @@ +#include +#include +#include +#include +#include "mbedtls/md.h" +#include "ConfigSettings.h" +#include "ConfigFile.h" +#include "Utils.h" +#include "SSDP.h" +#include "Somfy.h" +#include "WResp.h" +#include "Web.h" +#include "MQTT.h" +#include "GitOTA.h" +#include "Network.h" +#include +#include +#include + +extern ConfigSettings settings; +extern SSDPClass SSDP; +extern rebootDelay_t rebootDelay; +extern SomfyShadeController somfy; +extern Web webServer; +extern MQTTClass mqtt; +extern GitUpdater git; +extern Network net; + +//#define WEB_MAX_RESPONSE 34768 +#define WEB_MAX_RESPONSE 4096 +static char g_async_content[WEB_MAX_RESPONSE]; + + +// General responses +static const char _response_404[] = "404: Service Not Found"; + + +// Encodings +static const char _encoding_text[] = "text/plain"; +static const char _encoding_html[] = "text/html"; +static const char _encoding_json[] = "application/json"; + +AsyncWebServer asyncServer(80); +AsyncWebServer asyncApiServer(8081); +void Web::startup() { + Serial.println("Launching web server..."); + + asyncServer.on("/loginContext", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot().to(); + root["type"] = static_cast(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); + }); + asyncApiServer.begin(); + Serial.println("Async API server started on port 8081"); +} +void Web::loop() { + delay(1); +} +bool Web::isAuthenticated(AsyncWebServerRequest *request, bool cfg) { + Serial.println("Checking async authentication"); + if(settings.Security.type == security_types::None) return true; + else if(!cfg && (settings.Security.permissions & static_cast(security_permissions::ConfigOnly)) == 0x01) return true; + else if(request->hasHeader("apikey")) { + Serial.println("Checking API Key..."); + char token[65]; + memset(token, 0x00, sizeof(token)); + this->createAPIToken(request->client()->remoteIP(), token); + if(String(token) != request->getHeader("apikey")->value()) { + request->send(401, _encoding_text, "Unauthorized API Key"); + return false; + } + // Key is valid + } + else { + Serial.println("Not authenticated..."); + request->send(401, _encoding_text, "Unauthorized API Key"); + return false; + } + return true; +} +bool Web::createAPIPinToken(const IPAddress ipAddress, const char *pin, char *token) { + return this->createAPIToken((String(pin) + ":" + ipAddress.toString()).c_str(), token); +} +bool Web::createAPIPasswordToken(const IPAddress ipAddress, const char *username, const char *password, char *token) { + return this->createAPIToken((String(username) + ":" + String(password) + ":" + ipAddress.toString()).c_str(), token); +} +bool Web::createAPIToken(const char *payload, char *token) { + byte hmacResult[32]; + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1); + mbedtls_md_hmac_starts(&ctx, (const unsigned char *)settings.serverId, strlen(settings.serverId)); + mbedtls_md_hmac_update(&ctx, (const unsigned char *)payload, strlen(payload)); + mbedtls_md_hmac_finish(&ctx, hmacResult); + Serial.print("Hash: "); + token[0] = '\0'; + for(int i = 0; i < sizeof(hmacResult); i++){ + char str[3]; + sprintf(str, "%02x", (int)hmacResult[i]); + strcat(token, str); + } + Serial.println(token); + return true; +} +bool Web::createAPIToken(const IPAddress ipAddress, char *token) { + String payload; + if(settings.Security.type == security_types::Password) createAPIPasswordToken(ipAddress, settings.Security.username, settings.Security.password, token); + else if(settings.Security.type == security_types::PinEntry) createAPIPinToken(ipAddress, settings.Security.pin, token); + else createAPIToken(ipAddress.toString().c_str(), token); + return true; +} +// ===================================================== +// Async API Handlers +// ===================================================== +// Helper: get a query param as String, or empty if missing +static String asyncParam(AsyncWebServerRequest *request, const char *name) { + if(request->hasParam(name)) return request->getParam(name)->value(); + return String(); +} +static bool asyncHasParam(AsyncWebServerRequest *request, const char *name) { + return request->hasParam(name); +} + +// -- Serialization helpers (accept JsonFormatter& so both sync and async can use them) -- +static void serializeRoom(SomfyRoom *room, JsonFormatter &json) { + json.addElem("roomId", room->roomId); + json.addElem("name", room->name); + json.addElem("sortOrder", room->sortOrder); +} +static void serializeShadeRef(SomfyShade *shade, JsonFormatter &json) { + json.addElem("shadeId", shade->getShadeId()); + json.addElem("roomId", shade->roomId); + json.addElem("name", shade->name); + json.addElem("remoteAddress", (uint32_t)shade->getRemoteAddress()); + json.addElem("paired", shade->paired); + json.addElem("shadeType", static_cast(shade->shadeType)); + json.addElem("flipCommands", shade->flipCommands); + json.addElem("flipPosition", shade->flipCommands); + json.addElem("bitLength", shade->bitLength); + json.addElem("proto", static_cast(shade->proto)); + json.addElem("flags", shade->flags); + json.addElem("sunSensor", shade->hasSunSensor()); + json.addElem("hasLight", shade->hasLight()); + json.addElem("repeats", shade->repeats); +} +static void serializeShade(SomfyShade *shade, JsonFormatter &json) { + json.addElem("shadeId", shade->getShadeId()); + json.addElem("roomId", shade->roomId); + json.addElem("name", shade->name); + json.addElem("remoteAddress", (uint32_t)shade->getRemoteAddress()); + json.addElem("upTime", (uint32_t)shade->upTime); + json.addElem("downTime", (uint32_t)shade->downTime); + json.addElem("paired", shade->paired); + json.addElem("lastRollingCode", (uint32_t)shade->lastRollingCode); + json.addElem("position", shade->transformPosition(shade->currentPos)); + json.addElem("tiltType", static_cast(shade->tiltType)); + json.addElem("tiltPosition", shade->transformPosition(shade->currentTiltPos)); + json.addElem("tiltDirection", shade->tiltDirection); + json.addElem("tiltTime", (uint32_t)shade->tiltTime); + json.addElem("stepSize", (uint32_t)shade->stepSize); + json.addElem("tiltTarget", shade->transformPosition(shade->tiltTarget)); + json.addElem("target", shade->transformPosition(shade->target)); + json.addElem("myPos", shade->transformPosition(shade->myPos)); + json.addElem("myTiltPos", shade->transformPosition(shade->myTiltPos)); + json.addElem("direction", shade->direction); + json.addElem("shadeType", static_cast(shade->shadeType)); + json.addElem("bitLength", shade->bitLength); + json.addElem("proto", static_cast(shade->proto)); + json.addElem("flags", shade->flags); + json.addElem("flipCommands", shade->flipCommands); + json.addElem("flipPosition", shade->flipPosition); + json.addElem("inGroup", shade->isInGroup()); + json.addElem("sunSensor", shade->hasSunSensor()); + json.addElem("light", shade->hasLight()); + json.addElem("repeats", shade->repeats); + json.addElem("sortOrder", shade->sortOrder); + json.addElem("gpioUp", shade->gpioUp); + json.addElem("gpioDown", shade->gpioDown); + json.addElem("gpioMy", shade->gpioMy); + json.addElem("gpioLLTrigger", ((shade->gpioFlags & (uint8_t)gpio_flags_t::LowLevelTrigger) == 0) ? false : true); + json.addElem("simMy", shade->simMy()); + json.beginArray("linkedRemotes"); + for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) { + SomfyLinkedRemote &lremote = shade->linkedRemotes[i]; + if(lremote.getRemoteAddress() != 0) { + json.beginObject(); + json.addElem("remoteAddress", (uint32_t)lremote.getRemoteAddress()); + json.addElem("lastRollingCode", (uint32_t)lremote.lastRollingCode); + json.endObject(); + } + } + json.endArray(); +} +static void serializeGroupRef(SomfyGroup *group, JsonFormatter &json) { + group->updateFlags(); + json.addElem("groupId", group->getGroupId()); + json.addElem("roomId", group->roomId); + json.addElem("name", group->name); + json.addElem("remoteAddress", (uint32_t)group->getRemoteAddress()); + json.addElem("lastRollingCode", (uint32_t)group->lastRollingCode); + json.addElem("bitLength", group->bitLength); + json.addElem("proto", static_cast(group->proto)); + json.addElem("sunSensor", group->hasSunSensor()); + json.addElem("flipCommands", group->flipCommands); + json.addElem("flags", group->flags); + json.addElem("repeats", group->repeats); + json.addElem("sortOrder", group->sortOrder); +} +static void serializeGroup(SomfyGroup *group, JsonFormatter &json) { + serializeGroupRef(group, json); + json.beginArray("linkedShades"); + for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) { + uint8_t shadeId = group->linkedShades[i]; + if(shadeId > 0 && shadeId < 255) { + SomfyShade *shade = somfy.getShadeById(shadeId); + if(shade) { + json.beginObject(); + serializeShadeRef(shade, json); + json.endObject(); + } + } + } + json.endArray(); +} +static void serializeRooms(JsonFormatter &json) { + for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) { + SomfyRoom *room = &somfy.rooms[i]; + if(room->roomId != 0) { + json.beginObject(); + serializeRoom(room, json); + json.endObject(); + } + } +} +static void serializeShades(JsonFormatter &json) { + for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { + SomfyShade &shade = somfy.shades[i]; + if(shade.getShadeId() != 255) { + json.beginObject(); + serializeShade(&shade, json); + json.endObject(); + } + } +} +static void serializeGroups(JsonFormatter &json) { + for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) { + SomfyGroup &group = somfy.groups[i]; + if(group.getGroupId() != 255) { + json.beginObject(); + serializeGroup(&group, json); + json.endObject(); + } + } +} +static void serializeRepeaters(JsonFormatter &json) { + for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) { + if(somfy.repeaters[i] != 0) json.addElem((uint32_t)somfy.repeaters[i]); + } +} +static 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(cfg.proto)); + json.addElem("enabled", cfg.enabled); + json.addElem("radioInit", cfg.radioInit); +} +static 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); +} +static void serializeGitVersion(JsonFormatter &json) { + json.addElem("available", git.updateAvailable); + json.addElem("status", git.status); + json.addElem("error", (int32_t)git.error); + json.addElem("cancelled", git.cancelled); + json.addElem("checkForUpdate", settings.checkForUpdate); + json.addElem("inetAvailable", git.inetAvailable); + json.beginObject("fwVersion"); + serializeAppVersion(json, settings.fwVersion); + json.endObject(); + json.beginObject("appVersion"); + serializeAppVersion(json, settings.appVersion); + json.endObject(); + json.beginObject("latest"); + serializeAppVersion(json, git.latest); + json.endObject(); +} +static void serializeGitRelease(GitRelease *rel, JsonFormatter &json) { + Timestamp ts; + char buff[20]; + sprintf(buff, "%llu", rel->id); + json.addElem("id", buff); + json.addElem("name", rel->name); + json.addElem("date", ts.getISOTime(rel->releaseDate)); + json.addElem("draft", rel->draft); + json.addElem("preRelease", rel->preRelease); + json.addElem("main", rel->main); + json.addElem("hasFS", rel->hasFS); + json.addElem("hwVersions", rel->hwVersions); + json.beginObject("version"); + serializeAppVersion(json, rel->version); + json.endObject(); +} + +// -- Async handler implementations -- +void Web::handleDiscovery(AsyncWebServerRequest *request) { + if(request->method() == HTTP_POST || request->method() == HTTP_GET) { + Serial.println("Async Discovery Requested"); + char connType[10] = "Unknown"; + if(net.connType == conn_types_t::ethernet) strcpy(connType, "Ethernet"); + else if(net.connType == conn_types_t::wifi) strcpy(connType, "Wifi"); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + resp.addElem("serverId", settings.serverId); + resp.addElem("version", settings.fwVersion.name); + resp.addElem("latest", git.latest.name); + resp.addElem("model", "ESPSomfyRTS"); + resp.addElem("hostname", settings.hostname); + resp.addElem("authType", static_cast(settings.Security.type)); + resp.addElem("permissions", settings.Security.permissions); + resp.addElem("chipModel", settings.chipModel); + resp.addElem("connType", connType); + resp.addElem("checkForUpdate", settings.checkForUpdate); + resp.beginObject("memory"); + resp.addElem("max", ESP.getMaxAllocHeap()); + resp.addElem("free", ESP.getFreeHeap()); + resp.addElem("min", ESP.getMinFreeHeap()); + resp.addElem("total", ESP.getHeapSize()); + resp.addElem("uptime", (uint64_t)millis()); + resp.endObject(); + resp.beginArray("rooms"); + serializeRooms(resp); + resp.endArray(); + resp.beginArray("shades"); + serializeShades(resp); + resp.endArray(); + resp.beginArray("groups"); + serializeGroups(resp); + resp.endArray(); + resp.endObject(); + resp.endResponse(); + net.needsBroadcast = true; + } + 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) { + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginArray(); + serializeRooms(resp); + resp.endArray(); + resp.endResponse(); + } + 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) { + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginArray(); + serializeShades(resp); + resp.endArray(); + resp.endResponse(); + } + 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) { + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginArray(); + serializeGroups(resp); + resp.endArray(); + resp.endResponse(); + } + 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(); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + resp.addElem("maxRooms", (uint8_t)SOMFY_MAX_ROOMS); + resp.addElem("maxShades", (uint8_t)SOMFY_MAX_SHADES); + resp.addElem("maxGroups", (uint8_t)SOMFY_MAX_GROUPS); + resp.addElem("maxGroupedShades", (uint8_t)SOMFY_MAX_GROUPED_SHADES); + resp.addElem("maxLinkedRemotes", (uint8_t)SOMFY_MAX_LINKED_REMOTES); + resp.addElem("startingAddress", (uint32_t)somfy.startingAddress); + resp.beginObject("transceiver"); + resp.beginObject("config"); + serializeTransceiverConfig(resp); + resp.endObject(); + resp.endObject(); + resp.beginObject("version"); + serializeGitVersion(resp); + resp.endObject(); + resp.beginArray("rooms"); + serializeRooms(resp); + resp.endArray(); + resp.beginArray("shades"); + serializeShades(resp); + resp.endArray(); + resp.beginArray("groups"); + serializeGroups(resp); + resp.endArray(); + resp.beginArray("repeaters"); + serializeRepeaters(resp); + resp.endArray(); + resp.endObject(); + resp.endResponse(); + } + 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[65]; + memset(&token, 0x00, sizeof(token)); + this->createAPIToken(request->client()->remoteIP(), token); + if(settings.Security.type == security_types::None) { + snprintf(g_async_content, sizeof(g_async_content), + "{\"type\":%u,\"apiKey\":\"%s\",\"msg\":\"Success\",\"success\":true}", + static_cast(settings.Security.type), token); + request->send(200, _encoding_json, g_async_content); + return; + } + char username[33] = ""; + char password[33] = ""; + char pin[5] = ""; + // Try query params first + if(asyncHasParam(request, "username")) strlcpy(username, asyncParam(request, "username").c_str(), sizeof(username)); + if(asyncHasParam(request, "password")) strlcpy(password, asyncParam(request, "password").c_str(), sizeof(password)); + if(asyncHasParam(request, "pin")) strlcpy(pin, asyncParam(request, "pin").c_str(), sizeof(pin)); + // Override from JSON body if present + if(!json.isNull()) { + JsonObject obj = json.as(); + if(obj.containsKey("username")) strlcpy(username, obj["username"], sizeof(username)); + if(obj.containsKey("password")) strlcpy(password, obj["password"], sizeof(password)); + if(obj.containsKey("pin")) strlcpy(pin, obj["pin"], sizeof(pin)); + } + bool success = false; + if(settings.Security.type == security_types::PinEntry) { + char ptok[65]; + memset(ptok, 0x00, sizeof(ptok)); + this->createAPIPinToken(request->client()->remoteIP(), pin, ptok); + if(String(ptok) == String(token)) success = true; + } + else if(settings.Security.type == security_types::Password) { + char ptok[65]; + memset(ptok, 0x00, sizeof(ptok)); + this->createAPIPasswordToken(request->client()->remoteIP(), username, password, ptok); + if(String(ptok) == String(token)) success = true; + } + if(success) { + snprintf(g_async_content, sizeof(g_async_content), + "{\"type\":%u,\"apiKey\":\"%s\",\"msg\":\"Success\",\"success\":true}", + static_cast(settings.Security.type), token); + request->send(200, _encoding_json, g_async_content); + } + else { + snprintf(g_async_content, sizeof(g_async_content), + "{\"type\":%u,\"msg\":\"Invalid credentials\",\"success\":false}", + static_cast(settings.Security.type)); + request->send(401, _encoding_json, g_async_content); + } +} +void Web::handleShadeCommand(AsyncWebServerRequest *request, JsonVariant &json) { + if(request->method() == HTTP_OPTIONS) { request->send(200); return; } + if(!this->isAuthenticated(request)) return; + uint8_t shadeId = 255; + uint8_t target = 255; + uint8_t stepSize = 0; + int8_t repeat = -1; + somfy_commands command = somfy_commands::My; + // Try query params + if(asyncHasParam(request, "shadeId")) { + shadeId = asyncParam(request, "shadeId").toInt(); + if(asyncHasParam(request, "command")) command = translateSomfyCommand(asyncParam(request, "command")); + else if(asyncHasParam(request, "target")) target = asyncParam(request, "target").toInt(); + if(asyncHasParam(request, "repeat")) repeat = asyncParam(request, "repeat").toInt(); + if(asyncHasParam(request, "stepSize")) stepSize = asyncParam(request, "stepSize").toInt(); + } + else if(!json.isNull()) { + JsonObject obj = json.as(); + if(obj.containsKey("shadeId")) shadeId = obj["shadeId"]; + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); return; } + if(obj.containsKey("command")) { String scmd = obj["command"]; command = translateSomfyCommand(scmd); } + else if(obj.containsKey("target")) target = obj["target"].as(); + if(obj.containsKey("repeat")) repeat = obj["repeat"].as(); + if(obj.containsKey("stepSize")) stepSize = obj["stepSize"].as(); + } + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; } + SomfyShade *shade = somfy.getShadeById(shadeId); + if(shade) { + if(target <= 100) shade->moveToTarget(shade->transformPosition(target)); + else shade->sendCommand(command, repeat > 0 ? repeat : shade->repeats, stepSize); + 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.\"}")); +} +void Web::handleGroupCommand(AsyncWebServerRequest *request, JsonVariant &json) { + if(request->method() == HTTP_OPTIONS) { request->send(200); return; } + if(!this->isAuthenticated(request)) return; + uint8_t groupId = 255; + uint8_t stepSize = 0; + int8_t repeat = -1; + somfy_commands command = somfy_commands::My; + if(asyncHasParam(request, "groupId")) { + groupId = asyncParam(request, "groupId").toInt(); + if(asyncHasParam(request, "command")) command = translateSomfyCommand(asyncParam(request, "command")); + if(asyncHasParam(request, "repeat")) repeat = asyncParam(request, "repeat").toInt(); + if(asyncHasParam(request, "stepSize")) stepSize = asyncParam(request, "stepSize").toInt(); + } + else if(!json.isNull()) { + JsonObject obj = json.as(); + if(obj.containsKey("groupId")) groupId = obj["groupId"]; + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}")); return; } + if(obj.containsKey("command")) { String scmd = obj["command"]; command = translateSomfyCommand(scmd); } + if(obj.containsKey("repeat")) repeat = obj["repeat"].as(); + if(obj.containsKey("stepSize")) stepSize = obj["stepSize"].as(); + } + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); return; } + SomfyGroup *group = somfy.getGroupById(groupId); + if(group) { + group->sendCommand(command, repeat >= 0 ? repeat : group->repeats, stepSize); + 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\":\"Group with the specified id not found.\"}")); +} +void Web::handleTiltCommand(AsyncWebServerRequest *request, JsonVariant &json) { + if(request->method() == HTTP_OPTIONS) { request->send(200); return; } + if(!this->isAuthenticated(request)) return; + uint8_t shadeId = 255; + uint8_t target = 255; + somfy_commands command = somfy_commands::My; + if(asyncHasParam(request, "shadeId")) { + shadeId = asyncParam(request, "shadeId").toInt(); + if(asyncHasParam(request, "command")) command = translateSomfyCommand(asyncParam(request, "command")); + else if(asyncHasParam(request, "target")) target = asyncParam(request, "target").toInt(); + } + else if(!json.isNull()) { + JsonObject obj = json.as(); + if(obj.containsKey("shadeId")) shadeId = obj["shadeId"]; + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); return; } + if(obj.containsKey("command")) { String scmd = obj["command"]; command = translateSomfyCommand(scmd); } + else if(obj.containsKey("target")) target = obj["target"].as(); + } + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; } + SomfyShade *shade = somfy.getShadeById(shadeId); + if(shade) { + if(target <= 100) shade->moveToTiltTarget(shade->transformPosition(target)); + else shade->sendTiltCommand(command); + 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.\"}")); +} +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; + uint8_t stepSize = 0; + int8_t repeat = -1; + somfy_commands command = somfy_commands::My; + if(asyncHasParam(request, "shadeId")) shadeId = asyncParam(request, "shadeId").toInt(); + else if(asyncHasParam(request, "groupId")) groupId = asyncParam(request, "groupId").toInt(); + if(asyncHasParam(request, "command")) command = translateSomfyCommand(asyncParam(request, "command")); + if(asyncHasParam(request, "repeat")) repeat = asyncParam(request, "repeat").toInt(); + if(asyncHasParam(request, "stepSize")) stepSize = asyncParam(request, "stepSize").toInt(); + if(shadeId == 255 && groupId == 255 && !json.isNull()) { + JsonObject obj = json.as(); + if(obj.containsKey("shadeId")) shadeId = obj["shadeId"]; + if(obj.containsKey("groupId")) groupId = obj["groupId"]; + if(obj.containsKey("stepSize")) stepSize = obj["stepSize"]; + if(obj.containsKey("command")) { String scmd = obj["command"]; command = translateSomfyCommand(scmd); } + if(obj.containsKey("repeat")) repeat = obj["repeat"].as(); + } + if(shadeId != 255) { + SomfyShade *shade = somfy.getShadeById(shadeId); + if(!shade) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade reference could not be found.\"}")); return; } + if(shade->shadeType == shade_types::garage1 && command == somfy_commands::Prog) command = somfy_commands::Toggle; + if(!shade->isLastCommand(command)) shade->sendCommand(command, repeat >= 0 ? repeat : shade->repeats, stepSize); + else shade->repeatFrame(repeat >= 0 ? repeat : shade->repeats); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginArray(); + serializeShadeRef(shade, resp); + resp.endArray(); + resp.endResponse(); + } + else if(groupId != 255) { + SomfyGroup *group = somfy.getGroupById(groupId); + if(!group) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group reference could not be found.\"}")); return; } + if(!group->isLastCommand(command)) group->sendCommand(command, repeat >= 0 ? repeat : group->repeats, stepSize); + else group->repeatFrame(repeat >= 0 ? repeat : group->repeats); + 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(); + SomfyRoom *room = somfy.getRoomById(roomId); + if(room) { + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeRoom(room, resp); + resp.endObject(); + resp.endResponse(); + } + 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(); + SomfyShade *shade = somfy.getShadeById(shadeId); + if(shade) { + 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\":\"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(); + SomfyGroup *group = somfy.getGroupById(groupId); + if(group) { + 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\":\"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; + uint8_t shadeId = asyncHasParam(request, "shadeId") ? asyncParam(request, "shadeId").toInt() : 255; + int8_t pos = asyncHasParam(request, "position") ? asyncParam(request, "position").toInt() : -1; + int8_t tiltPos = asyncHasParam(request, "tiltPosition") ? asyncParam(request, "tiltPosition").toInt() : -1; + if(!json.isNull()) { + JsonObject obj = json.as(); + if(obj.containsKey("shadeId")) shadeId = obj["shadeId"]; + if(obj.containsKey("position")) pos = obj["position"]; + if(obj.containsKey("tiltPosition")) tiltPos = obj["tiltPosition"]; + } + if(shadeId != 255) { + SomfyShade *shade = somfy.getShadeById(shadeId); + if(shade) { + if(pos >= 0) shade->target = shade->currentPos = pos; + if(tiltPos >= 0 && shade->tiltType != tilt_types::none) shade->tiltTarget = shade->currentTiltPos = tiltPos; + shade->emitState(); + 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; + int8_t sunny = asyncHasParam(request, "sunny") ? (toBoolean(asyncParam(request, "sunny").c_str(), false) ? 1 : 0) : -1; + int8_t windy = asyncHasParam(request, "windy") ? asyncParam(request, "windy").toInt() : -1; + int8_t repeat = asyncHasParam(request, "repeat") ? asyncParam(request, "repeat").toInt() : -1; + if(!json.isNull()) { + JsonObject obj = json.as(); + if(obj.containsKey("shadeId")) shadeId = obj["shadeId"].as(); + if(obj.containsKey("groupId")) groupId = obj["groupId"].as(); + if(obj.containsKey("sunny")) { + if(obj["sunny"].is()) sunny = obj["sunny"].as() ? 1 : 0; + else sunny = obj["sunny"].as(); + } + if(obj.containsKey("windy")) { + if(obj["windy"].is()) windy = obj["windy"].as() ? 1 : 0; + else windy = obj["windy"].as(); + } + if(obj.containsKey("repeat")) repeat = obj["repeat"].as(); + } + if(shadeId != 255) { + SomfyShade *shade = somfy.getShadeById(shadeId); + if(shade) { + shade->sendSensorCommand(windy, sunny, repeat >= 0 ? (uint8_t)repeat : shade->repeats); + shade->emitState(); + 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) { + group->sendSensorCommand(windy, sunny, repeat >= 0 ? (uint8_t)repeat : group->repeats); + group->emitState(); + 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::handleDownloadFirmware(AsyncWebServerRequest *request) { + if(request->method() == HTTP_OPTIONS) { request->send(200); return; } + if(!this->isAuthenticated(request)) return; + GitRepo repo; + GitRelease *rel = nullptr; + int8_t err = repo.getReleases(); + Serial.println("Async downloadFirmware called..."); + if(err == 0) { + if(asyncHasParam(request, "ver")) { + String ver = asyncParam(request, "ver"); + if(ver == "latest") rel = &repo.releases[0]; + else if(ver == "main") rel = &repo.releases[GIT_MAX_RELEASES]; + else { + for(uint8_t i = 0; i < GIT_MAX_RELEASES; i++) { + if(repo.releases[i].id == 0) continue; + if(strcmp(repo.releases[i].name, ver.c_str()) == 0) { rel = &repo.releases[i]; break; } + } + } + if(rel) { + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeGitRelease(rel, resp); + resp.endObject(); + resp.endResponse(); + strcpy(git.targetRelease, rel->name); + git.status = GIT_AWAITING_UPDATE; + } + else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Release not found in repo.\"}")); + } + else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Release version not supplied.\"}")); + } + else request->send(err, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error communicating with Github.\"}")); +} +void Web::handleBackup(AsyncWebServerRequest *request) { + if(!this->isAuthenticated(request)) return; + bool attach = false; + if(asyncHasParam(request, "attach")) attach = toBoolean(asyncParam(request, "attach").c_str(), false); + Serial.println("Async saving current shade information"); + somfy.writeBackup(); + if(somfy.backupData.length() == 0) { + request->send(500, _encoding_text, "backup failed"); + return; + } + if(attach) { + char filename[120]; + Timestamp ts; + char *iso = ts.getISOTime(); + for(uint8_t i = 0; i < strlen(iso); i++) { + if(iso[i] == '.') { iso[i] = '\0'; break; } + if(iso[i] == ':') iso[i] = '_'; + } + snprintf(filename, sizeof(filename), "attachment; filename=\"ESPSomfyRTS %s.backup\"", iso); + AsyncWebServerResponse *response = request->beginResponse(200, _encoding_text, somfy.backupData); + response->addHeader("Content-Disposition", filename); + response->addHeader("Access-Control-Expose-Headers", "Content-Disposition"); + request->send(response); + } + else { + 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) { + Serial.println("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()); + request->send(404, _encoding_text, g_async_content); +} + +void Web::begin() { + Serial.println("Creating Web MicroServices..."); + // Async API Server (port 8081) + 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", "*"); + // GET endpoints + asyncApiServer.on("/discovery", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleDiscovery(r); }); + asyncApiServer.on("/rooms", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleGetRooms(r); }); + asyncApiServer.on("/shades", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleGetShades(r); }); + asyncApiServer.on("/groups", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleGetGroups(r); }); + asyncApiServer.on("/controller", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleController(r); }); + asyncApiServer.on("/room", HTTP_GET, [](AsyncWebServerRequest *r) { webServer.handleRoom(r); }); + asyncApiServer.on("/shade", HTTP_GET, [](AsyncWebServerRequest *r) { webServer.handleShade(r); }); + asyncApiServer.on("/group", HTTP_GET, [](AsyncWebServerRequest *r) { webServer.handleGroup(r); }); + asyncApiServer.on("/downloadFirmware", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleDownloadFirmware(r); }); + asyncApiServer.on("/backup", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *r) { webServer.handleBackup(r); }); + asyncApiServer.on("/reboot", WebRequestMethodComposite(HTTP_POST) | HTTP_PUT, [](AsyncWebServerRequest *r) { webServer.handleReboot(r); }); + // JSON body endpoints + asyncApiServer.addHandler(new AsyncCallbackJsonWebHandler("/shadeCommand", + [](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleShadeCommand(r, j); })); + asyncApiServer.addHandler(new AsyncCallbackJsonWebHandler("/groupCommand", + [](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleGroupCommand(r, j); })); + asyncApiServer.addHandler(new AsyncCallbackJsonWebHandler("/tiltCommand", + [](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleTiltCommand(r, j); })); + asyncApiServer.addHandler(new AsyncCallbackJsonWebHandler("/repeatCommand", + [](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleRepeatCommand(r, j); })); + asyncApiServer.addHandler(new AsyncCallbackJsonWebHandler("/setPositions", + [](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleSetPositions(r, j); })); + asyncApiServer.addHandler(new AsyncCallbackJsonWebHandler("/setSensor", + [](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleSetSensor(r, j); })); + asyncApiServer.addHandler(new AsyncCallbackJsonWebHandler("/login", + [](AsyncWebServerRequest *r, JsonVariant &j) { webServer.handleLogin(r, j); })); + // GET fallback for command endpoints (query params) + asyncApiServer.on("/shadeCommand", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleShadeCommand(r, v); }); + asyncApiServer.on("/groupCommand", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleGroupCommand(r, v); }); + asyncApiServer.on("/tiltCommand", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleTiltCommand(r, v); }); + asyncApiServer.on("/repeatCommand", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleRepeatCommand(r, v); }); + asyncApiServer.on("/setPositions", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleSetPositions(r, v); }); + asyncApiServer.on("/setSensor", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleSetSensor(r, v); }); + asyncApiServer.on("/login", HTTP_GET, [](AsyncWebServerRequest *r) { JsonVariant v; webServer.handleLogin(r, v); }); + // OPTIONS preflight + not found + asyncApiServer.onNotFound([](AsyncWebServerRequest *r) { + if(r->method() == HTTP_OPTIONS) { r->send(200); return; } + webServer.handleNotFound(r); + }); + + // Web Interface + + // Web Interface + // Command endpoints - delegate to async handler methods + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/tiltCommand", + [](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleTiltCommand(request, json); })); + asyncServer.on("/tiltCommand", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleTiltCommand(request, v); }); + + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/repeatCommand", + [](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleRepeatCommand(request, json); })); + asyncServer.on("/repeatCommand", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleRepeatCommand(request, v); }); + + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/shadeCommand", + [](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleShadeCommand(request, json); })); + asyncServer.on("/shadeCommand", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleShadeCommand(request, v); }); + + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/groupCommand", + [](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleGroupCommand(request, json); })); + asyncServer.on("/groupCommand", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleGroupCommand(request, v); }); + + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/setPositions", + [](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleSetPositions(request, json); })); + asyncServer.on("/setPositions", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleSetPositions(request, v); }); + + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/setSensor", + [](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleSetSensor(request, json); })); + asyncServer.on("/setSensor", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleSetSensor(request, v); }); + + asyncServer.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() + + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/login", + [](AsyncWebServerRequest *request, JsonVariant &json) { webServer.handleLogin(request, json); })); + asyncServer.on("/login", HTTP_GET, [](AsyncWebServerRequest *request) { JsonVariant v; webServer.handleLogin(request, v); }); + + asyncServer.on("/shades.cfg", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(LittleFS, "/shades.cfg", _encoding_text); + }); + asyncServer.on("/shades.tmp", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(LittleFS, "/shades.tmp", _encoding_text); + }); + + asyncServer.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(); + }); + + asyncServer.on("/downloadFirmware", HTTP_GET, [](AsyncWebServerRequest *request) { webServer.handleDownloadFirmware(request); }); + + asyncServer.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.\"}")); + } + }); + + asyncServer.on("/backup", HTTP_GET, [](AsyncWebServerRequest *request) { webServer.handleBackup(request); }); + + asyncServer.on("/restore", HTTP_POST, + [](AsyncWebServerRequest *request) { + if(webServer.uploadSuccess) { + request->send(200, _encoding_json, "{\"status\":\"Success\",\"desc\":\"Restoring Shade settings\"}"); + restore_options_t opts; + if(asyncHasParam(request, "data")) { + String dataStr = asyncParam(request, "data"); + Serial.println(dataStr); + StaticJsonDocument<256> 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(); + opts.fromJSON(obj); + } + } + else { + Serial.println("No restore options sent. Using defaults..."); + opts.shades = true; + } + ShadeConfigFile::restore(&somfy, "/shades.tmp", opts); + Serial.println("Rebooting ESP for restored settings..."); + rebootDelay.reboot = true; + rebootDelay.rebootTime = millis() + 1000; + } + }, + [](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; + Serial.printf("Restore: %s\n", 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() + + asyncServer.on("/controller", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleController(request); }); + asyncServer.on("/rooms", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleGetRooms(request); }); + asyncServer.on("/shades", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleGetShades(request); }); + asyncServer.on("/groups", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleGetGroups(request); }); + asyncServer.on("/room", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleRoom(request); }); + asyncServer.on("/shade", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleShade(request); }); + asyncServer.on("/group", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleGroup(request); }); + + asyncServer.on("/getNextRoom", HTTP_GET, [](AsyncWebServerRequest *request) { + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + resp.addElem("roomId", somfy.getNextRoomId()); + resp.endObject(); + resp.endResponse(); + }); + + asyncServer.on("/getNextShade", HTTP_GET, [](AsyncWebServerRequest *request) { + uint8_t shadeId = somfy.getNextShadeId(); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + resp.addElem("shadeId", shadeId); + resp.addElem("remoteAddress", (uint32_t)somfy.getNextRemoteAddress(shadeId)); + resp.addElem("bitLength", somfy.transceiver.config.type); + resp.addElem("stepSize", (uint8_t)100); + resp.addElem("proto", static_cast(somfy.transceiver.config.proto)); + resp.endObject(); + resp.endResponse(); + }); + + asyncServer.on("/getNextGroup", HTTP_GET, [](AsyncWebServerRequest *request) { + uint8_t groupId = somfy.getNextGroupId(); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + resp.addElem("groupId", groupId); + resp.addElem("remoteAddress", (uint32_t)somfy.getNextRemoteAddress(groupId)); + resp.addElem("bitLength", somfy.transceiver.config.type); + resp.addElem("proto", static_cast(somfy.transceiver.config.proto)); + resp.endObject(); + resp.endResponse(); + }); + + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/addRoom", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room object supplied.\"}")); return; } + Serial.println("Adding a room"); + JsonObject obj = json.as(); + if(somfy.roomCount() > SOMFY_MAX_ROOMS) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Maximum number of rooms exceeded.\"}")); + return; + } + SomfyRoom *room = somfy.addRoom(obj); + if(room) { + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeRoom(room, resp); + resp.endObject(); + resp.endResponse(); + } + else { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error adding room.\"}")); + } + })); + + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/addShade", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; } + Serial.println("Adding a shade"); + JsonObject obj = json.as(); + if(somfy.shadeCount() > SOMFY_MAX_SHADES) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Maximum number of shades exceeded.\"}")); + return; + } + SomfyShade *shade = somfy.addShade(obj); + if(shade) { + 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\":\"Error adding shade.\"}")); + } + })); + + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/addGroup", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); return; } + Serial.println("Adding a group"); + JsonObject obj = json.as(); + if(somfy.groupCount() > SOMFY_MAX_GROUPS) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Maximum number of groups exceeded.\"}")); + return; + } + SomfyGroup *group = somfy.addGroup(obj); + if(group) { + 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\":\"Error adding group.\"}")); + } + })); + + asyncServer.on("/groupOptions", HTTP_GET, [](AsyncWebServerRequest *request) { + if(asyncHasParam(request, "groupId")) { + int groupId = atoi(asyncParam(request, "groupId").c_str()); + SomfyGroup* group = somfy.getGroupById(groupId); + if(group) { + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeGroup(group, resp); + resp.beginArray("availShades"); + for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { + SomfyShade *shade = &somfy.shades[i]; + if(shade->getShadeId() != 255) { + bool isLinked = false; + for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) { + if(group->linkedShades[j] == shade->getShadeId()) { + isLinked = true; + break; + } + } + if(!isLinked) { + resp.beginObject(); + serializeShadeRef(shade, resp); + resp.endObject(); + } + } + } + resp.endArray(); + resp.endObject(); + resp.endResponse(); + } + 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.\"}")); + } + }); + + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/saveRoom", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room object supplied.\"}")); return; } + Serial.println("Updating a room"); + JsonObject obj = json.as(); + if(obj.containsKey("roomId")) { + SomfyRoom* room = somfy.getRoomById(obj["roomId"]); + if(room) { + room->fromJSON(obj); + room->save(); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeRoom(room, resp); + resp.endObject(); + resp.endResponse(); + } + else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Room Id not found.\"}")); + } + else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room id was supplied.\"}")); + })); + + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/saveShade", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; } + Serial.println("Updating a shade"); + JsonObject obj = json.as(); + if(obj.containsKey("shadeId")) { + SomfyShade* shade = somfy.getShadeById(obj["shadeId"]); + if(shade) { + int8_t err = shade->fromJSON(obj); + if(err == 0) { + shade->save(); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeShade(shade, resp); + resp.endObject(); + resp.endResponse(); + } + else { + snprintf(g_async_content, sizeof(g_async_content), "{\"status\":\"DATA\",\"desc\":\"Data Error.\", \"code\":%d}", err); + request->send(500, _encoding_json, g_async_content); + } + } + else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}")); + } + else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); + })); + + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/saveGroup", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); return; } + Serial.println("Updating a group"); + JsonObject obj = json.as(); + if(obj.containsKey("groupId")) { + SomfyGroup* group = somfy.getGroupById(obj["groupId"]); + if(group) { + group->fromJSON(obj); + group->save(); + 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\":\"Group Id not found.\"}")); + } + else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}")); + })); + + // setMyPosition - supports both GET with query params and POST/PUT with JSON body + asyncServer.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(); + if(obj.containsKey("shadeId")) shadeId = obj["shadeId"]; + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); return; } + if(obj.containsKey("pos")) pos = obj["pos"].as(); + if(obj.containsKey("tilt")) tilt = obj["tilt"].as(); + } + SomfyShade* shade = somfy.getShadeById(shadeId); + if(shade) { + if(tilt < 0) tilt = shade->myPos; + if(shade->tiltType == tilt_types::none) tilt = -1; + if(pos >= 0 && pos <= 100) + shade->setMyPosition(shade->transformPosition(pos), shade->transformPosition(tilt)); + 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.\"}")); + } + })); + asyncServer.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(tilt < 0) tilt = shade->myPos; + if(shade->tiltType == tilt_types::none) tilt = -1; + if(pos >= 0 && pos <= 100) + shade->setMyPosition(shade->transformPosition(pos), shade->transformPosition(tilt)); + 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 + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/setRollingCode", + [](AsyncWebServerRequest *request, JsonVariant &json) { + uint8_t shadeId = 255; + uint16_t rollingCode = 0; + if(!json.isNull()) { + JsonObject obj = json.as(); + if(obj.containsKey("shadeId")) shadeId = obj["shadeId"]; + if(obj.containsKey("rollingCode")) 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 { + shade->setRollingCode(rollingCode); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeShade(shade, resp); + resp.endObject(); + resp.endResponse(); + } + })); + asyncServer.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 { + shade->setRollingCode(rollingCode); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeShade(shade, resp); + resp.endObject(); + resp.endResponse(); + } + }); + + // setPaired - supports both query params and JSON body + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/setPaired", + [](AsyncWebServerRequest *request, JsonVariant &json) { + uint8_t shadeId = 255; + bool paired = false; + if(!json.isNull()) { + JsonObject obj = json.as(); + if(obj.containsKey("shadeId")) shadeId = obj["shadeId"]; + if(obj.containsKey("paired")) paired = obj["paired"]; + } + 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 pair\"}")); + } + else { + shade->paired = paired; + shade->save(); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeShade(shade, resp); + resp.endObject(); + resp.endResponse(); + } + })); + asyncServer.on("/setPaired", HTTP_GET, [](AsyncWebServerRequest *request) { + uint8_t shadeId = 255; + bool paired = false; + if(asyncHasParam(request, "shadeId")) + shadeId = atoi(asyncParam(request, "shadeId").c_str()); + if(asyncHasParam(request, "paired")) + paired = toBoolean(asyncParam(request, "paired").c_str(), false); + 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 pair\"}")); + } + else { + shade->paired = paired; + shade->save(); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeShade(shade, resp); + resp.endObject(); + resp.endResponse(); + } + }); + + // unpairShade + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/unpairShade", + [](AsyncWebServerRequest *request, JsonVariant &json) { + uint8_t shadeId = 255; + if(!json.isNull()) { + JsonObject obj = json.as(); + if(obj.containsKey("shadeId")) 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(shade->bitLength == 56) + shade->sendCommand(somfy_commands::Prog, 7); + else + shade->sendCommand(somfy_commands::Prog, 1); + shade->paired = false; + shade->save(); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeShade(shade, resp); + resp.endObject(); + resp.endResponse(); + } + })); + asyncServer.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(shade->bitLength == 56) + shade->sendCommand(somfy_commands::Prog, 7); + else + shade->sendCommand(somfy_commands::Prog, 1); + shade->paired = false; + shade->save(); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeShade(shade, resp); + resp.endObject(); + resp.endResponse(); + } + }); + + // linkRepeater + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/linkRepeater", + [](AsyncWebServerRequest *request, JsonVariant &json) { + uint32_t address = 0; + if(!json.isNull()) { + Serial.println("Linking a repeater"); + JsonObject obj = json.as(); + if(obj.containsKey("address")) address = obj["address"]; + else if(obj.containsKey("remoteAddress")) address = obj["remoteAddress"]; + } + if(address == 0) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No repeater address was supplied.\"}")); + } + else { + somfy.linkRepeater(address); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginArray(); + serializeRepeaters(resp); + resp.endArray(); + resp.endResponse(); + } + })); + asyncServer.on("/linkRepeater", HTTP_GET, [](AsyncWebServerRequest *request) { + uint32_t address = 0; + if(asyncHasParam(request, "address")) + address = atoi(asyncParam(request, "address").c_str()); + if(address == 0) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No repeater address was supplied.\"}")); + } + else { + somfy.linkRepeater(address); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginArray(); + serializeRepeaters(resp); + resp.endArray(); + resp.endResponse(); + } + }); + + // unlinkRepeater + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/unlinkRepeater", + [](AsyncWebServerRequest *request, JsonVariant &json) { + uint32_t address = 0; + if(!json.isNull()) { + Serial.println("Unlinking a repeater"); + JsonObject obj = json.as(); + if(obj.containsKey("address")) address = obj["address"]; + else if(obj.containsKey("remoteAddress")) address = obj["remoteAddress"]; + } + if(address == 0) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No repeater address was supplied.\"}")); + } + else { + somfy.unlinkRepeater(address); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginArray(); + serializeRepeaters(resp); + resp.endArray(); + resp.endResponse(); + } + })); + asyncServer.on("/unlinkRepeater", HTTP_GET, [](AsyncWebServerRequest *request) { + uint32_t address = 0; + if(asyncHasParam(request, "address")) + address = atoi(asyncParam(request, "address").c_str()); + if(address == 0) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No repeater address was supplied.\"}")); + } + else { + somfy.unlinkRepeater(address); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginArray(); + serializeRepeaters(resp); + resp.endArray(); + resp.endResponse(); + } + }); + + // unlinkRemote + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/unlinkRemote", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No remote object supplied.\"}")); return; } + JsonObject obj = json.as(); + if(obj.containsKey("shadeId")) { + SomfyShade* shade = somfy.getShadeById(obj["shadeId"]); + if(shade) { + if(obj.containsKey("remoteAddress")) { + shade->unlinkRemote(obj["remoteAddress"]); + } + else { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Remote address not provided.\"}")); + } + 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\":\"Shade Id not found.\"}")); + } + else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); + })); + + // linkRemote + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/linkRemote", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No remote object supplied.\"}")); return; } + Serial.println("Linking a remote"); + JsonObject obj = json.as(); + if(obj.containsKey("shadeId")) { + SomfyShade* shade = somfy.getShadeById(obj["shadeId"]); + if(shade) { + if(obj.containsKey("remoteAddress")) { + if(obj.containsKey("rollingCode")) shade->linkRemote(obj["remoteAddress"], obj["rollingCode"]); + else shade->linkRemote(obj["remoteAddress"]); + } + else { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Remote address not provided.\"}")); + } + 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\":\"Shade Id not found.\"}")); + } + else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); + })); + + // linkToGroup + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/linkToGroup", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No linking object supplied.\"}")); return; } + Serial.println("Linking a shade to a group"); + JsonObject obj = json.as(); + uint8_t shadeId = obj.containsKey("shadeId") ? obj["shadeId"] : 0; + uint8_t groupId = obj.containsKey("groupId") ? obj["groupId"] : 0; + if(groupId == 0) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group id not provided.\"}")); + return; + } + if(shadeId == 0) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade id not provided.\"}")); + return; + } + SomfyGroup *group = somfy.getGroupById(groupId); + if(!group) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group id not found.\"}")); + return; + } + SomfyShade *shade = somfy.getShadeById(shadeId); + if(!shade) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade id not found.\"}")); + return; + } + group->linkShade(shadeId); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeGroup(group, resp); + resp.endObject(); + resp.endResponse(); + })); + + // unlinkFromGroup + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/unlinkFromGroup", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No unlinking object supplied.\"}")); return; } + Serial.println("Unlinking a shade from a group"); + JsonObject obj = json.as(); + uint8_t shadeId = obj.containsKey("shadeId") ? obj["shadeId"] : 0; + uint8_t groupId = obj.containsKey("groupId") ? obj["groupId"] : 0; + if(groupId == 0) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group id not provided.\"}")); + return; + } + if(shadeId == 0) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade id not provided.\"}")); + return; + } + SomfyGroup *group = somfy.getGroupById(groupId); + if(!group) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group id not found.\"}")); + return; + } + SomfyShade *shade = somfy.getShadeById(shadeId); + if(!shade) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade id not found.\"}")); + return; + } + group->unlinkShade(shadeId); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeGroup(group, resp); + resp.endObject(); + resp.endResponse(); + })); + + // deleteRoom - supports GET with query params and POST/PUT with JSON body + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/deleteRoom", + [](AsyncWebServerRequest *request, JsonVariant &json) { + uint8_t roomId = 0; + if(!json.isNull()) { + Serial.println("Deleting a Room"); + JsonObject obj = json.as(); + if(obj.containsKey("roomId")) roomId = obj["roomId"]; + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room id was supplied.\"}")); return; } + } + SomfyRoom* room = somfy.getRoomById(roomId); + if(!room) request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Room with the specified id not found.\"}")); + else { + somfy.deleteRoom(roomId); + request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Room deleted.\"}")); + } + })); + asyncServer.on("/deleteRoom", HTTP_GET, [](AsyncWebServerRequest *request) { + uint8_t roomId = 0; + if(asyncHasParam(request, "roomId")) { + roomId = atoi(asyncParam(request, "roomId").c_str()); + } + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room object supplied.\"}")); return; } + SomfyRoom* room = somfy.getRoomById(roomId); + if(!room) request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Room with the specified id not found.\"}")); + else { + somfy.deleteRoom(roomId); + request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Room deleted.\"}")); + } + }); + + // deleteShade - supports GET with query params and POST/PUT with JSON body + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/deleteShade", + [](AsyncWebServerRequest *request, JsonVariant &json) { + uint8_t shadeId = 255; + if(!json.isNull()) { + Serial.println("Deleting a shade"); + JsonObject obj = json.as(); + if(obj.containsKey("shadeId")) shadeId = obj["shadeId"]; + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); return; } + } + SomfyShade* shade = somfy.getShadeById(shadeId); + if(!shade) request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}")); + else if(shade->isInGroup()) { + request->send(400, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"This shade is a member of a group and cannot be deleted.\"}")); + } + else { + somfy.deleteShade(shadeId); + request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Shade deleted.\"}")); + } + })); + asyncServer.on("/deleteShade", HTTP_GET, [](AsyncWebServerRequest *request) { + uint8_t shadeId = 255; + if(asyncHasParam(request, "shadeId")) { + shadeId = atoi(asyncParam(request, "shadeId").c_str()); + } + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; } + SomfyShade* shade = somfy.getShadeById(shadeId); + if(!shade) request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}")); + else if(shade->isInGroup()) { + request->send(400, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"This shade is a member of a group and cannot be deleted.\"}")); + } + else { + somfy.deleteShade(shadeId); + request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Shade deleted.\"}")); + } + }); + + // deleteGroup - supports GET with query params and POST/PUT with JSON body + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/deleteGroup", + [](AsyncWebServerRequest *request, JsonVariant &json) { + uint8_t groupId = 255; + if(!json.isNull()) { + Serial.println("Deleting a group"); + JsonObject obj = json.as(); + if(obj.containsKey("groupId")) groupId = obj["groupId"]; + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}")); return; } + } + SomfyGroup *group = somfy.getGroupById(groupId); + if(!group) request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group with the specified id not found.\"}")); + else { + somfy.deleteGroup(groupId); + request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Group deleted.\"}")); + } + })); + asyncServer.on("/deleteGroup", HTTP_GET, [](AsyncWebServerRequest *request) { + uint8_t groupId = 255; + if(asyncHasParam(request, "groupId")) { + groupId = atoi(asyncParam(request, "groupId").c_str()); + } + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); return; } + SomfyGroup *group = somfy.getGroupById(groupId); + if(!group) request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group with the specified id not found.\"}")); + else { + somfy.deleteGroup(groupId); + request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Group deleted.\"}")); + } + }); + + // updateFirmware - file upload + asyncServer.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; + Serial.printf("Update: %s\n", filename.c_str()); + if(!Update.begin(UPDATE_SIZE_UNKNOWN)) { + Update.printError(Serial); + } + else { + somfy.transceiver.end(); + mqtt.end(); + } + } + if(len > 0) { + if(Update.write(data, len) != len) { + Update.printError(Serial); + Serial.printf("Upload of %s aborted invalid size %d\n", filename.c_str(), len); + Update.abort(); + } + } + if(final) { + if(Update.end(true)) { + Serial.printf("Update Success: %u\nRebooting...\n", index + len); + webServer.uploadSuccess = true; + } + else { + Update.printError(Serial); + } + } + esp_task_wdt_reset(); + }); + + // updateShadeConfig - file upload + asyncServer.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) { + Serial.printf("Update: shades.cfg\n"); + 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) { + somfy.loadShadesFile("/shades.tmp"); + } + }); + + // updateApplication - file upload + asyncServer.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; + Serial.printf("Update: %s\n", filename.c_str()); + if(!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) { + Update.printError(Serial); + } + else { + somfy.transceiver.end(); + mqtt.end(); + } + } + if(len > 0) { + if(Update.write(data, len) != len) { + Update.printError(Serial); + Serial.printf("Upload of %s aborted invalid size %d\n", filename.c_str(), len); + Update.abort(); + } + } + if(final) { + if(Update.end(true)) { + webServer.uploadSuccess = true; + Serial.printf("Update Success: %u\nRebooting...\n", index + len); + somfy.commit(); + } + else { + somfy.commit(); + Update.printError(Serial); + } + } + esp_task_wdt_reset(); + }); + + asyncServer.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); + Serial.print("Scanned "); + Serial.print(n); + Serial.println(" networks"); + 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(); + }); + + asyncServer.on("/reboot", WebRequestMethodComposite(HTTP_POST) | HTTP_PUT, [](AsyncWebServerRequest *request) { webServer.handleReboot(request); }); + + // saveSecurity + asyncServer.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(); + settings.Security.fromJSON(obj); + settings.Security.save(); + char token[65]; + webServer.createAPIToken(request->client()->remoteIP(), token); + DynamicJsonDocument sdoc(1024); + JsonObject sobj = sdoc.to(); + 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 + asyncServer.on("/getSecurity", HTTP_GET, [](AsyncWebServerRequest *request) { + DynamicJsonDocument doc(512); + JsonObject obj = doc.to(); + settings.Security.toJSON(obj); + serializeJson(doc, g_async_content, sizeof(g_async_content)); + request->send(200, _encoding_json, g_async_content); + }); + + // saveRadio + asyncServer.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(); + somfy.transceiver.fromJSON(obj); + somfy.transceiver.save(); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + serializeTransceiverConfig(resp); + resp.endObject(); + resp.endResponse(); + })); + + // getRadio + asyncServer.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 + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/sendRemoteCommand", + [](AsyncWebServerRequest *request, JsonVariant &json) { + somfy_frame_t frame; + uint8_t repeats = 0; + if(!json.isNull()) { + JsonObject obj = json.as(); + String scmd; + if(obj.containsKey("address")) frame.remoteAddress = obj["address"]; + if(obj.containsKey("command")) scmd = obj["command"].as(); + if(obj.containsKey("repeats")) repeats = obj["repeats"]; + if(obj.containsKey("rcode")) frame.rollingCode = obj["rcode"]; + if(obj.containsKey("encKey")) frame.encKey = obj["encKey"]; + frame.cmd = translateSomfyCommand(scmd.c_str()); + } + if(frame.remoteAddress > 0 && frame.rollingCode > 0) { + somfy.sendFrame(frame, repeats); + request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Command Sent\"}")); + } + else + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No address or rolling code provided\"}")); + })); + asyncServer.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) { + somfy.sendFrame(frame, repeats); + request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Command Sent\"}")); + } + else + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No address or rolling code provided\"}")); + }); + + // setgeneral + asyncServer.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(); + if(obj.containsKey("hostname") || obj.containsKey("ssdpBroadcast") || obj.containsKey("checkForUpdate")) { + bool checkForUpdate = settings.checkForUpdate; + settings.fromJSON(obj); + settings.save(); + if(settings.checkForUpdate != checkForUpdate) git.emitUpdateCheck(); + if(obj.containsKey("hostname")) net.updateHostname(); + } + if(obj.containsKey("ntpServer") || obj.containsKey("ntpServer")) { + settings.NTP.fromJSON(obj); + settings.NTP.save(); + } + request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set General Settings\"}"); + })); + + // setNetwork + asyncServer.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(); + bool reboot = false; + if(obj.containsKey("connType") && obj["connType"].as() != static_cast(settings.connType)) { + settings.connType = static_cast(obj["connType"].as()); + settings.save(); + reboot = true; + } + if(obj.containsKey("wifi")) { + JsonObject objWifi = obj["wifi"]; + if(settings.connType == conn_types_t::wifi) { + if(objWifi.containsKey("ssid") && objWifi["ssid"].as().compareTo(settings.WIFI.ssid) != 0) { + if(WiFi.softAPgetStationNum() == 0) reboot = true; + } + if(objWifi.containsKey("passphrase") && objWifi["passphrase"].as().compareTo(settings.WIFI.passphrase) != 0) { + if(WiFi.softAPgetStationNum() == 0) reboot = true; + } + } + settings.WIFI.fromJSON(objWifi); + settings.WIFI.save(); + } + if(obj.containsKey("ethernet")) { + JsonObject objEth = obj["ethernet"]; + if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref) + reboot = true; + settings.Ethernet.fromJSON(objEth); + settings.Ethernet.save(); + } + if(reboot) { + Serial.println("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 + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/setIP", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { + request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}"); + return; + } + Serial.println("Setting IP..."); + JsonObject obj = json.as(); + settings.IP.fromJSON(obj); + settings.IP.save(); + request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set Network Settings\"}"); + })); + + // connectwifi + asyncServer.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(); + Serial.println("Settings WIFI connection..."); + String ssid = ""; + String passphrase = ""; + if(obj.containsKey("ssid")) ssid = obj["ssid"].as(); + if(obj.containsKey("passphrase")) passphrase = obj["passphrase"].as(); + 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) { + Serial.println("Rebooting ESP for new WiFi settings..."); + rebootDelay.reboot = true; + rebootDelay.rebootTime = millis() + 1000; + } + } + })); + + // modulesettings + asyncServer.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(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 + asyncServer.on("/networksettings", HTTP_GET, [](AsyncWebServerRequest *request) { + DynamicJsonDocument doc(2048); + JsonObject obj = doc.to(); + settings.toJSON(obj); + obj["fwVersion"] = settings.fwVersion.name; + JsonObject eth = obj.createNestedObject("ethernet"); + settings.Ethernet.toJSON(eth); + 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 + asyncServer.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(); + Serial.print("Saving MQTT "); + mqtt.disconnect(); + settings.MQTT.fromJSON(obj); + settings.MQTT.save(); + DynamicJsonDocument sdoc(1024); + JsonObject sobj = sdoc.to(); + settings.MQTT.toJSON(sobj); + serializeJson(sdoc, g_async_content, sizeof(g_async_content)); + request->send(200, _encoding_json, g_async_content); + })); + + // mqttsettings + asyncServer.on("/mqttsettings", HTTP_GET, [](AsyncWebServerRequest *request) { + DynamicJsonDocument doc(1024); + JsonObject obj = doc.to(); + settings.MQTT.toJSON(obj); + serializeJson(doc, g_async_content, sizeof(g_async_content)); + request->send(200, _encoding_json, g_async_content); + }); + + // roomSortOrder + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/roomSortOrder", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { + request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}"); + return; + } + JsonArray arr = json.as(); + uint8_t order = 0; + for(JsonVariant v : arr) { + uint8_t roomId = v.as(); + if(roomId != 0) { + SomfyRoom *room = somfy.getRoomById(roomId); + if(room) room->sortOrder = order++; + } + } + request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set room order\"}"); + })); + + // shadeSortOrder + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/shadeSortOrder", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { + request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}"); + return; + } + JsonArray arr = json.as(); + uint8_t order = 0; + for(JsonVariant v : arr) { + uint8_t shadeId = v.as(); + if(shadeId != 255) { + SomfyShade *shade = somfy.getShadeById(shadeId); + if(shade) shade->sortOrder = order++; + } + } + request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set shade order\"}"); + })); + + // groupSortOrder + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/groupSortOrder", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { + request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}"); + return; + } + JsonArray arr = json.as(); + uint8_t order = 0; + for(JsonVariant v : arr) { + uint8_t groupId = v.as(); + if(groupId != 255) { + SomfyGroup *group = somfy.getGroupById(groupId); + if(group) group->sortOrder = order++; + } + } + request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set group order\"}"); + })); + + asyncServer.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(); + }); + + asyncServer.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(); + }); + + asyncServer.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!!!\"}"); + } + }); + + asyncServer.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 + asyncServer.serveStatic("/", LittleFS, "/").setDefaultFile("index.html"); + + asyncServer.begin(); +} diff --git a/src/Web.h b/src/Web.h new file mode 100644 index 0000000..7cf3150 --- /dev/null +++ b/src/Web.h @@ -0,0 +1,40 @@ +#include +#include +#include "Somfy.h" +#ifndef webserver_h +#define webserver_h +class Web { + public: + bool uploadSuccess = false; + void startup(); + void begin(); + void loop(); + // Auth helpers + bool createAPIToken(const IPAddress ipAddress, char *token); + bool createAPIToken(const char *payload, char *token); + bool createAPIPinToken(const IPAddress ipAddress, const char *pin, char *token); + bool createAPIPasswordToken(const IPAddress ipAddress, const char *username, const char *password, char *token); + bool isAuthenticated(AsyncWebServerRequest *request, bool cfg = false); + + // Async API handlers + void handleDiscovery(AsyncWebServerRequest *request); + void handleGetRooms(AsyncWebServerRequest *request); + void handleGetShades(AsyncWebServerRequest *request); + void handleGetGroups(AsyncWebServerRequest *request); + void handleController(AsyncWebServerRequest *request); + void handleRoom(AsyncWebServerRequest *request); + void handleShade(AsyncWebServerRequest *request); + void handleGroup(AsyncWebServerRequest *request); + void handleLogin(AsyncWebServerRequest *request, JsonVariant &json); + void handleShadeCommand(AsyncWebServerRequest *request, JsonVariant &json); + void handleGroupCommand(AsyncWebServerRequest *request, JsonVariant &json); + void handleTiltCommand(AsyncWebServerRequest *request, JsonVariant &json); + void handleRepeatCommand(AsyncWebServerRequest *request, JsonVariant &json); + void handleSetPositions(AsyncWebServerRequest *request, JsonVariant &json); + void handleSetSensor(AsyncWebServerRequest *request, JsonVariant &json); + void handleDownloadFirmware(AsyncWebServerRequest *request); + void handleBackup(AsyncWebServerRequest *request); + void handleReboot(AsyncWebServerRequest *request); + void handleNotFound(AsyncWebServerRequest *request); +}; +#endif diff --git a/esp32.svd b/src/esp32.svd similarity index 100% rename from esp32.svd rename to src/esp32.svd diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html