diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..dcd067d5 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,166 @@ +name: ESPSomfy-RTS Build + +on: + push: + branches: [main] + pull_request: + release: + types: [published] + +jobs: + 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/C6: 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 + if: github.event_name == 'release' + id: get_release + uses: bruceadams/get-release@v1.3.2 + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Check out code + uses: actions/checkout@v4 + + - name: Update version from release tag + if: github.event_name == 'release' + run: | + 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: Commit version update + if: github.event_name == 'release' && matrix.env == 'esp32dev' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add src/ConfigSettings.h data-src/appversion data-src/index.html data-src/index.js + git commit -m "chore: bump version to ${{ github.event.release.tag_name }}" + git push origin HEAD:${{ github.event.release.target_commitish }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install PlatformIO and esptool + run: pip install platformio esptool + + - name: Build LittleFS image + run: pio run -e ${{ matrix.env }} -t buildfs + + - name: Save LittleFS image + run: cp .pio/build/${{ matrix.env }}/littlefs.bin littlefs.bin + + - name: Build firmware + run: pio run -e ${{ matrix.env }} + + - name: Restore LittleFS image + run: cp littlefs.bin .pio/build/${{ matrix.env }}/littlefs.bin + + - name: List build artifacts + run: find .pio/build/${{ matrix.env }} -maxdepth 1 -name "*.bin" | sort + + - 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 artifacts + if: github.event_name != 'release' + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.name }} + path: | + .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 + ${{ matrix.obname }}.zip + retention-days: 5 + + - name: Upload LittleFS + if: github.event_name == 'release' + 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.fsname }} + asset_path: .pio/build/${{ matrix.env }}/littlefs.bin + overwrite: true + + - name: Upload firmware + if: github.event_name == 'release' + 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: .pio/build/${{ matrix.env }}/firmware.bin + overwrite: true + + - name: Upload onboard image + if: github.event_name == 'release' + 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.obname }}.zip + asset_path: ${{ matrix.obname }}.zip + overwrite: true + asset_content_type: application/zip diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 6b4bec06..00000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,151 +0,0 @@ -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: - 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 - name: ESP32 - - board: lolin_c3_mini - addr_bootloader: 0x0 - chip: ESP32-C3 - fqbn: esp32:esp32:lolin_c3_mini - name: LOLIN-C3-mini - - board: lolin_s2_mini - addr_bootloader: 0x1000 - chip: ESP32-S2 - fqbn: esp32:esp32:lolin_s2_mini - name: LOLIN-S2-mini - - board: lolin_s3_mini - addr_bootloader: 0x0 - chip: ESP32-S3 - fqbn: esp32:esp32:lolin_s3_mini - name: LOLIN-S3-mini - - steps: - - name: Check out code - uses: actions/checkout@v3 - with: - path: SomfyController - - - 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: Build ${{ matrix.name }} - run: | - mkdir -p build - arduino-cli compile --clean --output-dir build --fqbn ${{ matrix.fqbn }} --warnings default ./SomfyController - - - 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: Upload ${{ matrix.name }} - uses: actions/upload-artifact@v3 - with: - name: ${{ matrix.name }} - path: | - build/SomfyController.ino.bin - build/SomfyController.ino.bootloader.bin - build/SomfyController.ino.partitions.bin - build/SomfyController.onboard.bin - retention-days: 5 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index fe6096de..00000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,200 +0,0 @@ -name: ESPSomfy-RTS Release - -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 - runs-on: ubuntu-latest - - 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 - - - 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 - run: | - make -C mklittlefs - - - name: Create LittleFS - run: | - ./mklittlefs/mklittlefs --create data --size 1441792 SomfyController.littlefs.bin - - - name: Upload binaries - uses: actions/upload-artifact@v4 - with: - name: LittleFS - path: SomfyController.littlefs.bin - retention-days: 5 - - - name: 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 - 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 }} - 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 - - - name: ${{ matrix.name }} Compress Onboard Image - run: | - zip ${{ matrix.obname }}.zip ./${{ matrix.obname }} - - - name: Upload Onboard ${{ matrix.name }} - 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 }} - asset_name: ${{ matrix.obname }}.zip - asset_path: ${{ matrix.obname }}.zip - overwrite: true - asset_content_type: application/zip - diff --git a/.gitignore b/.gitignore index be967884..784bafca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,40 @@ +# IDE and Tooling .theia/ +.vscode/ +.claude/ +.pio/ debug_custom.json +debug.cfg + +# Hardware / SVD Files esp32.vsd esp32s3.svd -debug.cfg +# Build, Logs and Output Folders +build/ +data/ +logs/ +managed_components/ + +# ESP-IDF Specific +sdkconfig +sdkconfig.old +sdkconfig.* + +# Binary and Archive Files +*.elf +elf_archive/ +coredump_report.txt +coredump.bin + +# Project Specific / Generated Source +src/SomfyController.ino.cpp + +# Ignore Somfy INO/Bin files SomfyController.ino.XIAO_ESP32S3.bin SomfyController.ino.esp32c3.bin SomfyController.ino.esp32s2.bin -.vscode/settings.json + +# Temporary and Backup Files +*.orig +*.bak diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..3f240ee6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16.0) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(ESPSomfy-RTS) diff --git a/README.md b/README.md index 53aedd2b..19c5a850 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ +## 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 + +## There is a partition layout modification it's just better to perform full ESP32 wipeout. Perform backup then wipeout the firmware and restore from the bkp. + * fwname: firmware-only binary for OTA updates + * obname: onboard image (bootloader + partitions + firmware + littlefs merged) for flashing a new chip via USB/serial + # 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 +86,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 51765aae..00000000 --- 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 464d1790..00000000 --- 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 bb87d586..00000000 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 76f303c0..00000000 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 98d9f83d..00000000 Binary files a/SomfyController.littlefs.bin and /dev/null differ diff --git a/Web.cpp b/Web.cpp deleted file mode 100644 index 425d1b22..00000000 --- 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 33392959..00000000 --- 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/app_version.py b/app_version.py new file mode 100644 index 00000000..533442ac --- /dev/null +++ b/app_version.py @@ -0,0 +1,32 @@ +import os +Import("env") + +# Define the folder and filename +DATA_FOLDER = "data-src" +FILENAME = "appversion" + +# Construct the full path: /your/project/path/data-src/appversion +project_dir = env.get("PROJECT_DIR") +version_file_path = os.path.join(project_dir, DATA_FOLDER, FILENAME) + +# Default fallback if something goes wrong +version = "0.0.0" + +if os.path.exists(version_file_path): + try: + with open(version_file_path, "r") as f: + version = f.read().strip() + # Clean output for the PlatformIO console + print(f"--- SUCCESS: Found version {version} in {version_file_path} ---") + except Exception as e: + print(f"--- ERROR: could not read version file: {e} ---") +else: + print(f"--- ERROR: File NOT FOUND at {version_file_path} ---") + +# Apply to the build environment +# We use escaped quotes so C++ treats it as a string literal "vX.X.X" +full_version_str = f'\\"v{version}\\"' + +env.Append(CPPDEFINES=[ + ("FW_VERSION", full_version_str) +]) \ No newline at end of file diff --git a/archive_elf.py b/archive_elf.py new file mode 100644 index 00000000..0e5416ee --- /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 00000000..9c25f93c --- /dev/null +++ b/data-src/appversion @@ -0,0 +1 @@ +3.0.13 \ 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 97% rename from data/index.html rename to data-src/index.html index fda06701..4cc3eef0 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: +

@@ -911,6 +913,10 @@

+
+ + +
@@ -986,7 +992,7 @@

-
KeyAddressCommandCodeRSSIBitsTime
+
KeyAddressCommandCodeRSSIBitsTimeRaw
@@ -1020,21 +1026,23 @@

Enter Pin

-