diff --git a/.github/actions/setup-platformio/action.yml b/.github/actions/setup-platformio/action.yml new file mode 100644 index 00000000..a8b3842b --- /dev/null +++ b/.github/actions/setup-platformio/action.yml @@ -0,0 +1,64 @@ +--- +########################### +## Setup PlatformIO ## +########################### +## Composite action that centralizes the repeated PlatformIO setup: +## Python, pip install, and cache restore/save (tools + libdeps). +## +## All PlatformIO-based workflows should use this instead of +## duplicating the setup steps. +## +## Inputs: +## python-version Python version for PlatformIO (default 3.11) +## pio-core-dir PlatformIO core directory; exported as +## PLATFORMIO_CORE_DIR for subsequent steps (default ~/.platformio) +## cache-key Full cache key for the PlatformIO cache +## restore-key Restore key prefix for the PlatformIO cache +name: "Set up PlatformIO" +description: "Cache and install PlatformIO CLI with Python" + +inputs: + python-version: + description: "Python version to use" + required: false + default: "3.11" + pio-core-dir: + description: "PlatformIO core directory (exported as PLATFORMIO_CORE_DIR)" + required: false + default: "~/.platformio" + cache-key: + description: "Cache key for the PlatformIO cache" + required: false + default: "${{ runner.os }}-pio-${{ hashFiles('**/platformio.ini') }}" + restore-key: + description: "Restore key prefix for the PlatformIO cache" + required: false + default: "${{ runner.os }}-pio-" + +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: ${{ inputs.python-version }} + + - name: Cache PlatformIO + uses: actions/cache@v6 + with: + path: | + ${{ inputs.pio-core-dir }} + .pio + key: ${{ inputs.cache-key }} + restore-keys: | + ${{ inputs.restore-key }} + + - name: Export PLATFORMIO_CORE_DIR + shell: bash + run: echo "PLATFORMIO_CORE_DIR=${{ inputs.pio-core-dir }}" >> "$GITHUB_ENV" + + - name: Install PlatformIO + shell: bash + run: | + python -m pip install --upgrade pip + pip install platformio intelhex diff --git a/.github/workflows/auto-merge-release.yml b/.github/workflows/auto-merge-release.yml index 5fabfb43..348c73a4 100644 --- a/.github/workflows/auto-merge-release.yml +++ b/.github/workflows/auto-merge-release.yml @@ -36,15 +36,11 @@ jobs: if: startsWith(github.event.pull_request.head.ref, 'release-please--') runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - name: Enable auto-merge via gh CLI env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh pr merge "${{ github.event.pull_request.number }}" \ + --repo "${{ github.repository }}" \ --auto --merge \ --subject "chore(main): release" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index daa26aed..b01916b1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -37,28 +37,15 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@v4.38.2 with: languages: ${{ matrix.language }} # Build the project using PlatformIO - - name: Set up Python - uses: actions/setup-python@v6 + - name: Set up PlatformIO + uses: ./.github/actions/setup-platformio with: python-version: "3.11" - - name: Cache PlatformIO - uses: actions/cache@v6 - with: - path: | - ~/.platformio - .pio - key: ${{ runner.os }}-pio-${{ hashFiles('**/platformio.ini') }} - restore-keys: | - ${{ runner.os }}-pio- - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install platformio intelhex - name: Run PlatformIO run: platformio run @@ -70,7 +57,7 @@ jobs: # The reliable fix is to post-process the SARIF and strip results # under .pio/libdeps/ before uploading. - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@v4.38.2 with: output: sarif-results upload: never @@ -87,6 +74,6 @@ jobs: done - name: Upload CodeQL results - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@v4.38.2 with: sarif_file: sarif-results diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 73cb76e8..aa7b0b6b 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -2,6 +2,11 @@ name: MegaLinter on: [push, pull_request] +# Cancel in-progress runs for the same workflow+ref when new commits are pushed +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest @@ -9,4 +14,4 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v7 - - uses: oxsecurity/megalinter/flavors/c_cpp@v9.6.0 + - uses: oxsecurity/megalinter/flavors/c_cpp@v10.1.0 diff --git a/.github/workflows/native-tests.yml b/.github/workflows/native-tests.yml index c53a2e94..eb2a9f90 100644 --- a/.github/workflows/native-tests.yml +++ b/.github/workflows/native-tests.yml @@ -33,25 +33,8 @@ jobs: - name: Checkout uses: actions/checkout@v7 - - name: Cache PlatformIO - uses: actions/cache@v6 - with: - path: | - ~/.platformio - .pio - key: ${{ runner.os }}-pio-${{ hashFiles('**/platformio.ini') }} - restore-keys: | - ${{ runner.os }}-pio- - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.x" - - - name: Install PlatformIO - run: | - python -m pip install --upgrade pip - pip install platformio + - name: Set up PlatformIO + uses: ./.github/actions/setup-platformio - name: Install project dependencies (ArduinoJson etc.) run: pio pkg install --environment esp32dev diff --git a/.github/workflows/pio-dependency-check.yml b/.github/workflows/pio-dependency-check.yml index 2a4c6df9..038f7247 100644 --- a/.github/workflows/pio-dependency-check.yml +++ b/.github/workflows/pio-dependency-check.yml @@ -5,12 +5,14 @@ on: - cron: "0 8 * * 1" workflow_dispatch: +permissions: + contents: read + issues: write + jobs: check-dependencies: name: Check PlatformIO Library Dependencies (${{ matrix.environment }}) runs-on: ubuntu-latest - permissions: - contents: read strategy: matrix: @@ -20,16 +22,37 @@ jobs: - name: Checkout uses: actions/checkout@v7 - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.11" - - - name: Install PlatformIO - run: | - python -m pip install --upgrade pip - pip install platformio intelhex + - name: Set up PlatformIO + uses: ./.github/actions/setup-platformio - name: Check outdated packages (${{ matrix.environment }}) + id: outdated run: pio pkg outdated --environment ${{ matrix.environment }} continue-on-error: true + + - name: Create issue if updates are available + if: steps.outdated.outcome == 'failure' + uses: actions/github-script@v9 + with: + script: | + const issueTitle = 'PlatformIO Updates Available'; + const issueBody = 'PlatformIO-Abhängigkeiten benötigen Updates. Bitte überprüfen.'; + + // Überprüfen, ob bereits ein Issue mit diesem Titel existiert + const existingIssues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + title: issueTitle + }); + + if (existingIssues.data.length === 0) { + // Erstelle ein neues Issue + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: issueTitle, + body: issueBody, + labels: ['enhancement', 'dependencies'] + }); + } diff --git a/.github/workflows/plaform.io.yml b/.github/workflows/plaform.io.yml index b3416352..54eb1783 100644 --- a/.github/workflows/plaform.io.yml +++ b/.github/workflows/plaform.io.yml @@ -35,36 +35,26 @@ jobs: build: runs-on: ubuntu-latest - permissions: - contents: read - strategy: matrix: - environment: [esp32dev, norvi_ae01_r] + include: + - environment: esp32dev + pio-core-dir: ~/.platformio + - environment: norvi_ae01_r + pio-core-dir: ~/.platformio + - environment: olimex_esp32_c6_evb + pio-core-dir: ~/.platformio-olimex-c6 steps: - name: Checkout uses: actions/checkout@v7 - - name: Cache PlatformIO - uses: actions/cache@v6 - with: - path: | - ~/.platformio - .pio - key: ${{ runner.os }}-pio-${{ hashFiles('**/platformio.ini') }} - restore-keys: | - ${{ runner.os }}-pio- - - - name: Set up Python - uses: actions/setup-python@v6 + - name: Set up PlatformIO + uses: ./.github/actions/setup-platformio with: - python-version: "3.x" - - - name: Install PlatformIO - run: | - python -m pip install --upgrade pip - pip install platformio intelhex + pio-core-dir: ${{ matrix.pio-core-dir }} + cache-key: ${{ runner.os }}-pio-${{ matrix.environment }}-${{ hashFiles('**/platformio.ini') }} + restore-key: ${{ runner.os }}-pio-${{ matrix.environment }}- - name: Run PlatformIO Check run: > diff --git a/.github/workflows/platformio-update-check.yml b/.github/workflows/platformio-update-check.yml deleted file mode 100644 index 7b14621c..00000000 --- a/.github/workflows/platformio-update-check.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: PlatformIO Update Check - -on: - schedule: - - cron: "0 0 * * *" # Täglich um Mitternacht ausführen - workflow_dispatch: # Ermöglicht manuelles Auslösen - -jobs: - check-updates: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v7 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.10" - - - name: Install PlatformIO - run: pip install platformio intelhex - - - name: Check for updates - run: | - # Hier kannst du ein Skript einfügen, das nach Updates für deine PlatformIO-Abhängigkeiten sucht - # Zum Beispiel: - pio pkg update - pio pkg outdated - - - name: Create issue if updates are available - if: failure() # Nur ausführen, wenn das vorherige Schritt fehlschlägt (Updates verfügbar) - uses: actions/github-script@v9 - with: - script: | - const issueTitle = 'PlatformIO Updates Available'; - const issueBody = 'PlatformIO-Abhängigkeiten benötigen Updates. Bitte überprüfen.'; - - // Überprüfen, ob bereits ein Issue mit diesem Titel existiert - const existingIssues = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - title: issueTitle - }); - - if (existingIssues.data.length === 0) { - // Erstelle ein neues Issue - await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: issueTitle, - body: issueBody, - labels: ['enhancement', 'dependencies'] - }); - } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be96db3f..4c87a18e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,31 +45,24 @@ jobs: strategy: matrix: - environment: [esp32dev, norvi_ae01_r] + include: + - environment: esp32dev + pio-core-dir: ~/.platformio + - environment: norvi_ae01_r + pio-core-dir: ~/.platformio + - environment: olimex_esp32_c6_evb + pio-core-dir: ~/.platformio-olimex-c6 steps: - name: Checkout uses: actions/checkout@v7 - - name: Cache PlatformIO - uses: actions/cache@v6 + - name: Set up PlatformIO + uses: ./.github/actions/setup-platformio with: - path: | - ~/.platformio - .pio - key: ${{ runner.os }}-pio-${{ hashFiles('**/platformio.ini') }} - restore-keys: | - ${{ runner.os }}-pio- - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.x" - - - name: Install PlatformIO - run: | - python -m pip install --upgrade pip - pip install platformio intelhex + pio-core-dir: ${{ matrix.pio-core-dir }} + cache-key: ${{ runner.os }}-pio-${{ matrix.environment }}-${{ hashFiles('**/platformio.ini') }} + restore-key: ${{ runner.os }}-pio-${{ matrix.environment }}- - name: Build firmware run: platformio run --environment ${{ matrix.environment }} diff --git a/.gitignore b/.gitignore index 5e838119..e6557f09 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,7 @@ test/native/relay_safety/build/ # Frontend (separate JS project) — ignore lockfiles and node_modules test/frontend/ +graphify-out/ + +# Pre-compressed web assets (generated by scripts/gzip-web-assets.sh) +data/web/*.gz diff --git a/.opencode/skills/deploy/SKILL.md b/.opencode/skills/deploy/SKILL.md index f541760f..a2925c0b 100644 --- a/.opencode/skills/deploy/SKILL.md +++ b/.opencode/skills/deploy/SKILL.md @@ -22,6 +22,8 @@ keywords: - version bump - ausrollen - flashen + - olimex esp32 c6 evb + - pioarduino --- # Deploy — Pool Controller @@ -38,13 +40,19 @@ serial flash, web filesystem upload, OTA update, and semver release management. │ - format │ │ -e norvi │ │ Serial: │ │ - version check│ │ - version │ │ │ │ flash + uploadfs │ │ - web UI │ └─────────────┘ └──────────────┘ │ OTA: │ └─────────────────┘ - │ 1. flash firmware │ - │ 2. upload web fs │ - │ via /api/fs/ │ - │ upload (6 files)│ - └──────────────────────┘ + │ 1. flash firmware │ + │ 2. upload web fs │ + │ via /api/fs/ │ + │ upload (6 files)│ + └──────────────────────┘ ``` +## Olimex local UI note + +- The Olimex ESP32-C6-EVB local TFT UI is separate from the browser/LittleFS web UI. +- For C6 build/deploy work, use the pinned pioarduino environment and a separate `PLATFORMIO_CORE_DIR` from upstream ESP32/NORVI builds. +- The QR page points to `http://pool-controller.local`; do not confuse it with the web dashboard assets in `data/web/`. + ## Prerequisites ```bash @@ -261,6 +269,7 @@ curl -b /tmp/ota-cookie.txt -X POST http:///api/fs/upload \ ``` Response `200 OK` means the file was written to LittleFS. The endpoint: + - Requires authentication (valid session cookie) - Only allows paths under `/web/` (security) - Blocks path traversal (`..`) @@ -272,13 +281,13 @@ Response `200 OK` means the file was written to LittleFS. The endpoint: ### OTA troubleshooting -| Symptom | Fix | -| ------------------------------------ | ------------------------------------------------------ | -| `HTTP 401` on upload | Session expired — re-login (cookie valid 10 min) | -| `HTTP 429` on login | Too many failed attempts — wait for lockout to expire | -| `curl: (7) Failed to connect` | Device unreachable — check WiFi, ping the IP/hostname | -| Upload succeeds but device stays off | Wait ~30s for reboot, then check `/api/status` uptime | -| Device doesn't boot after OTA | Serial flash a known-good firmware (see Rollback) | +| Symptom | Fix | +| ------------------------------------ | ----------------------------------------------------- | +| `HTTP 401` on upload | Session expired — re-login (cookie valid 10 min) | +| `HTTP 429` on login | Too many failed attempts — wait for lockout to expire | +| `curl: (7) Failed to connect` | Device unreachable — check WiFi, ping the IP/hostname | +| Upload succeeds but device stays off | Wait ~30s for reboot, then check `/api/status` uptime | +| Device doesn't boot after OTA | Serial flash a known-good firmware (see Rollback) | ## CI/CD Pipeline (GitHub Actions) diff --git a/.opencode/skills/hugo-docs/SKILL.md b/.opencode/skills/hugo-docs/SKILL.md index c6aa33f4..3d1fa6c1 100644 --- a/.opencode/skills/hugo-docs/SKILL.md +++ b/.opencode/skills/hugo-docs/SKILL.md @@ -22,6 +22,7 @@ keywords: - doc update - seo - canonical url + - olimex esp32 c6 evb --- # Hugo Documentation — Pool Controller @@ -60,6 +61,8 @@ smart-swimmingpool/website/ # Hugo site build → smart-swimmingpool.com ## Project-Specific Hugo Conventions +For Olimex local-UI hardware notes, keep the source-of-truth checklist in `docs/olimex-esp32-c6-evb.md` aligned with implementation and `src/Config.hpp` pin names. + ### Frontmatter All documentation files use Hugo frontmatter with these fields: diff --git a/.opencode/skills/platformio-env/SKILL.md b/.opencode/skills/platformio-env/SKILL.md index 85c67c40..f017c9d1 100644 --- a/.opencode/skills/platformio-env/SKILL.md +++ b/.opencode/skills/platformio-env/SKILL.md @@ -18,6 +18,9 @@ keywords: - pio cli - ci integration - platformio check + - olimex esp32 c6 evb + - pioarduino + - platformio core dir --- # PlatformIO Environment — Pool Controller @@ -27,6 +30,14 @@ covers the build system, platform configuration, library management, and environment setup — complementing the `platformio-workflow` skill for operational commands. +## Olimex ESP32-C6-EVB Notes + +- Use the `olimex_esp32_c6_evb` environment for the Olimex board. +- ESP32-C6 Arduino builds require the pinned pioarduino platform; keep a separate `PLATFORMIO_CORE_DIR` for C6/pioarduino builds versus upstream ESP32/NORVI builds. +- Native USB CDC is the serial console path; keep `ARDUINO_USB_CDC_ON_BOOT=1` enabled. +- Local UI wiring lives in `src/Config.hpp`; treat it as the source of truth for TFT, encoder, relay, and sensor pins. +- Current Olimex local UI pin highlights: TFT MOSI/SCLK/CS/DC = GPIO18/19/21/7, TFT MISO = -1 (write-only), KY-040 CLK/DT/SW = GPIO4/5/16, DS18B20 pool = GPIO20, relays = GPIO10/11, status LED = GPIO8. + ## Architecture Overview ```text diff --git a/.opencode/skills/platformio-workflow/SKILL.md b/.opencode/skills/platformio-workflow/SKILL.md index 4314d872..e8298aa7 100644 --- a/.opencode/skills/platformio-workflow/SKILL.md +++ b/.opencode/skills/platformio-workflow/SKILL.md @@ -14,6 +14,10 @@ keywords: - pio device monitor - dependency management - library dependency + - olimex build + - pioarduino + - native usb cdc + - olimex_esp32_c6_evb --- # PlatformIO Workflow — Pool Controller @@ -32,6 +36,13 @@ Build, flash, monitor, and OTA operations for the ESP32 pool-controller. - **Serial speed**: 115200 baud - **Upload speed**: 230400 baud +### Olimex C6 variant + +- Build target: `olimex_esp32_c6_evb` +- Use the pinned pioarduino platform and a separate `PLATFORMIO_CORE_DIR` when building C6, so the pioarduino packages do not interfere with upstream ESP32/NORVI builds. +- Native USB CDC is the serial console path on this board; keep `ARDUINO_USB_CDC_ON_BOOT=1` enabled. +- Olimex local UI hardware smoke order: build, boot without peripherals, wire TFT (3.3 V logic, reset tied high), then KY-040 on GPIO 4/5/16. + ## Build Commands ```bash diff --git a/.opencode/skills/web-ui/SKILL.md b/.opencode/skills/web-ui/SKILL.md index d1147463..fe2e4747 100644 --- a/.opencode/skills/web-ui/SKILL.md +++ b/.opencode/skills/web-ui/SKILL.md @@ -22,12 +22,16 @@ keywords: - pool dashboard - telemetry - hot reload + - olimex local tft ui + - qrcode page --- # Web UI — Pool Controller Web interface development for the ESP32 pool-controller. The UI lives in two layers: **LittleFS** (editable at runtime) and **PROGMEM** (embedded fallback in the firmware binary). +> **Note:** The Olimex local TFT UI is a separate on-device interface (Adafruit GFX + ILI9341/ST7789) and is not the browser/LittleFS dashboard. + > **🔍 Code Search**: Use `semble search "LittleFS.open"` or `semble search "kPortalPageHtml"` to find the fallback architecture. See `Agents.md` §7 for full `semble` usage. --- diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3d3c5268..9d5573ce 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.2.1" + ".": "5.0.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 992d41ea..6c9c7ff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,144 @@ All notable changes to this project will be documented in this file. +## [5.0.1](https://github.com/smart-swimmingpool/pool-controller/compare/v5.0.0...v5.0.1) (2026-08-20) + + +### Bug Fixes + +* **web:** stop telemetry poll from overwriting config edits ([#185](https://github.com/smart-swimmingpool/pool-controller/issues/185)) ([71ee560](https://github.com/smart-swimmingpool/pool-controller/commit/71ee560ae18c8539bf23de4c54c4ca71ab645e69)) + +## [5.0.0](https://github.com/smart-swimmingpool/pool-controller/compare/v4.2.1...v5.0.0) (2026-08-18) + + +### ⚠ BREAKING CHANGES + +* The Homie MQTT protocol has been removed entirely. The controller now exclusively uses Home Assistant MQTT Discovery. + +### Features + +* add Home Assistant climate/thermostat MQTT entity ([d5daeb9](https://github.com/smart-swimmingpool/pool-controller/commit/d5daeb9f7f4dcad2fb2573404db898bd765bb743)) +* add mDNS responder for pool-controller.local discovery ([#95](https://github.com/smart-swimmingpool/pool-controller/issues/95)) ([b1252e7](https://github.com/smart-swimmingpool/pool-controller/commit/b1252e724750c0fa2cfaac1a25f66cc69dce9ba2)) +* add Olimex ESP32-C6 local UI ([#175](https://github.com/smart-swimmingpool/pool-controller/issues/175)) ([2777cc9](https://github.com/smart-swimmingpool/pool-controller/commit/2777cc960a9d41afd77fe8edf43ec1fd54df1f02)) +* **calibration:** guided NORVI button calibration wizard ([#182](https://github.com/smart-swimmingpool/pool-controller/issues/182)) ([c4fc499](https://github.com/smart-swimmingpool/pool-controller/commit/c4fc499f458d20010e525050c7832c4dfa817675)) +* change default MQTT protocol to HomeAssistant ([5d4cad9](https://github.com/smart-swimmingpool/pool-controller/commit/5d4cad9a3173f67e0d3db6c8722207f1e91a8a80)) +* Cleanup and fixes ([#72](https://github.com/smart-swimmingpool/pool-controller/issues/72)) ([90d6e07](https://github.com/smart-swimmingpool/pool-controller/commit/90d6e07383b0f26f9675ba7f25e306bf4d5b3b51)) +* convert WebUI to Progressive Web App (PWA) ([b2f05d1](https://github.com/smart-swimmingpool/pool-controller/commit/b2f05d132902474539d9e92bf4d3029e42e73eda)) +* **display:** action menu, wizard-cancel, and step indicator ([b0d717d](https://github.com/smart-swimmingpool/pool-controller/commit/b0d717dfedc51fe695e942982ea18f02f8ceb90b)) +* **display:** horizontal text scrolling for long strings ([c7e03fb](https://github.com/smart-swimmingpool/pool-controller/commit/c7e03fbf34425185e0276d1193437ee524098925)) +* **display:** long-press progress bar and auto-return warning ([9b49135](https://github.com/smart-swimmingpool/pool-controller/commit/9b49135eccfa60f8bcb8b70832909db45c5899fd)) +* **display:** merge Aktionsmenü, Wizard-Cancel und Schritt-Indikator ([#148](https://github.com/smart-swimmingpool/pool-controller/issues/148)) ([4ac6cc2](https://github.com/smart-swimmingpool/pool-controller/commit/4ac6cc2d1ee8920c03cb40bd05ebce59a3fb59db)) +* **display:** merge Hint-Labels, Footer-Format und S1-Wrap ([#147](https://github.com/smart-swimmingpool/pool-controller/issues/147)) ([7264a33](https://github.com/smart-swimmingpool/pool-controller/commit/7264a33645e6ee9fad4bd203524abf28cfceb4c3)) +* **display:** merge Long-Press-Fortschrittsbalken und Auto-Return-Warnung ([#149](https://github.com/smart-swimmingpool/pool-controller/issues/149)) ([bc3e02e](https://github.com/smart-swimmingpool/pool-controller/commit/bc3e02e186910f986a87ead37f356439c7549d3c)) +* **display:** update hint labels, footer page format, and S1 wrap ([1da577a](https://github.com/smart-swimmingpool/pool-controller/commit/1da577a83928df0bab392dc9926278b625ea97ad)) +* **docs:** add KiCad 9.0 schematic generator and PDF exports ([#104](https://github.com/smart-swimmingpool/pool-controller/issues/104)) ([ff46191](https://github.com/smart-swimmingpool/pool-controller/commit/ff46191ddf8a77bf70babaede6aab5f7a07e1feb)) +* **docs:** Add Quick Start Guide, FAQ, and Safety Warnings ([#107](https://github.com/smart-swimmingpool/pool-controller/issues/107)) ([b0152e1](https://github.com/smart-swimmingpool/pool-controller/commit/b0152e1fc735dd44811d43082a439513ba065fac)) +* **ha:** add circulation-extension sensor showing extra minutes beyond base timer ([0270355](https://github.com/smart-swimmingpool/pool-controller/commit/0270355afa48ab645aed4a4fd658858233d8f7cb)) +* **ha:** add circulation-extension sensor showing extra minutes beyond base timer ([9339d25](https://github.com/smart-swimmingpool/pool-controller/commit/9339d25e5f03984da82c8aacd372992d531c43e7)) +* **ha:** add display precision, circulating action, outlet class, MQTT sensor, cleaner names ([85c1ffd](https://github.com/smart-swimmingpool/pool-controller/commit/85c1ffd3725cdcfbf0dffa37be515a52da46f8d3)) +* **ha:** add state_class, binary_sensor, RSSI class, and climate preset modes ([94daa52](https://github.com/smart-swimmingpool/pool-controller/commit/94daa5213f876b74921383e247100796877b38fd)) +* **ha:** Replace timer H/Min number entities with single HH:MM text entities ([#74](https://github.com/smart-swimmingpool/pool-controller/issues/74)) ([27c337f](https://github.com/smart-swimmingpool/pool-controller/commit/27c337fd1ed311cbe2752aa64ad76967adeccead)) +* **logging:** add log console view (LogCapture ring buffer, REST /api/logs, MQTT event export) ([#169](https://github.com/smart-swimmingpool/pool-controller/issues/169)) ([93cc7a0](https://github.com/smart-swimmingpool/pool-controller/commit/93cc7a0c875a2006902e2e8aa625faddaa0201c0)) +* **mqtt:** set HA entity_category for all discovery entities ([375f40f](https://github.com/smart-swimmingpool/pool-controller/commit/375f40fe7d8fd8c9daa4c2b02fa5572ff6ea127a)) +* NORVI AE01-R hardware support with OLED display ([#117](https://github.com/smart-swimmingpool/pool-controller/issues/117)) ([964a1bd](https://github.com/smart-swimmingpool/pool-controller/commit/964a1bdc746f80b196f6e48d944a3e7575046a82)) +* **norvi:** add OLED display with 4 info pages, button navigation, and QR code ([34681e2](https://github.com/smart-swimmingpool/pool-controller/commit/34681e23be3a3eccf483a77d42e1d6e92a84f286)) +* NTP server config via Web UI + MQTT, local time display ([#83](https://github.com/smart-swimmingpool/pool-controller/issues/83)) ([c45bd55](https://github.com/smart-swimmingpool/pool-controller/commit/c45bd55ad86564929a08b274fbec1e6ff8a9ad14)) +* NVS config backup, MQTT error reporting, web UI improvements ([#82](https://github.com/smart-swimmingpool/pool-controller/issues/82)) ([817500a](https://github.com/smart-swimmingpool/pool-controller/commit/817500a0332a15a1f9425737d78c7c3b25b3396b)) +* OLED Menu-Navigation, Sensor-Mapping in WebUI+HA, mDNS ([f72bbe2](https://github.com/smart-swimmingpool/pool-controller/commit/f72bbe2c7622259a1592afe66d52fa17f5e94fa5)) +* optimierte Pin-Belegung (GPIO32/33/25/26) + Status-LED mit Homie-Blinkcodes ([c6ef387](https://github.com/smart-swimmingpool/pool-controller/commit/c6ef387ee16d64f1be46933f2d5b1b2d4116b7d1)) +* OTA update from GitHub releases, semver release management, config safety ([#77](https://github.com/smart-swimmingpool/pool-controller/issues/77)) ([2972b34](https://github.com/smart-swimmingpool/pool-controller/commit/2972b3493d6bb9c2b8416fd281bdae76bf75ea70)) +* restructure web UI settings tabs and HA entity categories ([#87](https://github.com/smart-swimmingpool/pool-controller/issues/87)) ([712b037](https://github.com/smart-swimmingpool/pool-controller/commit/712b03763a32e6417c9ab5f7322bcaddb6b85668)) +* semver release pipeline via release-please (Conventional Commits) ([#29](https://github.com/smart-swimmingpool/pool-controller/issues/29)) ([102b18b](https://github.com/smart-swimmingpool/pool-controller/commit/102b18b64af6364e59147baf462b970cc73e5d7d)) +* **system:** log concrete reason when entering safe mode ([#179](https://github.com/smart-swimmingpool/pool-controller/issues/179)) ([e7ac809](https://github.com/smart-swimmingpool/pool-controller/commit/e7ac809bf0a24219843f29c21689e924692d14c6)) +* temperature-based circulation time with continuous extension ([b16a9d0](https://github.com/smart-swimmingpool/pool-controller/commit/b16a9d04251b28f5a5ebf0e2907b64bdaae4ca3d)) +* **ui:** add About section in More bottom sheet ([5919624](https://github.com/smart-swimmingpool/pool-controller/commit/59196247d43e934cb7b8ea60f642ed57f86320bb)) +* **ui:** iOS-style bottom tab bar with glassmorphism ([38eec43](https://github.com/smart-swimmingpool/pool-controller/commit/38eec4356f103e2711a2fa0165a09ba23fe43013)) +* **web-ui:** redesign timer display as intuitive time range instead of abstract minutes ([366d647](https://github.com/smart-swimmingpool/pool-controller/commit/366d64755e845375ae6bd0b7c6bd39d5a4f7b690)) +* **web-ui:** redesign timer display as intuitive time range instead of abstract minutes ([1ced447](https://github.com/smart-swimmingpool/pool-controller/commit/1ced447730549e4a73d077ecda8857de68093a38)) +* **web-ui:** show circulation extension (extra minutes) on dashboard ([69e21bf](https://github.com/smart-swimmingpool/pool-controller/commit/69e21bf1891f9178d4d556edb18d2dabc107f1c6)) +* **web-ui:** show circulation extension (extra minutes) on dashboard ([e7cbc24](https://github.com/smart-swimmingpool/pool-controller/commit/e7cbc24ab9952cdaa3aaa1ee3d90c8495b6f64cf)) +* **web:** add /api/fs/upload endpoint for OTA-safe web asset deployment ([cee0b4a](https://github.com/smart-swimmingpool/pool-controller/commit/cee0b4aad3eedcc8e322086898e15bea18f87fca)) +* **web:** add temperature-based circulation parameters to Web UI ([963bcde](https://github.com/smart-swimmingpool/pool-controller/commit/963bcde09209ebabb8f0c42bb17d1fe3491e77cc)) +* **web:** format effective runtime as duration in WebUI and HA ([b460b4f](https://github.com/smart-swimmingpool/pool-controller/commit/b460b4f5fea1bc054123adfd166d26f109309965)) +* **web:** make dashboard and operating parameters visible without login ([919bbde](https://github.com/smart-swimmingpool/pool-controller/commit/919bbde875d92ddf53a2a831bbcb356528344cc7)) +* **web:** replace pump text toggle with visible toggle-switch controls ([#166](https://github.com/smart-swimmingpool/pool-controller/issues/166)) ([e37e7bd](https://github.com/smart-swimmingpool/pool-controller/commit/e37e7bdcc6882590187b1172c1f51fc10772fadd)) +* **wifi:** add WPS onboarding for initial WiFi provisioning ([#67](https://github.com/smart-swimmingpool/pool-controller/issues/67)) ([6983af6](https://github.com/smart-swimmingpool/pool-controller/commit/6983af6f68110b1e38e286d5c28fdcc1becb5496)) + + +### Bug Fixes + +* add native test infrastructure with ASan and coverage reporting ([#98](https://github.com/smart-swimmingpool/pool-controller/issues/98)) ([1421a39](https://github.com/smart-swimmingpool/pool-controller/commit/1421a399b006d374d3e792495be230f1e73b02aa)) +* address CodeQL code scanning alerts — thread-safe time functions, path-exclude libdeps, update skills ([6ebbae1](https://github.com/smart-swimmingpool/pool-controller/commit/6ebbae1f487fcd87610931f417ae753a2649addf)) +* address Codex clean-code review findings ([f1d6da2](https://github.com/smart-swimmingpool/pool-controller/commit/f1d6da2afeba26f5f4a874424584b5bc25741ec7)) +* **button:** Add ADC filtering and improved debouncing for NORVI front panel buttons ([#177](https://github.com/smart-swimmingpool/pool-controller/issues/177)) ([206954b](https://github.com/smart-swimmingpool/pool-controller/commit/206954babbe94858c5de4b51cceebfb70d78307b)) +* **button:** stop phantom NORVI operation-mode switches ([#181](https://github.com/smart-swimmingpool/pool-controller/issues/181)) ([cdf4083](https://github.com/smart-swimmingpool/pool-controller/commit/cdf4083098adbc2ee9034d6755a89e3d93e3276e)) +* **ci:** add checkout step to auto-merge-release workflow ([bd19bea](https://github.com/smart-swimmingpool/pool-controller/commit/bd19bea4ee4773a18cf7896870301ef4648e070c)) +* **ci:** filter .pio/libdeps from CodeQL SARIF instead of paths-ignore ([a345790](https://github.com/smart-swimmingpool/pool-controller/commit/a34579099c4df7377929652c62538702351d80ae)) +* **ci:** fix native test build and clang-format violations ([ba6a1a8](https://github.com/smart-swimmingpool/pool-controller/commit/ba6a1a864d741e84ed9ab9632290de0cbc0aaac4)) +* **ci:** make website dispatch workflow non-blocking on token issues ([#69](https://github.com/smart-swimmingpool/pool-controller/issues/69)) ([4e03bd3](https://github.com/smart-swimmingpool/pool-controller/commit/4e03bd3a1e0eb71b7c3538e60a5da05d03a1a35a)) +* **ci:** suppress cppcheck syntaxError for ESP-IDF version macros ([028e62a](https://github.com/smart-swimmingpool/pool-controller/commit/028e62a1c1de030fbdc07c42f53d1e7e4d18c89f)) +* **ci:** use temp file to avoid gh release upload name collision ([d570745](https://github.com/smart-swimmingpool/pool-controller/commit/d57074557ca2314d99cf6ce8fe188a115848f646)) +* **clang-format:** apply clang-format to MqttPublisher.cpp ([8058c1d](https://github.com/smart-swimmingpool/pool-controller/commit/8058c1d5481f1be80564efa62d10fb16cda3a78c)) +* **config:** correct Doxygen description — uses NVS (Preferences), not LittleFS ([d1e9910](https://github.com/smart-swimmingpool/pool-controller/commit/d1e9910ba2e9327642e78b14e04bb7361cffd05d)) +* correct #pragmaonce to #pragma once in RuleAuto.hpp ([6963f6c](https://github.com/smart-swimmingpool/pool-controller/commit/6963f6cc60f1e59058e5174437f8ee22f226bfe4)) +* **display:** ADC threshold calibration and QR code sizing ([ca6810f](https://github.com/smart-swimmingpool/pool-controller/commit/ca6810f5b4777eb7fc3227a9aad3e3643b1d85c4)) +* **display:** address PR [#147](https://github.com/smart-swimmingpool/pool-controller/issues/147) review comments ([fddff74](https://github.com/smart-swimmingpool/pool-controller/commit/fddff74d37f67dfead71dad64f1b3c86377efa8e)) +* docs synced de/en ([909304d](https://github.com/smart-swimmingpool/pool-controller/commit/909304d27c94429fd9031a9f20a4c8ef6939d67c)) +* **docs:** align HA entity table with name-based generation, fix domain errors ([#157](https://github.com/smart-swimmingpool/pool-controller/issues/157)) ([689c24e](https://github.com/smart-swimmingpool/pool-controller/commit/689c24e957cb13e38ecaadd13e32f843d49a115b)) +* **docs:** correct relative paths for images and links ([#164](https://github.com/smart-swimmingpool/pool-controller/issues/164)) ([1870c3e](https://github.com/smart-swimmingpool/pool-controller/commit/1870c3eb93413bfd140fa14ca08bc74f91b0fdf0)) +* ESP32 compatibility, memory & buffer fixes + AGENTS.md update ([#68](https://github.com/smart-swimmingpool/pool-controller/issues/68)) ([b95efbd](https://github.com/smart-swimmingpool/pool-controller/commit/b95efbdbd946c99142c5f688a13c1bf77aa1e7db)) +* **ha:** select-Entity-Status ohne Temperatur publishen ([e6f32d7](https://github.com/smart-swimmingpool/pool-controller/commit/e6f32d70847f71d84468a25caf0756b1a72d0e59)) +* **ha:** Sensor-Mapping Discovery bei jedem MQTT-Reconnect publishen ([1ab82d9](https://github.com/smart-swimmingpool/pool-controller/commit/1ab82d99c806b2b8f7923b26e0c33a6fe6696708)) +* **ha:** use name-based entity IDs matching HA generation ([22c56bd](https://github.com/smart-swimmingpool/pool-controller/commit/22c56bd6c5b47562950cd36027929123c95082ac)) +* **homeassistant:** remove 'none' from preset_modes discovery config ([d05a083](https://github.com/smart-swimmingpool/pool-controller/commit/d05a08352cffb804d3eb1f0d3b8561a42273da57)) +* improve English grammar and word choice in documentation ([#47](https://github.com/smart-swimmingpool/pool-controller/issues/47)) ([2d71dcd](https://github.com/smart-swimmingpool/pool-controller/commit/2d71dcdf5081f0650fa1ad05ce9b447d95806c41)) +* **logging:** trace operation mode command sources ([#174](https://github.com/smart-swimmingpool/pool-controller/issues/174)) ([dd91530](https://github.com/smart-swimmingpool/pool-controller/commit/dd91530253f2752808bf3f6b646dbfc8b199a43b)) +* master Branch renamed to main ([f0d0223](https://github.com/smart-swimmingpool/pool-controller/commit/f0d0223ab34884d903e8ef541ae24fc02328faf6)) +* mount LittleFS during web portal startup ([#85](https://github.com/smart-swimmingpool/pool-controller/issues/85)) ([592a4c8](https://github.com/smart-swimmingpool/pool-controller/commit/592a4c8105b3ebf54391a371650100e08fbebab1)) +* move solar pump relay to R2 (GPIO13), add RC snubber docs ([#165](https://github.com/smart-swimmingpool/pool-controller/issues/165)) ([5f8b964](https://github.com/smart-swimmingpool/pool-controller/commit/5f8b964894c9e639b1b3c8aa923b14727668243c)) +* **mqtt:** prevent dangling pointer and duplicate callback on reconnect ([25090d7](https://github.com/smart-swimmingpool/pool-controller/commit/25090d772cbe6c58cba178e87a4e16bf318648e6)) +* **mqtt:** remove duplicate Home Assistant Discovery entities ([9ef1526](https://github.com/smart-swimmingpool/pool-controller/commit/9ef1526f3550b12cb3826bbb39af8e12823a02e3)) +* **network:** remove always-true WPS function pointer check ([b0a339b](https://github.com/smart-swimmingpool/pool-controller/commit/b0a339bc40e011bd0ba74d6bbf311e9ff70e7d7f)) +* **network:** remove always-true WPS function pointer check ([e9a01bf](https://github.com/smart-swimmingpool/pool-controller/commit/e9a01bfcac79cff3efab83d1c80b920381e6079e)) +* **norvi:** move solar pump relay from R1 (GPIO12) to R5 (GPIO33) ([#158](https://github.com/smart-swimmingpool/pool-controller/issues/158)) ([5cf8fda](https://github.com/smart-swimmingpool/pool-controller/commit/5cf8fdaecafcc48350a6ec4008fda075af844a0c)) +* **ota:** select correct firmware binary per board type ([34f93d2](https://github.com/smart-swimmingpool/pool-controller/commit/34f93d26547c37a6d60803427aad330d64b8de6a)) +* **pool-controller:** stop overriding persisted TimerSetting with hardcoded values ([3cfd43f](https://github.com/smart-swimmingpool/pool-controller/commit/3cfd43fbf67e643137710a84d27b333de9e55295)) +* **relay:** avoid int→bool ambiguity in constructor overloads ([fcc616c](https://github.com/smart-swimmingpool/pool-controller/commit/fcc616c96faa733e7fa16548c890994c37047ab0)) +* **relay:** correct relay polarity for NORVI AE01-R active-HIGH relays ([6e92aa0](https://github.com/smart-swimmingpool/pool-controller/commit/6e92aa087e4a192bd3f3bf8cd8b3c42d842e02b6)) +* Remove space between platform name and version specifier in nodemcuv2 environment ([0f34a68](https://github.com/smart-swimmingpool/pool-controller/commit/0f34a689038d78021b88aba3c528cf8c66bdd17c)) +* resolve MegaLinter issues in PR [#139](https://github.com/smart-swimmingpool/pool-controller/issues/139) ([0c3cea8](https://github.com/smart-swimmingpool/pool-controller/commit/0c3cea8b528cca90fbefb282def9ca1dced5f57f)) +* restore telemetry updates and HA entity visibility ([716dfe6](https://github.com/smart-swimmingpool/pool-controller/commit/716dfe66a49507b6a0cb33b904c28bfb6101e171)) +* review findings for temperature-based circulation ([8784f25](https://github.com/smart-swimmingpool/pool-controller/commit/8784f2506e5b0798759c76631a1703773c479efc)) +* sensor setup UX, button thresholds, QR page layout ([7028689](https://github.com/smart-swimmingpool/pool-controller/commit/70286894c8f9048508c2f984f25e3a655aa5b731)) +* **sensor:** apply address filter to running instance immediately ([#160](https://github.com/smart-swimmingpool/pool-controller/issues/160)) ([cbaad12](https://github.com/smart-swimmingpool/pool-controller/commit/cbaad120bed421c4921cdf01a28c4d51556277b9)) +* solar pump hysteresis at max pool temperature ([c5a4e76](https://github.com/smart-swimmingpool/pool-controller/commit/c5a4e763fefbab08140bea709b54be8348843893)) +* **test:** add missing saveSensorMapping/loadSensorMapping to mock ConfigManager ([00d349b](https://github.com/smart-swimmingpool/pool-controller/commit/00d349bbf706542b3ee14aca8467e102029bcbd3)) +* **test:** add strlcat fallback for native test builds ([c6463fb](https://github.com/smart-swimmingpool/pool-controller/commit/c6463fb101c7bbd9ec6b0305abc212d0eaa30be6)) +* **timer:** keep pump running when extension wraps past midnight ([#180](https://github.com/smart-swimmingpool/pool-controller/issues/180)) ([2a8f919](https://github.com/smart-swimmingpool/pool-controller/commit/2a8f91993e890dd12ffb54a7d22b68c417ef764f)) +* **ui:** display dashboard thresholds without-auth via /api/status, 4-across mode cards ([e68d4bf](https://github.com/smart-swimmingpool/pool-controller/commit/e68d4bfb268f6ab02ac15ce3186ecbe51ffc05ed)) +* use MegaLinter v9.6.0 (v9.7.0 does not exist as release tag) ([81b679f](https://github.com/smart-swimmingpool/pool-controller/commit/81b679f4de263caa16cc5e5cee9055356190b4fa)) +* **web-ui:** use consistent muted styling for Extension telemetry item ([b0988db](https://github.com/smart-swimmingpool/pool-controller/commit/b0988dbdee031f2be3225e71d4245ee9cca21942)) +* **web-ui:** use consistent muted styling for Extension telemetry item ([6ae6b99](https://github.com/smart-swimmingpool/pool-controller/commit/6ae6b99413e602cad61b708cca743d0892719baa)) +* **web:** clarify that timer start/end also applies in Auto mode ([5985a00](https://github.com/smart-swimmingpool/pool-controller/commit/5985a008e3a6168fa9b291c5210b3224eb2dd014)) +* **web:** correct license text from GPL-3.0 to MIT in dashboard footer ([1d22f05](https://github.com/smart-swimmingpool/pool-controller/commit/1d22f05d95d2929c63e5ff63f63e48b32dda958d)) +* **web:** make /api/fs/upload work by also accepting upload.filename ([09765b8](https://github.com/smart-swimmingpool/pool-controller/commit/09765b80922853933f3629fc3e439ee155828577)) +* **web:** remove leftover merge conflict markers in index.html ([#118](https://github.com/smart-swimmingpool/pool-controller/issues/118)) ([907b676](https://github.com/smart-swimmingpool/pool-controller/commit/907b67695357724857c8a73b54bd228dbf5d4459)) +* **web:** stop tabs from reopening WiFi settings after navigation ([726bde4](https://github.com/smart-swimmingpool/pool-controller/commit/726bde45fcf32aceab80d6751d8f33430b223d8f)) +* **web:** use streaming multipart upload for /api/fs/upload ([0c38f50](https://github.com/smart-swimmingpool/pool-controller/commit/0c38f506be573f97f58788885ddce26a384c0420)) +* **wps:** correct ESP-IDF version guard for esp_wifi_wps_start API ([ab98105](https://github.com/smart-swimmingpool/pool-controller/commit/ab98105cdd79088f312a577896fe9b3f584af0f4)) +* **wps:** correct ESP-IDF version guard for esp_wifi_wps_start API ([8d92db8](https://github.com/smart-swimmingpool/pool-controller/commit/8d92db896bf92dc92178ed60726722f9682672ca)) +* **wps:** correct ESP-IDF version guard for esp_wifi_wps_start API ([#124](https://github.com/smart-swimmingpool/pool-controller/issues/124)) ([ab98105](https://github.com/smart-swimmingpool/pool-controller/commit/ab98105cdd79088f312a577896fe9b3f584af0f4)) + + +### Performance Improvements + +* **web:** optimize PWA startup ([#184](https://github.com/smart-swimmingpool/pool-controller/issues/184)) ([3d61ebd](https://github.com/smart-swimmingpool/pool-controller/commit/3d61ebd096b9dcd2bd1b2a8f0ee07215745e2693)) + + +### Miscellaneous Chores + +* remove deprecated Homie references across codebase ([1df0705](https://github.com/smart-swimmingpool/pool-controller/commit/1df070560d0441c365ecd11ef86649976468a8b3)) + ## [4.2.0](https://github.com/smart-swimmingpool/pool-controller/compare/v4.1.1...v4.2.0) (2026-07-11) diff --git a/README.md b/README.md index 5386b757..1844acf0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Pool Controller | \ud83c\udfca Smart Swimming Pool +# Pool Controller | 🏊 Smart Swimming Pool [![Smart Swimmingpool](https://img.shields.io/badge/%F0%9F%8F%8A%20-Smart%20Swimmingpool-blue.svg)](https://github.com/smart-swimmingpool) [![PlatformIO CI](https://github.com/smart-swimmingpool/pool-controller/workflows/PlatformIO%20CI/badge.svg)](https://github.com/smart-swimmingpool/pool-controller/actions?query=workflow%3A%22PlatformIO+CI%22) @@ -7,7 +7,7 @@ --- -> **\u26a0\ufe0f WARNING: This project involves 230V AC mains voltage!** +> **⚠️ WARNING: This project involves 230V AC mains voltage!** > > - **Only proceed if you have basic electronics knowledge.** > - **Always use a Residual Current Device (RCD/FI circuit breaker) for the pump circuit.** @@ -18,51 +18,51 @@ --- -## \ud83c\udfca Overview +## 🏊 Overview The **Pool Controller** is an **ESP32-based control unit** that automates your swimming pool management. It provides intelligent circulation, solar heating control, and comprehensive monitoring via MQTT integration with Home Assistant and other smart home systems. **Key Features:** -- \u2705 Timed circulation for water cleaning -- \u2705 Solar heating control via additional pump -- \u2705 Multiple operation modes: Auto, Manual, Boost, Timer -- \u2705 Temperature-based automation -- \u2705 Home Assistant MQTT Discovery (v3.3.0+) -- \u2705 Built-in web interface with REST API -- \u2705 State persistence across reboots -- \u2705 System health monitoring with auto-recovery -- \u2705 OTA firmware updates - -**Cost:** ~45\u201375\u20ac (excluding pumps and pool infrastructure) +- ✅ Timed circulation for water cleaning +- ✅ Solar heating control via additional pump +- ✅ Multiple operation modes: Auto, Manual, Boost, Timer +- ✅ Temperature-based automation +- ✅ Home Assistant MQTT Discovery (v3.3.0+) +- ✅ Built-in web interface with REST API +- ✅ State persistence across reboots +- ✅ System health monitoring with auto-recovery +- ✅ OTA firmware updates + +**Cost:** ~45–75€ (excluding pumps and pool infrastructure) --- -## \ud83d\ude80 Quick Start +## 🚀 Quick Start **New to the project?** Begin with these resources: -- [\ud83d\udcd6 **Quick Start Guide**](docs/quick-start.md) \u2013 Step-by-step setup for beginners -- [\u2753 **Frequently Asked Questions**](docs/faq.md) \u2013 Troubleshooting common issues -- [\ud83c\udf10 **MQTT Configuration**](docs/mqtt-configuration.md) \u2013 Home Assistant integration +- [📖 **Quick Start Guide**](docs/quick-start.md) – Step-by-step setup for beginners +- [❓ **Frequently Asked Questions**](docs/faq.md) – Troubleshooting common issues +- [🌐 **MQTT Configuration**](docs/mqtt-configuration.md) – Home Assistant integration --- -## \ud83d\udcbb Hardware Requirements +## 💻 Hardware Requirements | Component | Qty | Approx. Cost | Notes | |-----------|:---:|:------------:|-------| -| ESP32 Development Board | 1 | 10\u201315\u20ac | 4MB+ flash required | -| DS18B20 Temperature Sensor (waterproof) | 2 | 8\u201312\u20ac | Pool + solar collector | -| 2-Channel 5V Relay Module | 1 | 5\u20138\u20ac | With optocoupler isolation | -| Resistor 4.7k\u03a9 | 2 | < 1\u20ac | Pull-up for OneWire | -| USB Power Supply 5V/\u22651A | 1 | 5\u201310\u20ac | For ESP32 | -| **Total** | | **~45\u201375\u20ac** | Without pumps | +| ESP32 Development Board | 1 | 10–15€ | 4MB+ flash required | +| DS18B20 Temperature Sensor (waterproof) | 2 | 8–12€ | Pool + solar collector | +| 2-Channel 5V Relay Module | 1 | 5–8€ | With optocoupler isolation | +| Resistor 4.7kΩ | 2 | < 1€ | Pull-up for OneWire | +| USB Power Supply 5V/≥1A | 1 | 5–10€ | For ESP32 | +| **Total** | | **~45–75€** | Without pumps | **Recommended Shops:** Amazon, AliExpress, Reichelt, Pollin, Conrad (DE/AT/CH) --- -## \ud83d\udce6 Software & Development +## 📦 Software & Development ### PlatformIO Setup @@ -94,7 +94,7 @@ pio run --target monitor --- -## \ud83c\udf10 MQTT Integration +## 🌐 MQTT Integration ### Home Assistant (Recommended) @@ -106,16 +106,16 @@ The Pool Controller supports **native Home Assistant MQTT Discovery** (v3.3.0+). | System | Integration Method | Status | |--------|-------------------|--------| -| Home Assistant | MQTT Discovery | \u2705 Native support | -| openHAB | MQTT Binding | \u2705 Manual configuration | -| Node-RED | MQTT nodes | \u2705 Works with any MQTT broker | -| ioBroker | MQTT adapter | \u2705 Works with any MQTT broker | +| Home Assistant | MQTT Discovery | ✅ Native support | +| openHAB | MQTT Binding | ✅ Manual configuration | +| Node-RED | MQTT nodes | ✅ Works with any MQTT broker | +| ioBroker | MQTT adapter | ✅ Works with any MQTT broker | **MQTT Topics:** See [MQTT Configuration Guide](docs/mqtt-configuration.md) for complete topic reference. --- -## \ud83d\udee1\ufe0f Reliability Features (v3.3.0) +## 🛡️ Reliability Features (v3.3.0) ### State Persistence All settings survive reboots and power failures: @@ -131,28 +131,28 @@ All settings survive reboots and power failures: ### Memory Optimization - 90% reduction in heap fragmentation -- 2,880\u201328,800 fewer allocations per day +- 2,880–28,800 fewer allocations per day - Fixed millis() overflow for operation beyond 49.7 days --- -## \ud83d\udcda Documentation +## 📚 Documentation | Guide | Description | Audience | |-------|-------------|----------| -| [Quick Start Guide](docs/quick-start.md) | Step-by-step setup for beginners | \ud83c\udd95 New users | -| [FAQ](docs/faq.md) | Troubleshooting common issues | \u2753 All users | -| [Users Guide](docs/users-guide.md) | Web dashboard, operation modes, MQTT | \ud83c\udf9b\ufe0f Intermediate users | -| [Hardware Guide](docs/hardware-guide.md) | Assembly, wiring, parts list | \ud83d\udd27 Builders | -| [MQTT Configuration](docs/mqtt-configuration.md) | Home Assistant Discovery, entity reference | \ud83c\udf10 Smart home integrators | -| [State Persistence](docs/state-persistence.md) | How settings are saved across reboots | \ud83d\udcbe Advanced users | -| [OTA Updates](docs/ota-updates.md) | Remote firmware updates | \ud83d\udce1 Developers | -| [Software Guide](docs/software-guide.md) | Development environment, build process | \ud83d\udd27 Developers | -| [ESP32 Schematic Optimization](docs/esp32-schematic-optimization.md) | Pin assignment and optimization | \ud83d\udd0c Hardware experts | +| [Quick Start Guide](docs/quick-start.md) | Step-by-step setup for beginners | 🆕 New users | +| [FAQ](docs/faq.md) | Troubleshooting common issues | ❓ All users | +| [Users Guide](docs/users-guide.md) | Web dashboard, operation modes, MQTT | 🎛️ Intermediate users | +| [Hardware Guide](docs/hardware-guide.md) | Assembly, wiring, parts list | 🔧 Builders | +| [MQTT Configuration](docs/mqtt-configuration.md) | Home Assistant Discovery, entity reference | 🌐 Smart home integrators | +| [State Persistence](docs/state-persistence.md) | How settings are saved across reboots | 💾 Advanced users | +| [OTA Updates](docs/ota-updates.md) | Remote firmware updates | 📡 Developers | +| [Software Guide](docs/software-guide.md) | Development environment, build process | 🔧 Developers | +| [ESP32 Schematic Optimization](docs/esp32-schematic-optimization.md) | Pin assignment and optimization | 🔌 Hardware experts | --- -## \ud83d\udce6 Recent Updates +## 📦 Recent Updates ### v3.3.0 (Current) - ESP8266 support removed (ESP32-only) @@ -170,7 +170,7 @@ All settings survive reboots and power failures: --- -## \ud83d\ude80 Planned Features +## 🚀 Planned Features - [ ] Configurable NTP Server - [ ] Smart learning: Improved pool pump circulation optimization @@ -183,7 +183,7 @@ All settings survive reboots and power failures: --- -## \ud83e\udd1d Contributing +## 🤝 Contributing We welcome contributions! Please follow these steps: @@ -195,19 +195,19 @@ We welcome contributions! Please follow these steps: 6. **Submit a Pull Request** **Quality Gates:** -- \u2705 Super-Linter (code quality) -- \u2705 PlatformIO CI (build verification) -- \u2705 Manual review by maintainers +- ✅ Super-Linter (code quality) +- ✅ PlatformIO CI (build verification) +- ✅ Manual review by maintainers --- -## \ud83d\udcdc License +## 📜 License -[MIT License](LICENSE) \u2013 Free to use, modify, and share. +[MIT License](LICENSE) – Free to use, modify, and share. --- -## \ud83c\udf10 Community & Support +## 🌐 Community & Support - **Discussions:** [GitHub Discussions](https://github.com/smart-swimmingpool/smart-swimmingpool.github.io/discussions) - **Website:** [smart-swimmingpool.com](https://smart-swimmingpool.com) @@ -220,7 +220,7 @@ We welcome contributions! Please follow these steps: --- -## \ud83d\udce2 Project Links +## 📢 Project Links | Module | Description | |--------|-------------| @@ -234,5 +234,5 @@ We welcome contributions! Please follow these steps: ---

- Made with \u2764\ufe0f by the Smart Swimming Pool community + Made with ❤️ by the Smart Swimming Pool community

diff --git a/data/web/app.js b/data/web/app.js index 7f556266..4594f9a7 100644 --- a/data/web/app.js +++ b/data/web/app.js @@ -12,7 +12,7 @@ function switchTab(tabName) { // Update bottom tab bar active state. // Tabs under "More" (wifi, mqtt, system, about) keep "more" highlighted. - const moreTabs = ['wifi', 'mqtt', 'system', 'about']; + const moreTabs = ['logs', 'wifi', 'mqtt', 'system', 'about']; const barTab = moreTabs.includes(tabName) ? 'more' : tabName; document.querySelectorAll('.tab-bar-item').forEach(item => { item.classList.toggle('active', item.dataset.tab === barTab); @@ -21,6 +21,18 @@ function switchTab(tabName) { // Close more menu if open const moreMenu = document.getElementById('moreMenu'); if (moreMenu) moreMenu.style.display = 'none'; + + // Lazy-load tab data on first activation instead of at page load: the + // single-threaded device server would otherwise queue config/sensor + // requests behind the dashboard telemetry poll on every page open. + if (!configLoaded && ['pool', 'time', 'wifi', 'mqtt', 'system'].includes(tabName)) { + configLoaded = true; + loadConfig(); + } + if (!sensorsLoaded && tabName === 'sensors') { + sensorsLoaded = true; + loadSensors(); + } } function toggleMoreMenu() { @@ -38,6 +50,9 @@ console.log('[pool] app.js loaded, version=2026-06-05'); let isAuthenticated = false; let hasAutoSwitchedToWifi = false; // one-shot guard so AP-mode redirect doesn't fight user navigation +let configLoaded = false; // lazy-load guard: /api/config fetched on first admin tab activation +let sensorsLoaded = false; // lazy-load guard: /api/sensors fetched on first Sensors tab activation +let configLoadsInFlight = 0; // >0 while /api/config is loading; config save buttons stay disabled async function loadTelemetry() { try { @@ -61,6 +76,13 @@ async function loadTelemetry() { document.getElementById('solarThreshold').textContent = 'min ' + data.temp_min_solar.toFixed(1) + '°C'; } + // NORVI capability: hide the calibration wizard on non-NORVI firmware + // (the /api/calibrate/* routes are compiled out there). + const calibSection = document.getElementById('calibrationSection'); + if (calibSection) { + calibSection.style.display = data.norvi ? '' : 'none'; + } + // Pumpen — Toggle-Switches aktualisieren if (data.pool_pump != null) { setPumpSwitch('pool', data.pool_pump); @@ -147,33 +169,40 @@ async function loadTelemetry() { updateAuthUI(); // ── Pool & Time Tab (read-only params from /api/status) ── - const statusFields = [ - ['loopInterval', data.loop_interval], - ['tempMaxPool', data.temp_max_pool], - ['tempMinSolar', data.temp_min_solar], - ['tempHysteresis', data.temp_hysteresis], - ['tempCircThreshold', data.temp_circ_threshold], - ['tempCircFactor', data.temp_circ_factor], - ['tempCircMaxRuntime', data.temp_circ_max_runtime], - ['timezone', data.timezone], - ['timeLossGreen', data.time_loss_green_hours], - ['timeLossRed', data.time_loss_red_hours], - ['ntpServer', data.ntp_server], - ]; - for (const [id, val] of statusFields) { - if (val != null) { - const el = document.getElementById(id); - if (el) el.value = val; + // Only populate these form fields when NOT authenticated: /api/config is + // the authoritative source once logged in (loadConfig fills them), and a + // telemetry poll here would otherwise overwrite the user's in-progress + // edits every 2 seconds. When unauthenticated the fields are disabled, + // so this read-only display is harmless. + if (!isAuthenticated) { + const statusFields = [ + ['loopInterval', data.loop_interval], + ['tempMaxPool', data.temp_max_pool], + ['tempMinSolar', data.temp_min_solar], + ['tempHysteresis', data.temp_hysteresis], + ['tempCircThreshold', data.temp_circ_threshold], + ['tempCircFactor', data.temp_circ_factor], + ['tempCircMaxRuntime', data.temp_circ_max_runtime], + ['timezone', data.timezone], + ['timeLossGreen', data.time_loss_green_hours], + ['timeLossRed', data.time_loss_red_hours], + ['ntpServer', data.ntp_server], + ]; + for (const [id, val] of statusFields) { + if (val != null) { + const el = document.getElementById(id); + if (el) el.value = val; + } } - } - // Timer start/end fields on Pool tab - if (data.timer_start_h != null) { - const pad2 = (n) => n.toString().padStart(2, '0'); - const stEl = document.getElementById('timerStart'); - if (stEl) stEl.value = pad2(data.timer_start_h) + ':' + pad2(data.timer_start_m); - const etEl = document.getElementById('timerEnd'); - if (etEl) etEl.value = pad2(data.timer_end_h) + ':' + pad2(data.timer_end_m); + // Timer start/end fields on Pool tab + if (data.timer_start_h != null) { + const pad2 = (n) => n.toString().padStart(2, '0'); + const stEl = document.getElementById('timerStart'); + if (stEl) stEl.value = pad2(data.timer_start_h) + ':' + pad2(data.timer_start_m); + const etEl = document.getElementById('timerEnd'); + if (etEl) etEl.value = pad2(data.timer_end_h) + ':' + pad2(data.timer_end_m); + } } // AP-Mode: WiFi-Tab anzeigen (nur einmalig — nicht bei jedem Poll erzwingen, @@ -262,10 +291,10 @@ function updateAuthUI() { const sensorsTabBtn = document.querySelector('.tab-bar-item[data-tab="sensors"]'); if (sensorsTabBtn) sensorsTabBtn.style.display = isAuthenticated ? '' : 'none'; - // More menu: hide admin items (wifi, mqtt, system) + // More menu: hide admin items (wifi, mqtt, system, logs) for (const item of document.querySelectorAll('.more-sheet-item')) { const text = item.textContent.trim().toLowerCase(); - if (text === 'wifi' || text === 'mqtt' || text.startsWith('system') || text.startsWith('🔒')) { + if (text === 'wifi' || text === 'mqtt' || text.startsWith('system') || text.startsWith('🔒') || text.includes('logs')) { item.style.display = isAuthenticated ? '' : 'none'; } } @@ -294,12 +323,20 @@ function updateAuthUI() { } } - // System / WiFi / MQTT / Sensors tabs: fully hide when not authenticated. Never + // Config fields and save buttons must stay disabled while /api/config is + // loading (or failed to load): the loops above re-enable every tab control + // on each telemetry poll and would otherwise undo setConfigFieldsDisabled() + // before the load finishes — or expose markup defaults after a failure. + if (isAuthenticated && (configLoadsInFlight > 0 || !configLoaded)) { + setConfigFieldsDisabled(true); + } + + // System / WiFi / MQTT / Logs / Sensors tabs: fully hide when not authenticated. Never // force-show here — that previously used `''` (empty string), which falls back // to the CSS default `display:block`, making the tab visible again on every 2s // poll regardless of which tab switchTab() had actually activated (the reported // "always jumps back to WiFi Settings" bug). - for (const id of ['tab-system', 'tab-wifi', 'tab-mqtt']) { + for (const id of ['tab-system', 'tab-wifi', 'tab-mqtt', 'tab-logs']) { const el = document.getElementById(id); if (el && !isAuthenticated) el.style.display = 'none'; } @@ -553,6 +590,13 @@ function validateSettings() { { id: 'tempCircThreshold', name: 'Circ. Temp Threshold', min: 0, max: 40, type: 'float' }, { id: 'tempCircFactor', name: 'Circ. Temp Factor', min: 0, max: 120, type: 'int' }, { id: 'tempCircMaxRuntime', name: 'Circ. Max Runtime', min: 60, max: 1440, type: 'int' }, + { id: 'btn1Min', name: 'Button 1 Min ADC', min: 0, max: 4095, type: 'int' }, + { id: 'btn1Max', name: 'Button 1 Max ADC', min: 0, max: 4095, type: 'int' }, + { id: 'btn2Min', name: 'Button 2 Min ADC', min: 0, max: 4095, type: 'int' }, + { id: 'btn2Max', name: 'Button 2 Max ADC', min: 0, max: 4095, type: 'int' }, + { id: 'btn3Min', name: 'Button 3 Min ADC', min: 0, max: 4095, type: 'int' }, + { id: 'btn3Max', name: 'Button 3 Max ADC', min: 0, max: 4095, type: 'int' }, + { id: 'btnNoPress', name: 'No-Press Threshold', min: 0, max: 4096, type: 'int' }, ]; for (const f of fields) { const el = document.getElementById(f.id); @@ -572,6 +616,25 @@ function validateSettings() { return false; } } + // Button ADC thresholds must form coherent, non-overlapping ranges + const btnVal = (id) => parseInt(document.getElementById(id).value, 10); + const b1Min = btnVal('btn1Min'), b1Max = btnVal('btn1Max'); + const b2Min = btnVal('btn2Min'), b2Max = btnVal('btn2Max'); + const b3Min = btnVal('btn3Min'), b3Max = btnVal('btn3Max'); + if (b1Min >= b1Max || b2Min >= b2Max || b3Min >= b3Max) { + alert('Each button Min must be less than its Max.'); + return false; + } + if (b1Max > b2Min || b2Max > b3Min) { + alert('Button ADC ranges must not overlap.'); + return false; + } + // No-press threshold must sit above every button range, otherwise + // detectButton() checks THRESH_NO_PRESS first and masks those readings as NONE. + if (btnVal('btnNoPress') <= b3Max) { + alert('No-Press Threshold must be above all button ranges.'); + return false; + } return true; } @@ -589,6 +652,13 @@ async function saveControllerSettings() { const circFactor = document.getElementById('tempCircFactor').value; const circMaxRuntime = document.getElementById('tempCircMaxRuntime').value; const tz = document.getElementById('timezone').value; + const btn1Min = document.getElementById('btn1Min').value; + const btn1Max = document.getElementById('btn1Max').value; + const btn2Min = document.getElementById('btn2Min').value; + const btn2Max = document.getElementById('btn2Max').value; + const btn3Min = document.getElementById('btn3Min').value; + const btn3Max = document.getElementById('btn3Max').value; + const btnNoPress = document.getElementById('btnNoPress').value; // Validate time fields included alongside pool fields const green = parseInt(document.getElementById('timeLossGreen').value, 10); @@ -608,7 +678,7 @@ async function saveControllerSettings() { const res = await fetch('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: 'type=settings&mode=' + mode + '&interval=' + interval + '&max_pool=' + maxPool + '&min_solar=' + minSolar + '&hysteresis=' + hysteresis + '&circ_threshold=' + circThreshold + '&circ_factor=' + circFactor + '&circ_max_runtime=' + circMaxRuntime + '&timezone=' + tz + '&green=' + green + '&red=' + red + timerParams() + '&ntp_server=' + ntpServer + body: 'type=settings&mode=' + mode + '&interval=' + interval + '&max_pool=' + maxPool + '&min_solar=' + minSolar + '&hysteresis=' + hysteresis + '&circ_threshold=' + circThreshold + '&circ_factor=' + circFactor + '&circ_max_runtime=' + circMaxRuntime + '&timezone=' + tz + '&green=' + green + '&red=' + red + timerParams() + '&ntp_server=' + ntpServer + '&btn1_min=' + btn1Min + '&btn1_max=' + btn1Max + '&btn2_min=' + btn2Min + '&btn2_max=' + btn2Max + '&btn3_min=' + btn3Min + '&btn3_max=' + btn3Max + '&btn_no_press=' + btnNoPress }); if (res.status === 200) { document.getElementById('poolThreshold').textContent = 'max ' + parseFloat(maxPool).toFixed(1) + '°C'; @@ -618,6 +688,200 @@ async function saveControllerSettings() { } } +// ── Button Calibration Wizard ── + +let calibPollTimer = null; + +function showCalibrationModal() { + document.getElementById('calibrationModal').style.display = 'flex'; +} + +function closeCalibrationModal() { + document.getElementById('calibrationModal').style.display = 'none'; + if (calibPollTimer) { clearInterval(calibPollTimer); calibPollTimer = null; } + if (calibCloseTimer) { clearTimeout(calibCloseTimer); calibCloseTimer = null; } + calibMsgPending = null; + calibMsgShownAt = 0; +} + +function startCalibrationPolling() { + // Guard against duplicate poll loops when the start button is activated + // twice before the first request completes. + if (calibPollTimer) { clearInterval(calibPollTimer); } + calibPollTimer = setInterval(pollCalibrationStatus, 500); + pollCalibrationStatus(); +} + +async function startCalibration() { + const res = await fetch('/api/calibrate/start', { method: 'POST' }); + // handleAuthentication() serves the login page with HTTP 200 when the + // session expired, so verify an actual API response first. + const type = res.headers.get('content-type') || ''; + if (type.includes('text/html')) { + showLoginForm(); + alert('Session expired — please log in again.'); + return; + } + if (res.status === 409) { + // Calibration is already running on the device (e.g. after a page + // reload or a lost start response) — resume the running wizard so the + // user can watch progress or cancel it instead of being stuck. + showCalibrationModal(); + startCalibrationPolling(); + return; + } + if (!res.ok) { alert('Calibration could not be started.'); return; } + showCalibrationModal(); + startCalibrationPolling(); +} + +// ── Calibration wizard UI state ── + +const CALIB_STEP_HEADLINES = { + 1: 'Release all buttons', + 2: 'Press and hold Button 1', + 3: 'Press and hold Button 2', + 4: 'Press and hold Button 3', + 5: 'Calibration complete', + 6: 'Calibration failed' +}; + +const CALIB_NEXT_UP = { + 1: 'Next: hold Button 1', + 2: 'Next: hold Button 2', + 3: 'Next: hold Button 3', + 4: 'Then the levels are computed and saved automatically.' +}; + +// Keep every status text readable: a new message is only shown once the +// previous one has been on screen for at least MIN_MSG_MS milliseconds. +const CALIB_MIN_MSG_MS = 1200; +let calibMsgShownAt = 0; +let calibMsgPending = null; +let calibCloseTimer = null; + +function showCalibrationMessage(text) { + const el = document.getElementById('calibStepText'); + if (!el) return; + const now = Date.now(); + if (now - calibMsgShownAt < CALIB_MIN_MSG_MS) { + // Current text still on screen — remember the newest one, promote later + calibMsgPending = text; + return; + } + if (calibMsgPending) { text = calibMsgPending; calibMsgPending = null; } + if (text !== el.textContent) { + el.textContent = text; + calibMsgShownAt = now; + } +} + +function calibPhase(message) { + if (!message) return 'waiting'; + if (message.includes('Computing')) return 'saving'; + if (message.includes('sampling')) return 'sampling'; + if (message.includes('try again') || message.includes('too close') || message.includes('changed')) return 'retry'; + if (message.includes('complete')) return 'done'; + return 'waiting'; +} + +function updateCalibrationUi(st) { + const phase = calibPhase(st.message || ''); + const step = st.step; + + // Bold instruction headline for the current action + let headline = CALIB_STEP_HEADLINES[step] || 'Calibration'; + if (phase === 'saving') { + headline = 'Computing thresholds…'; + } else if (phase === 'retry') { + headline = 'Try again — hold steady'; + } else if (phase === 'sampling' && step >= 2 && step <= 4) { + headline = 'Keep holding Button ' + (step - 1); + } else if (phase === 'sampling' && step === 1) { + headline = 'Hold steady…'; + } + const headlineEl = document.getElementById('calibActionHeadline'); + if (headlineEl) headlineEl.textContent = headline; + + // State chip + const chipLabels = { waiting: 'Waiting', sampling: 'Measuring', saving: 'Saving', retry: 'Retry', done: 'Done', error: 'Error' }; + const chip = document.getElementById('calibStateChip'); + if (chip) { + chip.textContent = chipLabels[phase] || chipLabels.waiting; + chip.className = 'calib-chip calib-chip-' + phase; + } + + // Firmware detail line (throttled so it stays readable) + showCalibrationMessage(st.message || ''); + + // What happens next + const nextUp = document.getElementById('calibNextUp'); + if (nextUp) nextUp.textContent = CALIB_NEXT_UP[step] || ''; + + // Live ADC meter (0-4095) + document.getElementById('calibLiveAdc').textContent = st.live_adc; + const fill = document.getElementById('calibMeterFill'); + if (fill) { + fill.style.width = Math.min(100, Math.round((st.live_adc / 4095) * 100)) + '%'; + fill.classList.toggle('sampling', phase === 'sampling'); + } + + // Step progress: current highlighted, completed marked with a check + const steps = ['calibP0', 'calibP1', 'calibP2', 'calibP3']; + steps.forEach((id, i) => { + const el = document.getElementById(id); + if (!el) return; + const done = (i + 1) < step; + const active = (i + 1) === step; + el.classList.toggle('active', active); + el.classList.toggle('done', done); + const dot = el.querySelector('.calib-step-dot'); + if (dot) dot.textContent = done ? '✓' : (i + 1); + }); +} + +async function pollCalibrationStatus() { + const res = await fetch('/api/calibrate/status'); + if (!res.ok) return; + // The login page is served with HTTP 200 on session expiry — stop + // polling instead of trying to parse HTML as JSON. + const type = res.headers.get('content-type') || ''; + if (type.includes('text/html')) { + if (calibPollTimer) { clearInterval(calibPollTimer); calibPollTimer = null; } + showLoginForm(); + alert('Session expired — please log in again to continue calibration.'); + return; + } + const st = await res.json(); + updateCalibrationUi(st); + + if (st.step === 5) { // DONE — show the success state briefly, then close + if (!calibCloseTimer) { + calibCloseTimer = setTimeout(() => { + calibCloseTimer = null; + closeCalibrationModal(); + loadConfig(); // refresh threshold fields + }, 1500); + } + } else if (st.step === 6) { // ERROR + closeCalibrationModal(); + alert('Calibration failed: ' + (st.message || 'unknown error')); + } +} + +async function cancelCalibration() { + const res = await fetch('/api/calibrate/cancel', { method: 'POST' }); + // handleAuthentication() serves the login page with HTTP 200 when the + // session expired, so verify an actual API response before closing. + const type = res.headers.get('content-type') || ''; + if (res.ok && !type.includes('text/html')) { + closeCalibrationModal(); + } else { + showLoginForm(); + alert('Session expired — please log in again to cancel calibration.'); + } +} + // ── Save Time Settings (Time Tab) ── async function saveTimeSettings() { @@ -707,9 +971,34 @@ async function factoryReset() { // ── Load Config ── +// Config save buttons stay disabled while /api/config is loading, so a save +// cannot submit markup defaults for fields not yet populated by the response. +// Config fields and save buttons stay disabled while /api/config is loading: +// a save must not submit markup defaults, and a late response must not +// overwrite edits entered before the load finished. +const CONFIG_FIELD_SELECTOR = '#tab-pool input, #tab-pool select, #tab-pool button, ' + + '#tab-time input, #tab-time select, #tab-time button, ' + + '#tab-wifi input, #tab-wifi select, #tab-wifi button, ' + + '#tab-mqtt input, #tab-mqtt select, #tab-mqtt button'; +const CONFIG_SAVE_BUTTONS = ['btnSavePassword']; // system tab save is outside the field tabs + +function setConfigFieldsDisabled(disabled) { + for (const el of document.querySelectorAll(CONFIG_FIELD_SELECTOR)) { + el.disabled = disabled; + } + CONFIG_SAVE_BUTTONS.forEach(id => { + const btn = document.getElementById(id); + if (btn) btn.disabled = disabled; + }); +} + async function loadConfig() { + configLoadsInFlight++; + setConfigFieldsDisabled(true); + let ok = false; try { const res = await fetch('/api/config'); + if (!res.ok) throw new Error('config request failed: ' + res.status); const data = await res.json(); document.getElementById('wifiSsid').value = data.wifi.ssid; @@ -729,14 +1018,30 @@ async function loadConfig() { document.getElementById('ntpServer').value = data.ntp.server; document.getElementById('timeLossGreen').value = data.settings.time_loss_green_hours; document.getElementById('timeLossRed').value = data.settings.time_loss_red_hours; + document.getElementById('btn1Min').value = data.settings.btn1_min; + document.getElementById('btn1Max').value = data.settings.btn1_max; + document.getElementById('btn2Min').value = data.settings.btn2_min; + document.getElementById('btn2Max').value = data.settings.btn2_max; + document.getElementById('btn3Min').value = data.settings.btn3_min; + document.getElementById('btn3Max').value = data.settings.btn3_max; + document.getElementById('btnNoPress').value = data.settings.btn_no_press; const pad2 = (n) => n.toString().padStart(2, '0'); document.getElementById('timerStart').value = pad2(data.settings.timer_start_hour) + ':' + pad2(data.settings.timer_start_min); document.getElementById('timerEnd').value = pad2(data.settings.timer_end_hour) + ':' + pad2(data.settings.timer_end_min); document.getElementById('poolThreshold').textContent = 'max ' + data.settings.temp_max_pool.toFixed(1) + '°C'; document.getElementById('solarThreshold').textContent = 'min ' + data.settings.temp_min_solar.toFixed(1) + '°C'; highlightMode(data.settings.op_mode); + ok = true; } catch (e) { - // Silent + // Transient failure — keep the save buttons disabled and reset the + // lazy-load guard so the next tab activation retries, instead of + // leaving markup defaults editable without a loaded config. + configLoaded = false; + } finally { + configLoadsInFlight--; + if (configLoadsInFlight === 0 && ok) { + setConfigFieldsDisabled(false); + } } } @@ -863,6 +1168,7 @@ let loadedMapping = { solar: null, pool: null }; async function loadSensors() { try { const res = await fetch('/api/sensors'); + if (!res.ok) throw new Error('sensors request failed: ' + res.status); const data = await res.json(); const solarAddr = data.mapping.solar || null; @@ -898,9 +1204,17 @@ async function loadSensors() { buildRadioGroup('poolRadioGroup', devices, solarAddr, poolAddr, 'pool'); updateSensorSaveBar(); + + // Only mark the guard loaded on success so a failed request retries on + // the next tab activation instead of being skipped forever. + sensorsLoaded = true; } catch (e) { + // Transient failure — reset the guard for a retry on the next tab + // activation and offer an inline refresh control. + sensorsLoaded = false; document.getElementById('sensorList').innerHTML = - '
Failed to load sensors: ' + e.message + '
'; + '
Failed to load sensors: ' + e.message + + '
'; } } @@ -1001,12 +1315,125 @@ async function saveSensorMapping() { } } +// ── Log Console ── + +var lastLogSeq = 0; +var lastLogBoot = 0; +var logLevelFilter = 'info'; +// Generation token: bumped on every request, filter change and clear. +// Responses carrying an older token are discarded, so a slow in-flight +// poll cannot append stale/duplicate entries or overwrite lastLogSeq +// after a newer poll, filter switch or clear has happened. +var logReqToken = 0; +// Serializes polls: fetch() responses taking longer than the 2s tick must not +// start a second concurrent poll (whose response would bump the token and +// discard the first one — leaving the console stuck until the next clear). +var logPollInFlight = false; + +function escapeHtml(s) { + return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); +} + +function loadLogs() { + // Only poll while the Logs tab is actually visible: the unconditional 2s + // timer used to keep appending DOM nodes (and fetching) in hidden tabs, + // growing the document by tens of thousands of nodes per day. + if (document.visibilityState !== 'visible') return; + var logTab = document.getElementById('tab-logs'); + if (!logTab || logTab.style.display === 'none') return; + + // Never overlap polls: a slow response would otherwise be superseded by the + // next tick's request (token bump) and discarded, stalling the console until + // a clear or filter change. The next tick resumes after this one settles. + if (logPollInFlight) return; + + var wasAtBottom, consoleEl = document.getElementById('logConsole'); + if (!consoleEl) return; + wasAtBottom = consoleEl.scrollTop + consoleEl.clientHeight >= consoleEl.scrollHeight - 40; + var token = ++logReqToken; + logPollInFlight = true; + // boot = epoch of the cursor: after a reboot the server forces a full dump + // (entries 1..N) even when the new seq is already past our stored cursor. + fetch('/api/logs?since=' + lastLogSeq + '&boot=' + lastLogBoot + '&count=200&level=' + logLevelFilter) + .then(function(res) { return res.json(); }) + .then(function(data) { + if (token !== logReqToken) return; // superseded by a newer poll/filter/clear + var empty = document.getElementById('logConsoleEmpty'); + // Boot change: the server re-sent the whole new-boot ring, so the old + // pre-reboot lines are stale — drop them instead of appending on top. + if (data.boot !== lastLogBoot) { + consoleEl.textContent = ''; + } + if (!data.entries || data.entries.length === 0) { + if (!consoleEl.hasChildNodes()) empty.style.display = 'block'; + return; + } + empty.style.display = 'none'; + data.entries.forEach(function(entry) { + var line = document.createElement('div'); + line.className = 'log-entry log-' + entry.level; + line.textContent = entry.msg; + consoleEl.appendChild(line); + }); + // Evict oldest entries beyond the client-side cap so an always-open + // dashboard cannot grow the log DOM without bound. + while (consoleEl.childNodes.length > 500) { + consoleEl.removeChild(consoleEl.firstChild); + } + lastLogSeq = data.next; + lastLogBoot = data.boot; + if (wasAtBottom && data.entries.length > 0) { + consoleEl.scrollTop = consoleEl.scrollHeight; + } + }) + .catch(function() { /* silent */ }) + .finally(function() { + logPollInFlight = false; + }); +} + +function clearLogs() { + logReqToken++; // invalidate any in-flight poll — it must not repopulate the console + fetch('/api/logs/clear', { method: 'POST' }).then(function() { + var c = document.getElementById('logConsole'); + if (c) c.textContent = ''; + var e = document.getElementById('logConsoleEmpty'); + if (e) e.style.display = 'block'; + lastLogSeq = 0; + }); +} + +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.log-chip').forEach(function(chip) { + chip.addEventListener('click', function() { + logReqToken++; // discard in-flight responses from the previous filter + document.querySelectorAll('.log-chip').forEach(function(c) { c.classList.remove('active'); }); + this.classList.add('active'); + logLevelFilter = this.dataset.level; + lastLogSeq = 0; + var c = document.getElementById('logConsole'); + if (c) c.textContent = ''; + var e = document.getElementById('logConsoleEmpty'); + if (e) e.style.display = 'none'; + loadLogs(); + }); + }); +}); + +var _origUpdateAuthUI = (typeof updateAuthUI === 'function') ? updateAuthUI : function(){}; +updateAuthUI = function() { + _origUpdateAuthUI(); + var clearBtn = document.getElementById('btnClearLogs'); + if (clearBtn) clearBtn.style.display = isAuthenticated ? 'inline-block' : 'none'; +}; + // ── Init ── setInterval(loadTelemetry, 2000); +setInterval(loadLogs, 2000); -window.onload = function() { - loadTelemetry(); - loadConfig(); - loadSensors(); -}; +// The script is deferred, so the DOM is fully parsed at this point. Start the +// telemetry loop immediately instead of waiting for window.onload (which waits +// for every resource, including the async stylesheet). Config and sensor data +// are lazy-loaded on first tab activation (see switchTab). +loadTelemetry(); diff --git a/data/web/index.html b/data/web/index.html index 1583c2fe..d3f53f58 100644 --- a/data/web/index.html +++ b/data/web/index.html @@ -19,8 +19,24 @@ - - + + + + + +

Pool Controller

@@ -39,6 +55,38 @@

+
+

🎯 Button Calibration

+

Four measurements run one after another. Keep the button pressed until its step turns green.

+ + +
+ Waiting +
Release all buttons
+

Release all buttons — measuring resting level

+

Next: hold Button 1

+
+ + +
+
1Resting
+
2Button 1
+
3Button 2
+
4Button 3
+
+ + +
+
+
—ADC
+
+ + +
+ +