diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6b4bec0..8e0a5c1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,150 +2,50 @@ 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 + - env: esp32c3 + name: ESP32-C3 + - env: esp32s3 + name: ESP32-S3 + - env: esp32c6 + name: ESP32-C6 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..eeb3401 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,27 +1,57 @@ 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: + # fwname: firmware-only binary for OTA updates + # obname: onboard image (bootloader + partitions + firmware + littlefs merged) + # for flashing a new chip via USB/serial + # addr_bootloader: chip-dependent (ESP32: 0x1000, C3/S3: 0x0) + # addr_fs: must match spiffs/littlefs offset in esp32_3MB.csv + include: + - env: esp32dev + name: ESP32 + chip: ESP32 + addr_bootloader: "0x1000" + addr_fs: "0x310000" + fwname: SomfyController.ino.esp32.bin + fsname: SomfyController.littlefs.esp32.bin + obname: SomfyController.onboard.esp32.bin + - env: esp32c3 + name: ESP32-C3 + chip: ESP32-C3 + addr_bootloader: "0x0" + addr_fs: "0x310000" + fwname: SomfyController.ino.esp32c3.bin + fsname: SomfyController.littlefs.esp32c3.bin + obname: SomfyController.onboard.esp32c3.bin + - env: esp32s3 + name: ESP32-S3 + chip: ESP32-S3 + addr_bootloader: "0x0" + addr_fs: "0x670000" + fwname: SomfyController.ino.esp32s3.bin + fsname: SomfyController.littlefs.esp32s3.bin + obname: SomfyController.onboard.esp32s3.bin + - env: esp32c6 + name: ESP32-C6 + chip: ESP32-C6 + addr_bootloader: "0x0" + addr_fs: "0x310000" + fwname: SomfyController.ino.esp32c6.bin + fsname: SomfyController.littlefs.esp32c6.bin + obname: SomfyController.onboard.esp32c6.bin + steps: - name: Get Release id: get_release @@ -32,164 +62,61 @@ jobs: - name: Check out code uses: actions/checkout@v4 - - name: Checkout mklittlefs - uses: actions/checkout@v4 - with: - repository: earlephilhower/mklittlefs - path: mklittlefs - ref: ${{ env.MKLITTLEFS_VERSION }} - - - name: Checkout LittleFS - uses: actions/checkout@v4 - with: - repository: littlefs-project/littlefs - path: mklittlefs/littlefs - ref: ${{ env.LITTLEFS_VERSION }} - - - name: Build mklittlefs + - name: Update version from release tag run: | - make -C mklittlefs + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" + sed -i "s/#define FW_VERSION \"v[0-9.]*\"/#define FW_VERSION \"v${VERSION}\"/" src/ConfigSettings.h + sed -i "s/^[0-9.].*/${VERSION}/" data-src/appversion + sed -i "s/\?v=[0-9.]*c/?v=${VERSION}c/g" data-src/index.html + sed -i "s/appVersion = 'v[0-9.]*'/appVersion = 'v${VERSION}'/" data-src/index.js - - name: Create LittleFS - run: | - ./mklittlefs/mklittlefs --create data --size 1441792 SomfyController.littlefs.bin - - - name: Upload binaries - uses: actions/upload-artifact@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - name: LittleFS - path: SomfyController.littlefs.bin - retention-days: 5 + python-version: "3.12" + + - name: Install PlatformIO and esptool + run: pip install platformio esptool + + - 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: | + 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 \ + ${{ matrix.addr_fs }} .pio/build/${{ matrix.env }}/littlefs.bin + + - name: Compress onboard image + run: zip ${{ matrix.obname }}.zip ${{ matrix.obname }} - name: Upload LittleFS uses: shogo82148/actions-upload-release-asset@v1.7.5 with: github_token: ${{ github.token }} upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_name: SomfyController.littlefs.bin - asset_path: SomfyController.littlefs.bin + asset_name: ${{ matrix.fsname }} + 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 +124,3 @@ jobs: asset_path: ${{ matrix.obname }}.zip overwrite: true asset_content_type: application/zip - diff --git a/.gitignore b/.gitignore index be96788..d384d46 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,12 @@ 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/ +elf_archive/ \ No newline at end of file diff --git a/README.md b/README.md index 53aedd2..57a6899 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +## This is a fork which was created because the original repo seems to be bandoned and I was annoyed by constant restarts of my ESPSomfy. I've fixed some of the major issues caused by watchdog restarts. There are also compiled binaries in my fork if someone is experiencing same issues + + * Fixed a lot of bugs causing constant restarts by migrating to AsyncWebServer and AsyncSockets + * Moved logging from serial to esp dedicated logging lib + * Gzipped the resources + * Changed to platformio build + * Backup does no longer use LITTLEFS. Only RAM is used. Storage should not wear out so quick. + * Added uptime info in web UI + # ESPSomfy-RTS A controller for Somfy RTS blinds and shades that supports up to 32 individual shades and 16 groups over 433MHz RTx protocols. If you have IO Home Control motors this project is not for you but you can use the IO Remote protocol to connect the ESPSomfy RTS device to a disected remote. Look in the [Wiki](https://github.com/rstrouse/ESPSomfy-RTS/wiki/Controlling-Motors-with-GPIO) for options and verify whether the solution is workable for you. @@ -73,12 +82,3 @@ You can find the documentation for the interfaces in the [Integrations](https:// I spent some time reading about a myriad of topics but in the end the primary source for this project comes from https://pushstack.wordpress.com/somfy-rts-protocol/. The work done on pushstack regarding the protocol timing made this feasible without burning a bunch of time measuring pulses. Configuration of the Transceiver is done with the ELECHOUSE_CC1101 library which you will need to include in your project should you want to compile the code. The one used for compiling this module can be found here. https://github.com/LSatan/SmartRC-CC1101-Driver-Lib - - - - - - - - - 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/SomfyController.ino b/SomfyController.ino deleted file mode 100644 index 464d179..0000000 --- a/SomfyController.ino +++ /dev/null @@ -1,86 +0,0 @@ -#include -#include -#include -#include "ConfigSettings.h" -#include "Network.h" -#include "Web.h" -#include "Sockets.h" -#include "Utils.h" -#include "Somfy.h" -#include "MQTT.h" -#include "GitOTA.h" - -ConfigSettings settings; -Web webServer; -SocketEmitter sockEmit; -Network net; -rebootDelay_t rebootDelay; -SomfyShadeController somfy; -MQTTClass mqtt; -GitUpdater git; - -uint32_t oldheap = 0; -void setup() { - Serial.begin(115200); - Serial.println(); - Serial.println("Startup/Boot...."); - Serial.println("Mounting File System..."); - if(LittleFS.begin()) Serial.println("File system mounted successfully"); - else Serial.println("Error mounting file system"); - settings.begin(); - if(WiFi.status() == WL_CONNECTED) WiFi.disconnect(true); - delay(10); - Serial.println(); - webServer.startup(); - webServer.begin(); - delay(1000); - net.setup(); - somfy.begin(); - //git.checkForUpdate(); - esp_task_wdt_init(7, true); //enable panic so ESP32 restarts - esp_task_wdt_add(NULL); //add current thread to WDT watch - -} - -void loop() { - // put your main code here, to run repeatedly: - //uint32_t heap = ESP.getFreeHeap(); - if(rebootDelay.reboot && millis() > rebootDelay.rebootTime) { - Serial.print("Rebooting after "); - Serial.print(rebootDelay.rebootTime); - Serial.println("ms"); - net.end(); - ESP.restart(); - return; - } - uint32_t timing = millis(); - - net.loop(); - if(millis() - timing > 100) Serial.printf("Timing Net: %ldms\n", millis() - timing); - timing = millis(); - esp_task_wdt_reset(); - somfy.loop(); - if(millis() - timing > 100) Serial.printf("Timing Somfy: %ldms\n", millis() - timing); - timing = millis(); - esp_task_wdt_reset(); - if(net.connected() || net.softAPOpened) { - if(!rebootDelay.reboot && net.connected() && !net.softAPOpened) { - git.loop(); - esp_task_wdt_reset(); - } - webServer.loop(); - esp_task_wdt_reset(); - if(millis() - timing > 100) Serial.printf("Timing WebServer: %ldms\n", millis() - timing); - esp_task_wdt_reset(); - timing = millis(); - sockEmit.loop(); - if(millis() - timing > 100) Serial.printf("Timing Socket: %ldms\n", millis() - timing); - esp_task_wdt_reset(); - timing = millis(); - } - if(rebootDelay.reboot && millis() > rebootDelay.rebootTime) { - net.end(); - ESP.restart(); - } - esp_task_wdt_reset(); -} diff --git a/SomfyController.ino.esp32.bin b/SomfyController.ino.esp32.bin deleted file mode 100644 index bb87d58..0000000 Binary files a/SomfyController.ino.esp32.bin and /dev/null differ diff --git a/SomfyController.ino.esp32s3.bin b/SomfyController.ino.esp32s3.bin deleted file mode 100644 index 76f303c..0000000 Binary files a/SomfyController.ino.esp32s3.bin and /dev/null differ diff --git a/SomfyController.littlefs.bin b/SomfyController.littlefs.bin deleted file mode 100644 index 98d9f83..0000000 Binary files a/SomfyController.littlefs.bin and /dev/null differ 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/archive_elf.py b/archive_elf.py new file mode 100644 index 0000000..0e5416e --- /dev/null +++ b/archive_elf.py @@ -0,0 +1,45 @@ +""" +PlatformIO post-build script: archive firmware.elf files. + +Copies firmware.elf to elf_archive/ with a timestamp after each build. +Keeps only the last 10 files to avoid filling up disk space. + +Usage in platformio.ini +----------------------- + extra_scripts = post:archive_elf.py +""" + +Import("env") + +import os +import shutil +from datetime import datetime + +MAX_ARCHIVES = 10 +ARCHIVE_DIR = os.path.join(env.subst("$PROJECT_DIR"), "elf_archive") + + +def archive_elf(source, target, env): + elf_path = os.path.join(env.subst("$BUILD_DIR"), "firmware.elf") + if not os.path.isfile(elf_path): + print("[archive_elf] firmware.elf not found, skipping.") + return + + os.makedirs(ARCHIVE_DIR, exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + dest = os.path.join(ARCHIVE_DIR, f"firmware_{timestamp}.elf") + shutil.copy2(elf_path, dest) + print(f"[archive_elf] Saved {dest}") + + # Keep only the last MAX_ARCHIVES files + files = sorted( + [f for f in os.listdir(ARCHIVE_DIR) if f.endswith(".elf")], + ) + while len(files) > MAX_ARCHIVES: + old = os.path.join(ARCHIVE_DIR, files.pop(0)) + os.remove(old) + print(f"[archive_elf] Removed old archive {old}") + + +env.AddPostAction("$BUILD_DIR/firmware.elf", archive_elf) 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/esp32_3MB.csv b/esp32_3MB.csv new file mode 100644 index 0000000..9457d66 --- /dev/null +++ b/esp32_3MB.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size +nvs, data, nvs, 0x9000, 0x5000 +otadata, data, ota, 0xE000, 0x2000 +app0, app, ota_0, 0x10000, 0x180000 +app1, app, ota_1, 0x190000, 0x180000 +spiffs, data, spiffs, 0x310000, 0x0F0000 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/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..3cbb462 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,66 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +default_envs = esp32devdbg + +[env] +platform = espressif32 @ 5.4.0 +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 + post:archive_elf.py + +board_build.filesystem = littlefs +build_flags = + -DCORE_DEBUG_LEVEL=1 + -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 +board_build.partitions = esp32_3MB.csv + +[env:esp32devdbg] +board = esp32dev +build_type = debug +board_build.partitions = esp32_3MB.csv + +[env:esp32c3] +board = esp32-c3-devkitm-1 +board_build.partitions = esp32_3MB.csv + +[env:esp32s3] +board = esp32-s3-devkitc-1 + +[env:esp32c6] +platform = https://github.com/mnowak32/platform-espressif32.git#boards/seeed_xiao_esp32c6 +platform_packages = + framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.4 + framework-arduinoespressif32-libs @ https://github.com/espressif/arduino-esp32/releases/download/3.0.4/esp32-arduino-libs-3.0.4.zip +board = seeed_xiao_esp32c6 +board_build.partitions = esp32_3MB.csv +build_flags = + ${env.build_flags} + -I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src diff --git a/ConfigFile.cpp b/src/ConfigFile.cpp similarity index 90% rename from ConfigFile.cpp rename to src/ConfigFile.cpp index e515bed..d496632 100644 --- a/ConfigFile.cpp +++ b/src/ConfigFile.cpp @@ -1,10 +1,13 @@ #include #include #include +#include "esp_log.h" #include "ConfigFile.h" #include "Utils.h" #include "ConfigSettings.h" +static const char *TAG = "ConfigFile"; + extern Preferences pref; #define SHADE_HDR_VER 24 @@ -22,10 +25,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; } @@ -63,7 +71,7 @@ bool ConfigFile::writeHeader(const config_header_t &hdr) { bool ConfigFile::readHeader() { if(!this->isOpen()) return false; //if(this->file.position() != 0) this->file.seek(0, SeekSet); - Serial.printf("Reading header at %u\n", this->file.position()); + ESP_LOGD(TAG, "Reading header at %u", this->file.position()); this->header.version = this->readUInt8(this->header.version); this->header.length = this->readUInt8(0); if(this->header.version >= 19) { @@ -88,7 +96,7 @@ bool ConfigFile::readHeader() { this->header.transRecordSize = this->readUInt16(this->header.transRecordSize); this->readString(this->header.serverId, sizeof(this->header.serverId)); } - Serial.printf("version:%u len:%u roomSize:%u roomRecs:%u shadeSize:%u shadeRecs:%u groupSize:%u groupRecs: %u pos:%d\n", this->header.version, this->header.length, this->header.roomRecordSize, this->header.roomRecords, this->header.shadeRecordSize, this->header.shadeRecords, this->header.groupRecordSize, this->header.groupRecords, this->file.position()); + ESP_LOGD(TAG, "version:%u len:%u roomSize:%u roomRecs:%u shadeSize:%u shadeRecs:%u groupSize:%u groupRecs: %u pos:%d", this->header.version, this->header.length, this->header.roomRecordSize, this->header.roomRecords, this->header.shadeRecordSize, this->header.shadeRecords, this->header.groupRecordSize, this->header.groupRecords, this->file.position()); return true; } /* @@ -187,10 +195,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 +216,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 +231,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; } @@ -384,39 +406,34 @@ bool ShadeConfigFile::backup(SomfyShadeController *s) { bool ShadeConfigFile::validate() { this->readHeader(); if(this->header.version < 1) { - Serial.print("Invalid Header Version:"); - Serial.println(this->header.version); + ESP_LOGE(TAG, "Invalid Header Version:%u", this->header.version); return false; } if(this->header.shadeRecordSize < 100) { - Serial.print("Invalid Shade Record Size:"); - Serial.println(this->header.shadeRecordSize); + ESP_LOGE(TAG, "Invalid Shade Record Size:%u", this->header.shadeRecordSize); return false; } /* if(this->header.shadeRecords != SOMFY_MAX_SHADES) { - Serial.print("Invalid Shade Record Count:"); - Serial.println(this->header.shadeRecords); + ESP_LOGE(TAG, "Invalid Shade Record Count:%u", this->header.shadeRecords); return false; } */ if(this->header.version > 10) { if(this->header.groupRecordSize < 100) { - Serial.print("Invalid Group Record Size:"); - Serial.println(this->header.groupRecordSize); + ESP_LOGE(TAG, "Invalid Group Record Size:%u", this->header.groupRecordSize); return false; } /* if(this->header.groupRecords != SOMFY_MAX_GROUPS) { - Serial.print("Invalid Group Record Count:"); - Serial.println(this->header.groupRecords); + ESP_LOGE(TAG, "Invalid Group Record Count:%u", this->header.groupRecords); return false; } */ } if(this->file.position() != this->header.length) { - Serial.printf("File not positioned at %u end of header: %d\n", this->header.length, this->file.position()); + ESP_LOGE(TAG, "File not positioned at %u end of header: %d", this->header.length, this->file.position()); return false; } @@ -433,7 +450,7 @@ bool ShadeConfigFile::validate() { fsize += (this->header.repeaterRecordSize * this->header.repeaterRecords); } if(this->file.size() != fsize) { - Serial.printf("File size is not correct should be %d and got %d\n", fsize, this->file.size()); + ESP_LOGE(TAG, "File size is not correct should be %d and got %d", fsize, this->file.size()); } // Next check to see if the records match the header length. uint8_t recs = 0; @@ -442,11 +459,11 @@ bool ShadeConfigFile::validate() { while(recs < this->header.roomRecords) { uint32_t pos = this->file.position(); if(!this->seekChar(CFG_REC_END)) { - Serial.printf("Failed to find the room record end %d\n", recs); + ESP_LOGE(TAG, "Failed to find the room record end %d", recs); return false; } if(this->file.position() - pos != this->header.roomRecordSize) { - Serial.printf("Room record length is %d and should be %d\n", this->file.position() - pos, this->header.roomRecordSize); + ESP_LOGE(TAG, "Room record length is %d and should be %d", this->file.position() - pos, this->header.roomRecordSize); return false; } recs++; @@ -456,11 +473,11 @@ bool ShadeConfigFile::validate() { while(recs < this->header.shadeRecords) { uint32_t pos = this->file.position(); if(!this->seekChar(CFG_REC_END)) { - Serial.printf("Failed to find the shade record end %d\n", recs); + ESP_LOGE(TAG, "Failed to find the shade record end %d", recs); return false; } if(this->file.position() - pos != this->header.shadeRecordSize) { - Serial.printf("Shade record length is %d and should be %d\n", this->file.position() - pos, this->header.shadeRecordSize); + ESP_LOGE(TAG, "Shade record length is %d and should be %d", this->file.position() - pos, this->header.shadeRecordSize); return false; } recs++; @@ -470,12 +487,12 @@ bool ShadeConfigFile::validate() { while(recs < this->header.groupRecords) { uint32_t pos = this->file.position(); if(!this->seekChar(CFG_REC_END)) { - Serial.printf("Failed to find the group record end %d\n", recs); + ESP_LOGE(TAG, "Failed to find the group record end %d", recs); return false; } recs++; if(this->file.position() - pos != this->header.groupRecordSize) { - Serial.printf("Group record length is %d and should be %d\n", this->file.position() - pos, this->header.groupRecordSize); + ESP_LOGE(TAG, "Group record length is %d and should be %d", this->file.position() - pos, this->header.groupRecordSize); return false; } } @@ -485,7 +502,7 @@ bool ShadeConfigFile::validate() { while(recs < this->header.repeaterRecords) { //uint32_t pos = this->file.position(); if(!this->seekChar(CFG_REC_END)) { - Serial.printf("Failed to find the repeater record end %d\n", recs); + ESP_LOGE(TAG, "Failed to find the repeater record end %d", recs); } recs++; @@ -515,30 +532,27 @@ bool ShadeConfigFile::restore(SomfyShadeController *s, const char *filename, res bool ShadeConfigFile::restoreFile(SomfyShadeController *s, const char *filename, restore_options_t &opts) { bool opened = false; if(!this->isOpen()) { - Serial.println("Opening shade restore file"); + ESP_LOGI(TAG, "Opening shade restore file"); this->begin(filename, true); opened = true; } if(!this->validate()) { - Serial.println("Shade restore file invalid!"); + ESP_LOGE(TAG, "Shade restore file invalid!"); if(opened) this->end(); return false; } if(opts.shades) { - Serial.println("Restoring Rooms..."); + ESP_LOGI(TAG, "Restoring Rooms..."); for(uint8_t i = 0; i < this->header.roomRecords; i++) { this->readRoomRecord(&s->rooms[i]); - if(i > 0) Serial.print(","); - Serial.print(s->rooms[i].roomId); + ESP_LOGD(TAG, "Room %u", s->rooms[i].roomId); } - Serial.println("Restoring Shades..."); + ESP_LOGI(TAG, "Restoring Shades..."); // We should be valid so start reading. for(uint8_t i = 0; i < this->header.shadeRecords; i++) { this->readShadeRecord(&s->shades[i]); - if(i > 0) Serial.print(","); - Serial.print(s->shades[i].getShadeId()); + ESP_LOGD(TAG, "Shade %u", s->shades[i].getShadeId()); } - Serial.println(""); if(this->header.shadeRecords < SOMFY_MAX_SHADES) { uint8_t ndx = this->header.shadeRecords; // Clear out any positions that are not in the shade file. @@ -546,13 +560,11 @@ bool ShadeConfigFile::restoreFile(SomfyShadeController *s, const char *filename, ((SomfyShade *)&s->shades[ndx++])->clear(); } } - Serial.println("Restoring Groups..."); + ESP_LOGI(TAG, "Restoring Groups..."); for(uint8_t i = 0; i < this->header.groupRecords; i++) { - if(i > 0) Serial.print(","); - Serial.print(s->groups[i].getGroupId()); + ESP_LOGD(TAG, "Group %u", s->groups[i].getGroupId()); this->readGroupRecord(&s->groups[i]); } - Serial.println(""); if(this->header.groupRecords < SOMFY_MAX_GROUPS) { uint8_t ndx = this->header.groupRecords; // Clear out any positions that are not in the shade file. @@ -562,14 +574,14 @@ bool ShadeConfigFile::restoreFile(SomfyShadeController *s, const char *filename, } } else { - Serial.println("Shade data ignored"); + ESP_LOGI(TAG, "Shade data ignored"); // FF past the shades and groups. this->file.seek(this->file.position() + (this->header.shadeRecords * this->header.shadeRecordSize) + (this->header.groupRecords * this->header.groupRecordSize), SeekSet); // Start at the beginning of the file after the header. } if(opts.repeaters) { - Serial.println("Restoring Repeaters..."); + ESP_LOGI(TAG, "Restoring Repeaters..."); if(this->header.repeaterRecords > 0) { memset(s->repeaters, 0x00, sizeof(uint32_t) * SOMFY_MAX_REPEATERS); for(uint8_t i = 0; i < this->header.repeaterRecords; i++) { @@ -604,7 +616,9 @@ bool ShadeConfigFile::restoreFile(SomfyShadeController *s, const char *filename, if(opts.network) { settings.IP.save(); settings.WIFI.save(); +#ifndef CONFIG_IDF_TARGET_ESP32C6 settings.Ethernet.save(); +#endif } if(opts.mqtt) settings.MQTT.save(); return true; @@ -613,7 +627,7 @@ bool ShadeConfigFile::readNetRecord(restore_options_t &opts) { if(this->header.netRecordSize > 0) { uint32_t startPos = this->file.position(); if(opts.network) { - Serial.println("Reading network settings from file..."); + ESP_LOGD(TAG, "Reading network settings from file..."); settings.connType = static_cast(this->readUInt8(static_cast(conn_types_t::unset))); settings.IP.dhcp = this->readBool(true); char ip[24]; @@ -659,7 +673,8 @@ bool ShadeConfigFile::readNetRecord(restore_options_t &opts) { // the ethernet phy settings. if(opts.network) { if(strncmp(settings.serverId, this->header.serverId, sizeof(settings.serverId)) == 0) { - Serial.println("Restoring Ethernet adapter settings"); + ESP_LOGI(TAG, "Restoring Ethernet adapter settings"); +#ifndef CONFIG_IDF_TARGET_ESP32C6 settings.Ethernet.boardType = this->readUInt8(1); settings.Ethernet.phyType = static_cast(this->readUInt8(0)); settings.Ethernet.CLKMode = static_cast(this->readUInt8(0)); @@ -667,10 +682,11 @@ bool ShadeConfigFile::readNetRecord(restore_options_t &opts) { settings.Ethernet.PWRPin = this->readInt8(1); settings.Ethernet.MDCPin = this->readInt8(16); settings.Ethernet.MDIOPin = this->readInt8(23); +#endif } } if(this->file.position() != startPos + this->header.netRecordSize) { - Serial.println("Reading to end of network record"); + ESP_LOGD(TAG, "Reading to end of network record"); this->seekChar(CFG_REC_END); } } @@ -679,7 +695,7 @@ bool ShadeConfigFile::readNetRecord(restore_options_t &opts) { bool ShadeConfigFile::readTransRecord(transceiver_config_t &cfg) { if(this->header.transRecordSize > 0) { uint32_t startPos = this->file.position(); - Serial.println("Reading Transceiver settings from file..."); + ESP_LOGD(TAG, "Reading Transceiver settings from file..."); cfg.enabled = this->readBool(false); cfg.proto = static_cast(this->readUInt8(0)); cfg.type = this->readUInt8(56); @@ -694,7 +710,7 @@ bool ShadeConfigFile::readTransRecord(transceiver_config_t &cfg) { cfg.deviation = this->readFloat(cfg.deviation); cfg.txPower = this->readInt8(cfg.txPower); if(this->file.position() != startPos + this->header.transRecordSize) { - Serial.println("Reading to end of transceiver record"); + ESP_LOGD(TAG, "Reading to end of transceiver record"); this->seekChar(CFG_REC_END); } @@ -704,7 +720,7 @@ bool ShadeConfigFile::readTransRecord(transceiver_config_t &cfg) { bool ShadeConfigFile::readSettingsRecord() { if(this->header.settingsRecordSize > 0) { uint32_t startPos = this->file.position(); - Serial.println("Reading settings from file..."); + ESP_LOGD(TAG, "Reading settings from file..."); char ver[24]; this->readVarString(ver, sizeof(ver)); this->readVarString(settings.hostname, sizeof(settings.hostname)); @@ -713,7 +729,7 @@ bool ShadeConfigFile::readSettingsRecord() { settings.ssdpBroadcast = this->readBool(false); if(this->header.version >= 20) settings.checkForUpdate = this->readBool(true); if(this->file.position() != startPos + this->header.settingsRecordSize) { - Serial.println("Reading to end of settings record"); + ESP_LOGD(TAG, "Reading to end of settings record"); this->seekChar(CFG_REC_END); } } @@ -753,7 +769,7 @@ bool ShadeConfigFile::readGroupRecord(SomfyGroup *group) { pref.end(); if(this->file.position() != startPos + this->header.groupRecordSize) { - Serial.println("Reading to end of group record"); + ESP_LOGD(TAG, "Reading to end of group record"); this->seekChar(CFG_REC_END); } return true; @@ -765,7 +781,7 @@ bool ShadeConfigFile::readRepeaterRecord(SomfyShadeController *s) { s->linkRepeater(this->readUInt32(0)); } if(this->file.position() != startPos + this->header.repeaterRecordSize) { - Serial.println("Reading to end of repeater record"); + ESP_LOGD(TAG, "Reading to end of repeater record"); this->seekChar(CFG_REC_END); } return true; @@ -776,7 +792,7 @@ bool ShadeConfigFile::readRoomRecord(SomfyRoom *room) { this->readString(room->name, sizeof(room->name)); room->sortOrder = this->readUInt8(room->roomId - 1); if(this->file.position() != startPos + this->header.roomRecordSize) { - Serial.println("Reading to end of room record"); + ESP_LOGD(TAG, "Reading to end of room record"); this->seekChar(CFG_REC_END); } return true; @@ -862,7 +878,7 @@ bool ShadeConfigFile::readShadeRecord(SomfyShade *shade) { pinMode(shade->gpioMy, OUTPUT); if(this->header.version >= 19) shade->roomId = this->readUInt8(0); if(this->file.position() != startPos + this->header.shadeRecordSize) { - Serial.println("Reading to end of shade record"); + ESP_LOGD(TAG, "Reading to end of shade record"); this->seekChar(CFG_REC_END); } return true; @@ -870,12 +886,12 @@ bool ShadeConfigFile::readShadeRecord(SomfyShade *shade) { bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) { bool opened = false; if(!this->isOpen()) { - Serial.println("Opening shade config file"); + ESP_LOGI(TAG, "Opening shade config file"); this->begin(filename, true); opened = true; } if(!this->validate()) { - Serial.println("Shade config file invalid!"); + ESP_LOGE(TAG, "Shade config file invalid!"); if(opened) this->end(); return false; } @@ -917,7 +933,7 @@ bool ShadeConfigFile::loadFile(SomfyShadeController *s, const char *filename) { this->readRepeaterRecord(s); } if(opened) { - Serial.println("Closing shade config file"); + ESP_LOGI(TAG, "Closing shade config file"); this->end(); } return true; @@ -1023,6 +1039,7 @@ bool ShadeConfigFile::writeNetRecord() { this->writeBool(settings.MQTT.pubDisco); this->writeVarString(settings.MQTT.rootTopic); this->writeVarString(settings.MQTT.discoTopic); +#ifndef CONFIG_IDF_TARGET_ESP32C6 this->writeUInt8(settings.Ethernet.boardType); this->writeUInt8(static_cast(settings.Ethernet.phyType)); this->writeUInt8(static_cast(settings.Ethernet.CLKMode)); @@ -1030,6 +1047,9 @@ bool ShadeConfigFile::writeNetRecord() { this->writeInt8(settings.Ethernet.PWRPin); this->writeInt8(settings.Ethernet.MDCPin); this->writeInt8(settings.Ethernet.MDIOPin, CFG_REC_END); +#else + this->writeUInt8(0, CFG_REC_END); +#endif return true; } bool ShadeConfigFile::writeTransRecord(transceiver_config_t &cfg) { 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 73% rename from ConfigSettings.cpp rename to src/ConfigSettings.cpp index 5deec5e..cb9a6a9 100644 --- a/ConfigSettings.cpp +++ b/src/ConfigSettings.cpp @@ -6,16 +6,19 @@ #include "ConfigSettings.h" #include "Utils.h" #include "esp_chip_info.h" +#include "esp_log.h" + +static const char *TAG = "Config"; Preferences pref; void restore_options_t::fromJSON(JsonObject &obj) { - if(obj.containsKey("shades")) this->shades = obj["shades"]; - if(obj.containsKey("settings")) this->settings = obj["settings"]; - if(obj.containsKey("network")) this->network = obj["network"]; - if(obj.containsKey("transceiver")) this->transceiver = obj["transceiver"]; - if(obj.containsKey("repeaters")) this->repeaters = obj["repeaters"]; - if(obj.containsKey("mqtt")) this->mqtt = obj["mqtt"]; + if(!obj["shades"].isNull()) this->shades = obj["shades"]; + if(!obj["settings"].isNull()) this->settings = obj["settings"]; + if(!obj["network"].isNull()) this->network = obj["network"]; + if(!obj["transceiver"].isNull()) this->transceiver = obj["transceiver"]; + if(!obj["repeaters"].isNull()) this->repeaters = obj["repeaters"]; + if(!obj["mqtt"].isNull()) this->mqtt = obj["mqtt"]; } int8_t appver_t::compare(appver_t &ver) { if(this->major == ver.major && this->minor == ver.minor && this->build == ver.build) return 0; @@ -85,13 +88,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); @@ -111,7 +107,7 @@ bool BaseSettings::loadFile(const char *filename) { char c = file.read(); data += c; } - DynamicJsonDocument doc(filesize); + JsonDocument doc; deserializeJson(doc, data); JsonObject obj = doc.as(); this->fromJSON(obj); @@ -121,7 +117,7 @@ bool BaseSettings::loadFile(const char *filename) { } bool BaseSettings::saveFile(const char *filename) { File file = LittleFS.open(filename, "w"); - DynamicJsonDocument doc(2048); + JsonDocument doc; JsonObject obj = doc.as(); this->toJSON(obj); serializeJson(doc, file); @@ -129,11 +125,11 @@ bool BaseSettings::saveFile(const char *filename) { return true; } bool BaseSettings::parseValueString(JsonObject &obj, const char *prop, char *pdest, size_t size) { - if(obj.containsKey(prop)) strlcpy(pdest, obj[prop], size); + if(!obj[prop].isNull()) strlcpy(pdest, obj[prop], size); return true; } bool BaseSettings::parseIPAddress(JsonObject &obj, const char *prop, IPAddress *pdest) { - if(obj.containsKey(prop)) { + if(!obj[prop].isNull()) { char buff[16]; strlcpy(buff, obj[prop], sizeof(buff)); pdest->fromString(buff); @@ -141,11 +137,11 @@ bool BaseSettings::parseIPAddress(JsonObject &obj, const char *prop, IPAddress * return true; } int BaseSettings::parseValueInt(JsonObject &obj, const char *prop, int defVal) { - if(obj.containsKey(prop)) return obj[prop]; + if(!obj[prop].isNull()) return obj[prop]; return defVal; } double BaseSettings::parseValueDouble(JsonObject &obj, const char *prop, double defVal) { - if(obj.containsKey(prop)) return obj[prop]; + if(!obj[prop].isNull()) return obj[prop]; return defVal; } bool ConfigSettings::begin() { @@ -159,26 +155,19 @@ bool ConfigSettings::begin() { case esp_chip_model_t::CHIP_ESP32S3: strcpy(this->chipModel, "s3"); break; - case esp_chip_model_t::CHIP_ESP32S2: - strcpy(this->chipModel, "s2"); - break; case esp_chip_model_t::CHIP_ESP32C3: strcpy(this->chipModel, "c3"); break; -// case esp_chip_model_t::CHIP_ESP32C2: -// strcpy(this->chipModel, "c2"); -// break; -// case esp_chip_model_t::CHIP_ESP32C6: -// strcpy(this->chipModel, "c6"); -// break; - case esp_chip_model_t::CHIP_ESP32H2: - strcpy(this->chipModel, "h2"); +#ifdef CHIP_ESP32C6 + case esp_chip_model_t::CHIP_ESP32C6: + strcpy(this->chipModel, "c6"); break; +#endif default: sprintf(this->chipModel, "UNK%d", static_cast(ci.model)); break; } - Serial.printf("Chip Model ESP32-%s\n", this->chipModel); + ESP_LOGD(TAG, "Chip Model ESP32-%s", this->chipModel); this->fwVersion.parse(FW_VERSION); uint64_t mac = ESP.getEfuseMac(); for(int i=0; i<17; i=i+8) { @@ -192,7 +181,9 @@ bool ConfigSettings::begin() { this->Security.begin(); this->IP.begin(); this->WIFI.begin(); +#ifndef CONFIG_IDF_TARGET_ESP32C6 this->Ethernet.begin(); +#endif this->NTP.begin(); this->MQTT.begin(); this->print(); @@ -249,28 +240,22 @@ 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"]; - if(obj.containsKey("hostname")) this->parseValueString(obj, "hostname", this->hostname, sizeof(this->hostname)); - if(obj.containsKey("connType")) this->connType = static_cast(obj["connType"].as()); - if(obj.containsKey("checkForUpdate")) this->checkForUpdate = obj["checkForUpdate"]; + if(!obj["ssdpBroadcast"].isNull()) this->ssdpBroadcast = obj["ssdpBroadcast"]; + if(!obj["hostname"].isNull()) this->parseValueString(obj, "hostname", this->hostname, sizeof(this->hostname)); + if(!obj["connType"].isNull()) this->connType = static_cast(obj["connType"].as()); + if(!obj["checkForUpdate"].isNull()) this->checkForUpdate = obj["checkForUpdate"]; return true; } void ConfigSettings::print() { this->Security.print(); - Serial.printf("Connection Type: %u\n", (unsigned int) this->connType); + ESP_LOGD(TAG, "Connection Type: %u", (unsigned int) this->connType); this->NTP.print(); if(this->connType == conn_types_t::wifi || this->connType == conn_types_t::unset) this->WIFI.print(); +#ifndef CONFIG_IDF_TARGET_ESP32C6 if(this->connType == conn_types_t::ethernet || this->connType == conn_types_t::ethernetpref) this->Ethernet.print(); +#endif } void ConfigSettings::emitSockets() {} void ConfigSettings::emitSockets(uint8_t num) {} @@ -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; @@ -333,15 +306,15 @@ bool MQTTSettings::toJSON(JsonObject &obj) { return true; } bool MQTTSettings::fromJSON(JsonObject &obj) { - if(obj.containsKey("enabled")) this->enabled = obj["enabled"]; - if(obj.containsKey("pubDisco")) this->pubDisco = obj["pubDisco"]; + if(!obj["enabled"].isNull()) this->enabled = obj["enabled"]; + if(!obj["pubDisco"].isNull()) this->pubDisco = obj["pubDisco"]; this->parseValueString(obj, "protocol", this->protocol, sizeof(this->protocol)); this->parseValueString(obj, "hostname", this->hostname, sizeof(this->hostname)); this->parseValueString(obj, "username", this->username, sizeof(this->username)); this->parseValueString(obj, "password", this->password, sizeof(this->password)); this->parseValueString(obj, "rootTopic", this->rootTopic, sizeof(this->rootTopic)); this->parseValueString(obj, "discoTopic", this->discoTopic, sizeof(this->discoTopic)); - if(obj.containsKey("port")) this->port = obj["port"]; + if(!obj["port"].isNull()) this->port = obj["port"]; return true; } bool MQTTSettings::save() { @@ -373,7 +346,7 @@ bool MQTTSettings::load() { pref.end(); return true; } -bool ConfigSettings::toJSON(DynamicJsonDocument &doc) { +bool ConfigSettings::toJSON(JsonDocument &doc) { doc["fwVersion"] = this->fwVersion.name; JsonObject objWIFI = doc.createNestedObject("WIFI"); this->WIFI.toJSON(objWIFI); @@ -408,21 +381,13 @@ bool NTPSettings::load() { return true; } void NTPSettings::print() { - Serial.println("NTP Settings "); - Serial.print(this->ntpServer); - Serial.print(" TZ:"); - Serial.println(this->posixZone); + ESP_LOGD(TAG, "NTP Settings %s TZ:%s", this->ntpServer, this->posixZone); } bool NTPSettings::fromJSON(JsonObject &obj) { this->parseValueString(obj, "ntpServer", this->ntpServer, sizeof(this->ntpServer)); 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; @@ -441,7 +406,7 @@ bool IPSettings::begin() { return true; } bool IPSettings::fromJSON(JsonObject &obj) { - if(obj.containsKey("dhcp")) this->dhcp = obj["dhcp"]; + if(!obj["dhcp"].isNull()) this->dhcp = obj["dhcp"]; this->parseIPAddress(obj, "ip", &this->ip); this->parseIPAddress(obj, "gateway", &this->gateway); this->parseIPAddress(obj, "subnet", &this->subnet); @@ -459,16 +424,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(); @@ -505,7 +460,7 @@ bool IPSettings::load() { pref.getString("dns2", buff, sizeof(buff)); this->dns2.fromString(buff); } - Serial.printf("Preference IP Free Entries: %d\n", pref.freeEntries()); + ESP_LOGD(TAG, "Preference IP Free Entries: %d", pref.freeEntries()); pref.end(); return true; } @@ -514,11 +469,11 @@ bool SecuritySettings::begin() { return true; } bool SecuritySettings::fromJSON(JsonObject &obj) { - if(obj.containsKey("type")) this->type = static_cast(obj["type"].as()); + if(!obj["type"].isNull()) this->type = static_cast(obj["type"].as()); this->parseValueString(obj, "username", this->username, sizeof(this->username)); this->parseValueString(obj, "password", this->password, sizeof(this->password)); this->parseValueString(obj, "pin", this->pin, sizeof(this->pin)); - if(obj.containsKey("permissions")) this->permissions = obj["permissions"]; + if(!obj["permissions"].isNull()) this->permissions = obj["permissions"]; return true; } bool SecuritySettings::toJSON(JsonObject &obj) { @@ -529,14 +484,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(); @@ -559,16 +506,8 @@ bool SecuritySettings::load() { return true; } void SecuritySettings::print() { - Serial.print("SECURITY Type:"); - Serial.print(static_cast(this->type)); - Serial.print(" Username:["); - Serial.print(this->username); - Serial.print("] Password:["); - Serial.print(this->password); - Serial.print("] Pin:["); - Serial.print(this->pin); - Serial.print("] Permissions:"); - Serial.println(this->permissions); + ESP_LOGD(TAG, "SECURITY Type:%u Username:[%s] Password:[%s] Pin:[%s] Permissions:%u", + static_cast(this->type), this->username, this->password, this->pin, this->permissions); } WifiSettings::WifiSettings() {} @@ -579,8 +518,8 @@ bool WifiSettings::begin() { bool WifiSettings::fromJSON(JsonObject &obj) { this->parseValueString(obj, "ssid", this->ssid, sizeof(this->ssid)); this->parseValueString(obj, "passphrase", this->passphrase, sizeof(this->passphrase)); - if(obj.containsKey("roaming")) this->roaming = obj["roaming"]; - if(obj.containsKey("hidden")) this->hidden = obj["hidden"]; + if(!obj["roaming"].isNull()) this->roaming = obj["roaming"]; + if(!obj["hidden"].isNull()) this->hidden = obj["hidden"]; return true; } bool WifiSettings::toJSON(JsonObject &obj) { @@ -590,13 +529,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(); @@ -636,34 +568,17 @@ String WifiSettings::mapEncryptionType(int type) { return "Unknown"; } void WifiSettings::print() { - Serial.println("WIFI Settings"); - Serial.print(" SSID: ["); - Serial.print(this->ssid); - Serial.print("] PassPhrase: ["); - Serial.print(this->passphrase); - Serial.println("]"); + ESP_LOGD(TAG, "WIFI Settings SSID: [%s] PassPhrase: [%s]", this->ssid, this->passphrase); } void WifiSettings::printNetworks() { int n = WiFi.scanNetworks(false, false); - Serial.print("Scanned "); - Serial.print(n); - Serial.println(" Networks..."); + ESP_LOGI(TAG, "Scanned %d Networks...", n); String network; for(int i = 0; i < n; i++) { - if(WiFi.SSID(i).compareTo(this->ssid) == 0) Serial.print("*"); - else Serial.print(" "); - Serial.print(i); - Serial.print(": "); - Serial.print(WiFi.SSID(i)); - Serial.print(" ("); - Serial.print(WiFi.RSSI(i)); - Serial.print("dBm) CH:"); - Serial.print(WiFi.channel(i)); - Serial.print(" MAC:"); - Serial.print(WiFi.BSSIDstr(i)); - Serial.println(); + ESP_LOGI(TAG, "%s%d: %s (%ddBm) CH:%d MAC:%s", + (WiFi.SSID(i).compareTo(this->ssid) == 0) ? "*" : " ", + i, WiFi.SSID(i).c_str(), WiFi.RSSI(i), WiFi.channel(i), WiFi.BSSIDstr(i).c_str()); } - } bool WifiSettings::ssidExists(const char *ssid) { int n = WiFi.scanNetworks(false, true); @@ -672,19 +587,20 @@ bool WifiSettings::ssidExists(const char *ssid) { } return false; } +#ifndef CONFIG_IDF_TARGET_ESP32C6 EthernetSettings::EthernetSettings() {} bool EthernetSettings::begin() { this->load(); return true; } bool EthernetSettings::fromJSON(JsonObject &obj) { - if(obj.containsKey("boardType")) this->boardType = obj["boardType"]; - if(obj.containsKey("phyAddress")) this->phyAddress = obj["phyAddress"]; - if(obj.containsKey("CLKMode")) this->CLKMode = static_cast(obj["CLKMode"]); - if(obj.containsKey("phyType")) this->phyType = static_cast(obj["phyType"]); - if(obj.containsKey("PWRPin")) this->PWRPin = obj["PWRPin"]; - if(obj.containsKey("MDCPin")) this->MDCPin = obj["MDCPin"]; - if(obj.containsKey("MDIOPin")) this->MDIOPin = obj["MDIOPin"]; + if(!obj["boardType"].isNull()) this->boardType = obj["boardType"]; + if(!obj["phyAddress"].isNull()) this->phyAddress = obj["phyAddress"]; + if(!obj["CLKMode"].isNull()) this->CLKMode = static_cast(obj["CLKMode"]); + if(!obj["phyType"].isNull()) this->phyType = static_cast(obj["phyType"]); + if(!obj["PWRPin"].isNull()) this->PWRPin = obj["PWRPin"]; + if(!obj["MDCPin"].isNull()) this->MDCPin = obj["MDCPin"]; + if(!obj["MDIOPin"].isNull()) this->MDIOPin = obj["MDIOPin"]; return true; } bool EthernetSettings::toJSON(JsonObject &obj) { @@ -697,16 +613,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; @@ -742,14 +648,12 @@ bool EthernetSettings::load() { return true; } void EthernetSettings::print() { - Serial.println("Ethernet Settings"); - Serial.printf("Board:%d PHYType:%d CLK:%d ADDR:%d PWR:%d MDC:%d MDIO:%d\n", this->boardType, this->phyType, this->CLKMode, this->phyAddress, this->PWRPin, this->MDCPin, this->MDIOPin); + ESP_LOGD(TAG, "Ethernet Settings Board:%d PHYType:%d CLK:%d ADDR:%d PWR:%d MDC:%d MDIO:%d", + this->boardType, this->phyType, this->CLKMode, this->phyAddress, this->PWRPin, this->MDCPin, this->MDIOPin); } +#endif // CONFIG_IDF_TARGET_ESP32C6 void ConfigSettings::printAvailHeap() { - Serial.print("Max Heap: "); - Serial.println(ESP.getMaxAllocHeap()); - Serial.print("Free Heap: "); - Serial.println(ESP.getFreeHeap()); - Serial.print("Min Heap: "); - Serial.println(ESP.getMinFreeHeap()); + ESP_LOGD(TAG, "Max Heap: %u", (unsigned int)ESP.getMaxAllocHeap()); + ESP_LOGD(TAG, "Free Heap: %u", (unsigned int)ESP.getFreeHeap()); + ESP_LOGD(TAG, "Min Heap: %u", (unsigned int)ESP.getMinFreeHeap()); } diff --git a/ConfigSettings.h b/src/ConfigSettings.h similarity index 92% rename from ConfigSettings.h rename to src/ConfigSettings.h index 350db96..bdd62a7 100644 --- a/ConfigSettings.h +++ b/src/ConfigSettings.h @@ -1,9 +1,11 @@ #include +#ifndef CONFIG_IDF_TARGET_ESP32C6 #include +#endif #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 +36,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 +48,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 +63,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 +81,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(); @@ -88,6 +90,7 @@ class WifiSettings: BaseSettings { void print(); }; +#ifndef CONFIG_IDF_TARGET_ESP32C6 class EthernetSettings: BaseSettings { public: EthernetSettings(); @@ -98,16 +101,17 @@ class EthernetSettings: BaseSettings { int8_t PWRPin = ETH_PHY_POWER; int8_t MDCPin = ETH_PHY_MDC; int8_t MDIOPin = ETH_PHY_MDIO; - + bool begin(); bool fromJSON(JsonObject &obj); bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); + bool load(); bool save(); void print(); bool usesPin(uint8_t pin); }; +#endif class IPSettings: BaseSettings { public: IPSettings(); @@ -120,7 +124,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 +149,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 +167,7 @@ class MQTTSettings: BaseSettings { bool save(); bool load(); bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); + bool fromJSON(JsonObject &obj); }; class ConfigSettings: BaseSettings { @@ -180,21 +184,23 @@ class ConfigSettings: BaseSettings { uint8_t status; IPSettings IP; WifiSettings WIFI; +#ifndef CONFIG_IDF_TARGET_ESP32C6 EthernetSettings Ethernet; +#endif NTPSettings NTP; MQTTSettings MQTT; SecuritySettings Security; bool requiresAuth(); bool fromJSON(JsonObject &obj); bool toJSON(JsonObject &obj); - void toJSON(JsonResponse &json); + bool begin(); bool save(); bool load(); void print(); void emitSockets(); void emitSockets(uint8_t num); - bool toJSON(DynamicJsonDocument &doc); + bool toJSON(JsonDocument &doc); uint16_t calcSettingsRecSize(); uint16_t calcNetRecSize(); bool getAppVersion(); diff --git a/Network.cpp b/src/ESPNetwork.cpp similarity index 79% rename from Network.cpp rename to src/ESPNetwork.cpp index ad8f2e1..bf4f5a0 100644 --- a/Network.cpp +++ b/src/ESPNetwork.cpp @@ -1,21 +1,26 @@ +#ifndef CONFIG_IDF_TARGET_ESP32C6 #include +#endif #include #include #include +#include "esp_log.h" #include "ConfigSettings.h" -#include "Network.h" +#include "ESPNetwork.h" #include "Web.h" #include "Sockets.h" #include "Utils.h" #include "SSDP.h" #include "MQTT.h" +static const char *TAG = "Network"; + extern ConfigSettings settings; extern Web webServer; extern SocketEmitter sockEmit; extern MQTTClass mqtt; extern rebootDelay_t rebootDelay; -extern Network net; +extern ESPNetwork net; extern SomfyShadeController somfy; static unsigned long _lastHeapEmit = 0; @@ -24,13 +29,13 @@ static bool _apScanning = false; static uint32_t _lastMaxHeap = 0; static uint32_t _lastHeap = 0; int connectRetries = 0; -void Network::end() { +void ESPNetwork::end() { SSDP.end(); mqtt.end(); sockEmit.end(); delay(100); } -bool Network::setup() { +bool ESPNetwork::setup() { WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); WiFi.persistent(false); @@ -41,29 +46,30 @@ bool Network::setup() { if(settings.connType == conn_types_t::wifi || settings.connType == conn_types_t::unset) { WiFi.persistent(false); if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname); - Serial.print("WiFi Mode: "); - Serial.println(WiFi.getMode()); + ESP_LOGI(TAG, "WiFi Mode: %d", WiFi.getMode()); WiFi.mode(WIFI_STA); } sockEmit.begin(); return true; } -conn_types_t Network::preferredConnType() { +conn_types_t ESPNetwork::preferredConnType() { switch(settings.connType) { case conn_types_t::wifi: return settings.WIFI.ssid[0] != '\0' ? conn_types_t::wifi : conn_types_t::ap; case conn_types_t::unset: case conn_types_t::ap: return conn_types_t::ap; +#ifndef CONFIG_IDF_TARGET_ESP32C6 case conn_types_t::ethernetpref: return settings.WIFI.ssid[0] != '\0' && (!ETH.linkUp() && this->ethStarted) ? conn_types_t::wifi : conn_types_t::ethernet; case conn_types_t::ethernet: return ETH.linkUp() || !this->ethStarted ? conn_types_t::ethernet : conn_types_t::ap; +#endif default: return settings.connType; } } -void Network::loop() { +void ESPNetwork::loop() { // ORDER OF OPERATIONS: // ---------------------------------------------- // 1. If we are in the middle of a connection process we need to simply bail after the connect method. The @@ -87,7 +93,7 @@ void Network::loop() { (this->connected() && !settings.WIFI.roaming) || // We are already connected and should not be roaming. (this->softAPOpened && WiFi.softAPgetStationNum() != 0) || // The Soft AP is open and a user is connected. (ctype != conn_types_t::wifi)) { // The Ethernet link is up so we should ignore this scan. - Serial.println("Cancelling WiFi STA Scan..."); + ESP_LOGI(TAG, "Cancelling WiFi STA Scan..."); _apScanning = false; WiFi.scanDelete(); } @@ -99,11 +105,11 @@ void Network::loop() { if(this->getStrongestAP(settings.WIFI.ssid, bssid, &channel)) { if(!WiFi.BSSID() || memcmp(bssid, WiFi.BSSID(), sizeof(bssid)) != 0) { if(!this->connected()) { - Serial.printf("Connecting to AP %02X:%02X:%02X:%02X:%02X:%02X CH: %d\n", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel); + ESP_LOGD(TAG, "Connecting to AP %02X:%02X:%02X:%02X:%02X:%02X CH: %d", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel); this->connectWiFi(bssid, channel); } else { - Serial.printf("Found stronger AP %02X:%02X:%02X:%02X:%02X:%02X CH: %d\n", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel); + ESP_LOGD(TAG, "Found stronger AP %02X:%02X:%02X:%02X:%02X:%02X CH: %d", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], channel); this->changeAP(bssid, channel); } } @@ -154,7 +160,7 @@ void Network::loop() { } else if(!settings.ssdpBroadcast && SSDP.isStarted) SSDP.end(); } -bool Network::changeAP(const uint8_t *bssid, const int32_t channel) { +bool ESPNetwork::changeAP(const uint8_t *bssid, const int32_t channel) { esp_task_wdt_reset(); // Make sure we do not reboot here. if(SSDP.isStarted) SSDP.end(); mqtt.disconnect(); @@ -167,7 +173,7 @@ bool Network::changeAP(const uint8_t *bssid, const int32_t channel) { this->connectStart = millis(); return false; } -void Network::emitSockets() { +void ESPNetwork::emitSockets() { this->emitHeap(); if(this->needsBroadcast || (this->connType == conn_types_t::wifi && (abs(abs(WiFi.RSSI()) - abs(this->lastRSSI)) > 1 || WiFi.channel() != this->lastChannel))) { @@ -176,7 +182,8 @@ void Network::emitSockets() { this->needsBroadcast = false; } } -void Network::emitSockets(uint8_t num) { +void ESPNetwork::emitSockets(uint8_t num) { +#ifndef CONFIG_IDF_TARGET_ESP32C6 if(this->connType == conn_types_t::ethernet) { JsonSockEvent *json = sockEmit.beginEmit("ethernet"); json->beginObject(); @@ -186,7 +193,9 @@ void Network::emitSockets(uint8_t num) { json->endObject(); sockEmit.endEmit(num); } - else { + else +#endif + { if(WiFi.status() == WL_CONNECTED) { JsonSockEvent *json = sockEmit.beginEmit("wifiStrength"); json->beginObject(); @@ -220,12 +229,12 @@ void Network::emitSockets(uint8_t num) { } this->emitHeap(num); } -void Network::setConnected(conn_types_t connType) { +void ESPNetwork::setConnected(conn_types_t connType) { esp_task_wdt_reset(); this->connType = connType; this->connectTime = millis(); connectRetries = 0; - Serial.println("Setting connected..."); + ESP_LOGI(TAG, "Setting connected..."); if(this->connType == conn_types_t::wifi) { if(this->softAPOpened && WiFi.softAPgetStationNum() == 0) { WiFi.softAPdisconnect(true); @@ -238,9 +247,10 @@ void Network::setConnected(conn_types_t connType) { this->channel = WiFi.channel(); this->connectAttempts++; } +#ifndef CONFIG_IDF_TARGET_ESP32C6 else if(this->connType == conn_types_t::ethernet) { if(this->softAPOpened) { - Serial.println("Disonnecting from SoftAP"); + ESP_LOGI(TAG, "Disconnecting from SoftAP"); WiFi.softAPdisconnect(true); WiFi.mode(WIFI_OFF); } @@ -248,18 +258,14 @@ void Network::setConnected(conn_types_t connType) { this->_connecting = false; this->wifiFallback = false; } +#endif // CONFIG_IDF_TARGET_ESP32C6 // NET: Begin this in the startup. //sockEmit.begin(); esp_task_wdt_reset(); - + if(this->connectAttempts == 1) { - Serial.println(); if(this->connType == conn_types_t::wifi) { - Serial.print("Successfully Connected to WiFi!!!!"); - Serial.print(WiFi.localIP()); - Serial.print(" ("); - Serial.print(this->strength); - Serial.println("dbm)"); + ESP_LOGI(TAG, "Successfully Connected to WiFi!!!! %s (%ddbm)", WiFi.localIP().toString().c_str(), this->strength); if(settings.IP.dhcp) { settings.IP.ip = WiFi.localIP(); settings.IP.subnet = WiFi.subnetMask(); @@ -268,15 +274,9 @@ void Network::setConnected(conn_types_t connType) { settings.IP.dns2 = WiFi.dnsIP(1); } } +#ifndef CONFIG_IDF_TARGET_ESP32C6 else { - Serial.print("Successfully Connected to Ethernet!!! "); - Serial.print(ETH.localIP()); - if(ETH.fullDuplex()) { - Serial.print(" FULL DUPLEX"); - } - Serial.print(" "); - Serial.print(ETH.linkSpeed()); - Serial.println("Mbps"); + ESP_LOGI(TAG, "Successfully Connected to Ethernet!!! %s%s %dMbps", ETH.localIP().toString().c_str(), ETH.fullDuplex() ? " FULL DUPLEX" : "", ETH.linkSpeed()); if(settings.IP.dhcp) { settings.IP.ip = ETH.localIP(); settings.IP.subnet = ETH.subnetMask(); @@ -294,34 +294,17 @@ void Network::setConnected(conn_types_t connType) { sockEmit.endEmit(); esp_task_wdt_reset(); } +#endif } else { - Serial.println(); - Serial.print("Reconnected after "); - Serial.print(1.0 * (millis() - this->connectStart)/1000); - Serial.print("sec IP: "); if(this->connType == conn_types_t::wifi) { - Serial.print(WiFi.localIP()); - Serial.print(" "); - Serial.print(this->mac); - Serial.print(" CH:"); - Serial.print(this->channel); - Serial.print(" ("); - Serial.print(this->strength); - Serial.print(" dBm)"); + ESP_LOGI(TAG, "Reconnected after %.3fsec IP: %s %s CH:%d (%d dBm) Disconnected %d times", 1.0 * (millis() - this->connectStart)/1000, WiFi.localIP().toString().c_str(), this->mac.c_str(), this->channel, this->strength, this->connectAttempts - 1); } +#ifndef CONFIG_IDF_TARGET_ESP32C6 else { - Serial.print(ETH.localIP()); - if(ETH.fullDuplex()) { - Serial.print(" FULL DUPLEX"); - } - Serial.print(" "); - Serial.print(ETH.linkSpeed()); - Serial.print("Mbps"); + ESP_LOGI(TAG, "Reconnected after %.3fsec IP: %s%s %dMbps Disconnected %d times", 1.0 * (millis() - this->connectStart)/1000, ETH.localIP().toString().c_str(), ETH.fullDuplex() ? " FULL DUPLEX" : "", ETH.linkSpeed(), this->connectAttempts - 1); } - Serial.print(" Disconnected "); - Serial.print(this->connectAttempts - 1); - Serial.println(" times"); +#endif } SSDP.setHTTPPort(80); SSDP.setSchemaURL(0, "upnp.xml"); @@ -345,7 +328,7 @@ void Network::setConnected(conn_types_t connType) { SSDP.setActive(0, true); esp_task_wdt_reset(); if(MDNS.begin(settings.hostname)) { - Serial.printf("MDNS Responder Started: serverId=%s\n", settings.serverId); + ESP_LOGI(TAG, "MDNS Responder Started: serverId=%s", settings.serverId); MDNS.addService("http", "tcp", 80); //MDNS.addServiceTxt("http", "tcp", "board", "ESP32"); //MDNS.addServiceTxt("http", "tcp", "model", "ESPSomfyRTS"); @@ -365,7 +348,8 @@ void Network::setConnected(conn_types_t connType) { settings.printAvailHeap(); this->needsBroadcast = true; } -bool Network::connectWired() { +#ifndef CONFIG_IDF_TARGET_ESP32C6 +bool ESPNetwork::connectWired() { if(ETH.linkUp()) { // If the ethernet link is re-established then we need to shut down wifi. if(WiFi.status() == WL_CONNECTED) { @@ -382,11 +366,10 @@ bool Network::connectWired() { return this->connectWiFi(); } if(this->connectAttempts > 0) { - Serial.printf("Ethernet Connection Lost... %d Reconnecting ", this->connectAttempts); - Serial.println(this->mac); + ESP_LOGW(TAG, "Ethernet Connection Lost... %d Reconnecting %s", this->connectAttempts, this->mac.c_str()); } else - Serial.println("Connecting to Wired Ethernet"); + ESP_LOGI(TAG, "Connecting to Wired Ethernet"); this->_connecting = true; this->connTarget = conn_types_t::ethernet; this->connType = conn_types_t::unset; @@ -398,10 +381,9 @@ bool Network::connectWired() { ETH.setHostname(settings.hostname); else ETH.setHostname("ESPSomfy-RTS"); - Serial.print("Set hostname to:"); - Serial.println(ETH.getHostname()); - if(!ETH.begin(settings.Ethernet.phyAddress, settings.Ethernet.PWRPin, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, settings.Ethernet.phyType, settings.Ethernet.CLKMode)) { - Serial.println("Ethernet Begin failed"); + ESP_LOGD(TAG, "Set hostname to: %s", ETH.getHostname()); + if(!ETH.begin(settings.Ethernet.phyAddress, settings.Ethernet.PWRPin, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, settings.Ethernet.phyType, settings.Ethernet.CLKMode)) { + ESP_LOGE(TAG, "Ethernet Begin failed"); this->ethStarted = false; if(settings.connType == conn_types_t::ethernetpref) { this->wifiFallback = true; @@ -412,7 +394,7 @@ bool Network::connectWired() { else { if(!settings.IP.dhcp) { if(!ETH.config(settings.IP.ip, settings.IP.gateway, settings.IP.subnet, settings.IP.dns1, settings.IP.dns2)) { - Serial.println("Unable to configure static IP address...."); + ESP_LOGE(TAG, "Unable to configure static IP address...."); ETH.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE); } } @@ -423,24 +405,28 @@ bool Network::connectWired() { this->connectStart = millis(); return true; } -void Network::updateHostname() { +#endif // CONFIG_IDF_TARGET_ESP32C6 +void ESPNetwork::updateHostname() { if(settings.hostname[0] != '\0' && this->connected()) { +#ifndef CONFIG_IDF_TARGET_ESP32C6 if(this->connType == conn_types_t::ethernet && strcmp(settings.hostname, ETH.getHostname()) != 0) { - Serial.printf("Updating host name to %s...\n", settings.hostname); + ESP_LOGD(TAG, "Updating host name to %s...", settings.hostname); ETH.setHostname(settings.hostname); - MDNS.setInstanceName(settings.hostname); + MDNS.setInstanceName(settings.hostname); SSDP.setName(0, settings.hostname); } - else if(strcmp(settings.hostname, WiFi.getHostname()) != 0) { - Serial.printf("Updating host name to %s...\n", settings.hostname); + else +#endif + if(strcmp(settings.hostname, WiFi.getHostname()) != 0) { + ESP_LOGD(TAG, "Updating host name to %s...", settings.hostname); WiFi.setHostname(settings.hostname); MDNS.setInstanceName(settings.hostname); SSDP.setName(0, settings.hostname); } } } -bool Network::connectWiFi(const uint8_t *bssid, const int32_t channel) { +bool ESPNetwork::connectWiFi(const uint8_t *bssid, const int32_t channel) { if(this->softAPOpened && WiFi.softAPgetStationNum() > 0) { // There is a client connected to the soft AP. We do not want to close out the connection. While both the // Soft AP and a wifi connection can coexist on ESP32 the performance is abysmal. @@ -467,7 +453,7 @@ bool Network::connectWiFi(const uint8_t *bssid, const int32_t channel) { } this->connTarget = conn_types_t::wifi; this->connType = conn_types_t::unset; - Serial.println("WiFi begin..."); + ESP_LOGI(TAG, "WiFi begin..."); this->_connecting = true; WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, channel, bssid); this->connectStart = millis(); @@ -484,26 +470,19 @@ bool Network::connectWiFi(const uint8_t *bssid, const int32_t channel) { this->connTarget = conn_types_t::wifi; this->connType = conn_types_t::unset; if(this->connectAttempts > 0) { - Serial.print("Connection Lost..."); - Serial.print(this->mac); - Serial.print(" CH:"); - Serial.print(this->channel); - Serial.print(" ("); - Serial.print(this->strength); - Serial.println("dbm) "); + ESP_LOGW(TAG, "Connection Lost... %s CH:%d (%ddbm)", this->mac.c_str(), this->channel, this->strength); } - else Serial.println("Connecting to AP"); + else ESP_LOGI(TAG, "Connecting to AP"); delay(100); // There is also another method simply called hostname() but this is legacy for esp8266. if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname); - Serial.print("Set hostname to:"); - Serial.println(WiFi.getHostname()); + ESP_LOGD(TAG, "Set hostname to: %s", WiFi.getHostname()); WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); uint8_t _bssid[6]; int32_t _channel = 0; if(!settings.WIFI.hidden && this->getStrongestAP(settings.WIFI.ssid, _bssid, &_channel)) { - Serial.printf("Found strongest AP %02X:%02X:%02X:%02X:%02X:%02X CH:%d\n", _bssid[0], _bssid[1], _bssid[2], _bssid[3], _bssid[4], _bssid[5], _channel); + ESP_LOGD(TAG, "Found strongest AP %02X:%02X:%02X:%02X:%02X:%02X CH:%d", _bssid[0], _bssid[1], _bssid[2], _bssid[3], _bssid[4], _bssid[5], _channel); WiFi.begin(settings.WIFI.ssid, settings.WIFI.passphrase, _channel, _bssid); } else @@ -513,15 +492,18 @@ bool Network::connectWiFi(const uint8_t *bssid, const int32_t channel) { this->connectStart = millis(); return true; } -bool Network::connect(conn_types_t ctype) { +bool ESPNetwork::connect(conn_types_t ctype) { esp_task_wdt_reset(); if(this->connecting()) return true; if(this->disconnectTime == 0) this->disconnectTime = millis(); +#ifndef CONFIG_IDF_TARGET_ESP32C6 if(ctype == conn_types_t::ethernet && this->connType != conn_types_t::ethernet) { // Here we need to call the connect to ethernet. this->connectWired(); } - else if(ctype == conn_types_t::ap || (!this->connected() && millis() > this->disconnectTime + CONNECT_TIMEOUT)) { + else +#endif + if(ctype == conn_types_t::ap || (!this->connected() && millis() > this->disconnectTime + CONNECT_TIMEOUT)) { if(!this->softAPOpened && !this->openingSoftAP) { this->disconnectTime = millis(); this->openSoftAP(); @@ -538,7 +520,7 @@ bool Network::connect(conn_types_t ctype) { return true; } -uint32_t Network::getChipId() { +uint32_t ESPNetwork::getChipId() { uint32_t chipId = 0; uint64_t mac = ESP.getEfuseMac(); for(int i=0; i<17; i=i+8) { @@ -546,7 +528,7 @@ uint32_t Network::getChipId() { } return chipId; } -bool Network::getStrongestAP(const char *ssid, uint8_t *bssid, int32_t *channel) { +bool ESPNetwork::getStrongestAP(const char *ssid, uint8_t *bssid, int32_t *channel) { // The new AP must be at least 10dbm greater. int32_t strength = this->connected() ? WiFi.RSSI() + 10 : -127; int32_t chan = -1; @@ -567,64 +549,64 @@ bool Network::getStrongestAP(const char *ssid, uint8_t *bssid, int32_t *channel) WiFi.scanDelete(); return chan > 0; } -bool Network::openSoftAP() { +bool ESPNetwork::openSoftAP() { if(this->softAPOpened || this->openingSoftAP) return true; if(this->connected()) WiFi.disconnect(false); this->openingSoftAP = true; - Serial.println(); - Serial.println("Turning the HotSpot On"); + ESP_LOGI(TAG, "Turning the HotSpot On"); esp_task_wdt_reset(); // Make sure we do not reboot here. WiFi.softAP(strlen(settings.hostname) > 0 ? settings.hostname : "ESPSomfy RTS", ""); delay(200); return true; } -bool Network::connected() { +bool ESPNetwork::connected() { if(this->connecting()) return false; else if(this->connType == conn_types_t::unset) return false; else if(this->connType == conn_types_t::wifi) return WiFi.status() == WL_CONNECTED; +#ifndef CONFIG_IDF_TARGET_ESP32C6 else if(this->connType == conn_types_t::ethernet) return ETH.linkUp(); +#endif else return this->connType != conn_types_t::unset; return false; } -bool Network::connecting() { +bool ESPNetwork::connecting() { if(this->_connecting && millis() > this->connectStart + CONNECT_TIMEOUT) this->_connecting = false; return this->_connecting; } -void Network::clearConnecting() { this->_connecting = false; } -void Network::networkEvent(WiFiEvent_t event) { +void ESPNetwork::clearConnecting() { this->_connecting = false; } +void ESPNetwork::networkEvent(WiFiEvent_t event) { switch(event) { - case ARDUINO_EVENT_WIFI_READY: Serial.println("(evt) WiFi interface ready"); break; + case ARDUINO_EVENT_WIFI_READY: ESP_LOGI(TAG, "(evt) WiFi interface ready"); break; case ARDUINO_EVENT_WIFI_SCAN_DONE: - Serial.printf("(evt) Completed scan for access points (%d)\n", WiFi.scanComplete()); + ESP_LOGI(TAG, "(evt) Completed scan for access points (%d)", WiFi.scanComplete()); //Serial.println("(evt) Completed scan for access points"); net.lastWifiScan = millis(); break; case ARDUINO_EVENT_WIFI_STA_START: - Serial.println("WiFi station mode started"); + ESP_LOGI(TAG, "WiFi station mode started"); if(settings.hostname[0] != '\0') WiFi.setHostname(settings.hostname); break; - case ARDUINO_EVENT_WIFI_STA_STOP: Serial.println("(evt) WiFi clients stopped"); break; - case ARDUINO_EVENT_WIFI_STA_CONNECTED: Serial.println("(evt) Connected to WiFi STA access point"); break; + case ARDUINO_EVENT_WIFI_STA_STOP: ESP_LOGI(TAG, "(evt) WiFi clients stopped"); break; + case ARDUINO_EVENT_WIFI_STA_CONNECTED: ESP_LOGI(TAG, "(evt) Connected to WiFi STA access point"); break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: - Serial.printf("(evt) Disconnected from WiFi STA access point. Connecting: %d\n", net.connecting()); + ESP_LOGI(TAG, "(evt) Disconnected from WiFi STA access point. Connecting: %d", net.connecting()); net.connType = conn_types_t::unset; net.disconnectTime = millis(); net.clearConnecting(); break; - case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: Serial.println("(evt) Authentication mode of STA access point has changed"); break; + case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: ESP_LOGI(TAG, "(evt) Authentication mode of STA access point has changed"); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP: - Serial.print("(evt) Got WiFi STA IP: "); - Serial.println(WiFi.localIP()); + ESP_LOGI(TAG, "(evt) Got WiFi STA IP: %s", WiFi.localIP().toString().c_str()); net.connType = conn_types_t::wifi; net.connectTime = millis(); net.setConnected(conn_types_t::wifi); break; - case ARDUINO_EVENT_WIFI_STA_LOST_IP: Serial.println("Lost IP address and IP address is reset to 0"); break; + case ARDUINO_EVENT_WIFI_STA_LOST_IP: ESP_LOGW(TAG, "Lost IP address and IP address is reset to 0"); break; +#ifndef CONFIG_IDF_TARGET_ESP32C6 case ARDUINO_EVENT_ETH_GOT_IP: // If the Wifi is connected then drop that connection if(WiFi.status() == WL_CONNECTED) WiFi.disconnect(true); - Serial.print("Got Ethernet IP "); - Serial.println(ETH.localIP()); + ESP_LOGI(TAG, "Got Ethernet IP %s", ETH.localIP().toString().c_str()); net.connectTime = millis(); net.connType = conn_types_t::ethernet; if(settings.IP.dhcp) { @@ -633,44 +615,46 @@ void Network::networkEvent(WiFiEvent_t event) { settings.IP.gateway = ETH.gatewayIP(); settings.IP.dns1 = ETH.dnsIP(0); settings.IP.dns2 = ETH.dnsIP(1); - } + } net.setConnected(conn_types_t::ethernet); break; case ARDUINO_EVENT_ETH_CONNECTED: - Serial.print("(evt) Ethernet Connected "); + ESP_LOGI(TAG, "(evt) Ethernet Connected"); break; case ARDUINO_EVENT_ETH_DISCONNECTED: - Serial.println("(evt) Ethernet Disconnected"); + ESP_LOGI(TAG, "(evt) Ethernet Disconnected"); net.connType = conn_types_t::unset; net.disconnectTime = millis(); net.clearConnecting(); break; - case ARDUINO_EVENT_ETH_START: - Serial.println("(evt) Ethernet Started"); + case ARDUINO_EVENT_ETH_START: + ESP_LOGI(TAG, "(evt) Ethernet Started"); net.ethStarted = true; break; case ARDUINO_EVENT_ETH_STOP: - Serial.println("(evt) Ethernet Stopped"); + ESP_LOGI(TAG, "(evt) Ethernet Stopped"); net.connType = conn_types_t::unset; net.ethStarted = false; break; +#endif case ARDUINO_EVENT_WIFI_AP_START: - Serial.print("(evt) WiFi SoftAP Started IP:"); - Serial.println(WiFi.softAPIP()); + ESP_LOGI(TAG, "(evt) WiFi SoftAP Started IP: %s", WiFi.softAPIP().toString().c_str()); net.openingSoftAP = false; net.softAPOpened = true; break; case ARDUINO_EVENT_WIFI_AP_STOP: - if(!net.openingSoftAP) Serial.println("(evt) WiFi SoftAP Stopped"); + if(!net.openingSoftAP) ESP_LOGI(TAG, "(evt) WiFi SoftAP Stopped"); net.softAPOpened = false; break; default: +#ifndef CONFIG_IDF_TARGET_ESP32C6 if(event > ARDUINO_EVENT_ETH_START) - Serial.printf("(evt) Unknown Ethernet Event %d\n", event); + ESP_LOGW(TAG, "(evt) Unknown Ethernet Event %d", event); +#endif break; } } -void Network::emitHeap(uint8_t num) { +void ESPNetwork::emitHeap(uint8_t num) { bool bEmit = false; bool bTimeEmit = millis() - _lastHeapEmit > 15000; bool bRoomEmit = false; @@ -691,6 +675,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/ESPNetwork.h similarity index 95% rename from Network.h rename to src/ESPNetwork.h index 864966c..338a282 100644 --- a/Network.h +++ b/src/ESPNetwork.h @@ -1,13 +1,14 @@ #include +#include -#ifndef Network_h -#define Network_h +#ifndef ESPNetwork_h +#define ESPNetwork_h //enum class conn_types_t : byte; #define CONNECT_TIMEOUT 20000 #define SSID_SCAN_INTERVAL 60000 -class Network { +class ESPNetwork { protected: unsigned long lastEmit = 0; unsigned long lastMDNS = 0; diff --git a/GitOTA.cpp b/src/GitOTA.cpp similarity index 83% rename from GitOTA.cpp rename to src/GitOTA.cpp index 49ea3c5..68de7ec 100644 --- a/GitOTA.cpp +++ b/src/GitOTA.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include "esp_log.h" #include "ConfigSettings.h" #include "GitOTA.h" #include "Utils.h" @@ -10,7 +12,7 @@ #include "Somfy.h" #include "Web.h" #include "WResp.h" -#include "Network.h" +#include "ESPNetwork.h" @@ -20,9 +22,9 @@ extern SocketEmitter sockEmit; extern SomfyShadeController somfy; extern rebootDelay_t rebootDelay; extern Web webServer; -extern Network net; - +extern ESPNetwork net; +static const char *TAG = "OTA"; #define MAX_BUFF_SIZE 4096 void GitRelease::setReleaseProperty(const char *key, const char *val) { @@ -72,22 +74,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) { @@ -111,10 +97,10 @@ int16_t GitRepo::getReleases(uint8_t num) { if(https.begin(sclient, url)) { esp_task_wdt_reset(); int httpCode = https.GET(); - Serial.printf("[HTTPS] GET... code: %d\n", httpCode); + ESP_LOGD(TAG, "[HTTPS] GET... code: %d", httpCode); if(httpCode > 0) { int len = https.getSize(); - Serial.printf("[HTTPS] GET... code: %d - %d\n", httpCode, len); + ESP_LOGD(TAG, "[HTTPS] GET... code: %d - %d", httpCode, len); if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) { WiFiClient *stream = https.getStreamPtr(); uint8_t buff[128] = {0}; @@ -230,22 +216,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 @@ -261,14 +231,14 @@ void GitUpdater::loop() { } } else if(this->status == GIT_AWAITING_UPDATE) { - Serial.println("Starting update process........."); + ESP_LOGI(TAG, "Starting update process........."); this->status = GIT_UPDATING; this->beginUpdate(this->targetRelease); this->status = GIT_STATUS_READY; this->emitUpdateCheck(); } else if(this->status == GIT_UPDATE_CANCELLING) { - Serial.println("Cancelling update process.........."); + ESP_LOGI(TAG, "Cancelling update process.........."); if(!this->lockFS) { this->status = GIT_UPDATE_CANCELLED; this->cancelled = true; @@ -278,8 +248,8 @@ void GitUpdater::loop() { } void GitUpdater::checkForUpdate() { if(this->status != 0) return; // If we are already checking. - Serial.println("Check github for updates..."); - + ESP_LOGI(TAG, "Check github for updates..."); + this->status = GIT_STATUS_CHECK; settings.printAvailHeap(); this->lastCheck = millis(); @@ -310,23 +280,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(); @@ -365,12 +318,12 @@ int GitUpdater::checkInternet() { esp_task_wdt_reset(); if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY || httpCode == HTTP_CODE_FOUND) { err = 0; - Serial.printf("Internet is Available: %ldms\n", millis() - t); + ESP_LOGI(TAG, "Internet is Available: %ldms", millis() - t); this->inetAvailable = true; } else { err = httpCode; - Serial.printf("Internet is Unavailable: %d: %ldms\n", err, millis() - t); + ESP_LOGE(TAG, "Internet is Unavailable: %d: %ldms", err, millis() - t); this->inetAvailable = false; } https.end(); @@ -420,7 +373,7 @@ void GitUpdater::setFirmwareFile() { } bool GitUpdater::beginUpdate(const char *version) { - Serial.println("Begin update called..."); + ESP_LOGI(TAG, "Begin update called..."); if(strcmp(version, "Main") == 0) strcpy(this->baseUrl, "https://raw.githubusercontent.com/rstrouse/ESPSomfy-RTS/master/"); else sprintf(this->baseUrl, "https://github.com/rstrouse/ESPSomfy-RTS/releases/download/%s/", version); @@ -441,7 +394,7 @@ bool GitUpdater::beginUpdate(const char *version) { if(this->error == 0) { settings.fwVersion.parse(version); delay(100); - Serial.println("Committing Configuration..."); + ESP_LOGI(TAG, "Committing Configuration..."); somfy.commit(); } rebootDelay.reboot = true; @@ -461,7 +414,7 @@ bool GitUpdater::recoverFilesystem() { this->lockFS = false; if(this->error == 0) { delay(100); - Serial.println("Committing Configuration..."); + ESP_LOGI(TAG, "Committing Configuration..."); somfy.commit(); } this->status = GIT_UPDATE_COMPLETE; @@ -471,28 +424,27 @@ bool GitUpdater::recoverFilesystem() { } bool GitUpdater::endUpdate() { return true; } int8_t GitUpdater::downloadFile() { - Serial.printf("Begin update %s\n", this->currentFile); + ESP_LOGI(TAG, "Begin update %s", this->currentFile); WiFiClientSecure sclient; sclient.setInsecure(); HTTPClient https; char url[196]; sprintf(url, "%s%s", this->baseUrl, this->currentFile); - Serial.println(url); + ESP_LOGD(TAG, "%s", url); esp_task_wdt_reset(); if(https.begin(sclient, url)) { https.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); - Serial.print("[HTTPS] GET...\n"); + ESP_LOGD(TAG, "[HTTPS] GET..."); int httpCode = https.GET(); if(httpCode > 0) { size_t len = https.getSize(); size_t total = 0; uint8_t pct = 0; - Serial.printf("[HTTPS] GET... code: %d - %d\n", httpCode, len); + ESP_LOGD(TAG, "[HTTPS] GET... code: %d - %d", httpCode, len); if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY || httpCode == HTTP_CODE_FOUND) { WiFiClient *stream = https.getStreamPtr(); if(!Update.begin(len, this->partition)) { - Serial.println("Update Error detected!!!!!"); - Update.printError(Serial); + ESP_LOGE(TAG, "Update Error detected!!!!!"); https.end(); return -(Update.getError() + UPDATE_ERR_OFFSET); } @@ -515,8 +467,7 @@ int8_t GitUpdater::downloadFile() { total += c; //Serial.println(total); if (Update.write(buff, c) != c) { - Update.printError(Serial); - Serial.printf("Upload of %s aborted invalid size %d\n", url, c); + ESP_LOGE(TAG, "Upload of %s aborted invalid size %d", url, c); free(buff); https.end(); sclient.stop(); @@ -526,17 +477,16 @@ int8_t GitUpdater::downloadFile() { uint8_t p = (uint8_t)floor(((float)total / (float)len) * 100.0f); if(p != pct) { pct = p; - Serial.printf("LEN:%d TOTAL:%d %d%%\n", len, total, pct); + ESP_LOGD(TAG, "LEN:%d TOTAL:%d %d%%", len, total, pct); this->emitDownloadProgress(len, total); } delay(1); if(total >= len) { if(!Update.end(true)) { - Serial.println("Error downloading update..."); - Update.printError(Serial); + ESP_LOGE(TAG, "Error downloading update..."); } else { - Serial.println("Update.end Called..."); + ESP_LOGI(TAG, "Update.end Called..."); } https.end(); sclient.stop(); @@ -548,7 +498,7 @@ int8_t GitUpdater::downloadFile() { Update.abort(); https.end(); free(buff); - Serial.println("Stream timeout!!!"); + ESP_LOGE(TAG, "Stream timeout!!!"); return -43; } sockEmit.loop(); @@ -560,28 +510,28 @@ int8_t GitUpdater::downloadFile() { if(len > total) { Update.abort(); somfy.commit(); - Serial.println("Error downloading file!!!"); + ESP_LOGE(TAG, "Error downloading file!!!"); return -42; } else - Serial.printf("Update %s complete\n", this->currentFile); + ESP_LOGI(TAG, "Update %s complete", this->currentFile); } else { // TODO: memory allocation error. - Serial.println("Unable to allocate memory for update!!!"); + ESP_LOGE(TAG, "Unable to allocate memory for update!!!"); } } else { - Serial.printf("Invalid HTTP Code... %d", httpCode); + ESP_LOGE(TAG, "Invalid HTTP Code... %d", httpCode); return httpCode; } } else { - Serial.printf("Invalid HTTP Code: %d\n", httpCode); + ESP_LOGE(TAG, "Invalid HTTP Code: %d", httpCode); } https.end(); sclient.stop(); - Serial.printf("End update %s\n", this->currentFile); + ESP_LOGI(TAG, "End update %s", this->currentFile); } esp_task_wdt_reset(); return 0; 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 93% rename from MQTT.cpp rename to src/MQTT.cpp index 55f693a..4332227 100644 --- a/MQTT.cpp +++ b/src/MQTT.cpp @@ -2,12 +2,15 @@ #include #include #include +#include "esp_log.h" #include "ConfigSettings.h" #include "MQTT.h" #include "Somfy.h" -#include "Network.h" +#include "ESPNetwork.h" #include "Utils.h" +static const char *TAG = "MQTT"; + WiFiClient tcpClient; PubSubClient mqttClient(tcpClient); @@ -16,7 +19,7 @@ static char g_content[MQTT_MAX_RESPONSE]; extern ConfigSettings settings; extern SomfyShadeController somfy; -extern Network net; +extern ESPNetwork net; extern rebootDelay_t rebootDelay; @@ -45,12 +48,7 @@ bool MQTTClass::loop() { } void MQTTClass::receive(const char *topic, byte*payload, uint32_t length) { esp_task_wdt_reset(); // Make sure we do not reboot here. - Serial.print("MQTT Topic:"); - Serial.print(topic); - Serial.print(" payload:"); - for(uint32_t i=0; i= 0 && val <= 100) + if(val >= 0 && val <= 100) { + ESP_LOGI(TAG, "MQTT shade %s target=%d", entityId, val); shade->moveToTarget(shade->transformPosition(atoi(value))); + } } if(strncmp(command, "tiltTarget", sizeof(command)) == 0) { if(val >= 0 && val <= 100) @@ -161,6 +154,7 @@ void MQTTClass::receive(const char *topic, byte*payload, uint32_t length) { SomfyGroup* group = somfy.getGroupById(atoi(entityId)); if (group) { int val = atoi(value); + ESP_LOGI(TAG, "MQTT group %s command=%s value=%d", entityId, command, val); if(strncmp(command, "direction", sizeof(command)) == 0) { if(val < 0) group->sendCommand(somfy_commands::Up); @@ -204,8 +198,7 @@ bool MQTTClass::connect() { snprintf(lwtTopic, sizeof(lwtTopic), "%s/status", settings.MQTT.rootTopic); esp_task_wdt_reset(); if(mqttClient.connect(this->clientId, settings.MQTT.username, settings.MQTT.password, lwtTopic, 0, true, "offline")) { - Serial.print("Successfully connected MQTT client "); - Serial.println(this->clientId); + ESP_LOGI(TAG, "Successfully connected MQTT client %s", this->clientId); this->publish("status", "online", true); this->publish("ipAddress", settings.IP.ip.toString().c_str(), true); this->publish("host", settings.hostname, true); @@ -228,14 +221,13 @@ bool MQTTClass::connect() { this->subscribe("groups/+/sunny/set"); this->subscribe("groups/+/windy/set"); mqttClient.setCallback(MQTTClass::receive); - Serial.println("MQTT Startup Completed"); + ESP_LOGI(TAG, "MQTT Startup Completed"); esp_task_wdt_reset(); this->lastConnect = millis(); return true; } else { - Serial.print("MQTT Connection failed for: "); - Serial.println(mqttClient.state()); + ESP_LOGE(TAG, "MQTT Connection failed for: %d", mqttClient.state()); this->lastConnect = millis(); return false; } @@ -273,8 +265,7 @@ bool MQTTClass::unsubscribe(const char *topic) { snprintf(top, sizeof(top), "%s/%s", settings.MQTT.rootTopic, topic); else strlcpy(top, topic, sizeof(top)); - Serial.print("MQTT Unsubscribed from:"); - Serial.println(top); + ESP_LOGD(TAG, "MQTT Unsubscribed from:%s", top); return mqttClient.unsubscribe(top); } return true; @@ -287,8 +278,7 @@ bool MQTTClass::subscribe(const char *topic) { snprintf(top, sizeof(top), "%s/%s", settings.MQTT.rootTopic, topic); else strlcpy(top, topic, sizeof(top)); - Serial.print("MQTT Subscribed to:"); - Serial.println(top); + ESP_LOGD(TAG, "MQTT Subscribed to:%s", top); return mqttClient.subscribe(top); } return true; diff --git a/MQTT.h b/src/MQTT.h similarity index 100% rename from MQTT.h rename to src/MQTT.h diff --git a/SSDP.cpp b/src/SSDP.cpp similarity index 97% rename from SSDP.cpp rename to src/SSDP.cpp index f866865..7dd4a62 100644 --- a/SSDP.cpp +++ b/src/SSDP.cpp @@ -1,5 +1,6 @@ #include #include +#include "esp_log.h" #include "Utils.h" #include "ConfigSettings.h" #include "SSDP.h" @@ -12,6 +13,7 @@ #define SSDP_MULTICAST_ADDR 239, 255, 255, 250 //#define DEBUG_SSDP Serial //#define DEBUG_SSDP_PACKET Serial +static const char *TAG = "SSDP"; extern ConfigSettings settings; static const char _ssdp_uuid_template[] PROGMEM = "C2496952-5610-47E6-A968-2FC1%02X%02X%02X%02X"; @@ -193,12 +195,12 @@ bool SSDPClass::begin() { return false; } for(uint8_t i = 0; i < this->m_cdeviceTypes; i++) { - Serial.printf("SSDP: %s - %s\n", this->deviceTypes[i].deviceType, this->deviceTypes[i].isActive ? "true" : "false"); + ESP_LOGI(TAG, "SSDP: %s - %s", this->deviceTypes[i].deviceType, this->deviceTypes[i].isActive ? "true" : "false"); } this->isStarted = true; this->_sendByeBye(); this->_sendNotify(); - Serial.println("Connected to SSDP..."); + ESP_LOGI(TAG, "Connected to SSDP..."); return true; } void SSDPClass::end() { @@ -209,7 +211,7 @@ void SSDPClass::end() { if(this->_server.connected()) { this->_sendByeBye(); this->_server.close(); - Serial.println("Disconnected from SSDP..."); + ESP_LOGI(TAG, "Disconnected from SSDP..."); } this->isStarted = false; // Clear out the last notified so if the user starts us up again it will notify @@ -381,6 +383,9 @@ void SSDPClass::_parsePacket(ssdp_packet_t *pkt, AsyncUDPPacket &p) { IPAddress SSDPClass::localIP() { // Make sure we don't get a null IPAddress. +#ifdef CONFIG_IDF_TARGET_ESP32C6 + return WiFi.localIP(); +#else tcpip_adapter_ip_info_t ip; if (WiFi.getMode() == WIFI_STA) { if (tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip)) { @@ -392,6 +397,7 @@ IPAddress SSDPClass::localIP() } } return IPAddress(ip.ip.addr); +#endif } void SSDPClass::_sendResponse(IPAddress addr, uint16_t port, UPNPDeviceType *d, const char *st, response_types_t responseType) { char buffer[1460]; @@ -432,7 +438,7 @@ void SSDPClass::_sendResponse(IPAddress addr, uint16_t port, const char *buff) { void SSDPClass::_sendNotify() { for(uint8_t i = 0; i < this->m_cdeviceTypes; i++) { UPNPDeviceType *dev = &this->deviceTypes[i]; - if(i == 0 && (strlen(dev->deviceType) == 0 || !dev->isActive)) Serial.printf("The device type is empty: %s\n", dev->isActive ? "true" : "false"); + if(i == 0 && (strlen(dev->deviceType) == 0 || !dev->isActive)) ESP_LOGD(TAG, "The device type is empty: %s", dev->isActive ? "true" : "false"); if(strlen(dev->deviceType) > 0 && dev->isActive) { unsigned long elapsed = (millis() - dev->lastNotified); if(!dev->lastNotified || (elapsed * 5) > (this->_interval * 1000)) { @@ -653,28 +659,23 @@ void SSDPClass::_sendQueuedResponses() { } } void SSDPClass::_printPacket(ssdp_packet_t *pkt) { - Serial.printf("Rec: %lu\n", pkt->recvd); + ESP_LOGD(TAG, "Rec: %lu", pkt->recvd); switch(pkt->method) { case NONE: - Serial.println("Method: NONE"); + ESP_LOGD(TAG, "Method: NONE"); break; case SEARCH: - Serial.println("Method: SEARCH"); + ESP_LOGD(TAG, "Method: SEARCH"); break; case NOTIFY: - Serial.println("Method: NOTIFY"); + ESP_LOGD(TAG, "Method: NOTIFY"); break; default: - Serial.println("Method: UNKOWN"); + ESP_LOGD(TAG, "Method: UNKNOWN"); break; } - Serial.printf("ST: %s\n", pkt->st); - Serial.printf("MAN: %s\n", pkt->man); - Serial.printf("AGENT: %s\n", pkt->agent); - Serial.printf("HOST: %s\n", pkt->host); - Serial.printf("MX: %d\n", pkt->mx); - Serial.printf("type: %d\n", pkt->type); - Serial.printf("valid: %d\n", pkt->valid); + ESP_LOGD(TAG, "ST: %s MAN: %s AGENT: %s HOST: %s MX: %d type: %d valid: %d", + pkt->st, pkt->man, pkt->agent, pkt->host, pkt->mx, pkt->type, pkt->valid); } void SSDPClass::_processRequest(AsyncUDPPacket &p) { // This pending BS should probably be for unicast request only but we will play along for now. 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..ba4babe --- /dev/null +++ b/src/Sockets.cpp @@ -0,0 +1,224 @@ +#include +#include +#include +#include "esp_log.h" +#include "Sockets.h" +#include "ConfigSettings.h" +#include "Somfy.h" +#include "ESPNetwork.h" +#include "GitOTA.h" + +static const char *TAG = "Sockets"; + +extern ConfigSettings settings; +extern ESPNetwork 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(); + ESP_LOGI(TAG, "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)) { + ESP_LOGD(TAG, "Initializing Socket Client %u (asyncId=%lu)", 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) { + ESP_LOGE(TAG, "Socket: No free client slots, closing %lu", asyncId); + client->close(); + return; + } + IPAddress ip = client->remoteIP(); + ESP_LOGD(TAG, "Socket [%lu] Connected from %d.%d.%d.%d (slot %u)", 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); + ESP_LOGD(TAG, "Socket [%lu] Disconnected (slot %u)", 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]); + ESP_LOGD(TAG, "Client %u joining room %u", 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]); + ESP_LOGD(TAG, "Client %u leaving room %u", slot, roomNum); + if(roomNum < SOCK_MAX_ROOMS && slot != 255) sockEmit.rooms[roomNum].leave(slot); + } + else { + ESP_LOGD(TAG, "Socket [%lu] text: %s", asyncId, data); + } + } + } + break; + case WS_EVT_ERROR: + ESP_LOGE(TAG, "Socket [%lu] Error", 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 89% rename from Somfy.cpp rename to src/Somfy.cpp index bffebd2..7a8ad51 100644 --- a/Somfy.cpp +++ b/src/Somfy.cpp @@ -1,8 +1,9 @@ #include #include #include -#include #include +#include +#include "esp_log.h" #include "Utils.h" #include "ConfigSettings.h" #include "Somfy.h" @@ -11,6 +12,8 @@ #include "ConfigFile.h" #include "GitOTA.h" +static const char *TAG = "Somfy"; + extern Preferences pref; extern SomfyShadeController somfy; extern SocketEmitter sockEmit; @@ -156,9 +159,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; @@ -217,37 +220,21 @@ void somfy_frame_t::decodeFrame(byte* frame) { } if(this->valid && this->encKey == 0) this->valid = false; if (!this->valid) { - Serial.print("INVALID FRAME "); - Serial.print("KEY:"); - Serial.print(this->encKey); - Serial.print(" ADDR:"); - Serial.print(this->remoteAddress); - Serial.print(" CMD:"); - Serial.print(translateSomfyCommand(this->cmd)); - Serial.print(" RCODE:"); - Serial.println(this->rollingCode); - Serial.println(" KEY 1 2 3 4 5 6 "); - Serial.println("--------------------------------"); - Serial.print("ENC "); + ESP_LOGW(TAG, "INVALID FRAME KEY:%d ADDR:%d CMD:%s RCODE:%d", + this->encKey, this->remoteAddress, + translateSomfyCommand(this->cmd).c_str(), this->rollingCode); + ESP_LOGD(TAG, " KEY 1 2 3 4 5 6 "); + ESP_LOGD(TAG, "--------------------------------"); + char enc_buf[64], dec_buf[64]; + int enc_pos = 0, dec_pos = 0; + enc_pos += snprintf(enc_buf + enc_pos, sizeof(enc_buf) - enc_pos, "ENC "); + dec_pos += snprintf(dec_buf + dec_pos, sizeof(dec_buf) - dec_pos, "DEC "); for (byte i = 0; i < 10; i++) { - if (frame[i] < 10) - Serial.print(" "); - else if (frame[i] < 100) - Serial.print(" "); - Serial.print(frame[i]); - Serial.print(" "); + enc_pos += snprintf(enc_buf + enc_pos, sizeof(enc_buf) - enc_pos, "%3d ", frame[i]); + dec_pos += snprintf(dec_buf + dec_pos, sizeof(dec_buf) - dec_pos, "%3d ", decoded[i]); } - Serial.println(); - Serial.print("DEC "); - for (byte i = 0; i < 10; i++) { - if (decoded[i] < 10) - Serial.print(" "); - else if (decoded[i] < 100) - Serial.print(" "); - Serial.print(decoded[i]); - Serial.print(" "); - } - Serial.println(); + ESP_LOGD(TAG, "%s", enc_buf); + ESP_LOGD(TAG, "%s", dec_buf); } } void somfy_frame_t::decodeFrame(somfy_rx_t *rx) { @@ -436,21 +423,11 @@ void somfy_frame_t::encodeFrame(byte *frame) { } } void somfy_frame_t::print() { - Serial.println("----------- Receiving -------------"); - Serial.print("RSSI:"); - Serial.print(this->rssi); - Serial.print(" LQI:"); - Serial.println(this->lqi); - Serial.print("CMD:"); - Serial.print(translateSomfyCommand(this->cmd)); - Serial.print(" ADDR:"); - Serial.print(this->remoteAddress); - Serial.print(" RCODE:"); - Serial.println(this->rollingCode); - Serial.print("KEY:"); - Serial.print(this->encKey, HEX); - Serial.print(" CS:"); - Serial.println(this->checksum); + ESP_LOGD(TAG, "----------- Receiving -------------"); + ESP_LOGD(TAG, "RSSI:%d LQI:%d", this->rssi, this->lqi); + ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d", + translateSomfyCommand(this->cmd).c_str(), this->remoteAddress, this->rollingCode); + ESP_LOGD(TAG, "KEY:%02X CS:%d", this->encKey, this->checksum); } bool somfy_frame_t::isSynonym(somfy_frame_t &frame) { return this->remoteAddress == frame.remoteAddress && this->cmd != frame.cmd && this->rollingCode == frame.rollingCode; } bool somfy_frame_t::isRepeat(somfy_frame_t &frame) { return this->remoteAddress == frame.remoteAddress && this->cmd == frame.cmd && this->rollingCode == frame.rollingCode; } @@ -514,7 +491,7 @@ void SomfyShadeController::updateGroupFlags() { } #ifdef USE_NVS bool SomfyShadeController::loadLegacy() { - Serial.println("Loading Legacy shades using NVS"); + ESP_LOGI(TAG, "Loading Legacy shades using NVS"); pref.begin("Shades", true); pref.getBytes("shadeIds", this->m_shadeIds, sizeof(this->m_shadeIds)); pref.end(); @@ -550,7 +527,6 @@ bool SomfyShadeController::loadLegacy() { DEBUG_SOMFY.print(this->m_shadeIds[i]); if(i < SOMFY_MAX_SHADES - 1) DEBUG_SOMFY.print(","); } - Serial.println(); #endif #ifdef USE_NVS if(!this->useNVS()) { @@ -566,12 +542,12 @@ bool SomfyShadeController::loadLegacy() { bool SomfyShadeController::begin() { // Load up all the configuration data. //ShadeConfigFile::getAppVersion(this->appVersion); - Serial.printf("App Version:%u.%u.%u\n", settings.appVersion.major, settings.appVersion.minor, settings.appVersion.build); + ESP_LOGI(TAG, "App Version:%u.%u.%u", settings.appVersion.major, settings.appVersion.minor, settings.appVersion.build); #ifdef USE_NVS if(!this->useNVS()) { // At 1.4 we started using the configuration file. If the file doesn't exist then booh. // We need to remove all the extraeneous data from NVS for the shades. From here on out we // will rely on the shade configuration. - Serial.println("No longer using NVS"); + ESP_LOGI(TAG, "No longer using NVS"); if(ShadeConfigFile::exists()) { ShadeConfigFile::load(this); } @@ -596,11 +572,11 @@ bool SomfyShadeController::begin() { } #endif if(ShadeConfigFile::exists()) { - Serial.println("shades.cfg exists so we are using that"); + ESP_LOGI(TAG, "shades.cfg exists so we are using that"); ShadeConfigFile::load(this); } else { - Serial.println("Starting clean"); + ESP_LOGI(TAG, "Starting clean"); #ifdef USE_NVS this->loadLegacy(); #endif @@ -633,8 +609,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(); } @@ -773,8 +751,7 @@ void SomfyShade::commitShadePosition() { char shadeKey[15]; if(somfy.useNVS()) { snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->shadeId); - Serial.print("Writing current shade position: "); - Serial.println(this->currentPos, 4); + ESP_LOGD(TAG, "Writing current shade position: %.4f", this->currentPos); pref.begin(shadeKey); pref.putFloat("currentPos", this->currentPos); pref.end(); @@ -787,9 +764,7 @@ void SomfyShade::commitMyPosition() { if(somfy.useNVS()) { char shadeKey[15]; snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->shadeId); - Serial.print("Writing my shade position:"); - Serial.print(this->myPos); - Serial.println("%"); + ESP_LOGD(TAG, "Writing my shade position:%.2f%%", this->myPos); pref.begin(shadeKey); pref.putUShort("myPos", this->myPos); pref.end(); @@ -802,8 +777,7 @@ void SomfyShade::commitTiltPosition() { if(somfy.useNVS()) { char shadeKey[15]; snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->shadeId); - Serial.print("Writing current shade tilt position: "); - Serial.println(this->currentTiltPos, 4); + ESP_LOGD(TAG, "Writing current shade tilt position: %.4f", this->currentTiltPos); pref.begin(shadeKey); pref.putFloat("currentTiltPos", this->currentTiltPos); pref.end(); @@ -955,19 +929,19 @@ void SomfyShade::setGPIOs() { case -1: digitalWrite(this->gpioDown, p_off); digitalWrite(this->gpioUp, p_on); - if(dir != this->gpioDir) Serial.printf("UP: true, DOWN: false\n"); + if(dir != this->gpioDir) ESP_LOGD(TAG, "UP: true, DOWN: false"); this->gpioDir = dir; break; case 1: digitalWrite(this->gpioUp, p_off); digitalWrite(this->gpioDown, p_on); - if(dir != this->gpioDir) Serial.printf("UP: false, DOWN: true\n"); + if(dir != this->gpioDir) ESP_LOGD(TAG, "UP: false, DOWN: true"); this->gpioDir = dir; break; default: digitalWrite(this->gpioUp, p_off); digitalWrite(this->gpioDown, p_off); - if(dir != this->gpioDir) Serial.printf("UP: false, DOWN: false\n"); + if(dir != this->gpioDir) ESP_LOGD(TAG, "UP: false, DOWN: false"); this->gpioDir = dir; break; } @@ -997,7 +971,7 @@ void SomfyShade::triggerGPIOs(somfy_frame_t &frame) { digitalWrite(this->gpioDown, p_off); digitalWrite(this->gpioMy, p_on); dir = 0; - if(dir != this->gpioDir) Serial.printf("UP: false, DOWN: false, MY: true\n"); + if(dir != this->gpioDir) ESP_LOGD(TAG, "UP: false, DOWN: false, MY: true"); } break; case somfy_commands::Up: @@ -1006,7 +980,7 @@ void SomfyShade::triggerGPIOs(somfy_frame_t &frame) { digitalWrite(this->gpioDown, p_off); digitalWrite(this->gpioUp, p_on); dir = -1; - Serial.printf("UP: true, DOWN: false, MY: false\n"); + ESP_LOGD(TAG, "UP: true, DOWN: false, MY: false"); } break; case somfy_commands::Toggle: @@ -1017,14 +991,14 @@ void SomfyShade::triggerGPIOs(somfy_frame_t &frame) { } digitalWrite(this->gpioDown, p_on); dir = 1; - Serial.printf("UP: false, DOWN: true, MY: false\n"); + ESP_LOGD(TAG, "UP: false, DOWN: true, MY: false"); break; case somfy_commands::MyUp: if(this->shadeType != shade_types::drycontact && !this->isToggle() && this->shadeType != shade_types::drycontact2) { digitalWrite(this->gpioDown, p_off); digitalWrite(this->gpioMy, p_on); digitalWrite(this->gpioUp, p_on); - Serial.printf("UP: true, DOWN: false, MY: true\n"); + ESP_LOGD(TAG, "UP: true, DOWN: false, MY: true"); } break; case somfy_commands::MyDown: @@ -1032,7 +1006,7 @@ void SomfyShade::triggerGPIOs(somfy_frame_t &frame) { digitalWrite(this->gpioUp, p_off); digitalWrite(this->gpioMy, p_on); digitalWrite(this->gpioDown, p_on); - Serial.printf("UP: false, DOWN: true, MY: true\n"); + ESP_LOGD(TAG, "UP: false, DOWN: true, MY: true"); } break; case somfy_commands::MyUpDown: @@ -1040,7 +1014,7 @@ void SomfyShade::triggerGPIOs(somfy_frame_t &frame) { digitalWrite(this->gpioUp, p_on); digitalWrite(this->gpioMy, p_on); digitalWrite(this->gpioDown, p_on); - Serial.printf("UP: true, DOWN: true, MY: true\n"); + ESP_LOGD(TAG, "UP: true, DOWN: true, MY: true"); } break; default: @@ -1088,7 +1062,7 @@ void SomfyShade::checkMovement() { this->p_target(this->myPos >= 0 ? this->myPos : 100.0f); //this->target = this->myPos >= 0 ? this->myPos : 100.0f; this->sunDone = true; - Serial.printf("[%u] Sun -> done\r\n", this->shadeId); + ESP_LOGD(TAG, "[%u] Sun -> done", this->shadeId); } if (!this->noWindDone && this->noWindStart @@ -1097,7 +1071,7 @@ void SomfyShade::checkMovement() { this->p_target(this->myPos >= 0 ? this->myPos : 100.0f); //this->target = this->myPos >= 0 ? this->myPos : 100.0f; this->noWindDone = true; - Serial.printf("[%u] No Wind -> done\r\n", this->shadeId); + ESP_LOGD(TAG, "[%u] No Wind -> done", this->shadeId); } } if (!isSunny @@ -1108,7 +1082,7 @@ void SomfyShade::checkMovement() { if(this->tiltType == tilt_types::tiltonly) this->p_tiltTarget(0.0f); this->p_target(0.0f); this->noSunDone = true; - Serial.printf("[%u] No Sun -> done\r\n", this->shadeId); + ESP_LOGD(TAG, "[%u] No Sun -> done", this->shadeId); } } @@ -1120,7 +1094,7 @@ void SomfyShade::checkMovement() { if(this->tiltType == tilt_types::tiltonly) this->p_tiltTarget(0.0f); this->p_target(0.0f); this->windDone = true; - Serial.printf("[%u] Wind -> done\r\n", this->shadeId); + ESP_LOGD(TAG, "[%u] Wind -> done", this->shadeId); } if(!tilt_first && this->direction > 0) { @@ -1166,7 +1140,7 @@ void SomfyShade::checkMovement() { // not moving otherwise the my function will kick in. if(this->settingPos) { if(!isAtTarget()) { - Serial.printf("We are not at our tilt target: %.2f\n", this->tiltTarget); + ESP_LOGD(TAG, "We are not at our tilt target: %.2f", this->tiltTarget); if(this->target != 100.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats); delay(100); // We now need to move the tilt to the position we requested. @@ -1218,7 +1192,7 @@ void SomfyShade::checkMovement() { // not moving otherwise the my function will kick in. if(this->settingPos) { if(!isAtTarget()) { - Serial.printf("We are not at our tilt target: %.2f\n", this->tiltTarget); + ESP_LOGD(TAG, "We are not at our tilt target: %.2f", this->tiltTarget); if(this->target != 0.0) SomfyRemote::sendCommand(somfy_commands::My, this->repeats); delay(100); // We now need to move the tilt to the position we requested. @@ -1329,7 +1303,7 @@ void SomfyShade::checkMovement() { } this->p_tiltDirection(0); this->settingTiltPos = false; - Serial.println("Stopping at tilt position"); + ESP_LOGI(TAG, "Stopping at tilt position"); if(this->isAtTarget()) this->commitShadePosition(); } } @@ -1401,16 +1375,9 @@ void SomfyShade::load() { this->tiltTarget = floor(this->currentTiltPos); pref.getBytes("linkedAddr", linkedAddresses, sizeof(linkedAddresses)); pref.end(); - Serial.print("shadeId:"); - Serial.print(this->getShadeId()); - Serial.print(" name:"); - Serial.print(this->name); - Serial.print(" address:"); - Serial.print(this->getRemoteAddress()); - Serial.print(" position:"); - Serial.print(this->currentPos); - Serial.print(" myPos:"); - Serial.println(this->myPos); + ESP_LOGI(TAG, "shadeId:%d name:%s address:%d position:%.2f myPos:%.2f", + this->getShadeId(), this->name, this->getRemoteAddress(), + this->currentPos, this->myPos); pref.begin("ShadeCodes"); this->lastRollingCode = pref.getUShort(this->m_remotePrefId, 0); for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) { @@ -1445,9 +1412,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); @@ -1469,7 +1436,7 @@ void SomfyShade::publishState() { void SomfyShade::publishDisco() { if(!mqtt.connected() || !settings.MQTT.pubDisco) return; char topic[128] = ""; - DynamicJsonDocument doc(2048); + JsonDocument doc; JsonObject obj = doc.to(); snprintf(topic, sizeof(topic), "%s/shades/%d", settings.MQTT.rootTopic, this->shadeId); obj["~"] = topic; @@ -1794,7 +1761,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) { @@ -2094,12 +2061,8 @@ void SomfyShade::processWaitingFrame() { this->p_tiltTarget(dir > 0 ? 100.0f : 0.0f); this->setTiltMovement(dir); this->lastFrame.processed = true; - Serial.print(this->name); - Serial.print(" Processing tilt "); - Serial.print(translateSomfyCommand(this->lastFrame.cmd)); - Serial.print(" after "); - Serial.print(this->lastFrame.repeats); - Serial.println(" repeats"); + ESP_LOGD(TAG, "%s Processing tilt %s after %d repeats", + this->name, translateSomfyCommand(this->lastFrame.cmd).c_str(), this->lastFrame.repeats); this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); } else { @@ -2120,12 +2083,8 @@ void SomfyShade::processWaitingFrame() { this->p_target(dir > 0 ? 100.0f : 0.0f); this->setMovement(dir); this->lastFrame.processed = true; - Serial.print(this->name); - Serial.print(" Processing "); - Serial.print(translateSomfyCommand(this->lastFrame.cmd)); - Serial.print(" after "); - Serial.print(this->lastFrame.repeats); - Serial.println(" repeats"); + ESP_LOGD(TAG, "%s Processing %s after %d repeats", + this->name, translateSomfyCommand(this->lastFrame.cmd).c_str(), this->lastFrame.repeats); this->emitCommand(cmd, "remote", this->lastFrame.remoteAddress); } else { @@ -2173,10 +2132,7 @@ void SomfyShade::processWaitingFrame() { } if(this->lastFrame.repeats > SETMY_REPEATS + 2) this->lastFrame.processed = true; if(this->lastFrame.processed) { - Serial.print(this->name); - Serial.print(" Processing MY after "); - Serial.print(this->lastFrame.repeats); - Serial.println(" repeats"); + ESP_LOGD(TAG, "%s Processing MY after %d repeats", this->name, this->lastFrame.repeats); } break; default: @@ -2263,25 +2219,25 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) { { this->sunStart = curTime; this->sunDone = false; - Serial.printf("[%u] Sun -> start\r\n", this->shadeId); + ESP_LOGD(TAG, "[%u] Sun -> start", this->shadeId); } else if (!isSunny && wasSunny) { this->noSunStart = curTime; this->noSunDone = false; - Serial.printf("[%u] No Sun -> start\r\n", this->shadeId); + ESP_LOGD(TAG, "[%u] No Sun -> start", this->shadeId); } if (isWindy && !wasWindy) { this->windStart = curTime; this->windDone = false; - Serial.printf("[%u] Wind -> start\r\n", this->shadeId); + ESP_LOGD(TAG, "[%u] Wind -> start", this->shadeId); } else if (!isWindy && wasWindy) { this->noWindStart = curTime; this->noWindDone = false; - Serial.printf("[%u] No Wind -> start\r\n", this->shadeId); + ESP_LOGD(TAG, "[%u] No Wind -> start", this->shadeId); } this->emitState(); somfy.updateGroupFlags(); @@ -2424,7 +2380,7 @@ void SomfyShade::processFrame(somfy_frame_t &frame, bool internal) { } else { if(this->lastFrame.processed) return; - Serial.println("Moving to My target"); + ESP_LOGI(TAG, "Moving to My target"); this->lastFrame.processed = true; if(this->myTiltPos >= 0.0f && this->myTiltPos <= 100.0f) this->p_tiltTarget(this->myTiltPos); if(this->myPos >= 0.0f && this->myPos <= 100.0f && this->tiltType != tilt_types::tiltonly) this->p_target(this->myPos); @@ -2609,7 +2565,7 @@ void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) { break; case somfy_commands::My: if(this->isIdle()) { - Serial.printf("Shade #%d is idle\n", this->getShadeId()); + ESP_LOGD(TAG, "Shade #%d is idle", this->getShadeId()); if(this->simMy()) { this->moveToMyPosition(); } @@ -2704,7 +2660,7 @@ void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) { this->emitState(); } else { - Serial.printf("Shade does not have sensor %d\n", this->flags); + ESP_LOGW(TAG, "Shade does not have sensor %d", this->flags); } break; case somfy_commands::SunFlag: @@ -2724,7 +2680,7 @@ void SomfyShade::processInternalCommand(somfy_commands cmd, uint8_t repeat) { this->emitState(); } else - Serial.printf("Shade does not have sensor %d\n", this->flags); + ESP_LOGW(TAG, "Shade does not have sensor %d", this->flags); break; default: dir = 0; @@ -2863,7 +2819,7 @@ void SomfyShade::setMyPosition(int8_t pos, int8_t tilt) { } void SomfyShade::moveToMyPosition() { if(!this->isIdle()) return; - Serial.println("Moving to My Position"); + ESP_LOGI(TAG, "Moving to My Position"); if(this->tiltType == tilt_types::tiltonly) { this->p_currentPos(100.0f); this->p_myPos(-1.0f); @@ -2880,7 +2836,7 @@ void SomfyShade::moveToMyPosition() { if(this->myTiltPos >= 0.0f && this->myTiltPos <= 100.0f) this->p_tiltTarget(this->myTiltPos); this->settingPos = false; if(this->simMy()) { - Serial.print("Moving to simulated favorite\n"); + ESP_LOGI(TAG, "Moving to simulated favorite"); this->moveToTarget(this->myPos, this->myTiltPos); } else @@ -2888,9 +2844,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) { + ESP_LOGD(TAG, "Send command start"); // 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)) { + ESP_LOGD(TAG, "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 +2898,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){ + ESP_LOGD(TAG, "Send command start 1"); + return; + } else if(this->isIdle()) { - this->moveToMyPosition(); + this->moveToMyPosition(); + ESP_LOGD(TAG, "Send command end 2"); return; } else { @@ -2951,14 +2923,16 @@ void SomfyShade::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSiz else { SomfyRemote::sendCommand(cmd, repeat, stepSize); } + ESP_LOGD(TAG, "Send command end"); } void SomfyGroup::sendCommand(somfy_commands cmd) { this->sendCommand(cmd, this->repeats); } void SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize) { + ESP_LOGI(TAG, "[Group %u] sendCommand cmd=%s repeat=%u", this->getGroupId(), translateSomfyCommand(cmd).c_str(), repeat); // 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; SomfyRemote::sendCommand(cmd, repeat, stepSize); - + switch(cmd) { case somfy_commands::My: this->p_direction(0); @@ -2977,6 +2951,7 @@ void SomfyGroup::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSiz if(this->linkedShades[i] != 0) { SomfyShade *shade = somfy.getShadeById(this->linkedShades[i]); if(shade) { + ESP_LOGI(TAG, "[Group %u] processInternalCommand on shade %u cmd=%s", this->getGroupId(), shade->getShadeId(), translateSomfyCommand(cmd).c_str()); shade->processInternalCommand(cmd, repeat); shade->emitCommand(cmd, "group", this->getRemoteAddress()); } @@ -3010,12 +2985,8 @@ void SomfyShade::moveToTiltTarget(float target) { // Only send a command if the lift is not moving. if(this->currentPos == this->target || this->tiltType == tilt_types::tiltmotor) { if(cmd != somfy_commands::My) { - Serial.print("Moving Tilt to "); - Serial.print(target); - Serial.print("% from "); - Serial.print(this->currentTiltPos); - Serial.print("% using "); - Serial.println(translateSomfyCommand(cmd)); + ESP_LOGI(TAG, "Moving Tilt to %.2f%% from %.2f%% using %s", + target, this->currentTiltPos, translateSomfyCommand(cmd).c_str()); SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::tiltmotor ? TILT_REPEATS : this->repeats); } // If the blind is currently moving then the command to stop it @@ -3026,6 +2997,8 @@ void SomfyShade::moveToTiltTarget(float target) { if(cmd != somfy_commands::My) this->settingTiltPos = true; } void SomfyShade::moveToTarget(float pos, float tilt) { + ESP_LOGI(TAG, "[Shade %u] moveToTarget(pos=%.2f, tilt=%.2f) settingPos=%d direction=%d currentTarget=%.2f currentPos=%.2f", + this->getShadeId(), pos, tilt, this->settingPos, this->direction, this->target, this->currentPos); somfy_commands cmd = somfy_commands::My; if(this->isToggle()) { // Overload this as we cannot seek a position on a garage door or single button device. @@ -3053,18 +3026,13 @@ void SomfyShade::moveToTarget(float pos, float tilt) { cmd = somfy_commands::Down; } if(cmd != somfy_commands::My) { - Serial.print("Moving to "); - Serial.print(pos); - Serial.print("% from "); - Serial.print(this->currentPos); if(tilt >= 0) { - Serial.print(" tilt "); - Serial.print(tilt); - Serial.print("% from "); - Serial.print(this->currentTiltPos); + ESP_LOGI(TAG, "Moving to %.2f%% from %.2f%% tilt %.2f%% from %.2f%% using %s", + pos, this->currentPos, tilt, this->currentTiltPos, translateSomfyCommand(cmd).c_str()); + } else { + ESP_LOGI(TAG, "Moving to %.2f%% from %.2f%% using %s", + pos, this->currentPos, translateSomfyCommand(cmd).c_str()); } - Serial.print("% using "); - Serial.println(translateSomfyCommand(cmd)); SomfyRemote::sendCommand(cmd, this->tiltType == tilt_types::euromode ? TILT_REPEATS : this->repeats); this->settingPos = true; this->p_target(pos); @@ -3079,7 +3047,9 @@ bool SomfyShade::save() { if(somfy.useNVS()) { char shadeKey[15]; snprintf(shadeKey, sizeof(shadeKey), "SomfyShade%u", this->getShadeId()); - pref.begin(shadeKey); + if(!pref.begin(shadeKey)) { + ESP_LOGE(TAG, "[Shade %u] save() pref.begin(%s) FAILED", this->getShadeId(), shadeKey); + } pref.clear(); pref.putChar("shadeType", static_cast(this->shadeType)); pref.putUInt("remoteAddress", this->getRemoteAddress()); @@ -3142,7 +3112,7 @@ bool SomfyShade::usesPin(uint8_t pin) { int8_t SomfyShade::validateJSON(JsonObject &obj) { int8_t ret = 0; shade_types type = this->shadeType; - if(obj.containsKey("shadeType")) { + if(!obj["shadeType"].isNull()) { if(obj["shadeType"].is()) { if(strncmp(obj["shadeType"].as(), "roller", 7) == 0) type = shade_types::roller; @@ -3171,14 +3141,14 @@ int8_t SomfyShade::validateJSON(JsonObject &obj) { this->shadeType = static_cast(obj["shadeType"].as()); } } - if(obj.containsKey("proto")) { + if(!obj["proto"].isNull()) { radio_proto proto = this->proto; if(proto == radio_proto::GP_Relay || proto == radio_proto::GP_Remote) { // Check to see if we are using the up and or down // GPIOs anywhere else. - uint8_t upPin = obj.containsKey("gpioUp") ? obj["gpioUp"].as() : this->gpioUp; - uint8_t downPin = obj.containsKey("gpioDown") ? obj["gpioDown"].as() : this->gpioDown; - uint8_t myPin = obj.containsKey("gpioMy") ? obj["gpioMy"].as() : this->gpioMy; + uint8_t upPin = !obj["gpioUp"].isNull() ? obj["gpioUp"].as() : this->gpioUp; + uint8_t downPin = !obj["gpioDown"].isNull() ? obj["gpioDown"].as() : this->gpioDown; + uint8_t myPin = !obj["gpioMy"].isNull() ? obj["gpioMy"].as() : this->gpioMy; if(type == shade_types::drycontact || ((type == shade_types::garage1 || type == shade_types::lgate1 || type == shade_types::cgate1 || type == shade_types::rgate1) && proto == radio_proto::GP_Remote)) upPin = myPin = 255; @@ -3190,12 +3160,14 @@ int8_t SomfyShade::validateJSON(JsonObject &obj) { (myPin != 255 && somfy.transceiver.usesPin(myPin))) ret = -10; } +#ifndef CONFIG_IDF_TARGET_ESP32C6 if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref) { if((upPin != 255 && settings.Ethernet.usesPin(upPin)) || (downPin != 255 && somfy.transceiver.usesPin(downPin)) || (myPin != 255 && somfy.transceiver.usesPin(myPin))) ret = -11; } +#endif if(ret == 0) { for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) { SomfyShade *shade = &somfy.shades[i]; @@ -3215,28 +3187,28 @@ int8_t SomfyShade::validateJSON(JsonObject &obj) { int8_t SomfyShade::fromJSON(JsonObject &obj) { int8_t err = this->validateJSON(obj); if(err == 0) { - if(obj.containsKey("name")) strlcpy(this->name, obj["name"], sizeof(this->name)); - if(obj.containsKey("roomId")) this->roomId = obj["roomId"]; - if(obj.containsKey("upTime")) this->upTime = obj["upTime"]; - if(obj.containsKey("downTime")) this->downTime = obj["downTime"]; - if(obj.containsKey("remoteAddress")) this->setRemoteAddress(obj["remoteAddress"]); - if(obj.containsKey("tiltTime")) this->tiltTime = obj["tiltTime"]; - if(obj.containsKey("stepSize")) this->stepSize = obj["stepSize"]; - if(obj.containsKey("hasTilt")) this->tiltType = static_cast(obj["hasTilt"]) ? tilt_types::none : tilt_types::tiltmotor; - if(obj.containsKey("bitLength")) this->bitLength = obj["bitLength"]; - if(obj.containsKey("proto")) this->proto = static_cast(obj["proto"].as()); - if(obj.containsKey("sunSensor")) this->setSunSensor(obj["sunSensor"]); - if(obj.containsKey("simMy")) this->setSimMy(obj["simMy"]); - if(obj.containsKey("light")) this->setLight(obj["light"]); - if(obj.containsKey("gpioFlags")) this->gpioFlags = obj["gpioFlags"]; - if(obj.containsKey("gpioLLTrigger")) { + if(!obj["name"].isNull()) strlcpy(this->name, obj["name"], sizeof(this->name)); + if(!obj["roomId"].isNull()) this->roomId = obj["roomId"]; + if(!obj["upTime"].isNull()) this->upTime = obj["upTime"]; + if(!obj["downTime"].isNull()) this->downTime = obj["downTime"]; + if(!obj["remoteAddress"].isNull()) this->setRemoteAddress(obj["remoteAddress"]); + if(!obj["tiltTime"].isNull()) this->tiltTime = obj["tiltTime"]; + if(!obj["stepSize"].isNull()) this->stepSize = obj["stepSize"]; + if(!obj["hasTilt"].isNull()) this->tiltType = static_cast(obj["hasTilt"]) ? tilt_types::none : tilt_types::tiltmotor; + if(!obj["bitLength"].isNull()) this->bitLength = obj["bitLength"]; + if(!obj["proto"].isNull()) this->proto = static_cast(obj["proto"].as()); + if(!obj["sunSensor"].isNull()) this->setSunSensor(obj["sunSensor"]); + if(!obj["simMy"].isNull()) this->setSimMy(obj["simMy"]); + if(!obj["light"].isNull()) this->setLight(obj["light"]); + if(!obj["gpioFlags"].isNull()) this->gpioFlags = obj["gpioFlags"]; + if(!obj["gpioLLTrigger"].isNull()) { if(obj["gpioLLTrigger"].as()) this->gpioFlags |= (uint8_t)gpio_flags_t::LowLevelTrigger; else this->gpioFlags &= ~(uint8_t)gpio_flags_t::LowLevelTrigger; } - if(obj.containsKey("shadeType")) { + if(!obj["shadeType"].isNull()) { if(obj["shadeType"].is()) { if(strncmp(obj["shadeType"].as(), "roller", 7) == 0) this->shadeType = shade_types::roller; @@ -3265,10 +3237,10 @@ int8_t SomfyShade::fromJSON(JsonObject &obj) { this->shadeType = static_cast(obj["shadeType"].as()); } } - if(obj.containsKey("flipCommands")) this->flipCommands = obj["flipCommands"].as(); - if(obj.containsKey("flipPosition")) this->flipPosition = obj["flipPosition"].as(); - if(obj.containsKey("repeats")) this->repeats = obj["repeats"]; - if(obj.containsKey("tiltType")) { + if(!obj["flipCommands"].isNull()) this->flipCommands = obj["flipCommands"].as(); + if(!obj["flipPosition"].isNull()) this->flipPosition = obj["flipPosition"].as(); + if(!obj["repeats"].isNull()) this->repeats = obj["repeats"]; + if(!obj["tiltType"].isNull()) { if(obj["tiltType"].is()) { if(strncmp(obj["tiltType"].as(), "none", 4) == 0) this->tiltType = tilt_types::none; @@ -3283,7 +3255,7 @@ int8_t SomfyShade::fromJSON(JsonObject &obj) { this->tiltType = static_cast(obj["tiltType"].as()); } } - if(obj.containsKey("linkedAddresses")) { + if(!obj["linkedAddresses"].isNull()) { uint32_t linkedAddresses[SOMFY_MAX_LINKED_REMOTES]; memset(linkedAddresses, 0x00, sizeof(linkedAddresses)); JsonArray arr = obj["linkedAddresses"]; @@ -3295,86 +3267,20 @@ int8_t SomfyShade::fromJSON(JsonObject &obj) { this->linkedRemotes[j].setRemoteAddress(linkedAddresses[j]); } } - if(obj.containsKey("flags")) this->flags = obj["flags"]; + if(!obj["flags"].isNull()) this->flags = obj["flags"]; if(this->proto == radio_proto::GP_Remote || this->proto == radio_proto::GP_Relay) { - if(obj.containsKey("gpioUp")) this->gpioUp = obj["gpioUp"]; - if(obj.containsKey("gpioDown")) this->gpioDown = obj["gpioDown"]; + if(!obj["gpioUp"].isNull()) this->gpioUp = obj["gpioUp"]; + if(!obj["gpioDown"].isNull()) this->gpioDown = obj["gpioDown"]; pinMode(this->gpioUp, OUTPUT); pinMode(this->gpioDown, OUTPUT); } if(this->proto == radio_proto::GP_Remote) { - if(obj.containsKey("gpioMy")) this->gpioMy = obj["gpioMy"]; + if(!obj["gpioMy"].isNull()) this->gpioMy = obj["gpioMy"]; pinMode(this->gpioMy, OUTPUT); } } 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:"); @@ -3430,8 +3336,8 @@ bool SomfyShade::toJSON(JsonObject &obj) { } */ bool SomfyRoom::fromJSON(JsonObject &obj) { - if(obj.containsKey("name")) strlcpy(this->name, obj["name"], sizeof(this->name)); - if(obj.containsKey("sortOrder")) this->sortOrder = obj["sortOrder"]; + if(!obj["name"].isNull()) strlcpy(this->name, obj["name"], sizeof(this->name)); + if(!obj["sortOrder"].isNull()) this->sortOrder = obj["sortOrder"]; return true; } /* @@ -3442,23 +3348,17 @@ 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"]; - if(obj.containsKey("remoteAddress")) this->setRemoteAddress(obj["remoteAddress"]); - if(obj.containsKey("bitLength")) this->bitLength = obj["bitLength"]; - if(obj.containsKey("proto")) this->proto = static_cast(obj["proto"].as()); - if(obj.containsKey("flipCommands")) this->flipCommands = obj["flipCommands"].as(); + if(!obj["name"].isNull()) strlcpy(this->name, obj["name"], sizeof(this->name)); + if(!obj["roomId"].isNull()) this->roomId = obj["roomId"]; + if(!obj["remoteAddress"].isNull()) this->setRemoteAddress(obj["remoteAddress"]); + if(!obj["bitLength"].isNull()) this->bitLength = obj["bitLength"]; + if(!obj["proto"].isNull()) this->proto = static_cast(obj["proto"].as()); + if(!obj["flipCommands"].isNull()) this->flipCommands = obj["flipCommands"].as(); - //if(obj.containsKey("sunSensor")) this->hasSunSensor() = obj["sunSensor"]; This is calculated - if(obj.containsKey("repeats")) this->repeats = obj["repeats"]; - if(obj.containsKey("linkedShades")) { + //if(!obj["sunSensor"].isNull()) this->hasSunSensor() = obj["sunSensor"]; This is calculated + if(!obj["repeats"].isNull()) this->repeats = obj["repeats"]; + if(!obj["linkedShades"].isNull()) { uint8_t linkedShades[SOMFY_MAX_GROUPED_SHADES]; memset(linkedShades, 0x00, sizeof(linkedShades)); JsonArray arr = obj["linkedShades"]; @@ -3469,50 +3369,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 +3400,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 +3423,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); } } @@ -3622,8 +3475,7 @@ uint8_t SomfyShadeController::getNextShadeId() { } } if(!id_exists) { - Serial.print("Got next Shade Id:"); - Serial.print(i); + ESP_LOGD(TAG, "Got next Shade Id:%d", i); return i; } } @@ -3660,8 +3512,7 @@ uint8_t SomfyShadeController::getNextGroupId() { } } if(!id_exists) { - Serial.print("Got next Group Id:"); - Serial.print(i); + ESP_LOGD(TAG, "Got next Group Id:%d", i); return i; } } @@ -3680,8 +3531,7 @@ uint8_t SomfyShadeController::getNextRoomId() { } } if(!id_exists) { - Serial.print("Got next room Id:"); - Serial.print(i); + ESP_LOGD(TAG, "Got next room Id:%d", i); return i; } } @@ -3760,7 +3610,7 @@ SomfyShade *SomfyShadeController::addShade() { if(shade) { shade->setShadeId(shadeId); shade->sortOrder = this->getMaxShadeOrder() + 1; - Serial.printf("Sort order set to %d\n", shade->sortOrder); + ESP_LOGD(TAG, "Sort order set to %d", shade->sortOrder); this->isDirty = true; #ifdef USE_NVS if(this->useNVS()) { @@ -3789,25 +3639,30 @@ SomfyShade *SomfyShadeController::addShade() { pref.begin("Shades"); pref.remove("shadeIds"); int x = pref.putBytes("shadeIds", this->m_shadeIds, sizeof(this->m_shadeIds)); - Serial.printf("WROTE %d bytes to shadeIds\n", x); + ESP_LOGD(TAG, "WROTE %d bytes to shadeIds", x); pref.end(); - for(uint8_t i = 0; i < sizeof(this->m_shadeIds); i++) { - if(i != 0) Serial.print(","); - else Serial.print("Shade Ids: "); - Serial.print(this->m_shadeIds[i]); + { + char shade_ids_buf[256]; + int spos = snprintf(shade_ids_buf, sizeof(shade_ids_buf), "Shade Ids: "); + for(uint8_t i = 0; i < sizeof(this->m_shadeIds); i++) { + if(i != 0) spos += snprintf(shade_ids_buf + spos, sizeof(shade_ids_buf) - spos, ","); + spos += snprintf(shade_ids_buf + spos, sizeof(shade_ids_buf) - spos, "%d", this->m_shadeIds[i]); + } + ESP_LOGD(TAG, "%s", shade_ids_buf); } - Serial.println(); pref.begin("Shades"); pref.getBytes("shadeIds", this->m_shadeIds, sizeof(this->m_shadeIds)); - Serial.print("LENGTH:"); - Serial.println(pref.getBytesLength("shadeIds")); + ESP_LOGD(TAG, "LENGTH:%d", pref.getBytesLength("shadeIds")); pref.end(); - for(uint8_t i = 0; i < sizeof(this->m_shadeIds); i++) { - if(i != 0) Serial.print(","); - else Serial.print("Shade Ids: "); - Serial.print(this->m_shadeIds[i]); + { + char shade_ids_buf[256]; + int spos = snprintf(shade_ids_buf, sizeof(shade_ids_buf), "Shade Ids: "); + for(uint8_t i = 0; i < sizeof(this->m_shadeIds); i++) { + if(i != 0) spos += snprintf(shade_ids_buf + spos, sizeof(shade_ids_buf) - spos, ","); + spos += snprintf(shade_ids_buf + spos, sizeof(shade_ids_buf) - spos, "%d", this->m_shadeIds[i]); + } + ESP_LOGD(TAG, "%s", shade_ids_buf); } - Serial.println(); } #endif } @@ -3919,14 +3774,9 @@ void SomfyRemote::sendSensorCommand(int8_t isWindy, int8_t isSunny, uint8_t repe this->lastFrame.encKey = 160; // Sensor commands are always encryption code 160. this->lastFrame.cmd = somfy_commands::Sensor; this->lastFrame.processed = false; - Serial.print("CMD:"); - Serial.print(translateSomfyCommand(this->lastFrame.cmd)); - Serial.print(" ADDR:"); - Serial.print(this->lastFrame.remoteAddress); - Serial.print(" RCODE:"); - Serial.print(this->lastFrame.rollingCode); - Serial.print(" REPEAT:"); - Serial.println(repeat); + ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d REPEAT:%d", + translateSomfyCommand(this->lastFrame.cmd).c_str(), + this->lastFrame.remoteAddress, this->lastFrame.rollingCode, repeat); somfy.sendFrame(this->lastFrame, repeat); somfy.processFrame(this->lastFrame, true); } @@ -3943,46 +3793,33 @@ void SomfyRemote::sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSi this->lastFrame.encKey = 0xA0 | static_cast(this->lastFrame.rollingCode & 0x000F); this->lastFrame.proto = this->proto; if(this->lastFrame.bitLength == 0) this->lastFrame.bitLength = bit_length; - if(this->lastFrame.rollingCode == 0) Serial.println("ERROR: Setting rcode to 0"); + if(this->lastFrame.rollingCode == 0) ESP_LOGE(TAG, "Setting rcode to 0"); this->p_lastRollingCode(this->lastFrame.rollingCode); // We have to set the processed to clear this if we are sending // another command. this->lastFrame.processed = false; if(this->proto == radio_proto::GP_Relay) { - Serial.print("CMD:"); - Serial.print(translateSomfyCommand(this->lastFrame.cmd)); - Serial.print(" ADDR:"); - Serial.print(this->lastFrame.remoteAddress); - Serial.print(" RCODE:"); - Serial.print(this->lastFrame.rollingCode); - Serial.println(" SETTING GPIO"); + ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d SETTING GPIO", + translateSomfyCommand(this->lastFrame.cmd).c_str(), + this->lastFrame.remoteAddress, this->lastFrame.rollingCode); } else if(this->proto == radio_proto::GP_Remote) { - Serial.print("CMD:"); - Serial.print(translateSomfyCommand(this->lastFrame.cmd)); - Serial.print(" ADDR:"); - Serial.print(this->lastFrame.remoteAddress); - Serial.print(" RCODE:"); - Serial.print(this->lastFrame.rollingCode); - Serial.println(" TRIGGER GPIO"); + ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d TRIGGER GPIO", + translateSomfyCommand(this->lastFrame.cmd).c_str(), + this->lastFrame.remoteAddress, this->lastFrame.rollingCode); this->triggerGPIOs(this->lastFrame); } else { - Serial.print("CMD:"); - Serial.print(translateSomfyCommand(this->lastFrame.cmd)); - Serial.print(" ADDR:"); - Serial.print(this->lastFrame.remoteAddress); - Serial.print(" RCODE:"); - Serial.print(this->lastFrame.rollingCode); - Serial.print(" REPEAT:"); - Serial.println(repeat); + ESP_LOGD(TAG, "CMD:%s ADDR:%d RCODE:%d REPEAT:%d", + translateSomfyCommand(this->lastFrame.cmd).c_str(), + this->lastFrame.remoteAddress, this->lastFrame.rollingCode, repeat); somfy.sendFrame(this->lastFrame, repeat); } somfy.processFrame(this->lastFrame, true); } bool SomfyRemote::isLastCommand(somfy_commands cmd) { if(this->lastFrame.cmd != cmd || this->lastFrame.rollingCode != this->lastRollingCode) { - Serial.printf("Not the last command %d: %d - %d\n", static_cast(this->lastFrame.cmd), this->lastFrame.rollingCode, this->lastRollingCode); + ESP_LOGD(TAG, "Not the last command %d: %d - %d", static_cast(this->lastFrame.cmd), this->lastFrame.rollingCode, this->lastRollingCode); return false; } return true; @@ -4087,7 +3924,9 @@ bool SomfyShadeController::deleteGroup(uint8_t groupId) { bool SomfyShadeController::loadShadesFile(const char *filename) { return ShadeConfigFile::load(this, filename); } uint16_t SomfyRemote::getNextRollingCode() { - pref.begin("ShadeCodes"); + if(!pref.begin("ShadeCodes")) { + ESP_LOGE(TAG, "getNextRollingCode() pref.begin(ShadeCodes) FAILED"); + } uint16_t code = pref.getUShort(this->m_remotePrefId, 0); code++; pref.putUShort(this->m_remotePrefId, code); @@ -4107,30 +3946,10 @@ uint16_t SomfyRemote::setRollingCode(uint16_t code) { pref.putUShort(this->m_remotePrefId, code); pref.end(); this->lastRollingCode = code; - Serial.printf("Setting Last Rolling code %d\n", this->lastRollingCode); + ESP_LOGD(TAG, "Setting Last Rolling code %d", this->lastRollingCode); } 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 +4004,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++) { @@ -4287,7 +4091,7 @@ void somfy_tx_queue_t::push(uint8_t hwsync, uint8_t *payload, uint8_t bit_length this->delay_time = millis() + TX_QUEUE_DELAY; // We do not want to process this frame until a full frame beat has passed. } void somfy_rx_queue_t::init() { - Serial.println("Initializing RX Queue"); + ESP_LOGD(TAG, "Initializing RX Queue"); for (uint8_t i = 0; i < MAX_RX_BUFFER; i++) this->items[i].clear(); memset(&this->index[0], 0xFF, MAX_RX_BUFFER); @@ -4531,7 +4335,7 @@ void Transceiver::beginFrequencyScan() { markFreq = currFreq = 433.0f; markRSSI = -100; ELECHOUSE_cc1101.setMHZ(currFreq); - Serial.printf("Begin frequency scan on Pin #%d\n", this->config.RXPin); + ESP_LOGD(TAG, "Begin frequency scan on Pin #%d", this->config.RXPin); attachInterrupt(interruptPin, handleReceive, CHANGE); this->emitFrequencyScan(); } @@ -4674,7 +4478,7 @@ void Transceiver::enableReceive(void) { ELECHOUSE_cc1101.SetRx(); //attachInterrupt(interruptPin, handleReceive, FALLING); attachInterrupt(interruptPin, handleReceive, CHANGE); - Serial.printf("Enabled receive on Pin #%d Timing: %ld\n", this->config.RXPin, millis() - timing); + ESP_LOGD(TAG, "Enabled receive on Pin #%d Timing: %ld", this->config.RXPin, millis() - timing); } } void Transceiver::disableReceive(void) { @@ -4683,11 +4487,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"); @@ -4697,7 +4496,7 @@ bool Transceiver::toJSON(JsonObject& obj) { } */ bool Transceiver::fromJSON(JsonObject& obj) { - if (obj.containsKey("config")) { + if (!obj["config"].isNull()) { JsonObject objConfig = obj["config"]; this->config.fromJSON(objConfig); } @@ -4726,61 +4525,45 @@ bool Transceiver::end() { } void transceiver_config_t::fromJSON(JsonObject& obj) { //Serial.print("Deserialize Radio JSON "); - if(obj.containsKey("type")) this->type = obj["type"]; - if(obj.containsKey("CSNPin")) this->CSNPin = obj["CSNPin"]; - if(obj.containsKey("MISOPin")) this->MISOPin = obj["MISOPin"]; - if(obj.containsKey("MOSIPin")) this->MOSIPin = obj["MOSIPin"]; - if(obj.containsKey("RXPin")) this->RXPin = obj["RXPin"]; - if(obj.containsKey("SCKPin")) this->SCKPin = obj["SCKPin"]; - if(obj.containsKey("TXPin")) this->TXPin = obj["TXPin"]; - if(obj.containsKey("rxBandwidth")) this->rxBandwidth = obj["rxBandwidth"]; // float - if(obj.containsKey("frequency")) this->frequency = obj["frequency"]; // float - if(obj.containsKey("deviation")) this->deviation = obj["deviation"]; // float - if(obj.containsKey("enabled")) this->enabled = obj["enabled"]; - if(obj.containsKey("txPower")) this->txPower = obj["txPower"]; - if(obj.containsKey("proto")) this->proto = static_cast(obj["proto"].as()); + if(!obj["type"].isNull()) this->type = obj["type"]; + if(!obj["CSNPin"].isNull()) this->CSNPin = obj["CSNPin"]; + if(!obj["MISOPin"].isNull()) this->MISOPin = obj["MISOPin"]; + if(!obj["MOSIPin"].isNull()) this->MOSIPin = obj["MOSIPin"]; + if(!obj["RXPin"].isNull()) this->RXPin = obj["RXPin"]; + if(!obj["SCKPin"].isNull()) this->SCKPin = obj["SCKPin"]; + if(!obj["TXPin"].isNull()) this->TXPin = obj["TXPin"]; + if(!obj["rxBandwidth"].isNull()) this->rxBandwidth = obj["rxBandwidth"]; // float + if(!obj["frequency"].isNull()) this->frequency = obj["frequency"]; // float + if(!obj["deviation"].isNull()) this->deviation = obj["deviation"]; // float + if(!obj["enabled"].isNull()) this->enabled = obj["enabled"]; + if(!obj["txPower"].isNull()) this->txPower = obj["txPower"]; + if(!obj["proto"].isNull()) this->proto = static_cast(obj["proto"].as()); /* - if (obj.containsKey("internalCCMode")) this->internalCCMode = obj["internalCCMode"]; - if (obj.containsKey("modulationMode")) this->modulationMode = obj["modulationMode"]; - if (obj.containsKey("channel")) this->channel = obj["channel"]; - if (obj.containsKey("channelSpacing")) this->channelSpacing = obj["channelSpacing"]; // float - if (obj.containsKey("dataRate")) this->dataRate = obj["dataRate"]; // float - if (obj.containsKey("syncMode")) this->syncMode = obj["syncMode"]; - if (obj.containsKey("syncWordHigh")) this->syncWordHigh = obj["syncWordHigh"]; - if (obj.containsKey("syncWordLow")) this->syncWordLow = obj["syncWordLow"]; - if (obj.containsKey("addrCheckMode")) this->addrCheckMode = obj["addrCheckMode"]; - if (obj.containsKey("checkAddr")) this->checkAddr = obj["checkAddr"]; - if (obj.containsKey("dataWhitening")) this->dataWhitening = obj["dataWhitening"]; - if (obj.containsKey("pktFormat")) this->pktFormat = obj["pktFormat"]; - if (obj.containsKey("pktLengthMode")) this->pktLengthMode = obj["pktLengthMode"]; - if (obj.containsKey("pktLength")) this->pktLength = obj["pktLength"]; - if (obj.containsKey("useCRC")) this->useCRC = obj["useCRC"]; - if (obj.containsKey("autoFlushCRC")) this->autoFlushCRC = obj["autoFlushCRC"]; - if (obj.containsKey("disableDCFilter")) this->disableDCFilter = obj["disableCRCFilter"]; - if (obj.containsKey("enableManchester")) this->enableManchester = obj["enableManchester"]; - if (obj.containsKey("enableFEC")) this->enableFEC = obj["enableFEC"]; - if (obj.containsKey("minPreambleBytes")) this->minPreambleBytes = obj["minPreambleBytes"]; - if (obj.containsKey("pqtThreshold")) this->pqtThreshold = obj["pqtThreshold"]; - if (obj.containsKey("appendStatus")) this->appendStatus = obj["appendStatus"]; - if (obj.containsKey("printBuffer")) this->printBuffer = obj["printBuffer"]; + if (!obj["internalCCMode"].isNull()) this->internalCCMode = obj["internalCCMode"]; + if (!obj["modulationMode"].isNull()) this->modulationMode = obj["modulationMode"]; + if (!obj["channel"].isNull()) this->channel = obj["channel"]; + if (!obj["channelSpacing"].isNull()) this->channelSpacing = obj["channelSpacing"]; // float + if (!obj["dataRate"].isNull()) this->dataRate = obj["dataRate"]; // float + if (!obj["syncMode"].isNull()) this->syncMode = obj["syncMode"]; + if (!obj["syncWordHigh"].isNull()) this->syncWordHigh = obj["syncWordHigh"]; + if (!obj["syncWordLow"].isNull()) this->syncWordLow = obj["syncWordLow"]; + if (!obj["addrCheckMode"].isNull()) this->addrCheckMode = obj["addrCheckMode"]; + if (!obj["checkAddr"].isNull()) this->checkAddr = obj["checkAddr"]; + if (!obj["dataWhitening"].isNull()) this->dataWhitening = obj["dataWhitening"]; + if (!obj["pktFormat"].isNull()) this->pktFormat = obj["pktFormat"]; + if (!obj["pktLengthMode"].isNull()) this->pktLengthMode = obj["pktLengthMode"]; + if (!obj["pktLength"].isNull()) this->pktLength = obj["pktLength"]; + if (!obj["useCRC"].isNull()) this->useCRC = obj["useCRC"]; + if (!obj["autoFlushCRC"].isNull()) this->autoFlushCRC = obj["autoFlushCRC"]; + if (!obj["disableDCFilter"].isNull()) this->disableDCFilter = obj["disableCRCFilter"]; + if (!obj["enableManchester"].isNull()) this->enableManchester = obj["enableManchester"]; + if (!obj["enableFEC"].isNull()) this->enableFEC = obj["enableFEC"]; + if (!obj["minPreambleBytes"].isNull()) this->minPreambleBytes = obj["minPreambleBytes"]; + if (!obj["pqtThreshold"].isNull()) this->pqtThreshold = obj["pqtThreshold"]; + if (!obj["appendStatus"].isNull()) this->appendStatus = obj["appendStatus"]; + if (!obj["printBuffer"].isNull()) this->printBuffer = obj["printBuffer"]; */ - 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); + ESP_LOGD(TAG, "SCK:%u MISO:%u MOSI:%u CSN:%u RX:%u TX:%u", this->SCKPin, this->MISOPin, this->MOSIPin, this->CSNPin, this->RXPin, this->TXPin); } /* void transceiver_config_t::toJSON(JsonObject& obj) { @@ -4871,12 +4654,11 @@ void transceiver_config_t::save() { */ pref.end(); - Serial.print("Save Radio Settings "); - 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); + ESP_LOGI(TAG, "Save Radio Settings SCK:%u MISO:%u MOSI:%u CSN:%u RX:%u TX:%u", this->SCKPin, this->MISOPin, this->MOSIPin, this->CSNPin, this->RXPin, this->TXPin); } void transceiver_config_t::removeNVSKey(const char *key) { if(pref.isKey(key)) { - Serial.printf("Removing NVS Key: CC1101.%s\n", key); + ESP_LOGD(TAG, "Removing NVS Key: CC1101.%s", key); pref.remove(key); } } @@ -4885,7 +4667,7 @@ void transceiver_config_t::load() { esp_chip_info(&ci); switch(ci.model) { case esp_chip_model_t::CHIP_ESP32S3: - Serial.println("Setting S3 Transceiver Defaults..."); + ESP_LOGD(TAG, "Setting S3 Transceiver Defaults..."); this->TXPin = 15; this->RXPin = 14; this->MOSIPin = 11; @@ -4909,6 +4691,16 @@ void transceiver_config_t::load() { this->SCKPin = 15; this->CSNPin = 14; break; +#ifdef CHIP_ESP32C6 + case esp_chip_model_t::CHIP_ESP32C6: + this->TXPin = 13; + this->RXPin = 12; + this->MOSIPin = 16; + this->MISOPin = 17; + this->SCKPin = 15; + this->CSNPin = 14; + break; +#endif default: this->TXPin = 13; this->RXPin = 12; @@ -4969,8 +4761,7 @@ void transceiver_config_t::apply() { this->radioInit = false; pref.end(); if(!radioInit) return; - Serial.print("Applying radio settings "); - Serial.printf("Setting Data Pins RX:%u TX:%u\n", this->RXPin, this->TXPin); + ESP_LOGD(TAG, "Applying radio settings - Setting Data Pins RX:%u TX:%u", this->RXPin, this->TXPin); //if(this->TXPin != this->RXPin) // pinMode(this->TXPin, OUTPUT); //pinMode(this->RXPin, INPUT); @@ -4979,9 +4770,9 @@ void transceiver_config_t::apply() { ELECHOUSE_cc1101.setGDO0(this->TXPin); // This pin may be shared. else ELECHOUSE_cc1101.setGDO(this->TXPin, this->RXPin); // GDO0, GDO2 - Serial.printf("Setting SPI Pins SCK:%u MISO:%u MOSI:%u CSN:%u\n", this->SCKPin, this->MISOPin, this->MOSIPin, this->CSNPin); + ESP_LOGD(TAG, "Setting SPI Pins SCK:%u MISO:%u MOSI:%u CSN:%u", this->SCKPin, this->MISOPin, this->MOSIPin, this->CSNPin); ELECHOUSE_cc1101.setSpiPin(this->SCKPin, this->MISOPin, this->MOSIPin, this->CSNPin); - Serial.println("Radio Pins Configured!"); + ESP_LOGD(TAG, "Radio Pins Configured!"); ELECHOUSE_cc1101.Init(); ELECHOUSE_cc1101.setCCMode(0); // set config for internal transmission mode. ELECHOUSE_cc1101.setMHZ(this->frequency); // Here you can set your basic frequency. The lib calculates the frequency automatically (default = 433.92).The cc1101 can: 300-348 MHZ, 387-464MHZ and 779-928MHZ. Read More info from datasheet. @@ -5017,11 +4808,11 @@ void transceiver_config_t::apply() { if (!ELECHOUSE_cc1101.getCC1101()) { - Serial.println("Error setting up the radio"); + ESP_LOGE(TAG, "Error setting up the radio"); this->radioInit = false; } else { - Serial.println("Successfully set up the radio"); + ESP_LOGI(TAG, "Successfully set up the radio"); somfy.transceiver.enableReceive(); this->radioInit = true; } @@ -5076,7 +4867,7 @@ void Transceiver::loop() { for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) { if(somfy.repeaters[i] == frame.remoteAddress) { tx_queue.push(&rx); - Serial.println("Queued repeater frame..."); + ESP_LOGD(TAG, "Queued repeater frame..."); break; } } @@ -5090,12 +4881,15 @@ void Transceiver::loop() { somfy_tx_t tx; tx_queue.pop(&tx); - Serial.printf("Sending frame %d - %d-BIT [", tx.hwsync, tx.bit_length); - for(uint8_t j = 0; j < 10; j++) { - Serial.print(tx.payload[j]); - if(j < 9) Serial.print(", "); + { + char frame_buf[128]; + int fpos = snprintf(frame_buf, sizeof(frame_buf), "Sending frame %d - %d-BIT [", tx.hwsync, tx.bit_length); + for(uint8_t j = 0; j < 10; j++) { + fpos += snprintf(frame_buf + fpos, sizeof(frame_buf) - fpos, "%d%s", tx.payload[j], j < 9 ? ", " : ""); + } + snprintf(frame_buf + fpos, sizeof(frame_buf) - fpos, "]"); + ESP_LOGD(TAG, "%s", frame_buf); } - Serial.println("]"); this->sendFrame(tx.payload, tx.hwsync, tx.bit_length); tx_queue.delay_time = millis() + TX_QUEUE_DELAY; diff --git a/Somfy.h b/src/Somfy.h similarity index 97% rename from Somfy.h rename to src/Somfy.h index bc5c99b..1ef4255 100644 --- a/Somfy.h +++ b/src/Somfy.h @@ -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/src/SomfyController.ino b/src/SomfyController.ino new file mode 100644 index 0000000..976bb44 --- /dev/null +++ b/src/SomfyController.ino @@ -0,0 +1,141 @@ +#include "esp_log.h" +#include +#include +#include +#include "ConfigSettings.h" +#include "ESPNetwork.h" +#include "Web.h" +#include "Sockets.h" +#include "Utils.h" +#include "Somfy.h" +#include "MQTT.h" +#include "GitOTA.h" +#include "esp_core_dump.h" + +static const char *TAG = "Main"; + +ConfigSettings settings; +Web webServer; +SocketEmitter sockEmit; +ESPNetwork net; +rebootDelay_t rebootDelay; +SomfyShadeController somfy; +MQTTClass mqtt; +GitUpdater git; + +uint32_t oldheap = 0; + +void listDir(const char *dirname, uint8_t levels) { + ESP_LOGI(TAG, "Listing: %s", dirname); + File root = LittleFS.open(dirname); + if (!root || !root.isDirectory()) { + ESP_LOGE(TAG, "Failed to open directory"); + return; + } + File file = root.openNextFile(); + while (file) { + if (file.isDirectory()) { + ESP_LOGI(TAG, " DIR : %s", file.name()); + if (levels) listDir(file.path(), levels - 1); + } else { + ESP_LOGI(TAG, " FILE: %-30s %d bytes", file.name(), file.size()); + } + file = root.openNextFile(); + } +} + +void setup() { + Serial.begin(115200); + ESP_LOGI(TAG, "Startup/Boot...."); + esp_core_dump_summary_t summary; + if (esp_core_dump_get_summary(&summary) == ESP_OK) { + ESP_LOGW(TAG, "*** Previous crash coredump found ***"); + ESP_LOGW(TAG, " Task: %s", summary.exc_task); + ESP_LOGW(TAG, " PC: 0x%08x", summary.exc_pc); +#ifdef CONFIG_IDF_TARGET_ARCH_XTENSA + ESP_LOGW(TAG, " Cause: %d", summary.ex_info.exc_cause); + char bt_buf[256] = {0}; + int pos = 0; + for (int i = 0; i < summary.exc_bt_info.depth; i++) { + pos += snprintf(bt_buf + pos, sizeof(bt_buf) - pos, " 0x%08x", summary.exc_bt_info.bt[i]); + } + ESP_LOGW(TAG, " Backtrace:%s", bt_buf); +#elif CONFIG_IDF_TARGET_ARCH_RISCV + ESP_LOGW(TAG, " Cause: %d", summary.ex_info.mcause); + ESP_LOGW(TAG, " MTVAL: 0x%08x RA: 0x%08x SP: 0x%08x", + summary.ex_info.mtval, summary.ex_info.ra, summary.ex_info.sp); +#endif + } + ESP_LOGI(TAG, "Mounting File System..."); + + + if (LittleFS.begin()) { + ESP_LOGI(TAG, "Total: %d bytes", LittleFS.totalBytes()); + ESP_LOGI(TAG, "Used: %d bytes", LittleFS.usedBytes()); + ESP_LOGI(TAG, "Free: %d bytes", LittleFS.totalBytes() - LittleFS.usedBytes()); + listDir("/", 3); + } else { + ESP_LOGE(TAG, "LittleFS mount failed!"); + } + + if(LittleFS.begin()) ESP_LOGI(TAG, "File system mounted successfully"); + else ESP_LOGE(TAG, "Error mounting file system"); + settings.begin(); + if(WiFi.status() == WL_CONNECTED) WiFi.disconnect(true); + delay(10); + webServer.startup(); + webServer.begin(); + delay(1000); + net.setup(); + somfy.begin(); + //git.checkForUpdate(); +#if ESP_ARDUINO_VERSION_MAJOR >= 3 + const esp_task_wdt_config_t wdt_config = { .timeout_ms = 15000, .idle_core_mask = 1, .trigger_panic = true }; + esp_task_wdt_init(&wdt_config); +#else + esp_task_wdt_init(15, true); //enable panic so ESP32 restarts +#endif + esp_task_wdt_add(NULL); //add current thread to WDT watch + +} + +void loop() { + // put your main code here, to run repeatedly: + //uint32_t heap = ESP.getFreeHeap(); + if(rebootDelay.reboot && millis() > rebootDelay.rebootTime) { + ESP_LOGI(TAG, "Rebooting after %lums", rebootDelay.rebootTime); + net.end(); + ESP.restart(); + return; + } + uint32_t timing = millis(); + + net.loop(); + if(millis() - timing > 100) ESP_LOGD(TAG, "Timing Net: %ldms", millis() - timing); + timing = millis(); + esp_task_wdt_reset(); + somfy.loop(); + if(millis() - timing > 100) ESP_LOGD(TAG, "Timing Somfy: %ldms", millis() - timing); + timing = millis(); + esp_task_wdt_reset(); + if(net.connected() || net.softAPOpened) { + if(!rebootDelay.reboot && net.connected() && !net.softAPOpened) { + git.loop(); + esp_task_wdt_reset(); + } + webServer.loop(); + esp_task_wdt_reset(); + if(millis() - timing > 100) ESP_LOGD(TAG, "Timing WebServer: %ldms", millis() - timing); + esp_task_wdt_reset(); + timing = millis(); + sockEmit.loop(); + if(millis() - timing > 100) ESP_LOGD(TAG, "Timing Socket: %ldms", millis() - timing); + esp_task_wdt_reset(); + timing = millis(); + } + if(rebootDelay.reboot && millis() > rebootDelay.rebootTime) { + net.end(); + ESP.restart(); + } + esp_task_wdt_reset(); +} 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 87% rename from WResp.cpp rename to src/WResp.cpp index e44e3ba..c00c1c5 100644 --- a/WResp.cpp +++ b/src/WResp.cpp @@ -1,5 +1,8 @@ #include "WResp.h" -void JsonSockEvent::beginEvent(WebSocketsServer *server, const char *evt, char *buff, size_t buffSize) { +#include "esp_log.h" + +static const char *TAG = "WResp"; +void JsonSockEvent::beginEvent(AsyncWebSocket *server, const char *evt, char *buff, size_t buffSize) { this->server = server; this->buff = buff; this->buffSize = buffSize; @@ -15,17 +18,16 @@ 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); if(escape) len += 2; if(len >= this->buffSize) { - Serial.printf("Socket exceeded buffer size %d - %d\n", this->buffSize, len); - Serial.println(this->buff); + ESP_LOGE(TAG, "Socket exceeded buffer size %d - %d: %s", this->buffSize, len, this->buff); return; } if(escape) strcat(this->buff, "\""); @@ -33,30 +35,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 +134,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..907f40f --- /dev/null +++ b/src/Web.cpp @@ -0,0 +1,2445 @@ +#include +#include +#include +#include +#include "esp_log.h" +#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 "ESPNetwork.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 ESPNetwork 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"; + +static const char *TAG = "Web"; + +static QueueHandle_t webCmdQueue = nullptr; +static SemaphoreHandle_t webCmdDone = nullptr; + +AsyncWebServer asyncServer(80); +AsyncWebServer asyncApiServer(8081); +void Web::startup() { + ESP_LOGI(TAG, "Launching web server..."); + if(!webCmdQueue) webCmdQueue = xQueueCreate(WEB_CMD_QUEUE_SIZE, sizeof(web_command_t)); + if(!webCmdDone) webCmdDone = xSemaphoreCreateBinary(); + + 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(); + ESP_LOGI(TAG, "Async API server started on port 8081"); +} +void Web::loop() { + this->processQueue(); + delay(1); +} +bool Web::queueCommand(const web_command_t &cmd) { + if(!webCmdQueue || !webCmdDone) return false; + // Clear any stale signal + xSemaphoreTake(webCmdDone, 0); + if(xQueueSend(webCmdQueue, &cmd, pdMS_TO_TICKS(100)) != pdTRUE) { + ESP_LOGE(TAG, "Command queue full, dropping command"); + return false; + } + // Wait for main loop to process it + if(xSemaphoreTake(webCmdDone, pdMS_TO_TICKS(WEB_CMD_TIMEOUT_MS)) != pdTRUE) { + ESP_LOGW(TAG, "Command queue timeout waiting for processing"); + return false; + } + return true; +} +void Web::processQueue() { + if(!webCmdQueue || !webCmdDone) return; + web_command_t cmd; + while(xQueueReceive(webCmdQueue, &cmd, 0) == pdTRUE) { + switch(cmd.type) { + case web_cmd_t::shade_command: { + SomfyShade *shade = somfy.getShadeById(cmd.shadeId); + if(shade) { + if(cmd.target <= 100) shade->moveToTarget(shade->transformPosition(cmd.target)); + else shade->sendCommand(cmd.command, cmd.repeat > 0 ? cmd.repeat : shade->repeats, cmd.stepSize); + } + break; + } + case web_cmd_t::group_command: { + SomfyGroup *group = somfy.getGroupById(cmd.groupId); + if(group) group->sendCommand(cmd.command, cmd.repeat >= 0 ? cmd.repeat : group->repeats, cmd.stepSize); + break; + } + case web_cmd_t::tilt_command: { + SomfyShade *shade = somfy.getShadeById(cmd.shadeId); + if(shade) { + if(cmd.target <= 100) shade->moveToTiltTarget(shade->transformPosition(cmd.target)); + else shade->sendTiltCommand(cmd.command); + } + break; + } + case web_cmd_t::shade_repeat: { + SomfyShade *shade = somfy.getShadeById(cmd.shadeId); + if(shade) { + if(shade->shadeType == shade_types::garage1 && cmd.command == somfy_commands::Prog) cmd.command = somfy_commands::Toggle; + if(!shade->isLastCommand(cmd.command)) shade->sendCommand(cmd.command, cmd.repeat >= 0 ? cmd.repeat : shade->repeats, cmd.stepSize); + else shade->repeatFrame(cmd.repeat >= 0 ? cmd.repeat : shade->repeats); + } + break; + } + case web_cmd_t::group_repeat: { + SomfyGroup *group = somfy.getGroupById(cmd.groupId); + if(group) { + if(!group->isLastCommand(cmd.command)) group->sendCommand(cmd.command, cmd.repeat >= 0 ? cmd.repeat : group->repeats, cmd.stepSize); + else group->repeatFrame(cmd.repeat >= 0 ? cmd.repeat : group->repeats); + } + break; + } + case web_cmd_t::set_positions: { + SomfyShade *shade = somfy.getShadeById(cmd.shadeId); + if(shade) { + if(cmd.position >= 0) shade->target = shade->currentPos = cmd.position; + if(cmd.tiltPosition >= 0 && shade->tiltType != tilt_types::none) shade->tiltTarget = shade->currentTiltPos = cmd.tiltPosition; + shade->emitState(); + } + break; + } + case web_cmd_t::shade_sensor: { + SomfyShade *shade = somfy.getShadeById(cmd.shadeId); + if(shade) { + shade->sendSensorCommand(cmd.windy, cmd.sunny, cmd.repeat >= 0 ? (uint8_t)cmd.repeat : shade->repeats); + shade->emitState(); + } + break; + } + case web_cmd_t::group_sensor: { + SomfyGroup *group = somfy.getGroupById(cmd.groupId); + if(group) { + group->sendSensorCommand(cmd.windy, cmd.sunny, cmd.repeat >= 0 ? (uint8_t)cmd.repeat : group->repeats); + group->emitState(); + } + break; + } + } + xSemaphoreGive(webCmdDone); + } +} +bool Web::isAuthenticated(AsyncWebServerRequest *request, bool cfg) { + ESP_LOGD(TAG, "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")) { + ESP_LOGD(TAG, "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 { + ESP_LOGE(TAG, "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); + token[0] = '\0'; + for(int i = 0; i < sizeof(hmacResult); i++){ + char str[3]; + sprintf(str, "%02x", (int)hmacResult[i]); + strcat(token, str); + } + ESP_LOGD(TAG, "Hash: %s", 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) { + ESP_LOGD(TAG, "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["username"].isNull()) strlcpy(username, obj["username"], sizeof(username)); + if(!obj["password"].isNull()) strlcpy(password, obj["password"], sizeof(password)); + if(!obj["pin"].isNull()) 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["shadeId"].isNull()) shadeId = obj["shadeId"]; + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); return; } + if(!obj["command"].isNull()) { String scmd = obj["command"]; command = translateSomfyCommand(scmd); } + else if(!obj["target"].isNull()) target = obj["target"].as(); + if(!obj["repeat"].isNull()) repeat = obj["repeat"].as(); + if(!obj["stepSize"].isNull()) 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) { + ESP_LOGI(TAG, "handleShadeCommand shade=%u target=%u command=%s", shadeId, target, translateSomfyCommand(command).c_str()); + web_command_t cmd = {}; + cmd.type = web_cmd_t::shade_command; + cmd.shadeId = shadeId; + cmd.target = target; + cmd.command = command; + cmd.repeat = repeat; + cmd.stepSize = stepSize; + this->queueCommand(cmd); + 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["groupId"].isNull()) groupId = obj["groupId"]; + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}")); return; } + if(!obj["command"].isNull()) { String scmd = obj["command"]; command = translateSomfyCommand(scmd); } + if(!obj["repeat"].isNull()) repeat = obj["repeat"].as(); + if(!obj["stepSize"].isNull()) 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) { + ESP_LOGI(TAG, "handleGroupCommand group=%u command=%s", groupId, translateSomfyCommand(command).c_str()); + web_command_t cmd = {}; + cmd.type = web_cmd_t::group_command; + cmd.groupId = groupId; + cmd.command = command; + cmd.repeat = repeat; + cmd.stepSize = stepSize; + this->queueCommand(cmd); + 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["shadeId"].isNull()) shadeId = obj["shadeId"]; + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); return; } + if(!obj["command"].isNull()) { String scmd = obj["command"]; command = translateSomfyCommand(scmd); } + else if(!obj["target"].isNull()) 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) { + ESP_LOGI(TAG, "handleTiltCommand shade=%u target=%u command=%s", shadeId, target, translateSomfyCommand(command).c_str()); + web_command_t cmd = {}; + cmd.type = web_cmd_t::tilt_command; + cmd.shadeId = shadeId; + cmd.target = target; + cmd.command = command; + this->queueCommand(cmd); + 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["shadeId"].isNull()) shadeId = obj["shadeId"]; + if(!obj["groupId"].isNull()) groupId = obj["groupId"]; + if(!obj["stepSize"].isNull()) stepSize = obj["stepSize"]; + if(!obj["command"].isNull()) { String scmd = obj["command"]; command = translateSomfyCommand(scmd); } + if(!obj["repeat"].isNull()) repeat = obj["repeat"].as(); + } + if(shadeId != 255) { + ESP_LOGI(TAG, "handleRepeatCommand shade=%u command=%s", shadeId, translateSomfyCommand(command).c_str()); + SomfyShade *shade = somfy.getShadeById(shadeId); + if(!shade) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade reference could not be found.\"}")); return; } + web_command_t cmd = {}; + cmd.type = web_cmd_t::shade_repeat; + cmd.shadeId = shadeId; + cmd.command = command; + cmd.repeat = repeat; + cmd.stepSize = stepSize; + this->queueCommand(cmd); + 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) { + ESP_LOGI(TAG, "handleRepeatCommand group=%u command=%s", groupId, translateSomfyCommand(command).c_str()); + SomfyGroup *group = somfy.getGroupById(groupId); + if(!group) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group reference could not be found.\"}")); return; } + web_command_t cmd = {}; + cmd.type = web_cmd_t::group_repeat; + cmd.groupId = groupId; + cmd.command = command; + cmd.repeat = repeat; + cmd.stepSize = stepSize; + this->queueCommand(cmd); + 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["shadeId"].isNull()) shadeId = obj["shadeId"]; + if(!obj["position"].isNull()) pos = obj["position"]; + if(!obj["tiltPosition"].isNull()) tiltPos = obj["tiltPosition"]; + } + if(shadeId != 255) { + ESP_LOGI(TAG, "handleSetPositions shade=%u pos=%d tiltPos=%d", shadeId, pos, tiltPos); + SomfyShade *shade = somfy.getShadeById(shadeId); + if(shade) { + web_command_t cmd = {}; + cmd.type = web_cmd_t::set_positions; + cmd.shadeId = shadeId; + cmd.position = pos; + cmd.tiltPosition = tiltPos; + this->queueCommand(cmd); + 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["shadeId"].isNull()) shadeId = obj["shadeId"].as(); + if(!obj["groupId"].isNull()) groupId = obj["groupId"].as(); + if(!obj["sunny"].isNull()) { + if(obj["sunny"].is()) sunny = obj["sunny"].as() ? 1 : 0; + else sunny = obj["sunny"].as(); + } + if(!obj["windy"].isNull()) { + if(obj["windy"].is()) windy = obj["windy"].as() ? 1 : 0; + else windy = obj["windy"].as(); + } + if(!obj["repeat"].isNull()) repeat = obj["repeat"].as(); + } + if(shadeId != 255) { + SomfyShade *shade = somfy.getShadeById(shadeId); + if(shade) { + web_command_t cmd = {}; + cmd.type = web_cmd_t::shade_sensor; + cmd.shadeId = shadeId; + cmd.sunny = sunny; + cmd.windy = windy; + cmd.repeat = repeat; + this->queueCommand(cmd); + 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) { + web_command_t cmd = {}; + cmd.type = web_cmd_t::group_sensor; + cmd.groupId = groupId; + cmd.sunny = sunny; + cmd.windy = windy; + cmd.repeat = repeat; + this->queueCommand(cmd); + 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(); + ESP_LOGI(TAG, "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); + ESP_LOGI(TAG, "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) { + ESP_LOGI(TAG, "Async Rebooting ESP..."); + rebootDelay.reboot = true; + rebootDelay.rebootTime = millis() + 500; + request->send(200, _encoding_json, "{\"status\":\"OK\",\"desc\":\"Successfully started reboot\"}"); + } + else request->send(201, _encoding_json, "{\"status\":\"ERROR\",\"desc\":\"Invalid HTTP Method\"}"); +} +void Web::handleNotFound(AsyncWebServerRequest *request) { + if(request->method() == HTTP_OPTIONS) { request->send(200); return; } + snprintf(g_async_content, sizeof(g_async_content), "404 Service Not Found: %s", request->url().c_str()); + request->send(404, _encoding_text, g_async_content); +} + +void Web::begin() { + ESP_LOGI(TAG, "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"); + ESP_LOGD(TAG, "%s", dataStr.c_str()); + JsonDocument doc; + DeserializationError err = deserializeJson(doc, dataStr); + if(err) { + request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}"); + return; + } + else { + JsonObject obj = doc.as(); + opts.fromJSON(obj); + } + } + else { + ESP_LOGD(TAG, "No restore options sent. Using defaults..."); + opts.shades = true; + } + ShadeConfigFile::restore(&somfy, "/shades.tmp", opts); + ESP_LOGI(TAG, "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; + ESP_LOGD(TAG, "Restore: %s", filename.c_str()); + File fup = LittleFS.open("/shades.tmp", "w"); + fup.close(); + } + if(len > 0) { + File fup = LittleFS.open("/shades.tmp", "a"); + fup.write(data, len); + fup.close(); + } + if(final) { + webServer.uploadSuccess = true; + } + }); + + // Static file routes removed - handled by serveStatic in startup() + + 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; } + ESP_LOGD(TAG, "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; } + ESP_LOGD(TAG, "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; } + ESP_LOGD(TAG, "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; } + ESP_LOGD(TAG, "Updating a room"); + JsonObject obj = json.as(); + if(!obj["roomId"].isNull()) { + 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; } + ESP_LOGD(TAG, "Updating a shade"); + JsonObject obj = json.as(); + if(!obj["shadeId"].isNull()) { + 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; } + ESP_LOGD(TAG, "Updating a group"); + JsonObject obj = json.as(); + if(!obj["groupId"].isNull()) { + 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["shadeId"].isNull()) shadeId = obj["shadeId"]; + else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); return; } + if(!obj["pos"].isNull()) pos = obj["pos"].as(); + if(!obj["tilt"].isNull()) 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["shadeId"].isNull()) shadeId = obj["shadeId"]; + if(!obj["rollingCode"].isNull()) rollingCode = obj["rollingCode"]; + } + SomfyShade* shade = nullptr; + if(shadeId != 255) shade = somfy.getShadeById(shadeId); + if(!shade) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to set rolling code\"}")); + } + else { + 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["shadeId"].isNull()) shadeId = obj["shadeId"]; + if(!obj["paired"].isNull()) 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["shadeId"].isNull()) shadeId = obj["shadeId"]; + } + SomfyShade* shade = nullptr; + if(shadeId != 255) shade = somfy.getShadeById(shadeId); + if(!shade) { + request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to unpair\"}")); + } + else { + if(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()) { + ESP_LOGD(TAG, "Linking a repeater"); + JsonObject obj = json.as(); + if(!obj["address"].isNull()) address = obj["address"]; + else if(!obj["remoteAddress"].isNull()) 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()) { + ESP_LOGD(TAG, "Unlinking a repeater"); + JsonObject obj = json.as(); + if(!obj["address"].isNull()) address = obj["address"]; + else if(!obj["remoteAddress"].isNull()) 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["shadeId"].isNull()) { + SomfyShade* shade = somfy.getShadeById(obj["shadeId"]); + if(shade) { + if(!obj["remoteAddress"].isNull()) { + 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; } + ESP_LOGD(TAG, "Linking a remote"); + JsonObject obj = json.as(); + if(!obj["shadeId"].isNull()) { + SomfyShade* shade = somfy.getShadeById(obj["shadeId"]); + if(shade) { + if(!obj["remoteAddress"].isNull()) { + if(!obj["rollingCode"].isNull()) 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; } + ESP_LOGD(TAG, "Linking a shade to a group"); + JsonObject obj = json.as(); + uint8_t shadeId = !obj["shadeId"].isNull() ? obj["shadeId"] : 0; + uint8_t groupId = !obj["groupId"].isNull() ? 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; } + ESP_LOGD(TAG, "Unlinking a shade from a group"); + JsonObject obj = json.as(); + uint8_t shadeId = !obj["shadeId"].isNull() ? obj["shadeId"] : 0; + uint8_t groupId = !obj["groupId"].isNull() ? 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()) { + ESP_LOGD(TAG, "Deleting a Room"); + JsonObject obj = json.as(); + if(!obj["roomId"].isNull()) 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()) { + ESP_LOGD(TAG, "Deleting a shade"); + JsonObject obj = json.as(); + if(!obj["shadeId"].isNull()) 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()) { + ESP_LOGD(TAG, "Deleting a group"); + JsonObject obj = json.as(); + if(!obj["groupId"].isNull()) 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; + ESP_LOGI(TAG, "Update: %s", filename.c_str()); + if(!Update.begin(UPDATE_SIZE_UNKNOWN)) { + ESP_LOGE(TAG, "Update begin failed"); + } + else { + somfy.transceiver.end(); + mqtt.end(); + } + } + if(len > 0) { + if(Update.write(data, len) != len) { + ESP_LOGE(TAG, "Upload of %s aborted invalid size %d", filename.c_str(), len); + Update.abort(); + } + } + if(final) { + if(Update.end(true)) { + ESP_LOGI(TAG, "Update Success: %u Rebooting...", index + len); + webServer.uploadSuccess = true; + } + else { + ESP_LOGE(TAG, "Update end failed"); + } + } + esp_task_wdt_reset(); + }); + + // updateShadeConfig - file upload + 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) { + ESP_LOGI(TAG, "Update: shades.cfg"); + File fup = LittleFS.open("/shades.tmp", "w"); + fup.close(); + } + if(len > 0) { + File fup = LittleFS.open("/shades.tmp", "a"); + fup.write(data, len); + fup.close(); + } + if(final) { + 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; + ESP_LOGI(TAG, "Update: %s", filename.c_str()); + if(!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) { + ESP_LOGE(TAG, "Update begin failed"); + } + else { + somfy.transceiver.end(); + mqtt.end(); + } + } + if(len > 0) { + if(Update.write(data, len) != len) { + ESP_LOGE(TAG, "Upload of %s aborted invalid size %d", filename.c_str(), len); + Update.abort(); + } + } + if(final) { + if(Update.end(true)) { + webServer.uploadSuccess = true; + ESP_LOGI(TAG, "Update Success: %u Rebooting...", index + len); + somfy.commit(); + } + else { + somfy.commit(); + ESP_LOGE(TAG, "Update end failed"); + } + } + esp_task_wdt_reset(); + }); + + 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); + ESP_LOGI(TAG, "Scanned %d networks", n); + AsyncJsonResp resp; + resp.beginResponse(request, g_async_content, sizeof(g_async_content)); + resp.beginObject(); + resp.beginObject("connected"); + resp.addElem("name", settings.WIFI.ssid); + resp.addElem("passphrase", settings.WIFI.passphrase); + resp.addElem("strength", (int32_t)WiFi.RSSI()); + resp.addElem("channel", (int32_t)WiFi.channel()); + resp.endObject(); + resp.beginArray("accessPoints"); + for(int i = 0; i < n; ++i) { + if(WiFi.SSID(i).length() == 0 || WiFi.RSSI(i) < -95) continue; + resp.beginObject(); + resp.addElem("name", WiFi.SSID(i).c_str()); + resp.addElem("channel", (int32_t)WiFi.channel(i)); + resp.addElem("strength", (int32_t)WiFi.RSSI(i)); + resp.addElem("macAddress", WiFi.BSSIDstr(i).c_str()); + resp.endObject(); + } + resp.endArray(); + resp.endObject(); + resp.endResponse(); + }); + + 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); + JsonDocument sdoc; + 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) { + JsonDocument doc; + 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["address"].isNull()) frame.remoteAddress = obj["address"]; + if(!obj["command"].isNull()) scmd = obj["command"].as(); + if(!obj["repeats"].isNull()) repeats = obj["repeats"]; + if(!obj["rcode"].isNull()) frame.rollingCode = obj["rcode"]; + if(!obj["encKey"].isNull()) frame.encKey = obj["encKey"]; + frame.cmd = translateSomfyCommand(scmd.c_str()); + } + if(frame.remoteAddress > 0 && frame.rollingCode > 0) { + 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["hostname"].isNull() || !obj["ssdpBroadcast"].isNull() || !obj["checkForUpdate"].isNull()) { + bool checkForUpdate = settings.checkForUpdate; + settings.fromJSON(obj); + settings.save(); + if(settings.checkForUpdate != checkForUpdate) git.emitUpdateCheck(); + if(!obj["hostname"].isNull()) net.updateHostname(); + } + if(!obj["ntpServer"].isNull() || !obj["ntpServer"].isNull()) { + settings.NTP.fromJSON(obj); + settings.NTP.save(); + } + request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set General Settings\"}"); + })); + + // setNetwork + 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["connType"].isNull() && obj["connType"].as() != static_cast(settings.connType)) { + settings.connType = static_cast(obj["connType"].as()); + settings.save(); + reboot = true; + } + if(!obj["wifi"].isNull()) { + JsonObject objWifi = obj["wifi"]; + if(settings.connType == conn_types_t::wifi) { + if(!objWifi["ssid"].isNull() && objWifi["ssid"].as().compareTo(settings.WIFI.ssid) != 0) { + if(WiFi.softAPgetStationNum() == 0) reboot = true; + } + if(!objWifi["passphrase"].isNull() && objWifi["passphrase"].as().compareTo(settings.WIFI.passphrase) != 0) { + if(WiFi.softAPgetStationNum() == 0) reboot = true; + } + } + settings.WIFI.fromJSON(objWifi); + settings.WIFI.save(); + } + if(!obj["ethernet"].isNull()) { + JsonObject objEth = obj["ethernet"]; + if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref) + reboot = true; +#ifndef CONFIG_IDF_TARGET_ESP32C6 + settings.Ethernet.fromJSON(objEth); + settings.Ethernet.save(); +#endif + } + if(reboot) { + ESP_LOGI(TAG, "Rebooting ESP for new Network settings..."); + rebootDelay.reboot = true; + rebootDelay.rebootTime = millis() + 1000; + } + request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set Network Settings\"}"); + })); + + // setIP + asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/setIP", + [](AsyncWebServerRequest *request, JsonVariant &json) { + if(json.isNull()) { + request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}"); + return; + } + ESP_LOGD(TAG, "Setting IP..."); + JsonObject obj = json.as(); + 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(); + ESP_LOGD(TAG, "Settings WIFI connection..."); + String ssid = ""; + String passphrase = ""; + if(!obj["ssid"].isNull()) ssid = obj["ssid"].as(); + if(!obj["passphrase"].isNull()) 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) { + ESP_LOGI(TAG, "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) { + JsonDocument doc; + JsonObject obj = doc.to(); + settings.toJSON(obj); + obj["fwVersion"] = settings.fwVersion.name; + JsonObject eth = obj.createNestedObject("ethernet"); +#ifndef CONFIG_IDF_TARGET_ESP32C6 + settings.Ethernet.toJSON(eth); +#endif + JsonObject wifi = obj.createNestedObject("wifi"); + settings.WIFI.toJSON(wifi); + JsonObject ip = obj.createNestedObject("ip"); + settings.IP.toJSON(ip); + serializeJson(doc, g_async_content, sizeof(g_async_content)); + request->send(200, _encoding_json, g_async_content); + }); + + // connectmqtt + 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(); + ESP_LOGD(TAG, "Saving MQTT"); + mqtt.disconnect(); + settings.MQTT.fromJSON(obj); + settings.MQTT.save(); + JsonDocument sdoc; + 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) { + JsonDocument doc; + 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..c764dd1 --- /dev/null +++ b/src/Web.h @@ -0,0 +1,74 @@ +#include +#include +#include +#include +#include "Somfy.h" +#ifndef webserver_h +#define webserver_h + +#define WEB_CMD_QUEUE_SIZE 8 +#define WEB_CMD_TIMEOUT_MS 3000 + +enum class web_cmd_t : uint8_t { + shade_command, // moveToTarget or sendCommand + group_command, // group sendCommand + tilt_command, // moveToTiltTarget or sendTiltCommand + shade_repeat, // shade sendCommand/repeatFrame + group_repeat, // group sendCommand/repeatFrame + set_positions, // set shade position directly + shade_sensor, // shade sensor command + group_sensor, // group sensor command +}; + +struct web_command_t { + web_cmd_t type; + uint8_t shadeId; + uint8_t groupId; + uint8_t target; // 0-100 or 255 (none) + somfy_commands command; + int8_t repeat; + uint8_t stepSize; + int8_t position; // for setPositions + int8_t tiltPosition; // for setPositions/tilt + int8_t sunny; // for sensor + int8_t windy; // for sensor +}; + +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); + private: + void processQueue(); + bool queueCommand(const web_command_t &cmd); +}; +#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