diff --git a/.github/.wordlist.txt b/.github/.wordlist.txt index 5d87934c2ee..86b105ff98f 100644 --- a/.github/.wordlist.txt +++ b/.github/.wordlist.txt @@ -284,10 +284,12 @@ COAP codeaurora codebase codec +Codecov codegen CodeGenerator CodeLab codelabs +CodeQL ColorControl Combinators Comcast @@ -1606,6 +1608,8 @@ TotalVolatileOrganicCompoundsConcentrationMeasurement TPK trackAlloc trackFree +tradeoff +tradeoffs transcoder TransferSession transitionTime @@ -1633,6 +1637,7 @@ TXD txt uargument UART +UBSan UDC udcport udhcpc diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml index 35b14ba29b9..eac255c4b0e 100644 --- a/.github/actions/bootstrap/action.yaml +++ b/.github/actions/bootstrap/action.yaml @@ -103,7 +103,7 @@ runs: build_overrides/pigweed_environment.gni - name: Upload bootstrap logs - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: ${{ always() && !env.ACT && fromJSON(steps.restore.outputs.outputs).cache-hit != 'true' }} with: name: ${{ inputs.bootstrap-log-name }} diff --git a/.github/actions/upload-size-reports/action.yaml b/.github/actions/upload-size-reports/action.yaml index 19f47fb5416..3d6a6ce8a18 100644 --- a/.github/actions/upload-size-reports/action.yaml +++ b/.github/actions/upload-size-reports/action.yaml @@ -9,7 +9,7 @@ runs: using: "composite" steps: - name: Uploading Size Reports - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: ${{ !env.ACT }} with: name: Size,${{ inputs.platform-name }}-Examples,${{ env.GH_EVENT_PR }},${{ env.GH_EVENT_HASH }},${{ env.GH_EVENT_PARENT }},${{ github.event_name }} diff --git a/.github/platform_maintainers.yaml b/.github/platform_maintainers.yaml index 96a6bec9613..54a9add204c 100644 --- a/.github/platform_maintainers.yaml +++ b/.github/platform_maintainers.yaml @@ -59,6 +59,7 @@ esp32: - "src/tracing/esp32_diagnostics/**" - "examples/platform/esp32/**" - "examples/**/esp32/**" + - "examples/**/esp32_zephyr/**" - "config/esp32/**" - "docs/platforms/esp32/**" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c7f5956002d..e3b38d1b071 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -69,10 +69,11 @@ jobs: - name: Checkout submodules & Bootstrap uses: ./.github/actions/checkout-submodules-and-bootstrap with: - platform: linux + # unit_tests pulls the submodules that chip_build_all_platform_tests needs + platform: linux unit_tests - name: Initialize CodeQL if: ${{ inputs.run-codeql }} - uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 with: languages: "cpp" queries: security-extended, security-and-quality @@ -81,9 +82,10 @@ jobs: # codeql.yml runs nightly on master only, so re-enable this if # the push/pull_request triggers are ever restored. trap-caching: false + # chip_build_all_platform_tests adds host-buildable platform unit tests (e.g. silabs) - name: Setup Build if: github.event.pull_request.number == null - run: scripts/build/gn_gen.sh --args="chip_config_memory_debug_checks=true chip_config_memory_debug_dmalloc=false" + run: scripts/build/gn_gen.sh --args="chip_config_memory_debug_checks=true chip_config_memory_debug_dmalloc=false chip_build_all_platform_tests=true" - name: Run Build if: github.event.pull_request.number == null run: scripts/run_in_build_env.sh "ninja -C ./out" @@ -122,12 +124,16 @@ jobs: - name: Clean out build output if: inputs.run-codeql != true && github.event.pull_request.number == null run: rm -rf ./out + # PR-only: master pushes build and test the memory-debug-checks configuration above. - name: Set up Build Without Logging - if: inputs.run-codeql != true - run: scripts/build/gn_gen.sh --args="chip_logging=false" + if: inputs.run-codeql != true && github.event.pull_request.number != null + run: scripts/build/gn_gen.sh --args="chip_logging=false chip_build_all_platform_tests=true" - name: Run Build Without Logging - if: inputs.run-codeql != true + if: inputs.run-codeql != true && github.event.pull_request.number != null run: scripts/run_in_build_env.sh "ninja -C ./out" + - name: Run Tests Without Logging + if: inputs.run-codeql != true && github.event.pull_request.number != null + run: scripts/tests/gn_tests.sh - name: Uploading core files uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: ${{ failure() && !env.ACT }} @@ -141,7 +147,7 @@ jobs: # scope, letting the pins bump together and stay consistent (see lint.yml check). - name: Perform CodeQL Analysis if: ${{ inputs.run-codeql }} - uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 with: category: "/language:cpp" upload: False @@ -161,7 +167,7 @@ jobs: output: "sarif-results/cpp.sarif" - name: Upload SARIF if: ${{ inputs.run-codeql }} - uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 with: sarif_file: "sarif-results/cpp.sarif" - name: Upload loc as a Build Artifact @@ -231,7 +237,7 @@ jobs: # deactivate until a better workaround is found # - name: Initialize CodeQL # if: ${{ inputs.run-codeql }} - # uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + # uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 # with: # languages: "cpp" - name: Setup and Build Simulated Device @@ -269,7 +275,6 @@ jobs: for BUILD_TYPE in asan tsan ubsan; do case $BUILD_TYPE in "asan") GN_ARGS='is_clang=true is_asan=true';; - "msan") GN_ARGS='is_clang=true is_msan=true';; "tsan") GN_ARGS='is_clang=true is_tsan=true chip_enable_wifi=false';; "ubsan") GN_ARGS='is_clang=true is_ubsan=true';; esac @@ -511,7 +516,7 @@ jobs: # Build on Darwin + CodeQL often takes 6 hours (which is more than the maximum allowed by GitHub Runners), Deactivate it until we can investigate this # - name: Initialize CodeQL # if: ${{ inputs.run-codeql }} - # uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + # uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 # with: # languages: "cpp" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 74610ad09bb..a3d155d4026 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -59,6 +59,11 @@ jobs: fi echo "codeql-action pinned consistently: ${shas:-}" + - name: Check unit-test steps snapshot is up to date + if: always() + # Keeps docs/ci-cd/host_unit_tests.md reviewed when workflows change which unit tests run. + run: scripts/tools/list_unit_test_steps.py --check + - name: Check for orphaned gn files if: always() # We should enforce that ALL new files are referenced in our build scripts. diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index 18e0e55edf6..6ca05aefaf6 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -330,6 +330,7 @@ jobs: with: slack-webhook: ${{ secrets.SWTT_SLACK_WEBHOOK }} + # See docs/ci-cd/host_unit_tests.md for which crypto backends run on PRs, pushes and nightly. nightly_asan_crypto_backends: name: Nightly ASAN Testing across Crypto Backends if: github.actor != 'restyled-io[bot]' @@ -356,7 +357,6 @@ jobs: ./scripts/run_in_build_env.sh \ "./scripts/build/build_examples.py --target linux-x64-tests-mbedtls-asan-clang build" \ && rm -rf out/linux-x64-tests-mbedtls-asan-clang - # PSA also runs in unit_integration_test.yaml, but only when crypto/credentials/pigweed paths change. - name: ASAN unit tests - PSA run: | ./scripts/run_in_build_env.sh \ @@ -494,3 +494,43 @@ jobs: uses: ./.github/actions/notify-slack-failure with: slack-webhook: ${{ secrets.SWTT_SLACK_WEBHOOK }} + + # See docs/ci-cd/host_unit_tests.md for which unit-test configurations run on PRs, pushes and nightly. + nightly_unit_tests_various_features: + name: Nightly Unit Tests - Linux (various features) + if: github.actor != 'restyled-io[bot]' + timeout-minutes: 120 + + runs-on: ubuntu-latest + + env: + BUILD_TYPE: various_features + + container: + image: ghcr.io/project-chip/chip-build:215 + volumes: + - "/:/runner-root-volume" + - "/tmp/log_output:/tmp/test_logs" + options: --privileged --sysctl "net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1" + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Checkout submodules & Bootstrap + uses: ./.github/actions/checkout-submodules-and-bootstrap + with: + platform: linux unit_tests + bootstrap-log-name: bootstrap-logs-unittest-various-features + - name: Setup Build + # Bundles ICD, ARL and rotating-device-id (with clang/asan/boringssl) into one build + run: | + scripts/build/gn_gen.sh --args='is_clang=true is_asan=true chip_crypto="boringssl" chip_enable_rotating_device_id=true chip_enable_icd_server=true chip_enable_icd_lit=true chip_enable_access_restrictions=true chip_build_all_platform_tests=true' + - name: Run Build + run: scripts/run_in_build_env.sh "ninja -C out/$BUILD_TYPE" + - name: Run Tests + run: scripts/tests/gn_tests.sh + - name: Notify Slack on Failure + if: failure() + uses: ./.github/actions/notify-slack-failure + with: + slack-webhook: ${{ secrets.SWTT_SLACK_WEBHOOK }} diff --git a/.github/workflows/restyled.yml b/.github/workflows/restyled.yml index d904c313681..f2d12a44a94 100644 --- a/.github/workflows/restyled.yml +++ b/.github/workflows/restyled.yml @@ -19,10 +19,10 @@ jobs: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: restyled-io/actions/setup@8467a405c8fafe4dff2f3c341e37b56bd1703c89 # v4 + - uses: restyled-io/actions/setup@ab22a0751549a3e4fe782295458b303a55f04f76 # v4 - id: restyler - uses: restyled-io/actions/run@8467a405c8fafe4dff2f3c341e37b56bd1703c89 # v4 + uses: restyled-io/actions/run@ab22a0751549a3e4fe782295458b303a55f04f76 # v4 - name: Save restyle patch if: ${{ steps.restyler.outputs.git-patch }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a06969df1a7..ee3412fef2e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -38,7 +38,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0 with: results_file: results.sarif results_format: sarif @@ -72,6 +72,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3 with: sarif_file: results.sarif diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a98011e6da2..d07561aac46 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1217,6 +1217,7 @@ jobs: run: | scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/test_testing/test_ota_images_versions.py -i objdir-clone/chip-ota-requestor-app_v2.min.ota -v 2' scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/test_testing/test_IDM_10_4.py' + scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/test_testing/test_idm_support.py' scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/test_testing/test_network_identity.py' scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/test_testing/test_TC_ICDM_2_1_full_pics.py' scripts/run_in_python_env.sh out/venv 'python3 src/python_testing/test_testing/test_TC_ICDM_2_1_min_pics.py' diff --git a/.github/workflows/unit_integration_test.yaml b/.github/workflows/unit_integration_test.yaml index 2d40e6bc09f..43293af8fce 100644 --- a/.github/workflows/unit_integration_test.yaml +++ b/.github/workflows/unit_integration_test.yaml @@ -27,73 +27,10 @@ concurrency: cancel-in-progress: true jobs: - unit_tests: - name: Unit / Integration Tests - if: github.actor != 'restyled-io[bot]' - - strategy: - matrix: - type: [main, mbedtls, all_features] - env: - BUILD_TYPE: ${{ matrix.type }} - - runs-on: ubuntu-latest - - container: - image: ghcr.io/project-chip/chip-build:215 - volumes: - - "/:/runner-root-volume" - - "/tmp/log_output:/tmp/test_logs" - options: --privileged --sysctl "net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1" - - steps: - - name: Checkout - uses: actions/checkout@v7 - - name: Checkout submodules & Bootstrap - uses: ./.github/actions/checkout-submodules-and-bootstrap - with: - platform: linux unit_tests - bootstrap-log-name: bootstrap-logs-unittest-${{ matrix.type }} - - name: Artifact suffix - id: outsuffix - uses: haya14busa/action-cond@v1 - if: ${{ !env.ACT }} - with: - cond: ${{ github.event.pull_request.number == '' }} - if_true: "${{ github.sha }}" - if_false: "pull-${{ github.event.pull_request.number }}" - - name: Setup Build - # all_features bundles ICD, ARL and rotating-device-id (with clang/asan/boringssl) into one matrix row - run: | - case $BUILD_TYPE in - "main") GN_ARGS='chip_build_all_platform_tests=true';; - "mbedtls") GN_ARGS='chip_crypto="mbedtls" chip_build_all_platform_tests=true';; - "all_features") GN_ARGS='is_clang=true is_asan=true chip_crypto="boringssl" chip_enable_rotating_device_id=true chip_enable_icd_server=true chip_enable_icd_lit=true chip_enable_access_restrictions=true chip_build_all_platform_tests=true';; - *) ;; - esac - - scripts/build/gn_gen.sh --args="$GN_ARGS" - - name: Run Build - run: scripts/run_in_build_env.sh "ninja -C out/$BUILD_TYPE" - - name: Run Tests - run: scripts/tests/gn_tests.sh - # TODO Log Upload https://github.com/project-chip/connectedhomeip/issues/2227 - # TODO https://github.com/project-chip/connectedhomeip/issues/1512 - # - name: Run Code Coverage - # if: ${{ contains('main', env.BUILD_TYPE) }} - # run: scripts/tools/codecoverage.sh - # - name: Upload Code Coverage - # if: ${{ contains('main', env.BUILD_TYPE) }} - # run: bash <(curl -s https://codecov.io/bash) - - name: Notify Slack on Failure - if: failure() - uses: ./.github/actions/notify-slack-failure - with: - slack-webhook: ${{ secrets.SWTT_SLACK_WEBHOOK }} - - # Builds and runs the host unit-test suite with the PSA crypto backend. - psa_crypto_unit_tests: - name: PSA Crypto Unit Tests + # Builds and runs the host unit-test suite with the PSA and mbedTLS crypto backends. + # See docs/ci-cd/host_unit_tests.md for which crypto backends run on PRs, pushes and nightly. + crypto_backend_unit_tests: + name: Crypto Backend Unit Tests (PSA, mbedTLS) if: github.actor != 'restyled-io[bot]' runs-on: ubuntu-latest @@ -108,32 +45,38 @@ jobs: steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Checkout submodules & Bootstrap - uses: ./.github/actions/checkout-submodules-and-bootstrap - with: - platform: linux unit_tests - bootstrap-log-name: bootstrap-logs-unittest-psa + # Runs before bootstrap so runs that do not match the filter skip the whole job body. - name: Detect changed paths uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4 id: changed_paths with: filters: | - credentials: - - 'src/credentials/**' + # PSA on Linux is provided by the mbedTLS library, so both backends share one filter. crypto: + - 'src/credentials/**' - 'src/crypto/**' - pigweed: - 'src/pw_backends/**' - 'third_party/pigweed/**' + - 'third_party/mbedtls/**' + - '.github/workflows/unit_integration_test.yaml' + - name: Checkout submodules & Bootstrap + if: steps.changed_paths.outputs.crypto == 'true' + uses: ./.github/actions/checkout-submodules-and-bootstrap + with: + platform: linux + bootstrap-log-name: bootstrap-logs-unittest-crypto-backends - name: Build and run PSA unit tests (ASan) - if: > - github.event_name == 'push' || - steps.changed_paths.outputs.credentials == 'true' || - steps.changed_paths.outputs.crypto == 'true' || - steps.changed_paths.outputs.pigweed == 'true' + if: steps.changed_paths.outputs.crypto == 'true' + run: | + ./scripts/run_in_build_env.sh \ + "./scripts/build/build_examples.py --target linux-x64-tests-psa-asan-clang build" \ + && rm -rf out/linux-x64-tests-psa-asan-clang + - name: Build and run mbedTLS unit tests (ASan) + if: steps.changed_paths.outputs.crypto == 'true' run: | ./scripts/run_in_build_env.sh \ - "./scripts/build/build_examples.py --target linux-x64-tests-psa-asan-clang build" + "./scripts/build/build_examples.py --target linux-x64-tests-mbedtls-asan-clang build" \ + && rm -rf out/linux-x64-tests-mbedtls-asan-clang - name: Notify Slack on Failure if: failure() uses: ./.github/actions/notify-slack-failure diff --git a/AGENTS.md b/AGENTS.md index 60ca4ad7300..67909c3ee5d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,6 +86,64 @@ for full details. - _Always_ include `{}` bracketing for control flows, even if using one liners (e.g. for `if`, `while`, `for` and such) +## Comments and Technical Writing + +- **Explain "Why", Not "What"**: + - In both code comments and PR descriptions, focus primarily on _why_ a + decision was made and the background context. + - Do not restate the code. Readers can see the code and the diff. + - In PR descriptions, state _what_ changed only as needed to explain _why_ + it fixes the problem, rather than listing file-by-file or mechanical + edits. +- **Code First, Comments Second**: + - Treat an inline implementation comment as an admission that the code is + not clear on its own. Prefer writing obviously correct, self-explanatory + code that needs no inline comments. + - Most comments should be API doc comments on interfaces explaining how + the API is used, its contract, lifetimes, locking, units, and _why_ it + exists. + - Inline comments near implementation code are suspicious. The longer an + inline comment gets, the stronger the code smell — simplify or + restructure the code instead of growing the comment. + - When an inline comment is necessary (e.g., for a non-obvious constraint + or spec requirement), keep it to 1–3 lines. Use a table or compact ASCII + diagram instead of paragraphs for state machines or event matrices. +- **Write for the Final Code, Not the Diff (Default to Deleting)**: + - Keep a comment only if a future reader modifying the code would need it + to avoid breaking a non-obvious invariant, contract, or spec constraint. + When in doubt, delete the comment rather than shortening it. + - Never narrate the diff or history in code comments (e.g., "moved X + before Y so Z does not happen", "this used to do Y") and do not explain + rejected alternatives that are not in the code. Put change history and + alternatives in the commit/PR description, and state only the current + invariant in the code. + - Do not add comments that label obvious guards ("defensive check"), point + out visible code structure ("using a map keyed by X"), or explain + standard language/library mechanics. +- **Document API Contracts, Never Implementation Coupling**: + - It is fine for an inline comment to note a non-obvious API contract that + the call site relies on, saving the reader from jumping to other headers + or definitions. + - Never justify local code by referencing the internal implementation + details of another function or module (e.g., "this is safe because + `Foo()` internally does X"). Implementation details are not a contract + and may change over time; relying on them is a code smell. If a + guarantee is required, make it part of the API contract or decouple the + code. +- **Plain Technical Language (No Prose or Literature)**: + - Code comments and technical docs are not literature. Use literal, plain + words instead of figurative language, metaphors, idioms, or fancy + synonyms. + - Write "set the bool to true" (not "signal" or "latch" the bool), "start + the timer" (not "arm" the timer), "clear the state" (not "burn" or + "spend" the state), "use X" (not "leverage X" or "key off X"), and "what + breaks / which case fails" (not "where it bites"). + - Avoid hype or self-praise ("robust", "seamless", "zero overhead"). State + tradeoffs only when naming a concrete engineering cost (e.g., "chosen to + reduce flash size at the cost of runtime"). + - Name the subject in every sentence (avoid vague "this" or "it") and keep + sentences short. + ## Testing - Unit tests are required for all changes unless unit testing is impossible diff --git a/docs/ci-cd/host_unit_tests.generated.txt b/docs/ci-cd/host_unit_tests.generated.txt new file mode 100644 index 00000000000..8bc80134e81 --- /dev/null +++ b/docs/ci-cd/host_unit_tests.generated.txt @@ -0,0 +1,121 @@ +# Generated by scripts/tools/list_unit_test_steps.py --update; do not edit. + +build.yaml / Build on Linux (gcc_debug) / Run Tests + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group, workflow_dispatch, workflow_call + job-if: github.actor != 'restyled-io[bot]' + if: github.event.pull_request.number == null + gn: chip_config_memory_debug_checks=true chip_config_memory_debug_dmalloc=false chip_build_all_platform_tests=true + +build.yaml / Build on Linux (gcc_debug) / Run Tests Without Logging + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group, workflow_dispatch, workflow_call + job-if: github.actor != 'restyled-io[bot]' + if: inputs.run-codeql != true && github.event.pull_request.number != null + gn: chip_logging=false chip_build_all_platform_tests=true + +build.yaml / Build on Linux (fake, gcc_release, clang, simulated) / Setup Build, Run Build and Run Tests (gcc_release) + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group, workflow_dispatch, workflow_call + job-if: github.actor != 'restyled-io[bot]' && inputs.run-codeql != true + if: github.event.pull_request.number == null + gn: is_debug=false + +build.yaml / Build on Linux (fake, gcc_release, clang, simulated) / Build and run AddressSanitizer (ASAN) unit tests (Pull Requests only) + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group, workflow_dispatch, workflow_call + job-if: github.actor != 'restyled-io[bot]' && inputs.run-codeql != true + if: github.event_name == 'pull_request' + targets: linux-x64-tests-asan-clang + +build.yaml / Build on Linux (fake, gcc_release, clang, simulated) / Run Tests with sanitizers + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group, workflow_dispatch, workflow_call + job-if: github.actor != 'restyled-io[bot]' && inputs.run-codeql != true + if: github.event.pull_request.number == null + gn: is_clang=true is_asan=true | is_clang=true is_tsan=true chip_enable_wifi=false | is_clang=true is_ubsan=true + +build.yaml / Build on Linux (fake, gcc_release, clang, simulated) / Run fake linux tests with build_examples + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group, workflow_dispatch, workflow_call + job-if: github.actor != 'restyled-io[bot]' && inputs.run-codeql != true + if: - + targets: linux-fake-tests + +build.yaml / Build on Linux (python_lib) / Setup Build, Run Build and Run Tests + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group, workflow_dispatch, workflow_call + job-if: github.actor != 'restyled-io[bot]' && inputs.run-codeql != true + if: - + gn: enable_rtti=true chip_config_memory_debug_checks=false chip_config_memory_debug_dmalloc=false chip_generate_link_map_file=false + +build.yaml / Build on Darwin (clang, simulated) / Setup Build, Run Build and Run Tests (asan + target_os=all) + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group, workflow_dispatch, workflow_call + job-if: github.actor != 'restyled-io[bot]' && inputs.run-codeql != true + if: - + gn: target_os="all" is_asan=true enable_host_clang_build=false + +build.yaml / Build on Linux (coverage) / Run Build Coverage + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group, workflow_dispatch, workflow_call + job-if: github.actor != 'restyled-io[bot]' && github.actor != 'dependabot[bot]' && inputs.run-codeql != true + if: - + gn: - + +examples-nrfconnect.yaml / nRF Connect SDK / Run unit tests for Zephyr native_posix_64 platform + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group + job-if: github.actor != 'restyled-io[bot]' + if: github.event_name == 'push' || steps.changed_paths.outputs.tests == 'true' || steps.changed_paths.outputs.nrfconnect == 'true' || steps.changed_paths.outputs.pigweed == 'true' || steps.changed_paths.outputs.crypto == 'true' + filter: changed_paths.crypto = src/crypto/** + filter: changed_paths.nrfconnect = **/nrfconnect/**, **/Zephyr/**, **/zephyr/** + filter: changed_paths.pigweed = src/pw_backends/**, third_party/pigweed/** + filter: changed_paths.tests = **/tests/** + targets: nrf-native-sim-tests + +nightly.yaml / Nightly ASAN Testing across Crypto Backends / ASAN unit tests - BoringSSL + on: schedule[{"cron": "0 0 * * *"}], workflow_dispatch + job-if: github.actor != 'restyled-io[bot]' + if: - + targets: linux-x64-tests-boringssl-asan-clang + +nightly.yaml / Nightly ASAN Testing across Crypto Backends / ASAN unit tests - mbedTLS + on: schedule[{"cron": "0 0 * * *"}], workflow_dispatch + job-if: github.actor != 'restyled-io[bot]' + if: - + targets: linux-x64-tests-mbedtls-asan-clang + +nightly.yaml / Nightly ASAN Testing across Crypto Backends / ASAN unit tests - PSA + on: schedule[{"cron": "0 0 * * *"}], workflow_dispatch + job-if: github.actor != 'restyled-io[bot]' + if: - + targets: linux-x64-tests-psa-asan-clang + +nightly.yaml / Nightly MSAN Tests - Linux / Build and Run MSAN unit tests + on: schedule[{"cron": "0 0 * * *"}], workflow_dispatch + job-if: github.actor != 'restyled-io[bot]' + if: - + targets: linux-x64-tests-msan-clang + +nightly.yaml / Nightly Unit Tests - Linux (various features) / Run Tests + on: schedule[{"cron": "0 0 * * *"}], workflow_dispatch + job-if: github.actor != 'restyled-io[bot]' + if: - + gn: is_clang=true is_asan=true chip_crypto="boringssl" chip_enable_rotating_device_id=true chip_enable_icd_server=true chip_enable_icd_lit=true chip_enable_access_restrictions=true chip_build_all_platform_tests=true + +qemu.yaml / ESP32_QEMU / Build ESP32 QEMU test images + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group + job-if: github.actor != 'restyled-io[bot]' + if: - + targets: esp32-qemu-tests + +qemu.yaml / Tizen / Build and run tests + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group + job-if: github.actor != 'restyled-io[bot]' + if: - + targets: tizen-arm-tests-no-ble-no-thread + +unit_integration_test.yaml / Crypto Backend Unit Tests (PSA, mbedTLS) / Build and run PSA unit tests (ASan) + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group + job-if: github.actor != 'restyled-io[bot]' + if: steps.changed_paths.outputs.crypto == 'true' + filter: changed_paths.crypto = src/credentials/**, src/crypto/**, src/pw_backends/**, third_party/pigweed/**, third_party/mbedtls/**, .github/workflows/unit_integration_test.yaml + targets: linux-x64-tests-psa-asan-clang + +unit_integration_test.yaml / Crypto Backend Unit Tests (PSA, mbedTLS) / Build and run mbedTLS unit tests (ASan) + on: push{"branches": ["master", "v*-branch"]}, pull_request, merge_group + job-if: github.actor != 'restyled-io[bot]' + if: steps.changed_paths.outputs.crypto == 'true' + filter: changed_paths.crypto = src/credentials/**, src/crypto/**, src/pw_backends/**, third_party/pigweed/**, third_party/mbedtls/**, .github/workflows/unit_integration_test.yaml + targets: linux-x64-tests-mbedtls-asan-clang diff --git a/docs/ci-cd/host_unit_tests.md b/docs/ci-cd/host_unit_tests.md new file mode 100644 index 00000000000..f2bd2fa9e72 --- /dev/null +++ b/docs/ci-cd/host_unit_tests.md @@ -0,0 +1,50 @@ +# Host unit tests in CI + +Which configurations of the host unit-test suite (`ninja check` / `gn_tests.sh` +/ `build_examples.py --target *-tests*`) run in CI, when, and why. + +**Keep this file in sync:** `scripts/tools/list_unit_test_steps.py` lists every +CI step that runs unit tests into `host_unit_tests.generated.txt`. When a +workflow change alters that list, run it with `--update` and update the tables +below in the same PR. The `code-lints` job in `lint.yml` runs it with `--check`. + +Release branches (`v*-branch`) run their own copy of each workflow. On push they +behave like a master push; scheduled (nightly) workflows do not run on them. + +## Linux and macOS host + +| Configuration | Job (workflow) | PR | Master push | Nightly | Why | +| ---------------------------------------------------- | -------------------------------------------------------------- | ------------ | ------------ | ------- | --------------------------------------------------- | +| gcc, OpenSSL, `enable_rtti` | Build on Linux (python_lib) (`build.yaml`) | yes | yes | – | Builds the Python controller; also runs the suite | +| gcc, OpenSSL, `use_coverage` | Build on Linux (coverage) (`build.yaml`) | yes | yes | – | Codecov report | +| gcc, OpenSSL, memory debug checks, platform tests | Build on Linux (gcc_debug) (`build.yaml`) | – | yes | CodeQL | Only run of `chip_config_memory_debug_checks` | +| gcc, OpenSSL, `chip_logging=false`, platform tests | Build on Linux (gcc_debug) (`build.yaml`) | yes | – | – | Logging-off build | +| gcc, detail / progress / error logging off (compile) | Build on Linux (gcc_debug) (`build.yaml`) | – | yes | – | Logging-level builds | +| gcc release (`is_debug=false`) | Build on Linux (fake, gcc_release, clang, simulated) | – | yes | – | Release-mode build | +| clang ASan, OpenSSL | Build on Linux (fake, gcc_release, clang, simulated) | yes | – | – | Sanitizer run on every PR | +| clang ASan, TSan, UBSan, OpenSSL | Build on Linux (fake, gcc_release, clang, simulated) | – | yes | – | Full sanitizer set after merge | +| fake platform (`linux-fake-tests`) | Build on Linux (fake, gcc_release, clang, simulated) | yes | yes | – | Platform-independent tests | +| clang ASan, PSA | Crypto Backend Unit Tests (`unit_integration_test.yaml`) | crypto paths | crypto paths | – | Crypto backend | +| clang ASan, mbedTLS | Crypto Backend Unit Tests (`unit_integration_test.yaml`) | crypto paths | crypto paths | – | Crypto backend | +| clang ASan, BoringSSL, ICD + ARL + rotating id | Nightly Unit Tests - Linux (various features) (`nightly.yaml`) | – | – | yes | Feature flags; ~66m, `TestRead` waits on ICD timers | +| clang ASan, BoringSSL / mbedTLS / PSA | Nightly ASAN Testing across Crypto Backends (`nightly.yaml`) | – | – | yes | Crypto backends with ASan | +| clang MSan | Nightly MSAN Tests - Linux (`nightly.yaml`) | – | – | yes | Uninitialized-memory checks | +| macOS ASan, BoringSSL, `target_os="all"` | Build on Darwin (clang, simulated) (`build.yaml`) | yes | yes | – | macOS host | + +"Crypto paths": `src/credentials`, `src/crypto`, `src/pw_backends`, +`third_party/pigweed`, `third_party/mbedtls`, `unit_integration_test.yaml`. +"Platform tests": `chip_build_all_platform_tests=true` (Silabs host tests). + +## Embedded targets + +| Configuration | Job (workflow) | PR | Master push | Why | +| ---------------------- | -------------------------------------------- | ---------- | ----------- | ------------ | +| ESP32 QEMU, mbedTLS | ESP32_QEMU (`qemu.yaml`) | yes | yes | ESP32 target | +| Tizen QEMU | Tizen (`qemu.yaml`) | yes | yes | Tizen target | +| Zephyr native_sim, PSA | nRF Connect SDK (`examples-nrfconnect.yaml`) | some paths | yes | nRF target | + +## Open questions + +- gcc runs the full suite three times on every PR (python_lib, coverage, + gcc_debug `chip_logging=false`). Is more than one needed? +- Release branches get no nightly coverage (#74387). diff --git a/docs/ci-cd/index.md b/docs/ci-cd/index.md index c2bfc51db6a..024d2f2978b 100644 --- a/docs/ci-cd/index.md +++ b/docs/ci-cd/index.md @@ -3,6 +3,7 @@ ```{toctree} :glob: +host_unit_tests tools/* ``` diff --git a/docs/ids_and_codes/ERROR_CODES.md b/docs/ids_and_codes/ERROR_CODES.md index d10834b5352..ddf50cd8bae 100644 --- a/docs/ids_and_codes/ERROR_CODES.md +++ b/docs/ids_and_codes/ERROR_CODES.md @@ -270,5 +270,7 @@ This file was **AUTOMATICALLY** generated by | 1487 | 0x5CF | `DYNAMIC_CONSTRAINT_ERROR` | | 1488 | 0x5D0 | `ALREADY_EXISTS` | | 1489 | 0x5D1 | `INVALID_TRANSPORT_TYPE` | +| 1490 | 0x5D2 | `PERSIST_FAIL_REJECT` | +| 1491 | 0x5D3 | `PERSIST_FAIL_ACCEPT` | | 1520 | 0x5F0 | `WRITE_IGNORED` | diff --git a/docs/requirements.txt b/docs/requirements.txt index 401e6e6babe..8d6fc0166b1 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,5 +4,5 @@ sphinx-book-theme sphinxcontrib-mermaid myst-parser>=5.1.0 breathe>=5.1.0 -pydata-sphinx-theme==0.22.0 +pydata-sphinx-theme==0.23.0 pydoc-markdown>=4.8.2 diff --git a/examples/air-purifier-app/air-purifier-common/air-purifier-app.matter b/examples/air-purifier-app/air-purifier-common/air-purifier-app.matter index a17d5e85823..4f898d1e575 100644 --- a/examples/air-purifier-app/air-purifier-common/air-purifier-app.matter +++ b/examples/air-purifier-app/air-purifier-common/air-purifier-app.matter @@ -3092,7 +3092,7 @@ endpoint 0 { callback attribute upTime; callback attribute totalOperationalHours; callback attribute bootReason; - callback attribute testEventTriggersEnabled default = false; + callback attribute testEventTriggersEnabled; callback attribute deviceLoadStatus; callback attribute featureMap; callback attribute clusterRevision; @@ -3105,7 +3105,7 @@ endpoint 0 { callback attribute windowStatus; callback attribute adminFabricIndex; callback attribute adminVendorId; - ram attribute featureMap default = 0; + callback attribute featureMap default = 0; callback attribute clusterRevision; handle command OpenCommissioningWindow; @@ -3224,21 +3224,21 @@ endpoint 1 { server cluster FanControl { callback attribute fanMode; - ram attribute fanModeSequence default = 2; + callback attribute fanModeSequence default = 2; callback attribute percentSetting; callback attribute percentCurrent; - ram attribute speedMax default = 10; + callback attribute speedMax default = 10; callback attribute speedSetting; callback attribute speedCurrent; - ram attribute rockSupport default = 0x01; + callback attribute rockSupport default = 0x01; callback attribute rockSetting; - ram attribute windSupport default = 0x03; + callback attribute windSupport default = 0x03; callback attribute windSetting; callback attribute airflowDirection; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 63; + callback attribute featureMap default = 63; callback attribute clusterRevision; handle command Step; @@ -3297,7 +3297,7 @@ endpoint 2 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster CarbonDioxideConcentrationMeasurement { @@ -3316,7 +3316,7 @@ endpoint 2 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster NitrogenDioxideConcentrationMeasurement { @@ -3335,7 +3335,7 @@ endpoint 2 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster OzoneConcentrationMeasurement { @@ -3354,7 +3354,7 @@ endpoint 2 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm25ConcentrationMeasurement { @@ -3373,7 +3373,7 @@ endpoint 2 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster FormaldehydeConcentrationMeasurement { @@ -3392,7 +3392,7 @@ endpoint 2 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm1ConcentrationMeasurement { @@ -3411,7 +3411,7 @@ endpoint 2 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm10ConcentrationMeasurement { @@ -3430,7 +3430,7 @@ endpoint 2 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster TotalVolatileOrganicCompoundsConcentrationMeasurement { @@ -3449,7 +3449,7 @@ endpoint 2 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster RadonConcentrationMeasurement { @@ -3468,7 +3468,7 @@ endpoint 2 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } } endpoint 3 { @@ -3502,9 +3502,9 @@ endpoint 3 { server cluster TemperatureMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0x8000; - ram attribute maxMeasuredValue default = 0x8000; - ram attribute tolerance default = 0; + callback attribute minMeasuredValue default = 0x8000; + callback attribute maxMeasuredValue default = 0x8000; + callback attribute tolerance default = 0; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; @@ -3543,9 +3543,9 @@ endpoint 4 { server cluster RelativeHumidityMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0; - ram attribute maxMeasuredValue default = 0x2710; - ram attribute tolerance; + callback attribute minMeasuredValue default = 0; + callback attribute maxMeasuredValue default = 0x2710; + callback attribute tolerance; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; @@ -3583,17 +3583,17 @@ endpoint 5 { } server cluster Thermostat { - ram attribute localTemperature; - ram attribute absMinHeatSetpointLimit default = 1000; - ram attribute absMaxHeatSetpointLimit default = 3000; - ram attribute occupiedHeatingSetpoint default = 2000; - ram attribute controlSequenceOfOperation default = 2; - ram attribute systemMode default = 0; - ram attribute thermostatRunningState default = 0; + callback attribute localTemperature; + callback attribute absMinHeatSetpointLimit default = 1000; + callback attribute absMaxHeatSetpointLimit default = 3000; + callback attribute occupiedHeatingSetpoint default = 2000; + callback attribute controlSequenceOfOperation default = 2; + callback attribute systemMode default = 0; + callback attribute thermostatRunningState default = 0; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; handle command SetpointRaiseLower; diff --git a/examples/air-quality-sensor-app/air-quality-sensor-common/air-quality-sensor-app.matter b/examples/air-quality-sensor-app/air-quality-sensor-common/air-quality-sensor-app.matter index b62d9008ced..c392b49639d 100644 --- a/examples/air-quality-sensor-app/air-quality-sensor-common/air-quality-sensor-app.matter +++ b/examples/air-quality-sensor-app/air-quality-sensor-common/air-quality-sensor-app.matter @@ -2665,7 +2665,7 @@ endpoint 0 { callback attribute upTime; callback attribute totalOperationalHours; callback attribute bootReason; - callback attribute testEventTriggersEnabled default = false; + callback attribute testEventTriggersEnabled; callback attribute deviceLoadStatus; callback attribute generatedCommandList; callback attribute acceptedCommandList; @@ -2752,7 +2752,7 @@ endpoint 0 { callback attribute channelPage0Mask; callback attribute operationalDatasetComponents; callback attribute activeNetworkFaultsList; - ram attribute featureMap default = 0x000F; + callback attribute featureMap default = 0x000F; callback attribute clusterRevision; handle command ResetCounts; @@ -2775,8 +2775,8 @@ endpoint 0 { callback attribute packetUnicastTxCount; callback attribute currentMaxRate; callback attribute overrunCount; - ram attribute featureMap default = 3; - callback attribute clusterRevision default = 1; + callback attribute featureMap default = 3; + callback attribute clusterRevision; handle command ResetCounts; } @@ -2791,7 +2791,7 @@ endpoint 0 { callback attribute overrunCount; callback attribute carrierDetect; callback attribute timeSinceReset; - ram attribute featureMap default = 3; + callback attribute featureMap default = 3; callback attribute clusterRevision; handle command ResetCounts; @@ -2804,7 +2804,7 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap default = 0; callback attribute clusterRevision; handle command OpenCommissioningWindow; @@ -2893,8 +2893,8 @@ endpoint 1 { server cluster TemperatureMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue; - ram attribute maxMeasuredValue; + callback attribute minMeasuredValue; + callback attribute maxMeasuredValue; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; @@ -2904,8 +2904,8 @@ endpoint 1 { server cluster RelativeHumidityMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue; - ram attribute maxMeasuredValue; + callback attribute minMeasuredValue; + callback attribute maxMeasuredValue; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; @@ -2929,7 +2929,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster CarbonDioxideConcentrationMeasurement { @@ -2948,7 +2948,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster NitrogenDioxideConcentrationMeasurement { @@ -2967,7 +2967,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster OzoneConcentrationMeasurement { @@ -2986,7 +2986,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm25ConcentrationMeasurement { @@ -3005,7 +3005,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster FormaldehydeConcentrationMeasurement { @@ -3024,7 +3024,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm1ConcentrationMeasurement { @@ -3043,7 +3043,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm10ConcentrationMeasurement { @@ -3062,7 +3062,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster TotalVolatileOrganicCompoundsConcentrationMeasurement { @@ -3081,7 +3081,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster RadonConcentrationMeasurement { @@ -3100,7 +3100,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } } diff --git a/examples/air-quality-sensor-app/air-quality-sensor-common/icd-lit-air-quality-sensor-app.matter b/examples/air-quality-sensor-app/air-quality-sensor-common/icd-lit-air-quality-sensor-app.matter index d3c6cc3f3fc..3a6aefb4e5a 100644 --- a/examples/air-quality-sensor-app/air-quality-sensor-common/icd-lit-air-quality-sensor-app.matter +++ b/examples/air-quality-sensor-app/air-quality-sensor-common/icd-lit-air-quality-sensor-app.matter @@ -2744,7 +2744,7 @@ endpoint 0 { callback attribute upTime; callback attribute totalOperationalHours; callback attribute bootReason; - callback attribute testEventTriggersEnabled default = false; + callback attribute testEventTriggersEnabled; callback attribute deviceLoadStatus; callback attribute featureMap; callback attribute clusterRevision; @@ -2828,7 +2828,7 @@ endpoint 0 { callback attribute channelPage0Mask; callback attribute operationalDatasetComponents; callback attribute activeNetworkFaultsList; - ram attribute featureMap default = 0x000F; + callback attribute featureMap default = 0x000F; callback attribute clusterRevision; handle command ResetCounts; @@ -2851,8 +2851,8 @@ endpoint 0 { callback attribute packetUnicastTxCount; callback attribute currentMaxRate; callback attribute overrunCount; - ram attribute featureMap default = 3; - ram attribute clusterRevision default = 1; + callback attribute featureMap default = 3; + callback attribute clusterRevision; handle command ResetCounts; } @@ -2867,7 +2867,7 @@ endpoint 0 { callback attribute overrunCount; callback attribute carrierDetect; callback attribute timeSinceReset; - ram attribute featureMap default = 3; + callback attribute featureMap default = 3; callback attribute clusterRevision; handle command ResetCounts; @@ -2877,7 +2877,7 @@ endpoint 0 { callback attribute windowStatus; callback attribute adminFabricIndex; callback attribute adminVendorId; - ram attribute featureMap default = 0; + callback attribute featureMap default = 0; callback attribute clusterRevision; handle command OpenCommissioningWindow; @@ -2892,7 +2892,7 @@ endpoint 0 { callback attribute commissionedFabrics; callback attribute trustedRootCertificates; callback attribute currentFabricIndex; - callback attribute featureMap; + callback attribute featureMap default = 0; callback attribute clusterRevision; handle command AttestationRequest; @@ -2928,8 +2928,8 @@ endpoint 0 { callback attribute registeredClients; callback attribute ICDCounter; callback attribute clientsSupportedPerFabric; - ram attribute userActiveModeTriggerHint; - ram attribute userActiveModeTriggerInstruction; + callback attribute userActiveModeTriggerHint; + callback attribute userActiveModeTriggerInstruction; callback attribute operatingMode; callback attribute maximumCheckInBackOff; callback attribute generatedCommandList; @@ -2982,8 +2982,8 @@ endpoint 1 { server cluster TemperatureMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue; - ram attribute maxMeasuredValue; + callback attribute minMeasuredValue; + callback attribute maxMeasuredValue; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; @@ -2993,8 +2993,8 @@ endpoint 1 { server cluster RelativeHumidityMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue; - ram attribute maxMeasuredValue; + callback attribute minMeasuredValue; + callback attribute maxMeasuredValue; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; @@ -3011,7 +3011,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster CarbonDioxideConcentrationMeasurement { @@ -3023,7 +3023,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster NitrogenDioxideConcentrationMeasurement { @@ -3035,7 +3035,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster OzoneConcentrationMeasurement { @@ -3047,7 +3047,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm25ConcentrationMeasurement { @@ -3059,7 +3059,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster FormaldehydeConcentrationMeasurement { @@ -3071,7 +3071,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm1ConcentrationMeasurement { @@ -3083,7 +3083,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm10ConcentrationMeasurement { @@ -3095,7 +3095,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster TotalVolatileOrganicCompoundsConcentrationMeasurement { @@ -3107,7 +3107,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster RadonConcentrationMeasurement { @@ -3119,7 +3119,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } } diff --git a/examples/air-quality-sensor-app/silabs/include/CHIPProjectConfig.h b/examples/air-quality-sensor-app/silabs/include/CHIPProjectConfig.h index a7a08747205..3daad08b827 100644 --- a/examples/air-quality-sensor-app/silabs/include/CHIPProjectConfig.h +++ b/examples/air-quality-sensor-app/silabs/include/CHIPProjectConfig.h @@ -109,5 +109,5 @@ */ #define CHIP_CONFIG_MRP_LOCAL_ACTIVE_RETRY_INTERVAL (2000_ms32) -// Temporary setting to use CustomerAppTask for apps which are not upgraded to new architecture +// Route shared platform callbacks through CustomerAppTask #define CHIP_SILABS_APP_USE_CUSTOMER_APP_TASK diff --git a/examples/all-clusters-app/all-clusters-common/all-clusters-app.matter b/examples/all-clusters-app/all-clusters-common/all-clusters-app.matter index 3f212090781..da1249aecb4 100644 --- a/examples/all-clusters-app/all-clusters-common/all-clusters-app.matter +++ b/examples/all-clusters-app/all-clusters-common/all-clusters-app.matter @@ -4406,6 +4406,56 @@ cluster ActivatedCarbonFilterMonitoring = 114 { command ResetCondition(): DefaultSuccess = 0; } +/** This generic cluster provides an interface to the current condition of a resource. */ +cluster WaterTankLevelMonitoring = 121 { + revision 1; + + shared enum ChangeIndicationEnum : enum8 { + kOK = 0 [spec_name = "OK"]; + kWarning = 1; + kCritical = 2; + } + + shared enum DegradationDirectionEnum : enum8 { + kUp = 0; + kDown = 1; + } + + shared enum ProductIdentifierTypeEnum : enum8 { + kUPC = 0 [spec_name = "UPC"]; + kGTIN8 = 1 [spec_name = "GTIN-8"]; + kEAN = 2 [spec_name = "EAN"]; + kGTIN14 = 3 [spec_name = "GTIN-14"]; + kOEM = 4 [spec_name = "OEM"]; + } + + bitmap Feature : bitmap32 { + kCondition = 0x1; + kWarning = 0x2; + kReplacementProductList = 0x4; + } + + struct ReplacementProductStruct { + ProductIdentifierTypeEnum productIdentifierType = 0; + char_string<20> productIdentifierValue = 1; + } + + readonly attribute optional percent condition = 0; + readonly attribute optional DegradationDirectionEnum degradationDirection = 1; + readonly attribute ChangeIndicationEnum changeIndication = 2; + readonly attribute optional boolean inPlaceIndicator = 3; + attribute optional nullable epoch_s lastChangedTime = 4; + readonly attribute optional ReplacementProductStruct replacementProductList[] = 5; + readonly attribute command_id generatedCommandList[] = 65528; + readonly attribute command_id acceptedCommandList[] = 65529; + readonly attribute attrib_id attributeList[] = 65531; + readonly attribute bitmap32 featureMap = 65532; + readonly attribute int16u clusterRevision = 65533; + + /** Upon receipt, the device SHALL reset the Condition and ChangeIndicator attributes, indicating full resource availability and readiness for use, as initially configured. */ + command ResetCondition(): DefaultSuccess = 0; +} + /** This cluster is used to configure a boolean sensor. */ cluster BooleanStateConfiguration = 128 { revision 2; @@ -7602,9 +7652,9 @@ endpoint 0 { binding cluster OtaSoftwareUpdateProvider; server cluster Groups { - ram attribute nameSupport; - ram attribute featureMap default = 0; - ram attribute clusterRevision default = 0x0005; + callback attribute nameSupport; + callback attribute featureMap default = 0; + callback attribute clusterRevision; handle command AddGroup; handle command ViewGroup; @@ -7702,18 +7752,18 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - callback attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; } server cluster TimeFormatLocalization { - ram attribute hourFormat default = 0; - ram attribute activeCalendarType default = 0; + callback attribute hourFormat default = 0; + callback attribute activeCalendarType default = 0; callback attribute supportedCalendarTypes; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; } @@ -7723,7 +7773,7 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0x1; + callback attribute featureMap default = 0x1; callback attribute clusterRevision; } @@ -7903,7 +7953,7 @@ endpoint 0 { callback attribute channelPage0Mask; callback attribute operationalDatasetComponents; callback attribute activeNetworkFaultsList; - ram attribute featureMap default = 0x000F; + callback attribute featureMap default = 0x000F; callback attribute clusterRevision; handle command ResetCounts; @@ -7926,8 +7976,8 @@ endpoint 0 { callback attribute packetUnicastTxCount; callback attribute currentMaxRate; callback attribute overrunCount; - ram attribute featureMap default = 3; - callback attribute clusterRevision default = 1; + callback attribute featureMap default = 3; + callback attribute clusterRevision; handle command ResetCounts; } @@ -7942,7 +7992,7 @@ endpoint 0 { callback attribute overrunCount; callback attribute carrierDetect; callback attribute timeSinceReset; - ram attribute featureMap default = 3; + callback attribute featureMap default = 3; callback attribute clusterRevision; handle command ResetCounts; @@ -7956,20 +8006,20 @@ endpoint 0 { emits event MissingTrustedTimeSource; callback attribute UTCTime; callback attribute granularity; - ram attribute timeSource default = 0x00; + callback attribute timeSource default = 0x00; callback attribute trustedTimeSource; callback attribute defaultNTP; callback attribute timeZone; callback attribute DSTOffset; callback attribute localTime; - ram attribute timeZoneDatabase default = 0; + callback attribute timeZoneDatabase default = 0; callback attribute timeZoneListMaxSize; callback attribute DSTOffsetListMaxSize; - ram attribute supportsDNSResolve default = true; + callback attribute supportsDNSResolve default = true; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0x0B; + callback attribute featureMap default = 0x0B; callback attribute clusterRevision; handle command SetUTCTime; @@ -7986,7 +8036,7 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; handle command OpenCommissioningWindow; @@ -8051,8 +8101,8 @@ endpoint 0 { server cluster RelativeHumidityMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0; - ram attribute maxMeasuredValue default = 0x2710; + callback attribute minMeasuredValue default = 0; + callback attribute maxMeasuredValue default = 0x2710; callback attribute featureMap; callback attribute clusterRevision; } @@ -8065,7 +8115,7 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; handle command ProvisionRootCertificate; @@ -8085,7 +8135,7 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; handle command ProvisionEndpoint; @@ -8124,12 +8174,12 @@ endpoint 1 { } server cluster Groups { - ram attribute nameSupport; + callback attribute nameSupport; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; - ram attribute clusterRevision default = 0x0005; + callback attribute featureMap default = 0; + callback attribute clusterRevision; handle command AddGroup; handle command ViewGroup; @@ -8210,7 +8260,7 @@ endpoint 1 { callback attribute actionList; callback attribute endpointLists; callback attribute setupURL default = "https://example.com"; - callback attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; handle command InstantAction; @@ -8245,9 +8295,9 @@ endpoint 1 { server cluster Switch { emits event SwitchLatched; - ram attribute numberOfPositions default = 2; + callback attribute numberOfPositions default = 2; callback attribute currentPosition; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; } @@ -8281,7 +8331,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; handle command Stop; @@ -8294,8 +8344,8 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - callback attribute featureMap; - ram attribute clusterRevision default = 0x0003; + callback attribute featureMap default = 0; + callback attribute clusterRevision; handle command ChangeToMode; } @@ -8311,8 +8361,8 @@ endpoint 1 { } server cluster ModeSelect { - ram attribute description default = "Coffee"; - ram attribute standardNamespace default = 0; + callback attribute description default = "Coffee"; + callback attribute standardNamespace default = 0; callback attribute supportedModes; callback attribute currentMode; callback attribute startUpMode; @@ -8320,7 +8370,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; handle command ChangeToMode; @@ -8333,7 +8383,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0004; + callback attribute clusterRevision; handle command ChangeToMode; } @@ -8345,7 +8395,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0004; + callback attribute clusterRevision; handle command ChangeToMode; } @@ -8358,7 +8408,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 3; + callback attribute featureMap default = 3; callback attribute clusterRevision; } @@ -8369,7 +8419,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0005; + callback attribute clusterRevision; handle command ChangeToMode; } @@ -8381,18 +8431,18 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; handle command ChangeToMode; } server cluster TemperatureControl { - ram attribute selectedTemperatureLevel default = 0; + callback attribute selectedTemperatureLevel default = 0; callback attribute supportedTemperatureLevels; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 2; + callback attribute featureMap default = 2; callback attribute clusterRevision; handle command SetTemperature; @@ -8406,7 +8456,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - callback attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; } @@ -8417,7 +8467,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0004; + callback attribute clusterRevision; handle command ChangeToMode; } @@ -8486,7 +8536,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0003; + callback attribute clusterRevision; } server cluster OperationalState { @@ -8501,7 +8551,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; handle command Pause; @@ -8522,7 +8572,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; handle command Pause; @@ -8531,12 +8581,12 @@ endpoint 1 { } server cluster ScenesManagement { - ram attribute sceneTableSize default = 16; + callback attribute sceneTableSize default = 16; callback attribute fabricSceneInfo; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; handle command AddScene; @@ -8552,13 +8602,13 @@ endpoint 1 { server cluster ThermostatMode { callback attribute supportedModes; callback attribute currentMode; - ram attribute startUpMode; + callback attribute startUpMode; callback attribute coreModeTags; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap default = 2; - ram attribute clusterRevision default = 1; + callback attribute clusterRevision; handle command ChangeToMode; handle command ChangeToModeByCoreTag; @@ -8596,21 +8646,37 @@ endpoint 1 { handle command ResetCondition; } + server cluster WaterTankLevelMonitoring { + callback attribute condition; + callback attribute degradationDirection; + callback attribute changeIndication; + callback attribute inPlaceIndicator; + callback attribute lastChangedTime; + callback attribute replacementProductList; + callback attribute generatedCommandList; + callback attribute acceptedCommandList; + callback attribute attributeList; + callback attribute featureMap; + callback attribute clusterRevision; + + handle command ResetCondition; + } + server cluster BooleanStateConfiguration { emits event AlarmsStateChanged; emits event SensorFault; callback attribute currentSensitivityLevel; - ram attribute supportedSensitivityLevels default = 3; - ram attribute defaultSensitivityLevel default = 2; + callback attribute supportedSensitivityLevels default = 3; + callback attribute defaultSensitivityLevel default = 2; callback attribute alarmsActive; callback attribute alarmsSuppressed; callback attribute alarmsEnabled; - ram attribute alarmsSupported default = 0x03; + callback attribute alarmsSupported default = 0x03; callback attribute sensorFault; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0x0F; + callback attribute featureMap default = 0x0F; callback attribute clusterRevision; handle command SuppressAlarm; @@ -8650,37 +8716,37 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 3; + callback attribute featureMap default = 3; callback attribute clusterRevision; } server cluster WindowCovering { - ram attribute type default = 0x08; - ram attribute physicalClosedLimitLift default = 0xFFFF; - ram attribute physicalClosedLimitTilt default = 0xFFFF; - persist attribute currentPositionLift default = 0x7FFF; - persist attribute currentPositionTilt default = 0x7FFF; - persist attribute numberOfActuationsLift default = 0x0000; - persist attribute numberOfActuationsTilt default = 0x0000; - persist attribute configStatus default = 0x03; - persist attribute currentPositionLiftPercentage default = 50; - persist attribute currentPositionTiltPercentage default = 50; + callback attribute type default = 0x08; + callback attribute physicalClosedLimitLift; + callback attribute physicalClosedLimitTilt; + callback attribute currentPositionLift; + callback attribute currentPositionTilt; + callback attribute numberOfActuationsLift; + callback attribute numberOfActuationsTilt; + callback attribute configStatus default = 0x03; + callback attribute currentPositionLiftPercentage; + callback attribute currentPositionTiltPercentage; callback attribute operationalStatus; - ram attribute targetPositionLiftPercent100ths default = 5000; - ram attribute targetPositionTiltPercent100ths default = 5000; - ram attribute endProductType default = 0x00; - persist attribute currentPositionLiftPercent100ths default = 5000; - persist attribute currentPositionTiltPercent100ths default = 5000; - persist attribute installedOpenLimitLift default = 0x0000; - persist attribute installedClosedLimitLift default = 0xFFFF; - persist attribute installedOpenLimitTilt default = 0x0000; - persist attribute installedClosedLimitTilt default = 0xFFFF; - persist attribute mode default = 0x00; + callback attribute targetPositionLiftPercent100ths default = 5000; + callback attribute targetPositionTiltPercent100ths default = 5000; + callback attribute endProductType default = 0x00; + callback attribute currentPositionLiftPercent100ths default = 5000; + callback attribute currentPositionTiltPercent100ths default = 5000; + callback attribute installedOpenLimitLift; + callback attribute installedClosedLimitLift; + callback attribute installedOpenLimitTilt; + callback attribute installedClosedLimitTilt; + callback attribute mode default = 0x00; callback attribute safetyStatus; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0x17; + callback attribute featureMap default = 0x17; callback attribute clusterRevision; handle command UpOrOpen; @@ -8727,33 +8793,33 @@ endpoint 1 { emits event RunningStateChange; emits event RunningModeChange; emits event ActivePresetChange; - ram attribute localTemperature; - ram attribute absMinHeatSetpointLimit default = 0x02BC; - ram attribute absMaxHeatSetpointLimit default = 0x0BB8; - ram attribute absMinCoolSetpointLimit default = 0x0640; - ram attribute absMaxCoolSetpointLimit default = 0x0C80; - ram attribute localTemperatureCalibration default = 0x00; - ram attribute occupiedCoolingSetpoint default = 0x0A28; - ram attribute occupiedHeatingSetpoint default = 0x07D0; - ram attribute minHeatSetpointLimit default = 0x02BC; - ram attribute maxHeatSetpointLimit default = 0x0BB8; - ram attribute minCoolSetpointLimit default = 0x0640; - ram attribute maxCoolSetpointLimit default = 0x0C80; - ram attribute minSetpointDeadBand default = 0x14; - ram attribute controlSequenceOfOperation default = 0x04; - ram attribute systemMode default = 0x01; - ram attribute temperatureSetpointHold default = 0; - ram attribute temperatureSetpointHoldDuration; + callback attribute localTemperature; + callback attribute absMinHeatSetpointLimit default = 0x02BC; + callback attribute absMaxHeatSetpointLimit default = 0x0BB8; + callback attribute absMinCoolSetpointLimit default = 0x0640; + callback attribute absMaxCoolSetpointLimit default = 0x0C80; + callback attribute localTemperatureCalibration default = 0x00; + callback attribute occupiedCoolingSetpoint default = 0x0A28; + callback attribute occupiedHeatingSetpoint default = 0x07D0; + callback attribute minHeatSetpointLimit default = 0x02BC; + callback attribute maxHeatSetpointLimit default = 0x0BB8; + callback attribute minCoolSetpointLimit default = 0x0640; + callback attribute maxCoolSetpointLimit default = 0x0C80; + callback attribute minSetpointDeadBand default = 0x14; + callback attribute controlSequenceOfOperation default = 0x04; + callback attribute systemMode default = 0x01; + callback attribute temperatureSetpointHold default = 0; + callback attribute temperatureSetpointHoldDuration; callback attribute presetTypes; callback attribute scheduleTypes; - ram attribute numberOfPresets default = 0; - ram attribute numberOfSchedules default = 0; - ram attribute numberOfScheduleTransitionPerDay default = 0xFF; - ram attribute activePresetHandle; - ram attribute activeScheduleHandle; + callback attribute numberOfPresets; + callback attribute numberOfSchedules; + callback attribute numberOfScheduleTransitionPerDay; + callback attribute activePresetHandle; + callback attribute activeScheduleHandle; callback attribute presets; callback attribute schedules; - ram attribute setpointHoldExpiryTimestamp; + callback attribute setpointHoldExpiryTimestamp; callback attribute maxThermostatSuggestions; callback attribute thermostatSuggestions; callback attribute currentThermostatSuggestion; @@ -8768,7 +8834,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0x0F23; + callback attribute featureMap default = 0x0F23; callback attribute clusterRevision; handle command SetpointRaiseLower; @@ -8781,51 +8847,51 @@ endpoint 1 { server cluster FanControl { callback attribute fanMode; - ram attribute fanModeSequence default = 0x02; + callback attribute fanModeSequence default = 0x02; callback attribute percentSetting; callback attribute percentCurrent; - ram attribute speedMax default = 100; + callback attribute speedMax default = 100; callback attribute speedSetting; callback attribute speedCurrent; - ram attribute rockSupport default = 0x03; + callback attribute rockSupport default = 0x03; callback attribute rockSetting; - ram attribute windSupport default = 0x03; + callback attribute windSupport default = 0x03; callback attribute windSetting; callback attribute airflowDirection; - ram attribute featureMap default = 0x3F; + callback attribute featureMap default = 0x3F; callback attribute clusterRevision; handle command Step; } server cluster ThermostatUserInterfaceConfiguration { - ram attribute temperatureDisplayMode default = 0x00; - ram attribute keypadLockout default = 0x00; - ram attribute scheduleProgrammingVisibility; + callback attribute temperatureDisplayMode default = 0x00; + callback attribute keypadLockout default = 0x00; + callback attribute scheduleProgrammingVisibility; callback attribute featureMap; callback attribute clusterRevision; } server cluster Humidistat { callback attribute supportedModes; - ram attribute mode default = 0; - ram attribute systemState default = 3; - ram attribute userSetpoint default = 40; - ram attribute minSetpoint default = 30; - ram attribute maxSetpoint default = 60; - ram attribute step default = 5; - ram attribute targetSetpoint default = 40; - ram attribute mistType default = 1; - ram attribute continuous default = 0; - ram attribute sleep default = 0; - ram attribute optimal default = 0; - ram attribute condPumpEnabled default = 0; - ram attribute condRunCount default = 0; + callback attribute mode default = 0; + callback attribute systemState default = 3; + callback attribute userSetpoint default = 40; + callback attribute minSetpoint default = 30; + callback attribute maxSetpoint default = 60; + callback attribute step default = 5; + callback attribute targetSetpoint default = 40; + callback attribute mistType default = 1; + callback attribute continuous default = 0; + callback attribute sleep default = 0; + callback attribute optimal default = 0; + callback attribute condPumpEnabled; + callback attribute condRunCount; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1023; - callback attribute clusterRevision default = 1; + callback attribute featureMap default = 1023; + callback attribute clusterRevision; handle command SetSettings; } @@ -8945,53 +9011,53 @@ endpoint 1 { server cluster IlluminanceMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0x01; - ram attribute maxMeasuredValue default = 0xFFFE; - ram attribute tolerance; - ram attribute lightSensorType default = 0xFF; + callback attribute minMeasuredValue default = 0x01; + callback attribute maxMeasuredValue default = 0xFFFE; + callback attribute tolerance; + callback attribute lightSensorType default = 0xFF; callback attribute featureMap; callback attribute clusterRevision; } server cluster TemperatureMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0x8000; - ram attribute maxMeasuredValue default = 0x8000; - ram attribute tolerance; + callback attribute minMeasuredValue default = 0x8000; + callback attribute maxMeasuredValue default = 0x8000; + callback attribute tolerance; callback attribute featureMap; callback attribute clusterRevision; } server cluster PressureMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0; - ram attribute maxMeasuredValue default = 32767; + callback attribute minMeasuredValue default = 0; + callback attribute maxMeasuredValue default = 32767; callback attribute featureMap default = 0; - callback attribute clusterRevision default = 0x0004; + callback attribute clusterRevision; } server cluster FlowMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0; - ram attribute maxMeasuredValue default = 100; - ram attribute tolerance default = 0; + callback attribute minMeasuredValue default = 0; + callback attribute maxMeasuredValue default = 100; + callback attribute tolerance default = 0; callback attribute featureMap; callback attribute clusterRevision; } server cluster RelativeHumidityMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0; - ram attribute maxMeasuredValue default = 0x2710; - ram attribute tolerance; + callback attribute minMeasuredValue default = 0; + callback attribute maxMeasuredValue default = 0x2710; + callback attribute tolerance; callback attribute featureMap; callback attribute clusterRevision; } server cluster OccupancySensing { - ram attribute occupancy; - ram attribute occupancySensorType; - ram attribute occupancySensorTypeBitmap default = 1; + callback attribute occupancy; + callback attribute occupancySensorType; + callback attribute occupancySensorTypeBitmap; callback attribute holdTime; callback attribute holdTimeLimits; callback attribute PIROccupiedToUnoccupiedDelay; @@ -9015,7 +9081,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster CarbonDioxideConcentrationMeasurement { @@ -9034,7 +9100,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster NitrogenDioxideConcentrationMeasurement { @@ -9053,7 +9119,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster OzoneConcentrationMeasurement { @@ -9072,7 +9138,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm25ConcentrationMeasurement { @@ -9091,7 +9157,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster FormaldehydeConcentrationMeasurement { @@ -9110,7 +9176,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm1ConcentrationMeasurement { @@ -9129,7 +9195,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm10ConcentrationMeasurement { @@ -9148,7 +9214,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster TotalVolatileOrganicCompoundsConcentrationMeasurement { @@ -9167,7 +9233,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster RadonConcentrationMeasurement { @@ -9186,7 +9252,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster SoilMeasurement { @@ -9215,7 +9281,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0004; + callback attribute clusterRevision; } server cluster WakeOnLan { @@ -9254,7 +9320,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - callback attribute featureMap; + callback attribute featureMap default = 0x02; callback attribute clusterRevision; handle command Mute; @@ -9272,8 +9338,8 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; - ram attribute clusterRevision default = 1; + callback attribute featureMap; + callback attribute clusterRevision; handle command PlayChimeSound; } @@ -9427,12 +9493,12 @@ endpoint 2 { } server cluster Groups { - ram attribute nameSupport; + callback attribute nameSupport; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; - ram attribute clusterRevision default = 0x0005; + callback attribute featureMap default = 0; + callback attribute clusterRevision; handle command AddGroup; handle command ViewGroup; @@ -9491,12 +9557,12 @@ endpoint 2 { } server cluster ScenesManagement { - ram attribute sceneTableSize default = 16; + callback attribute sceneTableSize default = 16; callback attribute fabricSceneInfo; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; handle command AddScene; @@ -9510,9 +9576,9 @@ endpoint 2 { } server cluster OccupancySensing { - ram attribute occupancy; - ram attribute occupancySensorType; - ram attribute occupancySensorTypeBitmap default = 1; + callback attribute occupancy; + callback attribute occupancySensorType; + callback attribute occupancySensorTypeBitmap; callback attribute holdTime; callback attribute holdTimeLimits; callback attribute PIROccupiedToUnoccupiedDelay; @@ -9555,10 +9621,10 @@ endpoint 3 { emits event LongPress; emits event LongRelease; emits event MultiPressComplete; - ram attribute numberOfPositions default = 2; + callback attribute numberOfPositions default = 2; callback attribute currentPosition; - ram attribute multiPressMax default = 3; - ram attribute featureMap default = 58; + callback attribute multiPressMax default = 3; + callback attribute featureMap default = 58; callback attribute clusterRevision; } } @@ -9590,10 +9656,10 @@ endpoint 4 { } server cluster Switch { - ram attribute numberOfPositions default = 2; + callback attribute numberOfPositions default = 2; callback attribute currentPosition; - ram attribute multiPressMax default = 3; - ram attribute featureMap default = 30; + callback attribute multiPressMax default = 3; + callback attribute featureMap default = 30; callback attribute clusterRevision; } } diff --git a/examples/all-clusters-app/all-clusters-common/all-clusters-app.zap b/examples/all-clusters-app/all-clusters-common/all-clusters-app.zap index 9de789a0dbc..be68fbb9abd 100644 --- a/examples/all-clusters-app/all-clusters-common/all-clusters-app.zap +++ b/examples/all-clusters-app/all-clusters-common/all-clusters-app.zap @@ -11938,6 +11938,202 @@ } ] }, + { + "name": "Water Tank Level Monitoring", + "code": 121, + "mfgCode": null, + "define": "WATER_TANK_LEVEL_MONITORING_CLUSTER", + "side": "server", + "enabled": 1, + "commands": [ + { + "name": "ResetCondition", + "code": 0, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + } + ], + "attributes": [ + { + "name": "Condition", + "code": 0, + "mfgCode": null, + "side": "server", + "type": "percent", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "DegradationDirection", + "code": 1, + "mfgCode": null, + "side": "server", + "type": "DegradationDirectionEnum", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "ChangeIndication", + "code": 2, + "mfgCode": null, + "side": "server", + "type": "ChangeIndicationEnum", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "InPlaceIndicator", + "code": 3, + "mfgCode": null, + "side": "server", + "type": "boolean", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "LastChangedTime", + "code": 4, + "mfgCode": null, + "side": "server", + "type": "epoch_s", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "ReplacementProductList", + "code": 5, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "GeneratedCommandList", + "code": 65528, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "AcceptedCommandList", + "code": 65529, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "AttributeList", + "code": 65531, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "FeatureMap", + "code": 65532, + "mfgCode": null, + "side": "server", + "type": "bitmap32", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "ClusterRevision", + "code": 65533, + "mfgCode": null, + "side": "server", + "type": "int16u", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + } + ] + }, { "name": "Boolean State Configuration", "code": 128, diff --git a/examples/all-clusters-app/all-clusters-common/include/resource-monitoring-delegates.h b/examples/all-clusters-app/all-clusters-common/include/resource-monitoring-delegates.h index ea7aecd59b7..2d914e889a1 100644 --- a/examples/all-clusters-app/all-clusters-common/include/resource-monitoring-delegates.h +++ b/examples/all-clusters-app/all-clusters-common/include/resource-monitoring-delegates.h @@ -66,6 +66,24 @@ void Shutdown(); } // namespace HepaFilterMonitoring +namespace WaterTankLevelMonitoring { +/// This is an application level Delegate to handle WaterTankLevelMonitoringDelegate commands according to the specific business +/// logic. +class WaterTankLevelMonitoringDelegate : public ResourceMonitoring::Delegate +{ +private: + CHIP_ERROR Init() override; + chip::Protocols::InteractionModel::Status PreResetCondition() override; + chip::Protocols::InteractionModel::Status PostResetCondition() override; + +public: + ~WaterTankLevelMonitoringDelegate() override = default; +}; + +void Shutdown(); + +} // namespace WaterTankLevelMonitoring + } // namespace Clusters } // namespace app } // namespace chip diff --git a/examples/all-clusters-app/all-clusters-common/src/resource-monitoring-delegates.cpp b/examples/all-clusters-app/all-clusters-common/src/resource-monitoring-delegates.cpp index de5e4306b63..87f79cf8505 100644 --- a/examples/all-clusters-app/all-clusters-common/src/resource-monitoring-delegates.cpp +++ b/examples/all-clusters-app/all-clusters-common/src/resource-monitoring-delegates.cpp @@ -25,6 +25,7 @@ using namespace chip::app::Clusters; using namespace chip::app::Clusters::ResourceMonitoring; using namespace chip::app::Clusters::ActivatedCarbonFilterMonitoring; using namespace chip::app::Clusters::HepaFilterMonitoring; +using namespace chip::app::Clusters::WaterTankLevelMonitoring; using chip::Protocols::InteractionModel::Status; constexpr std::bitset<4> gHepaFilterFeatureMap{ static_cast(ResourceMonitoring::Feature::kCondition) | @@ -33,6 +34,9 @@ constexpr std::bitset<4> gHepaFilterFeatureMap{ static_cast(ResourceMo constexpr std::bitset<4> gActivatedCarbonFeatureMap{ static_cast(ResourceMonitoring::Feature::kCondition) | static_cast(ResourceMonitoring::Feature::kWarning) | static_cast(ResourceMonitoring::Feature::kReplacementProductList) }; +constexpr std::bitset<4> gWaterTankLevelFeatureMap{ static_cast(ResourceMonitoring::Feature::kCondition) | + static_cast(ResourceMonitoring::Feature::kWarning) | + static_cast(ResourceMonitoring::Feature::kReplacementProductList) }; static ActivatedCarbonFilterMonitoringDelegate * gActivatedCarbonFilterDelegate = nullptr; static ResourceMonitoring::Instance * gActivatedCarbonFilterInstance = nullptr; @@ -40,6 +44,9 @@ static ResourceMonitoring::Instance * gActivatedCarbonFilterInstance static HepaFilterMonitoringDelegate * gHepaFilterDelegate = nullptr; static ResourceMonitoring::Instance * gHepaFilterInstance = nullptr; +static WaterTankLevelMonitoringDelegate * gWaterTankLevelDelegate = nullptr; +static ResourceMonitoring::Instance * gWaterTankLevelInstance = nullptr; + static ImmutableReplacementProductListManager sReplacementProductListManager; //-- Activated Carbon Filter Monitoring delegate methods @@ -110,6 +117,40 @@ void HepaFilterMonitoring::Shutdown() } } +//-- Water Tank Level Monitoring delegate methods +CHIP_ERROR WaterTankLevelMonitoringDelegate::Init() +{ + ChipLogDetail(Zcl, "WaterTankLevelMonitoringDelegate::Init()"); + GetInstance()->SetReplacementProductListManagerInstance(&sReplacementProductListManager); + return CHIP_NO_ERROR; +} + +Status WaterTankLevelMonitoringDelegate::PreResetCondition() +{ + ChipLogDetail(Zcl, "WaterTankLevelMonitoringDelegate::PreResetCondition()"); + return Status::Success; +} + +Status WaterTankLevelMonitoringDelegate::PostResetCondition() +{ + ChipLogDetail(Zcl, "WaterTankLevelMonitoringDelegate::PostResetCondition()"); + return Status::Success; +} + +void WaterTankLevelMonitoring::Shutdown() +{ + if (gWaterTankLevelInstance != nullptr) + { + delete gWaterTankLevelInstance; + gWaterTankLevelInstance = nullptr; + } + if (gWaterTankLevelDelegate != nullptr) + { + delete gWaterTankLevelDelegate; + gWaterTankLevelDelegate = nullptr; + } +} + void emberAfActivatedCarbonFilterMonitoringClusterInitCallback(chip::EndpointId endpoint) { VerifyOrDie(gActivatedCarbonFilterInstance == nullptr && gActivatedCarbonFilterDelegate == nullptr); @@ -131,6 +172,17 @@ void emberAfHepaFilterMonitoringClusterInitCallback(chip::EndpointId endpoint) TEMPORARY_RETURN_IGNORED gHepaFilterInstance->Init(); } +void emberAfWaterTankLevelMonitoringClusterInitCallback(chip::EndpointId endpoint) +{ + VerifyOrDie(gWaterTankLevelInstance == nullptr && gWaterTankLevelDelegate == nullptr); + + gWaterTankLevelDelegate = new WaterTankLevelMonitoringDelegate; + gWaterTankLevelInstance = new ResourceMonitoring::Instance(gWaterTankLevelDelegate, endpoint, WaterTankLevelMonitoring::Id, + static_cast(gWaterTankLevelFeatureMap.to_ulong()), + ResourceMonitoring::DegradationDirectionEnum::kDown, true); + TEMPORARY_RETURN_IGNORED gWaterTankLevelInstance->Init(); +} + void emberAfActivatedCarbonFilterMonitoringClusterShutdownCallback(chip::EndpointId endpoint) { ActivatedCarbonFilterMonitoring::Shutdown(); @@ -141,6 +193,11 @@ void emberAfHepaFilterMonitoringClusterShutdownCallback(chip::EndpointId endpoin HepaFilterMonitoring::Shutdown(); } +void emberAfWaterTankLevelMonitoringClusterShutdownCallback(chip::EndpointId endpoint) +{ + WaterTankLevelMonitoring::Shutdown(); +} + CHIP_ERROR ImmutableReplacementProductListManager::Next(ReplacementProductStruct & item) { if (mIndex >= kReplacementProductListMaxSize) diff --git a/examples/all-clusters-app/realtek/data_model/all-clusters-app.matter b/examples/all-clusters-app/realtek/data_model/all-clusters-app.matter index 918d28461f3..d49fe5b9b3b 100644 --- a/examples/all-clusters-app/realtek/data_model/all-clusters-app.matter +++ b/examples/all-clusters-app/realtek/data_model/all-clusters-app.matter @@ -8053,9 +8053,9 @@ endpoint 0 { binding cluster OtaSoftwareUpdateProvider; server cluster Groups { - ram attribute nameSupport; - ram attribute featureMap default = 0; - ram attribute clusterRevision default = 0x0005; + callback attribute nameSupport; + callback attribute featureMap default = 0; + callback attribute clusterRevision; handle command AddGroup; handle command ViewGroup; @@ -8153,18 +8153,18 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - callback attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; } server cluster TimeFormatLocalization { - persist attribute hourFormat default = 0; - persist attribute activeCalendarType default = 0; + callback attribute hourFormat default = 0; + callback attribute activeCalendarType default = 0; callback attribute supportedCalendarTypes; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; } @@ -8173,7 +8173,7 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0x1; + callback attribute featureMap default = 0x1; callback attribute clusterRevision; } @@ -8350,7 +8350,7 @@ endpoint 0 { callback attribute channelPage0Mask; callback attribute operationalDatasetComponents; callback attribute activeNetworkFaultsList; - ram attribute featureMap default = 0x000F; + callback attribute featureMap default = 0x000F; callback attribute clusterRevision; handle command ResetCounts; @@ -8373,8 +8373,8 @@ endpoint 0 { callback attribute packetUnicastTxCount; callback attribute currentMaxRate; callback attribute overrunCount; - ram attribute featureMap default = 3; - callback attribute clusterRevision default = 1; + callback attribute featureMap default = 3; + callback attribute clusterRevision; handle command ResetCounts; } @@ -8389,7 +8389,7 @@ endpoint 0 { callback attribute overrunCount; callback attribute carrierDetect; callback attribute timeSinceReset; - ram attribute featureMap default = 3; + callback attribute featureMap default = 3; callback attribute clusterRevision; handle command ResetCounts; @@ -8403,20 +8403,20 @@ endpoint 0 { emits event MissingTrustedTimeSource; callback attribute UTCTime; callback attribute granularity; - ram attribute timeSource default = 0x00; + callback attribute timeSource default = 0x00; callback attribute trustedTimeSource; callback attribute defaultNTP; callback attribute timeZone; callback attribute DSTOffset; callback attribute localTime; - ram attribute timeZoneDatabase default = 0; + callback attribute timeZoneDatabase default = 0; callback attribute timeZoneListMaxSize; callback attribute DSTOffsetListMaxSize; - ram attribute supportsDNSResolve default = true; + callback attribute supportsDNSResolve default = true; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0x0B; + callback attribute featureMap default = 0x0B; callback attribute clusterRevision; handle command SetUTCTime; @@ -8433,7 +8433,7 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap default = 0; callback attribute clusterRevision; handle command OpenCommissioningWindow; @@ -8451,7 +8451,7 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - callback attribute featureMap; + callback attribute featureMap default = 0; callback attribute clusterRevision; handle command AttestationRequest; @@ -8499,8 +8499,8 @@ endpoint 0 { callback attribute idleModeDuration; callback attribute activeModeDuration; callback attribute activeModeThreshold; - ram attribute userActiveModeTriggerHint; - ram attribute userActiveModeTriggerInstruction; + callback attribute userActiveModeTriggerHint; + callback attribute userActiveModeTriggerInstruction; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; @@ -8510,8 +8510,8 @@ endpoint 0 { server cluster RelativeHumidityMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0; - ram attribute maxMeasuredValue default = 0x2710; + callback attribute minMeasuredValue default = 0; + callback attribute maxMeasuredValue default = 0x2710; callback attribute featureMap; callback attribute clusterRevision; } @@ -8547,12 +8547,12 @@ endpoint 1 { } server cluster Groups { - ram attribute nameSupport; + callback attribute nameSupport; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; - ram attribute clusterRevision default = 0x0005; + callback attribute featureMap default = 0; + callback attribute clusterRevision; handle command AddGroup; handle command ViewGroup; @@ -8633,7 +8633,7 @@ endpoint 1 { callback attribute actionList; callback attribute endpointLists; callback attribute setupURL; - callback attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; } @@ -8655,9 +8655,9 @@ endpoint 1 { server cluster Switch { emits event SwitchLatched; - ram attribute numberOfPositions default = 2; + callback attribute numberOfPositions default = 2; callback attribute currentPosition; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; } @@ -8691,7 +8691,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; handle command Stop; @@ -8704,8 +8704,8 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - callback attribute featureMap; - ram attribute clusterRevision default = 0x0003; + callback attribute featureMap default = 0; + callback attribute clusterRevision; handle command ChangeToMode; } @@ -8721,8 +8721,8 @@ endpoint 1 { } server cluster ModeSelect { - ram attribute description default = "Coffee"; - ram attribute standardNamespace default = 0; + callback attribute description default = "Coffee"; + callback attribute standardNamespace default = 0; callback attribute supportedModes; callback attribute currentMode; callback attribute startUpMode; @@ -8730,7 +8730,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; handle command ChangeToMode; @@ -8743,7 +8743,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0004; + callback attribute clusterRevision; handle command ChangeToMode; } @@ -8755,7 +8755,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0004; + callback attribute clusterRevision; handle command ChangeToMode; } @@ -8768,7 +8768,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 3; + callback attribute featureMap default = 3; callback attribute clusterRevision; } @@ -8779,7 +8779,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0005; + callback attribute clusterRevision; handle command ChangeToMode; } @@ -8791,18 +8791,18 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; handle command ChangeToMode; } server cluster TemperatureControl { - ram attribute selectedTemperatureLevel default = 0; + callback attribute selectedTemperatureLevel default = 0; callback attribute supportedTemperatureLevels; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 2; + callback attribute featureMap default = 2; callback attribute clusterRevision; handle command SetTemperature; @@ -8816,7 +8816,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - callback attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; } @@ -8827,7 +8827,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0004; + callback attribute clusterRevision; handle command ChangeToMode; } @@ -8896,7 +8896,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0003; + callback attribute clusterRevision; } server cluster OperationalState { @@ -8911,7 +8911,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; handle command Pause; @@ -8932,7 +8932,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; handle command Pause; @@ -8941,12 +8941,12 @@ endpoint 1 { } server cluster ScenesManagement { - ram attribute sceneTableSize default = 16; + callback attribute sceneTableSize default = 16; callback attribute fabricSceneInfo; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; handle command AddScene; @@ -8995,17 +8995,17 @@ endpoint 1 { emits event AlarmsStateChanged; emits event SensorFault; callback attribute currentSensitivityLevel; - ram attribute supportedSensitivityLevels default = 3; - ram attribute defaultSensitivityLevel default = 2; + callback attribute supportedSensitivityLevels default = 3; + callback attribute defaultSensitivityLevel default = 2; callback attribute alarmsActive; callback attribute alarmsSuppressed; callback attribute alarmsEnabled; - ram attribute alarmsSupported default = 0x03; + callback attribute alarmsSupported default = 0x03; callback attribute sensorFault; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0x0F; + callback attribute featureMap default = 0x0F; callback attribute clusterRevision; handle command SuppressAlarm; @@ -9061,7 +9061,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 1; + callback attribute clusterRevision; } server cluster ElectricalEnergyMeasurement { @@ -9077,7 +9077,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 1; + callback attribute clusterRevision; } server cluster WaterHeaterManagement { @@ -9114,7 +9114,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0005; + callback attribute clusterRevision; handle command PowerAdjustRequest; handle command CancelPowerAdjustRequest; @@ -9154,7 +9154,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 2; + callback attribute clusterRevision; handle command Disable; handle command EnableCharging; @@ -9174,7 +9174,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 3; + callback attribute featureMap default = 3; callback attribute clusterRevision; } @@ -9185,7 +9185,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0002; + callback attribute clusterRevision; } server cluster EnergyEvseMode { @@ -9195,7 +9195,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0003; + callback attribute clusterRevision; handle command ChangeToMode; } @@ -9207,7 +9207,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0002; + callback attribute clusterRevision; handle command ChangeToMode; } @@ -9219,38 +9219,38 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0003; + callback attribute clusterRevision; handle command ChangeToMode; } server cluster WindowCovering { - ram attribute type default = 0x08; - ram attribute physicalClosedLimitLift default = 0xFFFF; - ram attribute physicalClosedLimitTilt default = 0xFFFF; - persist attribute currentPositionLift default = 0x7FFF; - persist attribute currentPositionTilt default = 0x7FFF; - persist attribute numberOfActuationsLift default = 0x0000; - persist attribute numberOfActuationsTilt default = 0x0000; - persist attribute configStatus default = 0x03; - persist attribute currentPositionLiftPercentage default = 50; - persist attribute currentPositionTiltPercentage default = 50; + callback attribute type default = 0x08; + callback attribute physicalClosedLimitLift; + callback attribute physicalClosedLimitTilt; + callback attribute currentPositionLift; + callback attribute currentPositionTilt; + callback attribute numberOfActuationsLift; + callback attribute numberOfActuationsTilt; + callback attribute configStatus default = 0x03; + callback attribute currentPositionLiftPercentage; + callback attribute currentPositionTiltPercentage; callback attribute operationalStatus; - ram attribute targetPositionLiftPercent100ths default = 5000; - ram attribute targetPositionTiltPercent100ths default = 5000; - ram attribute endProductType default = 0x00; - persist attribute currentPositionLiftPercent100ths default = 5000; - persist attribute currentPositionTiltPercent100ths default = 5000; - persist attribute installedOpenLimitLift default = 0x0000; - persist attribute installedClosedLimitLift default = 0xFFFF; - persist attribute installedOpenLimitTilt default = 0x0000; - persist attribute installedClosedLimitTilt default = 0xFFFF; - persist attribute mode default = 0x00; + callback attribute targetPositionLiftPercent100ths default = 5000; + callback attribute targetPositionTiltPercent100ths default = 5000; + callback attribute endProductType default = 0x00; + callback attribute currentPositionLiftPercent100ths default = 5000; + callback attribute currentPositionTiltPercent100ths default = 5000; + callback attribute installedOpenLimitLift; + callback attribute installedClosedLimitLift; + callback attribute installedOpenLimitTilt; + callback attribute installedClosedLimitTilt; + callback attribute mode default = 0x00; callback attribute safetyStatus; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0x17; + callback attribute featureMap default = 0x17; callback attribute clusterRevision; handle command UpOrOpen; @@ -9291,35 +9291,35 @@ endpoint 1 { } server cluster Thermostat { - ram attribute localTemperature; - ram attribute absMinHeatSetpointLimit default = 0x02BC; - ram attribute absMaxHeatSetpointLimit default = 0x0BB8; - ram attribute absMinCoolSetpointLimit default = 0x0640; - ram attribute absMaxCoolSetpointLimit default = 0x0C80; - ram attribute localTemperatureCalibration default = 0x00; - ram attribute occupiedCoolingSetpoint default = 0x0A28; - ram attribute occupiedHeatingSetpoint default = 0x07D0; - ram attribute minHeatSetpointLimit default = 0x02BC; - ram attribute maxHeatSetpointLimit default = 0x0BB8; - ram attribute minCoolSetpointLimit default = 0x0640; - ram attribute maxCoolSetpointLimit default = 0x0C80; - ram attribute minSetpointDeadBand default = 0x14; - ram attribute controlSequenceOfOperation default = 0x04; - ram attribute systemMode default = 0x01; + callback attribute localTemperature; + callback attribute absMinHeatSetpointLimit default = 0x02BC; + callback attribute absMaxHeatSetpointLimit default = 0x0BB8; + callback attribute absMinCoolSetpointLimit default = 0x0640; + callback attribute absMaxCoolSetpointLimit default = 0x0C80; + callback attribute localTemperatureCalibration default = 0x00; + callback attribute occupiedCoolingSetpoint default = 0x0A28; + callback attribute occupiedHeatingSetpoint default = 0x07D0; + callback attribute minHeatSetpointLimit default = 0x02BC; + callback attribute maxHeatSetpointLimit default = 0x0BB8; + callback attribute minCoolSetpointLimit default = 0x0640; + callback attribute maxCoolSetpointLimit default = 0x0C80; + callback attribute minSetpointDeadBand default = 0x14; + callback attribute controlSequenceOfOperation default = 0x04; + callback attribute systemMode default = 0x01; callback attribute presetTypes; callback attribute scheduleTypes; - ram attribute numberOfPresets default = 0; - ram attribute numberOfSchedules default = 0; - ram attribute numberOfScheduleTransitionPerDay default = 0xFF; - ram attribute activePresetHandle; - ram attribute activeScheduleHandle; + callback attribute numberOfPresets; + callback attribute numberOfSchedules; + callback attribute numberOfScheduleTransitionPerDay; + callback attribute activePresetHandle; + callback attribute activeScheduleHandle; callback attribute presets; callback attribute schedules; - ram attribute setpointHoldExpiryTimestamp; + callback attribute setpointHoldExpiryTimestamp; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0x0123; + callback attribute featureMap default = 0x0123; callback attribute clusterRevision; handle command SetpointRaiseLower; @@ -9330,27 +9330,27 @@ endpoint 1 { server cluster FanControl { callback attribute fanMode; - ram attribute fanModeSequence default = 0x02; + callback attribute fanModeSequence default = 0x02; callback attribute percentSetting; callback attribute percentCurrent; - ram attribute speedMax default = 100; + callback attribute speedMax default = 100; callback attribute speedSetting; callback attribute speedCurrent; - ram attribute rockSupport default = 0x03; + callback attribute rockSupport default = 0x03; callback attribute rockSetting; - ram attribute windSupport default = 0x03; + callback attribute windSupport default = 0x03; callback attribute windSetting; callback attribute airflowDirection; - ram attribute featureMap default = 0x3F; + callback attribute featureMap default = 0x3F; callback attribute clusterRevision; handle command Step; } server cluster ThermostatUserInterfaceConfiguration { - ram attribute temperatureDisplayMode default = 0x00; - ram attribute keypadLockout default = 0x00; - ram attribute scheduleProgrammingVisibility; + callback attribute temperatureDisplayMode default = 0x00; + callback attribute keypadLockout default = 0x00; + callback attribute scheduleProgrammingVisibility; callback attribute featureMap; callback attribute clusterRevision; } @@ -9456,53 +9456,53 @@ endpoint 1 { server cluster IlluminanceMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0x01; - ram attribute maxMeasuredValue default = 0xFFFE; - ram attribute tolerance; - ram attribute lightSensorType default = 0xFF; + callback attribute minMeasuredValue default = 0x01; + callback attribute maxMeasuredValue default = 0xFFFE; + callback attribute tolerance; + callback attribute lightSensorType default = 0xFF; callback attribute featureMap; callback attribute clusterRevision; } server cluster TemperatureMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0x8000; - ram attribute maxMeasuredValue default = 0x8000; - ram attribute tolerance; + callback attribute minMeasuredValue default = 0x8000; + callback attribute maxMeasuredValue default = 0x8000; + callback attribute tolerance; callback attribute featureMap; callback attribute clusterRevision; } server cluster PressureMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue; - ram attribute maxMeasuredValue; + callback attribute minMeasuredValue; + callback attribute maxMeasuredValue; callback attribute featureMap default = 0; - callback attribute clusterRevision default = 0x0004; + callback attribute clusterRevision; } server cluster FlowMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0; - ram attribute maxMeasuredValue default = 100; - ram attribute tolerance default = 0; + callback attribute minMeasuredValue default = 0; + callback attribute maxMeasuredValue default = 100; + callback attribute tolerance default = 0; callback attribute featureMap; callback attribute clusterRevision; } server cluster RelativeHumidityMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0; - ram attribute maxMeasuredValue default = 0x2710; - ram attribute tolerance; + callback attribute minMeasuredValue default = 0; + callback attribute maxMeasuredValue default = 0x2710; + callback attribute tolerance; callback attribute featureMap; callback attribute clusterRevision; } server cluster OccupancySensing { - ram attribute occupancy; - ram attribute occupancySensorType; - ram attribute occupancySensorTypeBitmap default = 1; + callback attribute occupancy; + callback attribute occupancySensorType; + callback attribute occupancySensorTypeBitmap; callback attribute holdTime; callback attribute holdTimeLimits; callback attribute PIROccupiedToUnoccupiedDelay; @@ -9526,7 +9526,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster CarbonDioxideConcentrationMeasurement { @@ -9545,7 +9545,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster NitrogenDioxideConcentrationMeasurement { @@ -9564,7 +9564,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster OzoneConcentrationMeasurement { @@ -9583,7 +9583,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm25ConcentrationMeasurement { @@ -9602,7 +9602,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster FormaldehydeConcentrationMeasurement { @@ -9621,7 +9621,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm1ConcentrationMeasurement { @@ -9640,7 +9640,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster Pm10ConcentrationMeasurement { @@ -9659,7 +9659,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster TotalVolatileOrganicCompoundsConcentrationMeasurement { @@ -9678,7 +9678,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster RadonConcentrationMeasurement { @@ -9697,7 +9697,7 @@ endpoint 1 { callback attribute acceptedCommandList; callback attribute attributeList; callback attribute featureMap; - ram attribute clusterRevision default = 0x0006; + callback attribute clusterRevision; } server cluster WakeOnLan { @@ -9852,12 +9852,12 @@ endpoint 2 { } server cluster Groups { - ram attribute nameSupport; + callback attribute nameSupport; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; - ram attribute clusterRevision default = 0x0005; + callback attribute featureMap default = 0; + callback attribute clusterRevision; handle command AddGroup; handle command ViewGroup; @@ -9916,12 +9916,12 @@ endpoint 2 { } server cluster ScenesManagement { - ram attribute sceneTableSize default = 16; + callback attribute sceneTableSize default = 16; callback attribute fabricSceneInfo; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; handle command AddScene; @@ -9935,9 +9935,9 @@ endpoint 2 { } server cluster OccupancySensing { - ram attribute occupancy; - ram attribute occupancySensorType; - ram attribute occupancySensorTypeBitmap default = 1; + callback attribute occupancy; + callback attribute occupancySensorType; + callback attribute occupancySensorTypeBitmap; callback attribute holdTime; callback attribute holdTimeLimits; callback attribute PIROccupiedToUnoccupiedDelay; @@ -9980,10 +9980,10 @@ endpoint 3 { emits event LongPress; emits event LongRelease; emits event MultiPressComplete; - ram attribute numberOfPositions default = 2; + callback attribute numberOfPositions default = 2; callback attribute currentPosition; - ram attribute multiPressMax default = 3; - ram attribute featureMap default = 58; + callback attribute multiPressMax default = 3; + callback attribute featureMap default = 58; callback attribute clusterRevision; } } @@ -10015,10 +10015,10 @@ endpoint 4 { } server cluster Switch { - ram attribute numberOfPositions default = 2; + callback attribute numberOfPositions default = 2; callback attribute currentPosition; - ram attribute multiPressMax default = 3; - ram attribute featureMap default = 30; + callback attribute multiPressMax default = 3; + callback attribute featureMap default = 30; callback attribute clusterRevision; } } diff --git a/examples/all-clusters-minimal-app/all-clusters-common/all-clusters-minimal-app.matter b/examples/all-clusters-minimal-app/all-clusters-common/all-clusters-minimal-app.matter index 63ee13b4194..cf8627c3c87 100644 --- a/examples/all-clusters-minimal-app/all-clusters-common/all-clusters-minimal-app.matter +++ b/examples/all-clusters-minimal-app/all-clusters-common/all-clusters-minimal-app.matter @@ -6661,9 +6661,9 @@ endpoint 0 { binding cluster OtaSoftwareUpdateProvider; server cluster Groups { - ram attribute nameSupport; - ram attribute featureMap default = 0; - ram attribute clusterRevision default = 0x0005; + callback attribute nameSupport; + callback attribute featureMap default = 0; + callback attribute clusterRevision; handle command AddGroup; handle command ViewGroup; @@ -6750,16 +6750,16 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - callback attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; } server cluster TimeFormatLocalization { - persist attribute hourFormat default = 0; + callback attribute hourFormat default = 0; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap default = 0; callback attribute clusterRevision; } @@ -6767,7 +6767,7 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap default = 0; callback attribute clusterRevision; } @@ -6873,7 +6873,7 @@ endpoint 0 { callback attribute channelPage0Mask; callback attribute operationalDatasetComponents; callback attribute activeNetworkFaultsList; - ram attribute featureMap default = 0; + callback attribute featureMap default = 0; callback attribute clusterRevision; } @@ -6883,8 +6883,8 @@ endpoint 0 { callback attribute wiFiVersion; callback attribute channelNumber; callback attribute rssi; - ram attribute featureMap default = 0; - callback attribute clusterRevision default = 1; + callback attribute featureMap default = 0; + callback attribute clusterRevision; } server cluster EthernetNetworkDiagnostics { @@ -6897,7 +6897,7 @@ endpoint 0 { callback attribute overrunCount; callback attribute carrierDetect; callback attribute timeSinceReset; - ram attribute featureMap default = 3; + callback attribute featureMap default = 3; callback attribute clusterRevision; handle command ResetCounts; @@ -6910,7 +6910,7 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap default = 0; callback attribute clusterRevision; handle command OpenCommissioningWindow; @@ -6927,7 +6927,7 @@ endpoint 0 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - callback attribute featureMap; + callback attribute featureMap default = 0; callback attribute clusterRevision; handle command AttestationRequest; @@ -6973,8 +6973,8 @@ endpoint 0 { server cluster RelativeHumidityMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0; - ram attribute maxMeasuredValue default = 0x2710; + callback attribute minMeasuredValue default = 0; + callback attribute maxMeasuredValue default = 0x2710; callback attribute featureMap; callback attribute clusterRevision; } @@ -6998,12 +6998,12 @@ endpoint 1 { } server cluster Groups { - ram attribute nameSupport; + callback attribute nameSupport; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; - ram attribute clusterRevision default = 0x0005; + callback attribute featureMap default = 0; + callback attribute clusterRevision; handle command AddGroup; handle command ViewGroup; @@ -7073,7 +7073,7 @@ endpoint 1 { server cluster Actions { callback attribute actionList; callback attribute endpointLists; - callback attribute featureMap default = 0; + callback attribute featureMap; callback attribute clusterRevision; } @@ -7090,9 +7090,9 @@ endpoint 1 { } server cluster Switch { - ram attribute numberOfPositions default = 2; + callback attribute numberOfPositions default = 2; callback attribute currentPosition; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; } @@ -7115,38 +7115,38 @@ endpoint 1 { } server cluster ModeSelect { - ram attribute description default = "Coffee"; - ram attribute standardNamespace default = 0; + callback attribute description default = "Coffee"; + callback attribute standardNamespace default = 0; callback attribute supportedModes; callback attribute currentMode; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; + callback attribute featureMap default = 0; callback attribute clusterRevision; handle command ChangeToMode; } server cluster TemperatureControl { - ram attribute selectedTemperatureLevel default = 0; + callback attribute selectedTemperatureLevel default = 0; callback attribute supportedTemperatureLevels; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 2; + callback attribute featureMap default = 2; callback attribute clusterRevision; handle command SetTemperature; } server cluster ScenesManagement { - ram attribute sceneTableSize default = 16; + callback attribute sceneTableSize default = 16; callback attribute fabricSceneInfo; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; handle command AddScene; @@ -7176,15 +7176,15 @@ endpoint 1 { } server cluster WindowCovering { - ram attribute type default = 0x00; - persist attribute configStatus default = 0x03; + callback attribute type default = 0x00; + callback attribute configStatus default = 0x03; callback attribute operationalStatus; - ram attribute endProductType default = 0x00; - persist attribute mode default = 0x00; + callback attribute endProductType default = 0x00; + callback attribute mode default = 0x00; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; handle command UpOrOpen; @@ -7207,11 +7207,11 @@ endpoint 1 { } server cluster Thermostat { - ram attribute localTemperature; - ram attribute occupiedHeatingSetpoint default = 0x07D0; - ram attribute controlSequenceOfOperation default = 0x04; - ram attribute systemMode default = 0x01; - ram attribute featureMap default = 1; + callback attribute localTemperature; + callback attribute occupiedHeatingSetpoint default = 0x07D0; + callback attribute controlSequenceOfOperation default = 0x04; + callback attribute systemMode default = 0x01; + callback attribute featureMap default = 1; callback attribute clusterRevision; handle command SetpointRaiseLower; @@ -7219,16 +7219,16 @@ endpoint 1 { server cluster FanControl { callback attribute fanMode; - ram attribute fanModeSequence default = 0x02; + callback attribute fanModeSequence default = 0x02; callback attribute percentSetting; callback attribute percentCurrent; - ram attribute featureMap default = 0; + callback attribute featureMap default = 0; callback attribute clusterRevision; } server cluster ThermostatUserInterfaceConfiguration { - ram attribute temperatureDisplayMode default = 0x00; - ram attribute keypadLockout default = 0x00; + callback attribute temperatureDisplayMode default = 0x00; + callback attribute keypadLockout default = 0x00; callback attribute featureMap; callback attribute clusterRevision; } @@ -7258,48 +7258,48 @@ endpoint 1 { server cluster IlluminanceMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0x01; - ram attribute maxMeasuredValue default = 0xFFFE; + callback attribute minMeasuredValue default = 0x01; + callback attribute maxMeasuredValue default = 0xFFFE; callback attribute featureMap; callback attribute clusterRevision; } server cluster TemperatureMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0x8000; - ram attribute maxMeasuredValue default = 0x8000; + callback attribute minMeasuredValue default = 0x8000; + callback attribute maxMeasuredValue default = 0x8000; callback attribute featureMap; callback attribute clusterRevision; } server cluster PressureMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue; - ram attribute maxMeasuredValue; + callback attribute minMeasuredValue; + callback attribute maxMeasuredValue; callback attribute featureMap default = 0; - callback attribute clusterRevision default = 0x0004; + callback attribute clusterRevision; } server cluster FlowMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue; - ram attribute maxMeasuredValue; + callback attribute minMeasuredValue; + callback attribute maxMeasuredValue; callback attribute featureMap; callback attribute clusterRevision; } server cluster RelativeHumidityMeasurement { callback attribute measuredValue; - ram attribute minMeasuredValue default = 0; - ram attribute maxMeasuredValue default = 0x2710; + callback attribute minMeasuredValue default = 0; + callback attribute maxMeasuredValue default = 0x2710; callback attribute featureMap; callback attribute clusterRevision; } server cluster OccupancySensing { - ram attribute occupancy; - ram attribute occupancySensorType; - ram attribute occupancySensorTypeBitmap; + callback attribute occupancy; + callback attribute occupancySensorType; + callback attribute occupancySensorTypeBitmap; callback attribute featureMap; callback attribute clusterRevision; } @@ -7539,12 +7539,12 @@ endpoint 2 { } server cluster Groups { - ram attribute nameSupport; + callback attribute nameSupport; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0; - ram attribute clusterRevision default = 0x0005; + callback attribute featureMap default = 0; + callback attribute clusterRevision; handle command AddGroup; handle command ViewGroup; @@ -7599,12 +7599,12 @@ endpoint 2 { } server cluster ScenesManagement { - ram attribute sceneTableSize default = 16; + callback attribute sceneTableSize default = 16; callback attribute fabricSceneInfo; callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 1; + callback attribute featureMap default = 1; callback attribute clusterRevision; handle command AddScene; @@ -7617,9 +7617,9 @@ endpoint 2 { } server cluster OccupancySensing { - ram attribute occupancy; - ram attribute occupancySensorType; - ram attribute occupancySensorTypeBitmap; + callback attribute occupancy; + callback attribute occupancySensorType; + callback attribute occupancySensorTypeBitmap; callback attribute featureMap; callback attribute clusterRevision; } diff --git a/examples/all-devices-app/README.md b/examples/all-devices-app/README.md index a98148aaf6f..f4539b1d3a1 100644 --- a/examples/all-devices-app/README.md +++ b/examples/all-devices-app/README.md @@ -117,6 +117,7 @@ The application supports the following device types (specified via the - `soil-sensor` - `speaker` - `temperature-sensor` +- `thermostat` - `thread-border-router` - `water-freeze-detector` - `water-leak-detector` @@ -132,7 +133,7 @@ Usage: ./out/linux-x64-all-devices-boringssl-no-ble/all-devices-app PROGRAM OPTIONS - --device + --device Select the device to start up. Format: 'type' or 'type:endpoint' or 'type:endpoint,parent=parentId' Can be specified multiple times for multi-endpoint devices. Example: --device chime:1 --device speaker:2,parent=1 diff --git a/examples/all-devices-app/all-devices-common/device-factory/BUILD.gn b/examples/all-devices-app/all-devices-common/device-factory/BUILD.gn index a3787e6e0ef..4f0f2776e45 100644 --- a/examples/all-devices-app/all-devices-common/device-factory/BUILD.gn +++ b/examples/all-devices-app/all-devices-common/device-factory/BUILD.gn @@ -81,8 +81,9 @@ source_set("device-factory") { "${chip_root}/examples/all-devices-app/all-devices-common/device/types/soil-sensor:increasing", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/speaker:logging", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/temperature-sensor:increasing", + "${chip_root}/examples/all-devices-app/all-devices-common/device/types/thermostat:logging", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/thread-border-router:simulated", - "${chip_root}/examples/all-devices-app/all-devices-common/device/types/water-valve", + "${chip_root}/examples/all-devices-app/all-devices-common/device/types/water-valve:logging", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/window-covering:simulated", "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors:oob-accessors", "${chip_root}/src/lib/support", diff --git a/examples/all-devices-app/all-devices-common/device-factory/DeviceFactory.h b/examples/all-devices-app/all-devices-common/device-factory/DeviceFactory.h index 9f3452ea274..64ccf5f0a6e 100644 --- a/examples/all-devices-app/all-devices-common/device-factory/DeviceFactory.h +++ b/examples/all-devices-app/all-devices-common/device-factory/DeviceFactory.h @@ -72,8 +72,9 @@ #include #include #include +#include #include -#include +#include #include #include #include @@ -700,6 +701,17 @@ class DeviceFactory { RegisterCreator("temperature-sensor", []() { return MakeDevice(); }); } + if constexpr (ALL_DEVICES_ENABLE_THERMOSTAT) + { + RegisterCreator("thermostat", [this]() { + VerifyOrDie(mContext.has_value()); + return MakeDevice(LoggingThermostat::Context{ + .fabricTable = mContext->fabricTable, + .groupDataProvider = mContext->groupDataProvider, + .timerDelegate = mContext->timerDelegate, + }); + }); + } if constexpr (ALL_DEVICES_ENABLE_THREAD_BORDER_ROUTER) { RegisterCreator("thread-border-router", [this](const std::string & nodeLabel) { @@ -846,7 +858,7 @@ class DeviceFactory { RegisterCreator("water-valve", [this]() { VerifyOrDie(mContext.has_value()); - return MakeDevice(mContext->timerDelegate); + return MakeDevice(mContext->timerDelegate); }); } if constexpr (ALL_DEVICES_ENABLE_HUMIDITY_CONDITIONER) diff --git a/examples/all-devices-app/all-devices-common/device-factory/enabled_devices.cmake b/examples/all-devices-app/all-devices-common/device-factory/enabled_devices.cmake index 30e68364f71..d59761ee170 100644 --- a/examples/all-devices-app/all-devices-common/device-factory/enabled_devices.cmake +++ b/examples/all-devices-app/all-devices-common/device-factory/enabled_devices.cmake @@ -123,9 +123,12 @@ set(ALL_DEVICES_DEVICE_SOURCES "${ALL_DEVICES_COMMON_DIR}/device/types/temperature-controlled-cabinet/impl/LoggingTemperatureControlledCabinetPart.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/temperature-sensor/TemperatureSensor.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/temperature-sensor/impl/IncreasingTemperatureSensor.cpp" + "${ALL_DEVICES_COMMON_DIR}/device/types/thermostat/Thermostat.cpp" + "${ALL_DEVICES_COMMON_DIR}/device/types/thermostat/impl/LoggingThermostat.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/thread-border-router/ThreadBorderRouter.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/thread-border-router/impl/SimulatedThreadBorderRouter.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/water-valve/WaterValve.cpp" + "${ALL_DEVICES_COMMON_DIR}/device/types/water-valve/impl/LoggingWaterValve.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/window-covering/WindowCovering.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/window-covering/impl/SimulatedWindowCovering.cpp" # keep-sorted: end @@ -224,6 +227,7 @@ foreach(_key soil-sensor speaker temperature-sensor + thermostat thread-border-router water-freeze-detector water-leak-detector diff --git a/examples/all-devices-app/all-devices-common/device-factory/enabled_devices.gni b/examples/all-devices-app/all-devices-common/device-factory/enabled_devices.gni index 52ad2c75fec..0b3876d40fd 100644 --- a/examples/all-devices-app/all-devices-common/device-factory/enabled_devices.gni +++ b/examples/all-devices-app/all-devices-common/device-factory/enabled_devices.gni @@ -242,6 +242,10 @@ _available_devices = [ "temperature-sensor", "TEMPERATURE_SENSOR", ], + [ + "thermostat", + "THERMOSTAT", + ], [ "thread-border-router", "THREAD_BORDER_ROUTER", diff --git a/examples/all-devices-app/all-devices-common/device-factory/enabled_devices_config.h.in b/examples/all-devices-app/all-devices-common/device-factory/enabled_devices_config.h.in index 4a2a2e0f51f..84e6ff0fbdf 100644 --- a/examples/all-devices-app/all-devices-common/device-factory/enabled_devices_config.h.in +++ b/examples/all-devices-app/all-devices-common/device-factory/enabled_devices_config.h.in @@ -55,6 +55,7 @@ #cmakedefine01 ALL_DEVICES_ENABLE_SOIL_SENSOR #cmakedefine01 ALL_DEVICES_ENABLE_SPEAKER #cmakedefine01 ALL_DEVICES_ENABLE_TEMPERATURE_SENSOR +#cmakedefine01 ALL_DEVICES_ENABLE_THERMOSTAT #cmakedefine01 ALL_DEVICES_ENABLE_THREAD_BORDER_ROUTER #cmakedefine01 ALL_DEVICES_ENABLE_WATER_FREEZE_DETECTOR #cmakedefine01 ALL_DEVICES_ENABLE_WATER_LEAK_DETECTOR diff --git a/examples/all-devices-app/all-devices-common/device/types/power-source/BUILD.gn b/examples/all-devices-app/all-devices-common/device/types/power-source/BUILD.gn index 20846b07568..d4a9cd9d6ec 100644 --- a/examples/all-devices-app/all-devices-common/device/types/power-source/BUILD.gn +++ b/examples/all-devices-app/all-devices-common/device/types/power-source/BUILD.gn @@ -19,12 +19,17 @@ source_set("power-source") { sources = [ "BatteryPowerSource.cpp", "BatteryPowerSource.h", + "BatteryPowerSourceAccessor.cpp", + "BatteryPowerSourceAccessor.h", + "OOBAccessors.cpp", + "OOBAccessors.h", "impl/DecreasingBatteryPowerSource.cpp", "impl/DecreasingBatteryPowerSource.h", ] public_deps = [ "${chip_root}/examples/all-devices-app/all-devices-common/device/api:single-endpoint-device", + "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors", "${chip_root}/src/app/clusters/power-source-server", "${chip_root}/src/data-model-providers/codedriven", "${chip_root}/src/lib/core:error", diff --git a/examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.cpp b/examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.cpp new file mode 100644 index 00000000000..d04d29cc650 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +using namespace chip::app::Clusters; + +namespace chip::app { + +std::optional BatteryPowerSourceAccessor::HandleAction(CharSpan actionName, ByteSpan tlvBuffer) +{ + if (!actionName.data_equal(OOBDataSerializer::kSetAttributeAction)) + { + return std::nullopt; + } + + auto parseResult = OOBDataSerializer::ParseAttributeRequest(tlvBuffer); + if (std::holds_alternative(parseResult)) + { + CHIP_ERROR err = std::get(parseResult); + ChipLogError(Support, "Failed to parse attribute request: %" CHIP_ERROR_FORMAT, err.Format()); + return err; + } + + auto & request = std::get(parseResult); + if (request.path.mEndpointId != mEndpointId) + { + return std::nullopt; + } + + switch (request.path.mClusterId) + { + case PowerSource::Id: { + switch (request.path.mAttributeId) + { + case PowerSource::Attributes::BatPercentRemaining::Id: { + DataModel::Nullable batteryValue; + ReturnErrorOnFailure(DataModel::Decode(request.value, batteryValue)); + return mCluster.SetBatPercentRemaining(batteryValue); + } + case PowerSource::Attributes::BatVoltage::Id: { + DataModel::Nullable voltageValue; + ReturnErrorOnFailure(DataModel::Decode(request.value, voltageValue)); + mCluster.SetBatVoltage(voltageValue); + return CHIP_NO_ERROR; + } + default: + return std::nullopt; + } + } + default: + return std::nullopt; + } +} + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.h b/examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.h new file mode 100644 index 00000000000..2a557c45261 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/power-source/BatteryPowerSourceAccessor.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace chip::app { + +class BatteryPowerSourceAccessor : public OOBAccessor +{ +public: + BatteryPowerSourceAccessor(BatteryPowerSource::SimpleBatteryPowerSourceCluster & cluster, EndpointId endpointId) : + mCluster(cluster), mEndpointId(endpointId) + {} + + ~BatteryPowerSourceAccessor() override = default; + + std::optional HandleAction(CharSpan actionName, ByteSpan tlvBuffer) override; + +private: + BatteryPowerSource::SimpleBatteryPowerSourceCluster & mCluster; + EndpointId mEndpointId; +}; + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.cpp b/examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.cpp new file mode 100644 index 00000000000..022b80d7ccd --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OOBAccessors.h" +#include "BatteryPowerSourceAccessor.h" + +#include + +namespace chip::app { + +void RegisterOOBAccessors(BatteryPowerSource & device, OOBAccessorRegistry & registry) +{ + LogErrorOnFailure(registry.Register( + std::make_unique(device.BatteryPowerSourceCluster(), device.GetEndpointId()))); +} + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.h b/examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.h new file mode 100644 index 00000000000..d5fe6811810 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/power-source/OOBAccessors.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace chip::app { + +void RegisterOOBAccessors(BatteryPowerSource & device, OOBAccessorRegistry & registry); + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/root-node/RootNode.cpp b/examples/all-devices-app/all-devices-common/device/types/root-node/RootNode.cpp index d988c462e75..45367ea3870 100644 --- a/examples/all-devices-app/all-devices-common/device/types/root-node/RootNode.cpp +++ b/examples/all-devices-app/all-devices-common/device/types/root-node/RootNode.cpp @@ -79,10 +79,7 @@ CHIP_ERROR RootNode::RegisterRootClusters(EndpointId endpointId, CodeDrivenDataM endpointId, BitFlags{}, AdministratorCommissioningCluster::Context{ .commissioningWindowManager = mContext.commissioningWindowManager, .fabricTable = mContext.fabricTable, - // Note: We pull FailSafeContext directly from Server instead of via - // CommissioningWindowManager because the WindowManager's internal - // Server pointer is not yet initialized at this stage of registration. - .failSafeContext = mContext.failSafeContext }); + .failSafeContext = mContext.failSafeContext }); ReturnErrorOnFailure(provider.AddCluster(mAdministratorCommissioningCluster.Registration())); mGeneralDiagnosticsCluster.Create(GeneralDiagnosticsCluster::OptionalAttributeSet{}, BitFlags{}, diff --git a/examples/all-devices-app/all-devices-common/device/types/root-node/RootNodeWith.h b/examples/all-devices-app/all-devices-common/device/types/root-node/RootNodeWith.h index 3bd7461c327..d931c79c7f9 100644 --- a/examples/all-devices-app/all-devices-common/device/types/root-node/RootNodeWith.h +++ b/examples/all-devices-app/all-devices-common/device/types/root-node/RootNodeWith.h @@ -56,6 +56,19 @@ class RootNodeWith : public RootNode {} ~RootNodeWith() override = default; + template + Feature & GetFeature() + { + return std::get(mFeatures); + } + + template + const Feature & GetFeature() const + { + return std::get(mFeatures); + } + + using RootNode::Register; CHIP_ERROR Register(EndpointId endpoint, CodeDrivenDataModelProvider & provider, EndpointComposition composition = {}) override { DeviceRegistrationTransaction transaction(*this, provider); diff --git a/examples/all-devices-app/all-devices-common/device/types/root-node/features/OtaFeature.h b/examples/all-devices-app/all-devices-common/device/types/root-node/features/OtaFeature.h index d757276ae21..c1e509e9e02 100644 --- a/examples/all-devices-app/all-devices-common/device/types/root-node/features/OtaFeature.h +++ b/examples/all-devices-app/all-devices-common/device/types/root-node/features/OtaFeature.h @@ -60,6 +60,8 @@ class OtaFeature void UnregisterFeatureClusters(CodeDrivenDataModelProvider & provider); + Clusters::OTARequestorCluster & Cluster() { return mOtaRequestorCluster.Cluster(); } + private: Context mContext; LazyRegisteredServerCluster mOtaRequestorCluster; diff --git a/examples/all-devices-app/all-devices-common/device/types/soil-sensor/BUILD.gn b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/BUILD.gn index 633f8c9a083..ce765b6312a 100644 --- a/examples/all-devices-app/all-devices-common/device/types/soil-sensor/BUILD.gn +++ b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/BUILD.gn @@ -17,12 +17,17 @@ import("//build_overrides/chip.gni") source_set("soil-sensor") { sources = [ + "OOBAccessors.cpp", + "OOBAccessors.h", "SoilSensor.cpp", "SoilSensor.h", + "SoilSensorAccessor.cpp", + "SoilSensorAccessor.h", ] public_deps = [ "${chip_root}/examples/all-devices-app/all-devices-common/device/api:single-endpoint-device", + "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors", "${chip_root}/src/app/clusters/identify-server", "${chip_root}/src/app/clusters/soil-measurement-server", "${chip_root}/src/app/clusters/temperature-measurement-server", diff --git a/examples/all-devices-app/all-devices-common/device/types/soil-sensor/OOBAccessors.cpp b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/OOBAccessors.cpp new file mode 100644 index 00000000000..c157c910bff --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/OOBAccessors.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OOBAccessors.h" +#include +#include + +namespace chip::app { + +void RegisterOOBAccessors(SoilSensor & device, OOBAccessorRegistry & registry) +{ + LogErrorOnFailure(registry.Register(std::make_unique(device))); +} + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/soil-sensor/OOBAccessors.h b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/OOBAccessors.h new file mode 100644 index 00000000000..ec93c9fbb2e --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/OOBAccessors.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace chip::app { + +void RegisterOOBAccessors(SoilSensor & device, OOBAccessorRegistry & registry); + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/soil-sensor/SoilSensorAccessor.cpp b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/SoilSensorAccessor.cpp new file mode 100644 index 00000000000..569eea51bae --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/SoilSensorAccessor.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include +#include +#include + +using namespace chip::app::Clusters; + +namespace chip::app { + +std::optional SoilSensorAccessor::HandleAction(CharSpan actionName, ByteSpan tlvBuffer) +{ + if (!actionName.data_equal(OOBDataSerializer::kSetAttributeAction)) + { + return std::nullopt; + } + + auto parseResult = OOBDataSerializer::ParseAttributeRequest(tlvBuffer); + if (std::holds_alternative(parseResult)) + { + CHIP_ERROR err = std::get(parseResult); + ChipLogError(Support, "Failed to parse attribute request: %" CHIP_ERROR_FORMAT, err.Format()); + return err; + } + + auto & request = std::get(parseResult); + if (request.path.mEndpointId != mDevice.GetEndpointId()) + { + return std::nullopt; + } + + return SetAttribute(request.path, request.value); +} + +std::optional SoilSensorAccessor::SetAttribute(const ConcreteDataAttributePath & path, TLV::TLVReader & reader) +{ + switch (path.mClusterId) + { + case SoilMeasurement::Id: { + switch (path.mAttributeId) + { + case SoilMeasurement::Attributes::SoilMoistureMeasuredValue::Id: { + DataModel::Nullable measuredValue; + ReturnErrorOnFailure(DataModel::Decode(reader, measuredValue)); + return mDevice.SoilMeasurementCluster().SetSoilMoistureMeasuredValue(measuredValue); + } + default: + return std::nullopt; + } + } + case TemperatureMeasurement::Id: { + switch (path.mAttributeId) + { + case TemperatureMeasurement::Attributes::MeasuredValue::Id: { + DataModel::Nullable tempValue; + ReturnErrorOnFailure(DataModel::Decode(reader, tempValue)); + return mDevice.TemperatureMeasurementCluster().SetMeasuredValue(tempValue); + } + default: + return std::nullopt; + } + } + default: + return std::nullopt; + } +} + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/soil-sensor/SoilSensorAccessor.h b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/SoilSensorAccessor.h new file mode 100644 index 00000000000..70117bc83d9 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/SoilSensorAccessor.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip::app { + +class SoilSensorAccessor : public OOBAccessor +{ +public: + explicit SoilSensorAccessor(SoilSensor & device) : mDevice(device) {} + ~SoilSensorAccessor() override = default; + + std::optional HandleAction(CharSpan actionName, ByteSpan tlvBuffer) override; + +private: + std::optional SetAttribute(const ConcreteDataAttributePath & path, TLV::TLVReader & reader); + + SoilSensor & mDevice; +}; + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/soil-sensor/impl/IncreasingMoistureSoilSensor.cpp b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/impl/IncreasingMoistureSoilSensor.cpp index cd4e2935554..143f3b31b8d 100644 --- a/examples/all-devices-app/all-devices-common/device/types/soil-sensor/impl/IncreasingMoistureSoilSensor.cpp +++ b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/impl/IncreasingMoistureSoilSensor.cpp @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2025 Project CHIP Authors + * Copyright (c) 2026 Project CHIP Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ #include "IncreasingMoistureSoilSensor.h" #include #include -#include using namespace chip::app::Clusters; using namespace chip::app::Clusters::SoilMeasurement; @@ -29,22 +28,32 @@ namespace app { namespace { constexpr System::Clock::Seconds16 kIncreaseMoistureIntervalSec = System::Clock::Seconds16(10); +constexpr chip::Percent kMinMoisturePercent = 0; +constexpr chip::Percent kMaxMoisturePercent = 100; +constexpr chip::Percent kMoistureStepPercent = 1; + +constexpr int16_t kMinTemperatureCentiCelsius = -1000; // -10.00°C +constexpr int16_t kMaxTemperatureCentiCelsius = 5000; // +50.00°C +constexpr int16_t kTemperatureStepCentiCelsius = 100; // 1.00°C + const Globals::Structs::MeasurementAccuracyRangeStruct::Type kDefaultSoilMoistureMeasurementLimitsAccuracyRange[] = { - { .rangeMin = 0, .rangeMax = 100, .percentMax = MakeOptional(static_cast(10)) } + { .rangeMin = kMinMoisturePercent, + .rangeMax = kMaxMoisturePercent, + .percentMax = MakeOptional(static_cast(10)) } }; const SoilMoistureMeasurementLimits::TypeInfo::Type kDefaultSoilMoistureMeasurementLimits = { .measurementType = Globals::MeasurementTypeEnum::kSoilMoisture, .measured = true, - .minMeasuredValue = 0, - .maxMeasuredValue = 100, + .minMeasuredValue = kMinMoisturePercent, + .maxMeasuredValue = kMaxMoisturePercent, .accuracyRanges = DataModel::List( kDefaultSoilMoistureMeasurementLimitsAccuracyRange) }; const TemperatureMeasurementCluster::StartupConfiguration kDefaultTemperatureConfig = { - .minMeasuredValue = DataModel::MakeNullable(static_cast(-10)), - .maxMeasuredValue = DataModel::MakeNullable(static_cast(50)), + .minMeasuredValue = DataModel::MakeNullable(kMinTemperatureCentiCelsius), + .maxMeasuredValue = DataModel::MakeNullable(kMaxTemperatureCentiCelsius), .tolerance = 0, }; @@ -63,8 +72,15 @@ CHIP_ERROR IncreasingMoistureSoilSensor::Register(EndpointId endpoint, CodeDrive EndpointComposition composition) { ReturnErrorOnFailure(SoilSensor::Register(endpoint, provider, composition)); - // Kick off the timer loop to increase moisture every few seconds - return mTimerDelegate.StartTimer(this, kIncreaseMoistureIntervalSec); + + CHIP_ERROR err = mTimerDelegate.StartTimer(this, kIncreaseMoistureIntervalSec); + if (err != CHIP_NO_ERROR) + { + SoilSensor::Unregister(provider); + return err; + } + + return CHIP_NO_ERROR; } void IncreasingMoistureSoilSensor::Unregister(CodeDrivenDataModelProvider & provider) @@ -75,38 +91,35 @@ void IncreasingMoistureSoilSensor::Unregister(CodeDrivenDataModelProvider & prov void IncreasingMoistureSoilSensor::TimerFired() { - if (mSoilMoistureMeasuredValue.IsNull()) - { - mSoilMoistureMeasuredValue.SetNonNull(kDefaultSoilMoistureMeasurementLimits.minMeasuredValue); - } - else if (mSoilMoistureMeasuredValue.Value() >= kDefaultSoilMoistureMeasurementLimits.maxMeasuredValue) + auto & soilCluster = SoilMeasurementCluster(); + auto moisture = soilCluster.GetSoilMoistureMeasuredValue(); + + if (moisture.IsNull() || moisture.Value() + kMoistureStepPercent > kMaxMoisturePercent) { - mSoilMoistureMeasuredValue.SetNonNull(kDefaultSoilMoistureMeasurementLimits.minMeasuredValue); + moisture.SetNonNull(kMinMoisturePercent); } else { - mSoilMoistureMeasuredValue.SetNonNull(mSoilMoistureMeasuredValue.Value() + 1U); + moisture.SetNonNull(static_cast(moisture.Value() + kMoistureStepPercent)); } - ChipLogProgress(AppServer, "IncreasingMoistureValue: Increasing to %d", mSoilMoistureMeasuredValue.Value()); - LogErrorOnFailure(mSoilMeasurementCluster.Cluster().SetSoilMoistureMeasuredValue(mSoilMoistureMeasuredValue)); + ChipLogProgress(AppServer, "IncreasingMoistureValue: Increasing to %d", moisture.Value()); + LogErrorOnFailure(soilCluster.SetSoilMoistureMeasuredValue(moisture)); - // Simulate increasing temperature - if (mTemperatureMeasuredValue.IsNull()) - { - mTemperatureMeasuredValue.SetNonNull(kDefaultTemperatureConfig.minMeasuredValue.Value()); - } - else if (mTemperatureMeasuredValue.Value() >= kDefaultTemperatureConfig.maxMeasuredValue.Value()) + auto & tempCluster = TemperatureMeasurementCluster(); + auto temperature = tempCluster.GetMeasuredValue(); + + if (temperature.IsNull() || temperature.Value() + kTemperatureStepCentiCelsius > kMaxTemperatureCentiCelsius) { - mTemperatureMeasuredValue.SetNonNull(kDefaultTemperatureConfig.minMeasuredValue.Value()); + temperature.SetNonNull(kMinTemperatureCentiCelsius); } else { - mTemperatureMeasuredValue.SetNonNull(static_cast(mTemperatureMeasuredValue.Value() + 1)); + temperature.SetNonNull(static_cast(temperature.Value() + kTemperatureStepCentiCelsius)); } - ChipLogProgress(AppServer, "IncreasingTemperatureValue: Increasing to %d", mTemperatureMeasuredValue.Value()); - LogErrorOnFailure(mTemperatureMeasurementCluster.Cluster().SetMeasuredValue(mTemperatureMeasuredValue)); + ChipLogProgress(AppServer, "IncreasingTemperatureValue: Increasing to %d", temperature.Value()); + LogErrorOnFailure(tempCluster.SetMeasuredValue(temperature)); VerifyOrDie(mTimerDelegate.StartTimer(this, kIncreaseMoistureIntervalSec) == CHIP_NO_ERROR); } diff --git a/examples/all-devices-app/all-devices-common/device/types/soil-sensor/impl/IncreasingMoistureSoilSensor.h b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/impl/IncreasingMoistureSoilSensor.h index f4939fadb23..c4869a712ba 100644 --- a/examples/all-devices-app/all-devices-common/device/types/soil-sensor/impl/IncreasingMoistureSoilSensor.h +++ b/examples/all-devices-app/all-devices-common/device/types/soil-sensor/impl/IncreasingMoistureSoilSensor.h @@ -41,8 +41,6 @@ class IncreasingMoistureSoilSensor : public SoilSensor, public TimerContext private: DefaultTimerDelegate mTimerDelegate; - Clusters::SoilMeasurement::Attributes::SoilMoistureMeasuredValue::TypeInfo::Type mSoilMoistureMeasuredValue; - DataModel::Nullable mTemperatureMeasuredValue; }; } // namespace app diff --git a/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/BUILD.gn b/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/BUILD.gn index 3b4b5f43ebd..fe0cecbfe0f 100644 --- a/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/BUILD.gn +++ b/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/BUILD.gn @@ -38,5 +38,8 @@ source_set("increasing") { "impl/IncreasingTemperatureSensor.h", ] - public_deps = [ ":temperature-sensor" ] + public_deps = [ + ":temperature-sensor", + "${chip_root}/src/app/clusters/thermostat-user-interface-configuration-server", + ] } diff --git a/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/TemperatureSensor.cpp b/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/TemperatureSensor.cpp index d2a674bc4b5..aa666368927 100644 --- a/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/TemperatureSensor.cpp +++ b/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/TemperatureSensor.cpp @@ -44,6 +44,8 @@ CHIP_ERROR TemperatureSensor::Register(EndpointId endpoint, CodeDrivenDataModelP mTemperatureMeasurementCluster.Create(endpoint, mOptionalAttributes, mTempConfig); ReturnErrorOnFailure(provider.AddCluster(mTemperatureMeasurementCluster.Registration())); + ReturnErrorOnFailure(RegisterAdditionalClusters(endpoint, provider)); + ReturnErrorOnFailure(provider.AddEndpoint(mEndpointRegistration)); transaction.Commit(); return CHIP_NO_ERROR; @@ -52,6 +54,7 @@ CHIP_ERROR TemperatureSensor::Register(EndpointId endpoint, CodeDrivenDataModelP void TemperatureSensor::Unregister(CodeDrivenDataModelProvider & provider) { UnregisterDescriptor(provider); + UnregisterAdditionalClusters(provider); if (mTemperatureMeasurementCluster.IsConstructed()) { LogErrorOnFailure(provider.RemoveCluster(&mTemperatureMeasurementCluster.Cluster())); diff --git a/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/TemperatureSensor.h b/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/TemperatureSensor.h index b01365a3d82..edc0ea3fa81 100644 --- a/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/TemperatureSensor.h +++ b/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/TemperatureSensor.h @@ -41,6 +41,16 @@ class TemperatureSensor : public SingleEndpoint Clusters::TemperatureMeasurementCluster & TemperatureMeasurementCluster() { return mTemperatureMeasurementCluster.Cluster(); } protected: + /// Called before the endpoint is registered, within the registration transaction. + virtual CHIP_ERROR RegisterAdditionalClusters(EndpointId endpoint, CodeDrivenDataModelProvider & provider) + { + return CHIP_NO_ERROR; + } + + /// Called after the endpoint is removed, including on partial registration failure. + /// Overrides must tolerate clusters that were not constructed or registered. + virtual void UnregisterAdditionalClusters(CodeDrivenDataModelProvider & provider) {} + TimerDelegate & mTimerDelegate; Clusters::TemperatureMeasurementCluster::StartupConfiguration mTempConfig; Clusters::TemperatureMeasurementCluster::OptionalAttributeSet mOptionalAttributes; diff --git a/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/impl/IncreasingTemperatureSensor.cpp b/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/impl/IncreasingTemperatureSensor.cpp index 17d8c620be5..7f3e4ca4402 100644 --- a/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/impl/IncreasingTemperatureSensor.cpp +++ b/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/impl/IncreasingTemperatureSensor.cpp @@ -65,6 +65,25 @@ void IncreasingTemperatureSensor::Unregister(CodeDrivenDataModelProvider & provi TemperatureSensor::Unregister(provider); } +CHIP_ERROR IncreasingTemperatureSensor::RegisterAdditionalClusters(EndpointId endpoint, CodeDrivenDataModelProvider & provider) +{ + ThermostatUserInterfaceConfigurationCluster::Config config; + config.optionalAttributes.Set(); + mUserInterfaceCluster.Create(endpoint, config); + mUserInterfaceCluster.Cluster().SetDelegate(this); + return provider.AddCluster(mUserInterfaceCluster.Registration()); +} + +void IncreasingTemperatureSensor::UnregisterAdditionalClusters(CodeDrivenDataModelProvider & provider) +{ + if (mUserInterfaceCluster.IsConstructed()) + { + mUserInterfaceCluster.Cluster().SetDelegate(nullptr); + LogErrorOnFailure(provider.RemoveCluster(&mUserInterfaceCluster.Cluster())); + mUserInterfaceCluster.Destroy(); + } +} + void IncreasingTemperatureSensor::TimerFired() { // Simulate increasing temperature @@ -87,5 +106,16 @@ void IncreasingTemperatureSensor::TimerFired() LogErrorOnFailure(mTimerDelegate.StartTimer(this, kIncreaseTemperatureIntervalSec)); } +void IncreasingTemperatureSensor::OnTemperatureDisplayModeChanged( + ThermostatUserInterfaceConfiguration::TemperatureDisplayModeEnum value) +{ + ChipLogProgress(AppServer, "TemperatureSensor: TemperatureDisplayMode changed to %u", static_cast(value)); +} + +void IncreasingTemperatureSensor::OnKeypadLockoutChanged(ThermostatUserInterfaceConfiguration::KeypadLockoutEnum value) +{ + ChipLogProgress(AppServer, "TemperatureSensor: KeypadLockout changed to %u", static_cast(value)); +} + } // namespace app } // namespace chip diff --git a/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/impl/IncreasingTemperatureSensor.h b/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/impl/IncreasingTemperatureSensor.h index 28f245ef07c..05617d9e5f2 100644 --- a/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/impl/IncreasingTemperatureSensor.h +++ b/examples/all-devices-app/all-devices-common/device/types/temperature-sensor/impl/IncreasingTemperatureSensor.h @@ -17,6 +17,7 @@ #pragma once #include +#include #include #include #include @@ -27,7 +28,9 @@ namespace app { /** * Simulated temperature sensor that increases the reported temperature over time. */ -class IncreasingTemperatureSensor : public TemperatureSensor, public TimerContext +class IncreasingTemperatureSensor : public TemperatureSensor, + public TimerContext, + public Clusters::ThermostatUserInterfaceConfiguration::Delegate { public: IncreasingTemperatureSensor(); @@ -39,9 +42,18 @@ class IncreasingTemperatureSensor : public TemperatureSensor, public TimerContex // TimerContext void TimerFired() override; + // ThermostatUserInterfaceConfiguration::Delegate + void OnTemperatureDisplayModeChanged(Clusters::ThermostatUserInterfaceConfiguration::TemperatureDisplayModeEnum value) override; + void OnKeypadLockoutChanged(Clusters::ThermostatUserInterfaceConfiguration::KeypadLockoutEnum value) override; + +protected: + CHIP_ERROR RegisterAdditionalClusters(EndpointId endpoint, CodeDrivenDataModelProvider & provider) override; + void UnregisterAdditionalClusters(CodeDrivenDataModelProvider & provider) override; + private: DefaultTimerDelegate mTimerDelegate; DataModel::Nullable mTemperatureMeasuredValue; + LazyRegisteredServerCluster mUserInterfaceCluster; }; } // namespace app diff --git a/examples/all-devices-app/all-devices-common/device/types/thermostat/BUILD.gn b/examples/all-devices-app/all-devices-common/device/types/thermostat/BUILD.gn new file mode 100644 index 00000000000..9a22146b3b6 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/thermostat/BUILD.gn @@ -0,0 +1,43 @@ +# Copyright (c) 2026 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import("//build_overrides/chip.gni") + +source_set("thermostat") { + sources = [ + "Thermostat.cpp", + "Thermostat.h", + ] + + public_deps = [ + "${chip_root}/examples/all-devices-app/all-devices-common/device/api:single-endpoint-device", + "${chip_root}/src/app/clusters/identify-server", + "${chip_root}/src/app/clusters/thermostat-server", + "${chip_root}/zzz_generated/app-common/devices", + ] +} + +source_set("logging") { + sources = [ + "impl/LoggingThermostat.cpp", + "impl/LoggingThermostat.h", + ] + + public_deps = [ + ":thermostat", + "${chip_root}/src/app/clusters/groups-server", + "${chip_root}/src/app/clusters/thermostat-user-interface-configuration-server", + "${chip_root}/src/app/persistence", + ] +} diff --git a/examples/all-devices-app/all-devices-common/device/types/thermostat/Thermostat.cpp b/examples/all-devices-app/all-devices-common/device/types/thermostat/Thermostat.cpp new file mode 100644 index 00000000000..5f7e34a760f --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/thermostat/Thermostat.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +namespace chip::app { + +FullThermostat::FullThermostat(const Context & context, Clusters::IdentifyDelegate & identifyDelegate, + Clusters::Thermostat::Delegate & thermostatDelegate, + Clusters::Thermostat::ThermostatHeatingSetpoints::Delegate & heatingDelegate, + Clusters::Thermostat::ThermostatCoolingSetpoints::Delegate & coolingDelegate, + Clusters::Thermostat::ThermostatAutoSetpoints::Delegate & autoDelegate, + Clusters::Thermostat::ThermostatPresets::Delegate & presetsDelegate, + Clusters::Thermostat::ThermostatHold::Delegate & holdDelegate) : + SingleEndpoint(Span(&Device::Type::kThermostat, 1)), + mContext(context), mIdentifyDelegate(identifyDelegate), mThermostatDelegate(thermostatDelegate), + mHeatingDelegate(heatingDelegate), mCoolingDelegate(coolingDelegate), mAutoDelegate(autoDelegate), + mPresetsDelegate(presetsDelegate), mHoldDelegate(holdDelegate) +{} + +CHIP_ERROR FullThermostat::Register(EndpointId endpoint, CodeDrivenDataModelProvider & provider, EndpointComposition composition) +{ + VerifyOrReturnError(mEndpointId == kInvalidEndpointId, CHIP_ERROR_INCORRECT_STATE); + DeviceRegistrationTransaction transaction(*this, provider); + + ReturnErrorOnFailure(RegisterDescriptor(endpoint, provider, composition)); + + mIdentifyCluster.Create(Clusters::IdentifyCluster::Config(endpoint, mContext.timerDelegate).WithDelegate(&mIdentifyDelegate)); + ReturnErrorOnFailure(provider.AddCluster(mIdentifyCluster.Registration())); + + mThermostatCluster.Create( + endpoint, Features(), ThermostatClusterType::Config(mContext.optionalAttributes, mContext.timerDelegate), + mThermostatDelegate, mHeatingDelegate, mCoolingDelegate, mAutoDelegate, mPresetsDelegate, mHoldDelegate); + ReturnErrorOnFailure(provider.AddCluster(mThermostatCluster.Registration())); + + ReturnErrorOnFailure(RegisterAdditionalClusters(endpoint, provider)); + + ReturnErrorOnFailure(provider.AddEndpoint(mEndpointRegistration)); + transaction.Commit(); + return CHIP_NO_ERROR; +} + +void FullThermostat::Unregister(CodeDrivenDataModelProvider & provider) +{ + UnregisterDescriptor(provider); + UnregisterAdditionalClusters(provider); + if (mThermostatCluster.IsConstructed()) + { + LogErrorOnFailure(provider.RemoveCluster(&mThermostatCluster.Cluster())); + mThermostatCluster.Destroy(); + } + if (mIdentifyCluster.IsConstructed()) + { + LogErrorOnFailure(provider.RemoveCluster(&mIdentifyCluster.Cluster())); + mIdentifyCluster.Destroy(); + } +} + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/thermostat/Thermostat.h b/examples/all-devices-app/all-devices-common/device/types/thermostat/Thermostat.h new file mode 100644 index 00000000000..446339884b8 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/thermostat/Thermostat.h @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace chip::app { + +/// A Thermostat device with heating, cooling, AutoMode and Presets support and the +/// delegates they require. Device types are expected to be generic; devices that +/// need a narrower cluster composition should build their own class instead. +class FullThermostat : public SingleEndpoint +{ +public: + using ThermostatClusterType = Clusters::Thermostat::ThermostatCluster< + Clusters::Thermostat::Delegate, Clusters::Thermostat::ThermostatHeatingSetpoints::Delegate, + Clusters::Thermostat::ThermostatCoolingSetpoints::Delegate, Clusters::Thermostat::ThermostatAutoSetpoints::Delegate, + Clusters::Thermostat::ThermostatPresets::Delegate, Clusters::Thermostat::ThermostatHold::Delegate>; + + struct Context + { + TimerDelegate & timerDelegate; + Clusters::Thermostat::OptionalAttributes optionalAttributes = {}; + }; + + FullThermostat(const Context & context, Clusters::IdentifyDelegate & identifyDelegate, + Clusters::Thermostat::Delegate & thermostatDelegate, + Clusters::Thermostat::ThermostatHeatingSetpoints::Delegate & heatingDelegate, + Clusters::Thermostat::ThermostatCoolingSetpoints::Delegate & coolingDelegate, + Clusters::Thermostat::ThermostatAutoSetpoints::Delegate & autoDelegate, + Clusters::Thermostat::ThermostatPresets::Delegate & presetsDelegate, + Clusters::Thermostat::ThermostatHold::Delegate & holdDelegate); + ~FullThermostat() override = default; + + CHIP_ERROR Register(EndpointId endpoint, CodeDrivenDataModelProvider & provider, EndpointComposition composition = {}) override; + void Unregister(CodeDrivenDataModelProvider & provider) override; + + /// Fixed feature set: it must match ThermostatClusterType, which registers all the + /// attributes and commands these features imply at compile time. + static constexpr BitFlags kFeatures = { + Clusters::Thermostat::Feature::kHeating, + Clusters::Thermostat::Feature::kCooling, + Clusters::Thermostat::Feature::kAutoMode, + Clusters::Thermostat::Feature::kPresets, + }; + + const BitFlags & Features() const { return kFeatures; } + bool HasThermostatCluster() const { return mThermostatCluster.IsConstructed(); } + Clusters::IdentifyCluster & IdentifyCluster() { return mIdentifyCluster.Cluster(); } + ThermostatClusterType & ThermostatCluster() { return mThermostatCluster.Cluster(); } + +protected: + /// Called before the endpoint is registered, within the registration transaction. + virtual CHIP_ERROR RegisterAdditionalClusters(EndpointId endpoint, CodeDrivenDataModelProvider & provider) + { + return CHIP_NO_ERROR; + } + + /// Called after the endpoint is removed, including on partial registration failure. + /// Overrides must tolerate clusters that were not constructed or registered. + virtual void UnregisterAdditionalClusters(CodeDrivenDataModelProvider & provider) {} + +private: + const Context mContext; + Clusters::IdentifyDelegate & mIdentifyDelegate; + Clusters::Thermostat::Delegate & mThermostatDelegate; + Clusters::Thermostat::ThermostatHeatingSetpoints::Delegate & mHeatingDelegate; + Clusters::Thermostat::ThermostatCoolingSetpoints::Delegate & mCoolingDelegate; + Clusters::Thermostat::ThermostatAutoSetpoints::Delegate & mAutoDelegate; + Clusters::Thermostat::ThermostatPresets::Delegate & mPresetsDelegate; + Clusters::Thermostat::ThermostatHold::Delegate & mHoldDelegate; + + LazyRegisteredServerCluster mIdentifyCluster; + LazyRegisteredServerCluster mThermostatCluster; +}; + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/thermostat/impl/LoggingThermostat.cpp b/examples/all-devices-app/all-devices-common/device/types/thermostat/impl/LoggingThermostat.cpp new file mode 100644 index 00000000000..46627c7edc3 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/thermostat/impl/LoggingThermostat.cpp @@ -0,0 +1,701 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +namespace chip::app { + +namespace { +namespace thermostat = Clusters::Thermostat; +using Protocols::InteractionModel::Status; + +constexpr thermostat::PresetScenarioEnum kPresetScenarios[] = { + thermostat::PresetScenarioEnum::kOccupied, + thermostat::PresetScenarioEnum::kUnoccupied, + thermostat::PresetScenarioEnum::kUserDefined, +}; + +bool IsSupportedMode(thermostat::SystemModeEnum mode) +{ + if (mode == thermostat::SystemModeEnum::kOff || mode == thermostat::SystemModeEnum::kAuto || + mode == thermostat::SystemModeEnum::kHeat || mode == thermostat::SystemModeEnum::kCool) + { + return true; + } + return false; +} + +FullThermostat::Context MakeLoggingContext(const LoggingThermostat::Context & context) +{ + FullThermostat::Context loggingContext{ + .timerDelegate = context.timerDelegate, + .optionalAttributes = context.optionalAttributes, + }; + loggingContext.optionalAttributes.ThermostatRunningMode = true; + loggingContext.optionalAttributes.ThermostatRunningState = true; + loggingContext.optionalAttributes.RemoteSensing = true; + loggingContext.optionalAttributes.LocalTemperatureCalibration = true; + loggingContext.optionalAttributes.AbsMinHeatSetpointLimit = true; + loggingContext.optionalAttributes.AbsMaxHeatSetpointLimit = true; + loggingContext.optionalAttributes.AbsMinCoolSetpointLimit = true; + loggingContext.optionalAttributes.AbsMaxCoolSetpointLimit = true; + loggingContext.optionalAttributes.MinHeatSetpointLimit = true; + loggingContext.optionalAttributes.MaxHeatSetpointLimit = true; + loggingContext.optionalAttributes.MinCoolSetpointLimit = true; + loggingContext.optionalAttributes.MaxCoolSetpointLimit = true; + loggingContext.optionalAttributes.TemperatureSetpointHold = true; + loggingContext.optionalAttributes.TemperatureSetpointHoldDuration = true; + loggingContext.optionalAttributes.SetpointHoldExpiryTimestamp = true; + loggingContext.optionalAttributes.CriticalFreezeProtection = true; + loggingContext.optionalAttributes.CriticalOverheatProtection = true; + return loggingContext; +} +} // namespace + +LoggingThermostat::LoggingThermostat(const Context & context) : + FullThermostat(MakeLoggingContext(context), *this, *this, *this, *this, *this, *this, *this), mFabricTable(context.fabricTable), + mGroupDataProvider(context.groupDataProvider) +{ + static_assert(MATTER_ARRAY_SIZE(kPresetScenarios) == kPresetCapacity); + // Start with two built-in presets, leaving room for a user-defined preset. + for (uint8_t index = 0; index < 2; ++index) + { + auto & preset = mPresets[index]; + const uint8_t handle[] = { to_underlying(kPresetScenarios[index]) }; + VerifyOrDie(preset.SetPresetHandle(DataModel::MakeNullable(ByteSpan(handle))) == CHIP_NO_ERROR); + preset.SetPresetScenario(kPresetScenarios[index]); + preset.SetBuiltIn(DataModel::MakeNullable(true)); + preset.SetHeatingSetpoint(MakeOptional(thermostat::kDefaultHeatingSetpoint)); + preset.SetCoolingSetpoint(MakeOptional(thermostat::kDefaultCoolingSetpoint)); + ++mPresetCount; + } +} + +CHIP_ERROR LoggingThermostat::RegisterAdditionalClusters(EndpointId endpoint, CodeDrivenDataModelProvider & provider) +{ + mGroupsCluster.Create(endpoint, + Clusters::GroupsCluster::Context{ + .groupDataProvider = mGroupDataProvider, + .identifyIntegration = &IdentifyCluster(), + }); + ReturnErrorOnFailure(provider.AddCluster(mGroupsCluster.Registration())); + + Clusters::ThermostatUserInterfaceConfigurationCluster::Config config; + config.optionalAttributes.Set(); + mUserInterfaceCluster.Create(endpoint, config); + mUserInterfaceCluster.Cluster().SetDelegate(this); + ReturnErrorOnFailure(provider.AddCluster(mUserInterfaceCluster.Registration())); + + return CHIP_NO_ERROR; +} + +void LoggingThermostat::UnregisterAdditionalClusters(CodeDrivenDataModelProvider & provider) +{ + if (mUserInterfaceCluster.IsConstructed()) + { + mUserInterfaceCluster.Cluster().SetDelegate(nullptr); + LogErrorOnFailure(provider.RemoveCluster(&mUserInterfaceCluster.Cluster())); + mUserInterfaceCluster.Destroy(); + } + if (mGroupsCluster.IsConstructed()) + { + LogErrorOnFailure(provider.RemoveCluster(&mGroupsCluster.Cluster())); + mGroupsCluster.Destroy(); + } +} + +CHIP_ERROR LoggingThermostat::Startup(ServerClusterContext & context) +{ + // Startup is called for the main delegate and both setpoint delegates. + VerifyOrReturnError(mAttributeStorage == nullptr, CHIP_NO_ERROR); + mAttributeStorage = &context.attributeStorage; + AttributePersistence persistence(*mAttributeStorage); + persistence.LoadNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::SystemMode::Id }, mSystemMode, + thermostat::SystemModeEnum::kOff); + persistence.LoadNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::OccupiedHeatingSetpoint::Id }, + mHeatingSetpoint, thermostat::kDefaultHeatingSetpoint); + persistence.LoadNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::OccupiedCoolingSetpoint::Id }, + mCoolingSetpoint, thermostat::kDefaultCoolingSetpoint); + persistence.LoadNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::MinHeatSetpointLimit::Id }, + mMinHeatSetpointLimit, thermostat::kDefaultAbsMinHeatSetpointLimit); + persistence.LoadNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::MaxHeatSetpointLimit::Id }, + mMaxHeatSetpointLimit, thermostat::kDefaultAbsMaxHeatSetpointLimit); + persistence.LoadNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::MinCoolSetpointLimit::Id }, + mMinCoolSetpointLimit, thermostat::kDefaultAbsMinCoolSetpointLimit); + persistence.LoadNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::MaxCoolSetpointLimit::Id }, + mMaxCoolSetpointLimit, thermostat::kDefaultAbsMaxCoolSetpointLimit); + persistence.LoadNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::TemperatureSetpointHold::Id }, + mSetpointHold, thermostat::TemperatureSetpointHoldEnum::kSetpointHoldOff); + persistence.LoadNativeEndianValue( + { GetEndpointId(), thermostat::Id, thermostat::Attributes::TemperatureSetpointHoldDuration::Id }, mSetpointHoldDuration, + DataModel::NullNullable); + persistence.LoadNativeEndianValue( + { GetEndpointId(), thermostat::Id, thermostat::Attributes::SetpointHoldExpiryTimestamp::Id }, mSetpointHoldExpiryTimestamp, + DataModel::NullNullable); + + if (!IsSupportedMode(mSystemMode)) + { + mSystemMode = thermostat::SystemModeEnum::kOff; + } + + // The cluster validates the complete setpoint state on every write; on startup the loaded + // persisted state is re-checked the same way, and anything inconsistent falls back to defaults. + if (!HasThermostatCluster() || !ThermostatCluster().GetSetpoints().Valid()) + { + mMinHeatSetpointLimit = thermostat::kDefaultAbsMinHeatSetpointLimit; + mMaxHeatSetpointLimit = thermostat::kDefaultAbsMaxHeatSetpointLimit; + mMinCoolSetpointLimit = thermostat::kDefaultAbsMinCoolSetpointLimit; + mMaxCoolSetpointLimit = thermostat::kDefaultAbsMaxCoolSetpointLimit; + mHeatingSetpoint = thermostat::kDefaultHeatingSetpoint; + mCoolingSetpoint = thermostat::kDefaultCoolingSetpoint; + } + + UpdateSimulatedRunningState(); + return CHIP_NO_ERROR; +} + +void LoggingThermostat::Shutdown(ClusterShutdownType type) +{ + mAttributeStorage = nullptr; +} + +CHIP_ERROR LoggingThermostat::GetPresetTypeAtIndex(size_t index, thermostat::Structs::PresetTypeStruct::Type & value) +{ + VerifyOrReturnError(index < MATTER_ARRAY_SIZE(kPresetScenarios), CHIP_ERROR_PROVIDER_LIST_EXHAUSTED); + value.presetScenario = kPresetScenarios[index]; + value.numberOfPresets = 1; + value.presetTypeFeatures = BitMask(thermostat::PresetTypeFeaturesBitmap::kSupportsNames); + return CHIP_NO_ERROR; +} + +CHIP_ERROR LoggingThermostat::GetPresetAtIndex(size_t index, thermostat::PresetStructWithOwnedMembers & value) +{ + VerifyOrReturnError(index < mPresetCount, CHIP_ERROR_PROVIDER_LIST_EXHAUSTED); + value = mPresets[index]; + return CHIP_NO_ERROR; +} + +CHIP_ERROR LoggingThermostat::GetPendingPresetAtIndex(size_t index, thermostat::PresetStructWithOwnedMembers & value) +{ + VerifyOrReturnError(index < mPendingPresetCount, CHIP_ERROR_PROVIDER_LIST_EXHAUSTED); + value = mPendingPresets[index]; + return CHIP_NO_ERROR; +} + +CHIP_ERROR LoggingThermostat::GetActivePresetHandle(DataModel::Nullable & value) +{ + if (mActivePresetHandle.IsNull()) + { + value.SetNull(); + return CHIP_NO_ERROR; + } + VerifyOrReturnError(!value.IsNull(), CHIP_ERROR_INVALID_ARGUMENT); + const uint8_t handle[] = { mActivePresetHandle.Value() }; + return CopySpanToMutableSpan(ByteSpan(handle), value.Value()); +} + +CHIP_ERROR LoggingThermostat::SetActivePresetHandle(const DataModel::Nullable & value) +{ + if (value.IsNull()) + { + mActivePresetHandle.SetNull(); + } + else + { + VerifyOrReturnError(value.Value().size() == 1, CHIP_ERROR_INVALID_ARGUMENT); + mActivePresetHandle.SetNonNull(value.Value()[0]); + } + return CHIP_NO_ERROR; +} + +void LoggingThermostat::InitializePendingPresets() +{ + mPendingPresets = mPresets; + mPendingPresetCount = mPresetCount; +} + +CHIP_ERROR LoggingThermostat::AppendToPendingPresetList(const thermostat::PresetStructWithOwnedMembers & value) +{ + VerifyOrReturnError(mPendingPresetCount < mPendingPresets.size(), CHIP_ERROR_NO_MEMORY); + auto & pending = mPendingPresets[mPendingPresetCount]; + pending = value; + if (pending.GetPresetHandle().IsNull()) + { + // Do not derive new handles from the scenario: an existing preset can change scenarios. + // At most kPresetCapacity handles in each list are in use, so this search always has a free slot. + static_assert(2 * kPresetCapacity + 1 <= UINT8_MAX); + for (int handle = 1; handle <= 2 * kPresetCapacity + 1; ++handle) + { + const uint8_t handleByte = static_cast(handle); + const ByteSpan candidate(&handleByte, 1); + const auto containsHandle = [&](const auto & presets, uint8_t count) { + for (uint8_t index = 0; index < count; ++index) + { + const auto existing = presets[index].GetPresetHandle(); + if (!existing.IsNull() && existing.Value().data_equal(candidate)) + { + return true; + } + } + return false; + }; + if (!containsHandle(mPresets, mPresetCount) && !containsHandle(mPendingPresets, mPendingPresetCount)) + { + ReturnErrorOnFailure(pending.SetPresetHandle(DataModel::MakeNullable(candidate))); + break; + } + } + } + ++mPendingPresetCount; + return CHIP_NO_ERROR; +} + +CHIP_ERROR LoggingThermostat::CommitPendingPresets() +{ + mPresets = mPendingPresets; + mPresetCount = mPendingPresetCount; + return CHIP_NO_ERROR; +} + +std::optional LoggingThermostat::GetMaxAtomicWriteTimeout(AttributeId attributeId) +{ + if (attributeId == thermostat::Attributes::Presets::Id) + { + return System::Clock::Milliseconds16(3000); + } + return std::nullopt; +} + +Status LoggingThermostat::SetTemperatureSetpointHold(thermostat::TemperatureSetpointHoldEnum value, bool & changed) +{ + changed = false; + VerifyOrReturnValue(mSetpointHold != value, Status::Success); + VerifyOrReturnValue(mAttributeStorage != nullptr, Status::Failure); + ReturnValueOnFailure(AttributePersistence(*mAttributeStorage) + .StoreNativeEndianValue( + { GetEndpointId(), thermostat::Id, thermostat::Attributes::TemperatureSetpointHold::Id }, value), + Status::Failure); + mSetpointHold = value; + changed = true; + return Status::Success; +} + +Status LoggingThermostat::SetTemperatureSetpointHoldDuration(DataModel::Nullable value, bool & changed) +{ + changed = false; + VerifyOrReturnValue(mSetpointHoldDuration != value, Status::Success); + VerifyOrReturnValue(mAttributeStorage != nullptr, Status::Failure); + ReturnValueOnFailure( + AttributePersistence(*mAttributeStorage) + .StoreNativeEndianValue( + { GetEndpointId(), thermostat::Id, thermostat::Attributes::TemperatureSetpointHoldDuration::Id }, value), + Status::Failure); + mSetpointHoldDuration = value; + changed = true; + return Status::Success; +} + +Status LoggingThermostat::SetSetpointHoldExpiryTimestamp(DataModel::Nullable value, bool & changed) +{ + changed = false; + VerifyOrReturnValue(mSetpointHoldExpiryTimestamp != value, Status::Success); + VerifyOrReturnValue(mAttributeStorage != nullptr, Status::Failure); + ReturnValueOnFailure( + AttributePersistence(*mAttributeStorage) + .StoreNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::SetpointHoldExpiryTimestamp::Id }, + value), + Status::Failure); + mSetpointHoldExpiryTimestamp = value; + changed = true; + return Status::Success; +} + +void LoggingThermostat::OnIdentifyStart(Clusters::IdentifyCluster & cluster) +{ + ChipLogProgress(AppServer, "Thermostat: Identify started"); +} + +void LoggingThermostat::OnIdentifyStop(Clusters::IdentifyCluster & cluster) +{ + ChipLogProgress(AppServer, "Thermostat: Identify stopped"); +} + +Status LoggingThermostat::SetLocalTemperature(DataModel::Nullable value, bool & changed) +{ + changed = false; + VerifyOrReturnValue(value.IsNull() || value.Value() >= -27315, Status::ConstraintError); + changed = mLocalTemperature != value; + mLocalTemperature = value; + if (changed) + { + UpdateSimulatedRunningState(); + } + return Status::Success; +} + +Status LoggingThermostat::SetSystemMode(thermostat::SystemModeEnum value, bool & changed) +{ + changed = false; + VerifyOrReturnValue(IsSupportedMode(value), Status::ConstraintError); + VerifyOrReturnValue(mSystemMode != value, Status::Success); + VerifyOrReturnValue(mAttributeStorage != nullptr, Status::Failure); + CHIP_ERROR err = + AttributePersistence(*mAttributeStorage) + .StoreNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::SystemMode::Id }, value); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "Thermostat: persisting SystemMode failed: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + mSystemMode = value; + changed = true; + ChipLogProgress(AppServer, "Thermostat: SystemMode changed to %u", static_cast(value)); + UpdateSimulatedRunningState(); + return Status::Success; +} + +thermostat::ControlSequenceOfOperationEnum LoggingThermostat::GetControlSequenceOfOperation() const +{ + return thermostat::ControlSequenceOfOperationEnum::kCoolingAndHeating; +} + +Status LoggingThermostat::SetControlSequenceOfOperation(thermostat::ControlSequenceOfOperationEnum value, bool & changed) +{ + changed = false; + return value == GetControlSequenceOfOperation() ? Status::Success : Status::ConstraintError; +} + +Status LoggingThermostat::GetOccupiedHeatingSetpoint(int16_t & value) const +{ + value = mHeatingSetpoint; + return Status::Success; +} + +Status LoggingThermostat::SetOccupiedHeatingSetpoint(int16_t value, bool & changed) +{ + changed = false; + VerifyOrReturnValue(value >= mMinHeatSetpointLimit && value <= mMaxHeatSetpointLimit, Status::ConstraintError); + VerifyOrReturnValue(mHeatingSetpoint != value, Status::Success); + VerifyOrReturnValue(mAttributeStorage != nullptr, Status::Failure); + CHIP_ERROR err = AttributePersistence(*mAttributeStorage) + .StoreNativeEndianValue( + { GetEndpointId(), thermostat::Id, thermostat::Attributes::OccupiedHeatingSetpoint::Id }, value); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "Thermostat: persisting heating setpoint failed: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + mHeatingSetpoint = value; + changed = true; + ChipLogProgress(AppServer, "Thermostat: heating setpoint changed to %d", value); + UpdateSimulatedRunningState(); + return Status::Success; +} + +Status LoggingThermostat::GetOccupiedCoolingSetpoint(int16_t & value) const +{ + value = mCoolingSetpoint; + return Status::Success; +} + +Status LoggingThermostat::SetOccupiedCoolingSetpoint(int16_t value, bool & changed) +{ + changed = false; + VerifyOrReturnValue(value >= mMinCoolSetpointLimit && value <= mMaxCoolSetpointLimit, Status::ConstraintError); + VerifyOrReturnValue(mCoolingSetpoint != value, Status::Success); + VerifyOrReturnValue(mAttributeStorage != nullptr, Status::Failure); + CHIP_ERROR err = AttributePersistence(*mAttributeStorage) + .StoreNativeEndianValue( + { GetEndpointId(), thermostat::Id, thermostat::Attributes::OccupiedCoolingSetpoint::Id }, value); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "Thermostat: persisting cooling setpoint failed: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + mCoolingSetpoint = value; + changed = true; + ChipLogProgress(AppServer, "Thermostat: cooling setpoint changed to %d", value); + UpdateSimulatedRunningState(); + return Status::Success; +} + +Status LoggingThermostat::GetAbsMinHeatSetpointLimit(int16_t & value) const +{ + value = thermostat::kDefaultAbsMinHeatSetpointLimit; + return Status::Success; +} + +Status LoggingThermostat::GetAbsMaxHeatSetpointLimit(int16_t & value) const +{ + value = thermostat::kDefaultAbsMaxHeatSetpointLimit; + return Status::Success; +} + +Status LoggingThermostat::GetMinHeatSetpointLimit(int16_t & value) const +{ + value = mMinHeatSetpointLimit; + return Status::Success; +} + +Status LoggingThermostat::SetMinHeatSetpointLimit(int16_t value, bool & changed) +{ + changed = false; + // The cluster validates the complete setpoint state before saving it. Limit setters must not + // compare against the other stored limit, which may still be awaiting its update. + VerifyOrReturnValue(value >= thermostat::kDefaultAbsMinHeatSetpointLimit && + value <= thermostat::kDefaultAbsMaxHeatSetpointLimit, + Status::ConstraintError); + VerifyOrReturnValue(mMinHeatSetpointLimit != value, Status::Success); + VerifyOrReturnValue(mAttributeStorage != nullptr, Status::Failure); + CHIP_ERROR err = + AttributePersistence(*mAttributeStorage) + .StoreNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::MinHeatSetpointLimit::Id }, value); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "Thermostat: persisting min heating setpoint limit failed: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + mMinHeatSetpointLimit = value; + changed = true; + ChipLogProgress(AppServer, "Thermostat: min heating setpoint limit changed to %d", value); + return Status::Success; +} + +Status LoggingThermostat::GetMaxHeatSetpointLimit(int16_t & value) const +{ + value = mMaxHeatSetpointLimit; + return Status::Success; +} + +Status LoggingThermostat::SetMaxHeatSetpointLimit(int16_t value, bool & changed) +{ + changed = false; + VerifyOrReturnValue(value >= thermostat::kDefaultAbsMinHeatSetpointLimit && + value <= thermostat::kDefaultAbsMaxHeatSetpointLimit, + Status::ConstraintError); + VerifyOrReturnValue(mMaxHeatSetpointLimit != value, Status::Success); + VerifyOrReturnValue(mAttributeStorage != nullptr, Status::Failure); + CHIP_ERROR err = + AttributePersistence(*mAttributeStorage) + .StoreNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::MaxHeatSetpointLimit::Id }, value); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "Thermostat: persisting max heating setpoint limit failed: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + mMaxHeatSetpointLimit = value; + changed = true; + ChipLogProgress(AppServer, "Thermostat: max heating setpoint limit changed to %d", value); + return Status::Success; +} + +Status LoggingThermostat::GetAbsMinCoolSetpointLimit(int16_t & value) const +{ + value = thermostat::kDefaultAbsMinCoolSetpointLimit; + return Status::Success; +} + +Status LoggingThermostat::GetAbsMaxCoolSetpointLimit(int16_t & value) const +{ + value = thermostat::kDefaultAbsMaxCoolSetpointLimit; + return Status::Success; +} + +Status LoggingThermostat::GetMinCoolSetpointLimit(int16_t & value) const +{ + value = mMinCoolSetpointLimit; + return Status::Success; +} + +Status LoggingThermostat::SetMinCoolSetpointLimit(int16_t value, bool & changed) +{ + changed = false; + // As with heating limits, the other stored limit may not yet reflect the validated state. + VerifyOrReturnValue(value >= thermostat::kDefaultAbsMinCoolSetpointLimit && + value <= thermostat::kDefaultAbsMaxCoolSetpointLimit, + Status::ConstraintError); + VerifyOrReturnValue(mMinCoolSetpointLimit != value, Status::Success); + VerifyOrReturnValue(mAttributeStorage != nullptr, Status::Failure); + CHIP_ERROR err = + AttributePersistence(*mAttributeStorage) + .StoreNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::MinCoolSetpointLimit::Id }, value); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "Thermostat: persisting min cooling setpoint limit failed: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + mMinCoolSetpointLimit = value; + changed = true; + ChipLogProgress(AppServer, "Thermostat: min cooling setpoint limit changed to %d", value); + return Status::Success; +} + +Status LoggingThermostat::GetMaxCoolSetpointLimit(int16_t & value) const +{ + value = mMaxCoolSetpointLimit; + return Status::Success; +} + +Status LoggingThermostat::SetMaxCoolSetpointLimit(int16_t value, bool & changed) +{ + changed = false; + VerifyOrReturnValue(value >= thermostat::kDefaultAbsMinCoolSetpointLimit && + value <= thermostat::kDefaultAbsMaxCoolSetpointLimit, + Status::ConstraintError); + VerifyOrReturnValue(mMaxCoolSetpointLimit != value, Status::Success); + VerifyOrReturnValue(mAttributeStorage != nullptr, Status::Failure); + CHIP_ERROR err = + AttributePersistence(*mAttributeStorage) + .StoreNativeEndianValue({ GetEndpointId(), thermostat::Id, thermostat::Attributes::MaxCoolSetpointLimit::Id }, value); + if (err != CHIP_NO_ERROR) + { + ChipLogError(AppServer, "Thermostat: persisting max cooling setpoint limit failed: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + mMaxCoolSetpointLimit = value; + changed = true; + ChipLogProgress(AppServer, "Thermostat: max cooling setpoint limit changed to %d", value); + return Status::Success; +} + +Status LoggingThermostat::GetMinDeadband(int16_t & value) const +{ + value = thermostat::kDefaultDeadBand; + return Status::Success; +} + +Status LoggingThermostat::GetRunningMode(thermostat::ThermostatRunningModeEnum & value) const +{ + value = mRunningMode; + return Status::Success; +} + +Status LoggingThermostat::SetRunningMode(thermostat::ThermostatRunningModeEnum value, bool & changed) +{ + changed = false; + VerifyOrReturnValue(value == thermostat::ThermostatRunningModeEnum::kOff || + value == thermostat::ThermostatRunningModeEnum::kCool || + value == thermostat::ThermostatRunningModeEnum::kHeat, + Status::InvalidValue); + changed = (mRunningMode != value); + mRunningMode = value; + if (changed) + { + ChipLogProgress(AppServer, "Thermostat: RunningMode set to %u", static_cast(value)); + } + return Status::Success; +} + +Status LoggingThermostat::GetRunningState(BitMask & value) const +{ + value = mRunningState; + return Status::Success; +} + +Status LoggingThermostat::SetRunningState(BitMask value, bool & changed) +{ + changed = false; + VerifyOrReturnValue(mRunningState == value, Status::Success); + mRunningState = value; + changed = true; + ChipLogProgress(AppServer, "Thermostat: RunningState set to 0x%04x", value.Raw()); + return Status::Success; +} + +int8_t LoggingThermostat::GetLocalTemperatureCalibration() const +{ + return mCalibration; +} + +Status LoggingThermostat::SetLocalTemperatureCalibration(int8_t value, bool & changed) +{ + changed = false; + VerifyOrReturnValue(value >= -30 && value <= 30, Status::ConstraintError); + changed = (mCalibration != value); + mCalibration = value; + if (changed) + { + ChipLogProgress(AppServer, "Thermostat: LocalTemperatureCalibration set to %d", value); + } + return Status::Success; +} + +Status LoggingThermostat::GetRemoteSensing(BitMask & value) const +{ + value = mRemoteSensing; + return Status::Success; +} + +Status LoggingThermostat::SetRemoteSensing(BitMask value, bool & changed) +{ + changed = (mRemoteSensing != value); + mRemoteSensing = value; + if (changed) + { + ChipLogProgress(AppServer, "Thermostat: RemoteSensing set to 0x%02x", value.Raw()); + } + return Status::Success; +} + +void LoggingThermostat::UpdateSimulatedRunningState() +{ + thermostat::ThermostatRunningModeEnum newRunningMode = thermostat::ThermostatRunningModeEnum::kOff; + BitMask newRunningState; + + if (!mLocalTemperature.IsNull()) + { + int16_t currentTemp = mLocalTemperature.Value(); + if ((mSystemMode == thermostat::SystemModeEnum::kHeat || mSystemMode == thermostat::SystemModeEnum::kAuto) && + currentTemp < mHeatingSetpoint) + { + newRunningMode = thermostat::ThermostatRunningModeEnum::kHeat; + newRunningState.Set(thermostat::RelayStateBitmap::kHeat); + } + else if ((mSystemMode == thermostat::SystemModeEnum::kCool || mSystemMode == thermostat::SystemModeEnum::kAuto) && + currentTemp > mCoolingSetpoint) + { + newRunningMode = thermostat::ThermostatRunningModeEnum::kCool; + newRunningState.Set(thermostat::RelayStateBitmap::kCool); + } + } + + if (HasThermostatCluster()) + { + ThermostatCluster().SetRunningMode(newRunningMode); + ThermostatCluster().SetRunningState(newRunningState); + } + else + { + mRunningMode = newRunningMode; + mRunningState = newRunningState; + } +} + +void LoggingThermostat::OnTemperatureDisplayModeChanged( + Clusters::ThermostatUserInterfaceConfiguration::TemperatureDisplayModeEnum value) +{ + ChipLogProgress(AppServer, "Thermostat: TemperatureDisplayMode changed to %u", static_cast(value)); +} + +void LoggingThermostat::OnKeypadLockoutChanged(Clusters::ThermostatUserInterfaceConfiguration::KeypadLockoutEnum value) +{ + ChipLogProgress(AppServer, "Thermostat: KeypadLockout changed to %u", static_cast(value)); +} + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/thermostat/impl/LoggingThermostat.h b/examples/all-devices-app/all-devices-common/device/types/thermostat/impl/LoggingThermostat.h new file mode 100644 index 00000000000..b61fc598a52 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/thermostat/impl/LoggingThermostat.h @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +namespace chip::app { + +class LoggingThermostat : public Clusters::IdentifyDelegate, + public Clusters::Thermostat::Delegate, + public Clusters::Thermostat::ThermostatHeatingSetpoints::Delegate, + public Clusters::Thermostat::ThermostatCoolingSetpoints::Delegate, + public Clusters::Thermostat::ThermostatAutoSetpoints::Delegate, + public Clusters::Thermostat::ThermostatPresets::Delegate, + public Clusters::Thermostat::ThermostatHold::Delegate, + public Clusters::ThermostatUserInterfaceConfiguration::Delegate, + public FullThermostat +{ +public: + struct Context + { + FabricTable & fabricTable; + Credentials::GroupDataProvider & groupDataProvider; + TimerDelegate & timerDelegate; + Clusters::Thermostat::OptionalAttributes optionalAttributes = {}; + }; + + explicit LoggingThermostat(const Context & context); + ~LoggingThermostat() override = default; + + // IdentifyDelegate + void OnIdentifyStart(Clusters::IdentifyCluster & cluster) override; + void OnIdentifyStop(Clusters::IdentifyCluster & cluster) override; + void OnTriggerEffect(Clusters::IdentifyCluster & cluster) override {} + bool IsTriggerEffectEnabled() const override { return false; } + + // Thermostat and setpoint delegates share startup/shutdown and persistence. + CHIP_ERROR Startup(ServerClusterContext & context) override; + void Shutdown(ClusterShutdownType type) override; + FabricTable & GetFabricTable() const override { return mFabricTable; } + + DataModel::Nullable GetLocalTemperature() const override { return mLocalTemperature; } + Protocols::InteractionModel::Status SetLocalTemperature(DataModel::Nullable value, bool & changed) override; + + Clusters::Thermostat::SystemModeEnum GetSystemMode() const override { return mSystemMode; } + Protocols::InteractionModel::Status SetSystemMode(Clusters::Thermostat::SystemModeEnum value, bool & changed) override; + + Clusters::Thermostat::ControlSequenceOfOperationEnum GetControlSequenceOfOperation() const override; + Protocols::InteractionModel::Status SetControlSequenceOfOperation(Clusters::Thermostat::ControlSequenceOfOperationEnum value, + bool & changed) override; + + Protocols::InteractionModel::Status GetOccupiedHeatingSetpoint(int16_t & value) const override; + Protocols::InteractionModel::Status SetOccupiedHeatingSetpoint(int16_t value, bool & changed) override; + Protocols::InteractionModel::Status GetOccupiedCoolingSetpoint(int16_t & value) const override; + Protocols::InteractionModel::Status SetOccupiedCoolingSetpoint(int16_t value, bool & changed) override; + + // Optional Setpoint Limit Getters/Setters + Protocols::InteractionModel::Status GetAbsMinHeatSetpointLimit(int16_t & value) const override; + Protocols::InteractionModel::Status GetAbsMaxHeatSetpointLimit(int16_t & value) const override; + Protocols::InteractionModel::Status GetMinHeatSetpointLimit(int16_t & value) const override; + Protocols::InteractionModel::Status SetMinHeatSetpointLimit(int16_t value, bool & changed) override; + Protocols::InteractionModel::Status GetMaxHeatSetpointLimit(int16_t & value) const override; + Protocols::InteractionModel::Status SetMaxHeatSetpointLimit(int16_t value, bool & changed) override; + + Protocols::InteractionModel::Status GetAbsMinCoolSetpointLimit(int16_t & value) const override; + Protocols::InteractionModel::Status GetAbsMaxCoolSetpointLimit(int16_t & value) const override; + Protocols::InteractionModel::Status GetMinCoolSetpointLimit(int16_t & value) const override; + Protocols::InteractionModel::Status SetMinCoolSetpointLimit(int16_t value, bool & changed) override; + Protocols::InteractionModel::Status GetMaxCoolSetpointLimit(int16_t & value) const override; + Protocols::InteractionModel::Status SetMaxCoolSetpointLimit(int16_t value, bool & changed) override; + + // Optional Deadband Override + Protocols::InteractionModel::Status GetMinDeadband(int16_t & value) const override; + + // ThermostatPresets::Delegate + CHIP_ERROR GetPresetTypeAtIndex(size_t index, Clusters::Thermostat::Structs::PresetTypeStruct::Type & value) override; + uint8_t GetNumberOfPresets() override { return kPresetCapacity; } + CHIP_ERROR GetPresetAtIndex(size_t index, Clusters::Thermostat::PresetStructWithOwnedMembers & value) override; + CHIP_ERROR GetPendingPresetAtIndex(size_t index, Clusters::Thermostat::PresetStructWithOwnedMembers & value) override; + CHIP_ERROR GetActivePresetHandle(DataModel::Nullable & value) override; + CHIP_ERROR SetActivePresetHandle(const DataModel::Nullable & value) override; + void InitializePendingPresets() override; + void ClearPendingPresetList() override { mPendingPresetCount = 0; } + CHIP_ERROR AppendToPendingPresetList(const Clusters::Thermostat::PresetStructWithOwnedMembers & value) override; + CHIP_ERROR CommitPendingPresets() override; + std::optional GetMaxAtomicWriteTimeout(AttributeId attributeId) override; + + // ThermostatHold::Delegate + Clusters::Thermostat::TemperatureSetpointHoldEnum GetTemperatureSetpointHold() const override { return mSetpointHold; } + Protocols::InteractionModel::Status SetTemperatureSetpointHold(Clusters::Thermostat::TemperatureSetpointHoldEnum value, + bool & changed) override; + DataModel::Nullable GetTemperatureSetpointHoldDuration() const override { return mSetpointHoldDuration; } + Protocols::InteractionModel::Status SetTemperatureSetpointHoldDuration(DataModel::Nullable value, + bool & changed) override; + DataModel::Nullable GetSetpointHoldExpiryTimestamp() const override { return mSetpointHoldExpiryTimestamp; } + Protocols::InteractionModel::Status SetSetpointHoldExpiryTimestamp(DataModel::Nullable value, + bool & changed) override; + + Protocols::InteractionModel::Status GetRunningMode(Clusters::Thermostat::ThermostatRunningModeEnum & value) const override; + Protocols::InteractionModel::Status SetRunningMode(Clusters::Thermostat::ThermostatRunningModeEnum value, + bool & changed) override; + Protocols::InteractionModel::Status GetRunningState(BitMask & value) const override; + Protocols::InteractionModel::Status SetRunningState(BitMask value, + bool & changed) override; + + int8_t GetLocalTemperatureCalibration() const override; + Protocols::InteractionModel::Status SetLocalTemperatureCalibration(int8_t value, bool & changed) override; + + Protocols::InteractionModel::Status GetRemoteSensing(BitMask & value) const override; + Protocols::InteractionModel::Status SetRemoteSensing(BitMask value, + bool & changed) override; + + // ThermostatUserInterfaceConfiguration::Delegate + void OnTemperatureDisplayModeChanged(Clusters::ThermostatUserInterfaceConfiguration::TemperatureDisplayModeEnum value) override; + void OnKeypadLockoutChanged(Clusters::ThermostatUserInterfaceConfiguration::KeypadLockoutEnum value) override; + +protected: + CHIP_ERROR RegisterAdditionalClusters(EndpointId endpoint, CodeDrivenDataModelProvider & provider) override; + void UnregisterAdditionalClusters(CodeDrivenDataModelProvider & provider) override; + +private: + void UpdateSimulatedRunningState(); + + FabricTable & mFabricTable; + Credentials::GroupDataProvider & mGroupDataProvider; + AttributePersistenceProvider * mAttributeStorage = nullptr; + DataModel::Nullable mLocalTemperature = DataModel::MakeNullable(2500); + Clusters::Thermostat::SystemModeEnum mSystemMode = Clusters::Thermostat::SystemModeEnum::kOff; + int16_t mHeatingSetpoint = Clusters::Thermostat::kDefaultHeatingSetpoint; + int16_t mCoolingSetpoint = Clusters::Thermostat::kDefaultCoolingSetpoint; + int16_t mMinHeatSetpointLimit = Clusters::Thermostat::kDefaultAbsMinHeatSetpointLimit; + int16_t mMaxHeatSetpointLimit = Clusters::Thermostat::kDefaultAbsMaxHeatSetpointLimit; + int16_t mMinCoolSetpointLimit = Clusters::Thermostat::kDefaultAbsMinCoolSetpointLimit; + int16_t mMaxCoolSetpointLimit = Clusters::Thermostat::kDefaultAbsMaxCoolSetpointLimit; + Clusters::Thermostat::ThermostatRunningModeEnum mRunningMode = Clusters::Thermostat::ThermostatRunningModeEnum::kOff; + BitMask mRunningState; + BitMask mRemoteSensing; + int8_t mCalibration = 0; + + // One preset per scenario, with bounded storage for committed and pending lists. + static constexpr uint8_t kPresetCapacity = 3; + std::array mPresets; + std::array mPendingPresets; + uint8_t mPresetCount = 0; + uint8_t mPendingPresetCount = 0; + DataModel::Nullable mActivePresetHandle; + Clusters::Thermostat::TemperatureSetpointHoldEnum mSetpointHold = + Clusters::Thermostat::TemperatureSetpointHoldEnum::kSetpointHoldOff; + DataModel::Nullable mSetpointHoldDuration; + DataModel::Nullable mSetpointHoldExpiryTimestamp; + + LazyRegisteredServerCluster mGroupsCluster; + LazyRegisteredServerCluster mUserInterfaceCluster; +}; + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/water-valve/BUILD.gn b/examples/all-devices-app/all-devices-common/device/types/water-valve/BUILD.gn index 8cbb15529c6..3f4bf0b44a2 100644 --- a/examples/all-devices-app/all-devices-common/device/types/water-valve/BUILD.gn +++ b/examples/all-devices-app/all-devices-common/device/types/water-valve/BUILD.gn @@ -17,12 +17,17 @@ import("//build_overrides/chip.gni") source_set("water-valve") { sources = [ + "OOBAccessors.cpp", + "OOBAccessors.h", "WaterValve.cpp", "WaterValve.h", + "WaterValveAccessor.cpp", + "WaterValveAccessor.h", ] public_deps = [ "${chip_root}/examples/all-devices-app/all-devices-common/device/api:single-endpoint-device", + "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors", "${chip_root}/src/app/clusters/identify-server", "${chip_root}/src/app/clusters/valve-configuration-and-control-server", "${chip_root}/src/data-model-providers/codedriven", @@ -31,3 +36,12 @@ source_set("water-valve") { "${chip_root}/zzz_generated/app-common/devices", ] } + +source_set("logging") { + sources = [ + "impl/LoggingWaterValve.cpp", + "impl/LoggingWaterValve.h", + ] + + public_deps = [ ":water-valve" ] +} diff --git a/examples/all-devices-app/all-devices-common/device/types/water-valve/OOBAccessors.cpp b/examples/all-devices-app/all-devices-common/device/types/water-valve/OOBAccessors.cpp new file mode 100644 index 00000000000..36b7aa57ff4 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/water-valve/OOBAccessors.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OOBAccessors.h" +#include "WaterValve.h" +#include "WaterValveAccessor.h" +#include +#include + +namespace chip::app { + +void RegisterOOBAccessors(WaterValve & device, OOBAccessorRegistry & registry) +{ + LogErrorOnFailure(registry.Register( + std::make_unique(device.ValveConfigurationAndControlCluster(), device.GetEndpointId()))); +} + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/water-valve/OOBAccessors.h b/examples/all-devices-app/all-devices-common/device/types/water-valve/OOBAccessors.h new file mode 100644 index 00000000000..94dfc8f0cfc --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/water-valve/OOBAccessors.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace chip::app { + +void RegisterOOBAccessors(WaterValve & device, OOBAccessorRegistry & registry); + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValve.cpp b/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValve.cpp index 661d371e157..9768c2c9ffa 100644 --- a/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValve.cpp +++ b/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValve.cpp @@ -15,17 +15,19 @@ * limitations under the License. */ #include + +#include #include -#include using namespace chip::app::Clusters; namespace chip::app { -WaterValve::WaterValve(TimerDelegate & timerDelegate) : - SingleEndpoint(Span(&Device::Type::kWaterValve, 1)), mTimerDelegate(timerDelegate) +WaterValve::WaterValve(TimerDelegate & timerDelegate, Clusters::ValveConfigurationAndControl::Delegate & delegate, + const WaterValveConfig & config) : + SingleEndpoint(Span(&Device::Type::kWaterValve, 1)), + mTimerDelegate(timerDelegate), mValveDelegate(delegate), mConfig(config) {} - CHIP_ERROR WaterValve::Register(chip::EndpointId endpoint, CodeDrivenDataModelProvider & provider, EndpointComposition composition) { VerifyOrReturnError(mEndpointId == kInvalidEndpointId, CHIP_ERROR_INCORRECT_STATE); @@ -36,19 +38,13 @@ CHIP_ERROR WaterValve::Register(chip::EndpointId endpoint, CodeDrivenDataModelPr mIdentifyCluster.Create(IdentifyCluster::Config(endpoint, mTimerDelegate)); ReturnErrorOnFailure(provider.AddCluster(mIdentifyCluster.Registration())); - ValveConfigurationAndControlCluster::StartupConfiguration config{ DataModel::NullNullable, - ValveConfigurationAndControlCluster::kDefaultOpenLevel, - ValveConfigurationAndControlCluster::kDefaultLevelStep }; + mValveCluster.Create(endpoint, + ValveConfigurationAndControlCluster::ValveContext{ .features = mConfig.features, + .optionalAttributeSet = mConfig.optionalAttributes, + .config = mConfig.startup, + .tsTracker = mConfig.tsTracker, + .delegate = &mValveDelegate }); - ValveConfigurationAndControlCluster::ValveContext valveContext = { - .features = BitFlags(ValveConfigurationAndControl::Feature::kLevel), - .optionalAttributeSet = {}, - .config = config, - .tsTracker = nullptr, - .delegate = this, - }; - - mValveCluster.Create(endpoint, valveContext); ReturnErrorOnFailure(provider.AddCluster(mValveCluster.Registration())); ReturnErrorOnFailure(provider.AddEndpoint(mEndpointRegistration)); @@ -79,26 +75,4 @@ Clusters::ValveConfigurationAndControlCluster & WaterValve::ValveConfigurationAn return mValveCluster.Cluster(); } -DataModel::Nullable WaterValve::HandleOpenValve(DataModel::Nullable level) -{ - Percent targetLevel = level.ValueOr(100); - ChipLogProgress(AppServer, "WaterValve: Opening valve to level %u", targetLevel); - return DataModel::MakeNullable(targetLevel); -} - -CHIP_ERROR WaterValve::HandleCloseValve() -{ - ChipLogProgress(AppServer, "WaterValve: Closing valve"); - if (mValveCluster.IsConstructed()) - { - mValveCluster.Cluster().UpdateCurrentLevel(0); - } - return CHIP_NO_ERROR; -} - -void WaterValve::HandleRemainingDurationTick(uint32_t duration) -{ - ChipLogProgress(AppServer, "WaterValve: Remaining duration tick: %lu", static_cast(duration)); -} - } // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValve.h b/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValve.h index e66b9d2fd11..e082e586daa 100644 --- a/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValve.h +++ b/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValve.h @@ -24,10 +24,19 @@ namespace chip::app { -class WaterValve : public SingleEndpoint, public Clusters::ValveConfigurationAndControl::Delegate +struct WaterValveConfig +{ + BitFlags features{ Clusters::ValveConfigurationAndControl::Feature::kLevel }; + Clusters::ValveConfigurationAndControlCluster::OptionalAttributeSet optionalAttributes; + Clusters::ValveConfigurationAndControlCluster::StartupConfiguration startup{ DataModel::NullNullable }; + Clusters::ValveConfigurationAndControl::TimeSyncTracker * tsTracker = nullptr; +}; +class WaterValve : public SingleEndpoint { public: - WaterValve(TimerDelegate & timerDelegate); + explicit WaterValve(TimerDelegate & timerDelegate, Clusters::ValveConfigurationAndControl::Delegate & delegate, + const WaterValveConfig & config = {}); + ~WaterValve() override = default; CHIP_ERROR Register(chip::EndpointId endpoint, CodeDrivenDataModelProvider & provider, @@ -39,13 +48,17 @@ class WaterValve : public SingleEndpoint, public Clusters::ValveConfigurationAnd Clusters::ValveConfigurationAndControlCluster & ValveConfigurationAndControlCluster(); - // Clusters::ValveConfigurationAndControl::Delegate implementation - DataModel::Nullable HandleOpenValve(DataModel::Nullable level) override; - CHIP_ERROR HandleCloseValve() override; - void HandleRemainingDurationTick(uint32_t duration) override; + bool IsOpen() const + { + VerifyOrReturnValue(mValveCluster.IsConstructed(), false); + auto state = mValveCluster.Cluster().GetCurrentState(); + return !state.IsNull() && state.Value() == Clusters::ValveConfigurationAndControl::ValveStateEnum::kOpen; + } protected: TimerDelegate & mTimerDelegate; + Clusters::ValveConfigurationAndControl::Delegate & mValveDelegate; + const WaterValveConfig mConfig; LazyRegisteredServerCluster mIdentifyCluster; LazyRegisteredServerCluster mValveCluster; }; diff --git a/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValveAccessor.cpp b/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValveAccessor.cpp new file mode 100644 index 00000000000..d8cf4ddcdfb --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValveAccessor.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include +#include +#include + +using namespace chip::app::Clusters; + +namespace chip::app { + +std::optional WaterValveAccessor::HandleAction(CharSpan actionName, ByteSpan tlvBuffer) +{ + if (!actionName.data_equal(OOBDataSerializer::kSetAttributeAction)) + { + return std::nullopt; + } + + auto parseResult = OOBDataSerializer::ParseAttributeRequest(tlvBuffer); + if (std::holds_alternative(parseResult)) + { + CHIP_ERROR err = std::get(parseResult); + ChipLogError(Support, "Failed to parse attribute request: %" CHIP_ERROR_FORMAT, err.Format()); + return err; + } + + auto & request = std::get(parseResult); + if (request.path.mEndpointId != mEndpointId) + { + return std::nullopt; + } + + return SetAttribute(request.path, request.value); +} + +std::optional WaterValveAccessor::SetAttribute(const ConcreteDataAttributePath & path, TLV::TLVReader & reader) +{ + switch (path.mClusterId) + { + case ValveConfigurationAndControl::Id: { + switch (path.mAttributeId) + { + case ValveConfigurationAndControl::Attributes::CurrentState::Id: { + ValveConfigurationAndControl::ValveStateEnum state; + ReturnErrorOnFailure(DataModel::Decode(reader, state)); + VerifyOrReturnError(state == ValveConfigurationAndControl::ValveStateEnum::kOpen || + state == ValveConfigurationAndControl::ValveStateEnum::kClosed, + CHIP_IM_GLOBAL_STATUS(ConstraintError)); + + auto currentLevel = mCluster.GetCurrentLevel(); + if (!currentLevel.IsNull() && currentLevel.Value() > 0) + { + mLastOpenLevel = currentLevel.Value(); + } + + if (state == ValveConfigurationAndControl::ValveStateEnum::kClosed) + { + return mCluster.CloseValve(); + } + + return mCluster.OpenValve(DataModel::MakeNullable(mLastOpenLevel), mCluster.GetDefaultOpenDuration()); + } + case ValveConfigurationAndControl::Attributes::CurrentLevel::Id: { + DataModel::Nullable level; + ReturnErrorOnFailure(DataModel::Decode(reader, level)); + VerifyOrReturnError(!level.IsNull(), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + VerifyOrReturnError(level.Value() <= 100, CHIP_IM_GLOBAL_STATUS(ConstraintError)); + + if (level.Value() == 0) + { + auto currentLevel = mCluster.GetCurrentLevel(); + if (!currentLevel.IsNull() && currentLevel.Value() > 0) + { + mLastOpenLevel = currentLevel.Value(); + } + return mCluster.CloseValve(); + } + + mLastOpenLevel = level.Value(); + return mCluster.OpenValve(level, mCluster.GetDefaultOpenDuration()); + } + default: + return std::nullopt; + } + } + default: + return std::nullopt; + } +} + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValveAccessor.h b/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValveAccessor.h new file mode 100644 index 00000000000..1c8accfa473 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/water-valve/WaterValveAccessor.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip::app { + +class WaterValveAccessor : public OOBAccessor +{ +public: + WaterValveAccessor(Clusters::ValveConfigurationAndControlCluster & cluster, EndpointId endpointId) : + mCluster(cluster), mEndpointId(endpointId) + {} + + explicit WaterValveAccessor(WaterValve & device) : + WaterValveAccessor(device.ValveConfigurationAndControlCluster(), device.GetEndpointId()) + {} + + ~WaterValveAccessor() override = default; + + std::optional HandleAction(CharSpan actionName, ByteSpan tlvBuffer) override; + +private: + std::optional SetAttribute(const ConcreteDataAttributePath & path, TLV::TLVReader & reader); + + Clusters::ValveConfigurationAndControlCluster & mCluster; + EndpointId mEndpointId; + Percent mLastOpenLevel = Clusters::ValveConfigurationAndControlCluster::kDefaultOpenLevel; +}; + +} // namespace chip::app diff --git a/examples/all-devices-app/all-devices-common/device/types/water-valve/impl/LoggingWaterValve.cpp b/examples/all-devices-app/all-devices-common/device/types/water-valve/impl/LoggingWaterValve.cpp new file mode 100644 index 00000000000..5832d24d9cb --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/water-valve/impl/LoggingWaterValve.cpp @@ -0,0 +1,58 @@ +/* + * + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +namespace chip { +namespace app { + +DataModel::Nullable LoggingWaterValve::HandleOpenValve(DataModel::Nullable level) +{ + if (mValveCluster.IsConstructed() && + !mValveCluster.Cluster().GetFeatureMap().Has(Clusters::ValveConfigurationAndControl::Feature::kLevel)) + { + ChipLogProgress(AppServer, "WaterValve: Opening valve"); + mValveCluster.Cluster().UpdateCurrentState(Clusters::ValveConfigurationAndControl::ValveStateEnum::kOpen); + return DataModel::NullNullable; + } + + Percent targetLevel = level.ValueOr(100); + ChipLogProgress(AppServer, "WaterValve: Opening valve to level %u", targetLevel); + + return DataModel::MakeNullable(targetLevel); +} + +CHIP_ERROR LoggingWaterValve::HandleCloseValve() +{ + ChipLogProgress(AppServer, "WaterValve: Closing valve"); + if (mValveCluster.IsConstructed()) + { + mValveCluster.Cluster().UpdateCurrentLevel(0); + mValveCluster.Cluster().UpdateCurrentState(Clusters::ValveConfigurationAndControl::ValveStateEnum::kClosed); + } + + return CHIP_NO_ERROR; +} + +void LoggingWaterValve::HandleRemainingDurationTick(uint32_t duration) +{ + ChipLogProgress(AppServer, "WaterValve: Remaining duration tick: %lu", static_cast(duration)); +} +} // namespace app +} // namespace chip diff --git a/examples/all-devices-app/all-devices-common/device/types/water-valve/impl/LoggingWaterValve.h b/examples/all-devices-app/all-devices-common/device/types/water-valve/impl/LoggingWaterValve.h new file mode 100644 index 00000000000..c49d53ae0d2 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/device/types/water-valve/impl/LoggingWaterValve.h @@ -0,0 +1,37 @@ +/* + * + * Copyright (c) 2026 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include + +namespace chip { +namespace app { + +class LoggingWaterValve : public Clusters::ValveConfigurationAndControl::Delegate, public WaterValve +{ +public: + explicit LoggingWaterValve(TimerDelegate & timerDelegate, const WaterValveConfig & config = {}) : + WaterValve(timerDelegate, *this){}; + ~LoggingWaterValve() override = default; + + DataModel::Nullable HandleOpenValve(DataModel::Nullable level) override; + CHIP_ERROR HandleCloseValve() override; + void HandleRemainingDurationTick(uint32_t duration) override; +}; + +} // namespace app +} // namespace chip diff --git a/examples/all-devices-app/all-devices-common/oob-accessors/OOBAccessorHook.h b/examples/all-devices-app/all-devices-common/oob-accessors/OOBAccessorHook.h index 273b9d5ea98..f34f8555274 100644 --- a/examples/all-devices-app/all-devices-common/oob-accessors/OOBAccessorHook.h +++ b/examples/all-devices-app/all-devices-common/oob-accessors/OOBAccessorHook.h @@ -32,8 +32,11 @@ #include #include #include +#include #include #include +#include +#include namespace chip::app { diff --git a/examples/all-devices-app/all-devices-common/oob-accessors/all_devices_config.cmake b/examples/all-devices-app/all-devices-common/oob-accessors/all_devices_config.cmake index fe7879e1861..f61f92c429b 100644 --- a/examples/all-devices-app/all-devices-common/oob-accessors/all_devices_config.cmake +++ b/examples/all-devices-app/all-devices-common/oob-accessors/all_devices_config.cmake @@ -54,7 +54,13 @@ if(ALL_DEVICES_APP_ENABLE_OOB_ACCESSORS) "${ALL_DEVICES_COMMON_DIR}/device/types/occupancy-sensor/OOBAccessors.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/on-off-light/OOBAccessors.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/on-off-plug-in-unit/OOBAccessors.cpp" + "${ALL_DEVICES_COMMON_DIR}/device/types/power-source/BatteryPowerSourceAccessor.cpp" + "${ALL_DEVICES_COMMON_DIR}/device/types/power-source/OOBAccessors.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/robotic-vacuum-cleaner/OOBAccessors.cpp" "${ALL_DEVICES_COMMON_DIR}/device/types/root-node/OOBAccessors.cpp" + "${ALL_DEVICES_COMMON_DIR}/device/types/soil-sensor/OOBAccessors.cpp" + "${ALL_DEVICES_COMMON_DIR}/device/types/soil-sensor/SoilSensorAccessor.cpp" + "${ALL_DEVICES_COMMON_DIR}/device/types/water-valve/OOBAccessors.cpp" + "${ALL_DEVICES_COMMON_DIR}/device/types/water-valve/WaterValveAccessor.cpp" ) endif() diff --git a/examples/all-devices-app/all-devices-common/oob-accessors/tests/BUILD.gn b/examples/all-devices-app/all-devices-common/oob-accessors/tests/BUILD.gn index d4c169e59fd..9cf7c2209fa 100644 --- a/examples/all-devices-app/all-devices-common/oob-accessors/tests/BUILD.gn +++ b/examples/all-devices-app/all-devices-common/oob-accessors/tests/BUILD.gn @@ -22,13 +22,19 @@ if (chip_build_tests) { output_name = "TestOOBAccessors" test_sources = [ + "TestBatteryPowerSourceAccessor.cpp", "TestOOBAccessors.cpp", "TestOOBDataSerializer.cpp", + "TestSoilSensorAccessor.cpp", + "TestWaterValveAccessor.cpp", ] cflags = [ "-Wconversion" ] public_deps = [ + "${chip_root}/examples/all-devices-app/all-devices-common/device/types/power-source", + "${chip_root}/examples/all-devices-app/all-devices-common/device/types/soil-sensor:increasing", + "${chip_root}/examples/all-devices-app/all-devices-common/device/types/water-valve:logging", "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors", "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors/clusters:ambient-context", "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors/clusters:basic-information", @@ -38,7 +44,9 @@ if (chip_build_tests) { "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors/clusters:occupancy", "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors/clusters:on-off", "${chip_root}/examples/all-devices-app/all-devices-common/oob-accessors/clusters:rvc", + "${chip_root}/src/app/persistence:default", "${chip_root}/src/app/server-cluster/testing", + "${chip_root}/src/data-model-providers/codedriven", "${chip_root}/src/lib/support:testing", "${chip_root}/src/platform", ] diff --git a/examples/all-devices-app/all-devices-common/oob-accessors/tests/TestBatteryPowerSourceAccessor.cpp b/examples/all-devices-app/all-devices-common/oob-accessors/tests/TestBatteryPowerSourceAccessor.cpp new file mode 100644 index 00000000000..76b3609ba93 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/oob-accessors/tests/TestBatteryPowerSourceAccessor.cpp @@ -0,0 +1,172 @@ +/* + * + * Copyright (c) 2026 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; + +namespace { + +class TestBatteryPowerSourceAccessor : public ::testing::Test +{ +public: + static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } + static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } + +protected: + void SetUp() override + { + EXPECT_EQ(chip::DeviceLayer::PlatformMgr().InitChipStack(), CHIP_NO_ERROR); + EXPECT_EQ(mDevice.Register(1, mProvider), CHIP_NO_ERROR); + EXPECT_EQ(mRegistry.Register( + std::make_unique(mDevice.BatteryPowerSourceCluster(), mDevice.GetEndpointId())), + CHIP_NO_ERROR); + } + + void TearDown() override + { + mDevice.Unregister(mProvider); + chip::DeviceLayer::PlatformMgr().Shutdown(); + } + + DefaultTimerDelegate mTimerDelegate; + Testing::TestServerClusterContext mClusterContext; + CodeDrivenDataModelProvider mProvider{ mClusterContext.StorageDelegate(), mClusterContext.AttributePersistenceProvider() }; + BatteryPowerSource mDevice{ "Test Battery"_span, Clusters::PowerSource::BatReplaceabilityEnum::kUserReplaceable, + mTimerDelegate }; + InMemoryOOBAccessorRegistry mRegistry; +}; + +// Initial state: verifies non-null default battery percentage +TEST_F(TestBatteryPowerSourceAccessor, InitialAttributeValues) +{ + auto battery = mDevice.BatteryPowerSourceCluster().GetBatPercentRemaining(); + ASSERT_FALSE(battery.IsNull()); + EXPECT_EQ(battery.Value(), 200); +} + +// Test 1: Setting battery percentage (e.g. 170 = 85%) +TEST_F(TestBatteryPowerSourceAccessor, SetBatPercentRemaining_ValidValue) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, PowerSource::Id, PowerSource::Attributes::BatPercentRemaining::Id), static_cast(170)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto battery = mDevice.BatteryPowerSourceCluster().GetBatPercentRemaining(); + ASSERT_FALSE(battery.IsNull()); + EXPECT_EQ(battery.Value(), 170); +} + +// Test 2: Setting null battery percentage +TEST_F(TestBatteryPowerSourceAccessor, SetBatPercentRemaining_Null) +{ + ConcreteDataAttributePath path(1, PowerSource::Id, PowerSource::Attributes::BatPercentRemaining::Id); + + uint8_t buffer[64]; + TLV::TLVWriter writer; + writer.Init(buffer); + EXPECT_EQ(writer.PutNull(TLV::AnonymousTag()), CHIP_NO_ERROR); + EXPECT_EQ(writer.Finalize(), CHIP_NO_ERROR); + + TLV::TLVReader reader; + reader.Init(buffer, writer.GetLengthWritten()); + EXPECT_EQ(reader.Next(), CHIP_NO_ERROR); + + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest(path, reader); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto battery = mDevice.BatteryPowerSourceCluster().GetBatPercentRemaining(); + EXPECT_TRUE(battery.IsNull()); +} + +// Test 3: Setting battery percentage > 200 returns ConstraintError +TEST_F(TestBatteryPowerSourceAccessor, SetBatPercentRemaining_ConstraintError) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, PowerSource::Id, PowerSource::Attributes::BatPercentRemaining::Id), static_cast(201)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_IM_GLOBAL_STATUS(ConstraintError)); +} + +// Test 4: Setting battery voltage (e.g. 3300 mV) +TEST_F(TestBatteryPowerSourceAccessor, SetBatVoltage_ValidValue) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, PowerSource::Id, PowerSource::Attributes::BatVoltage::Id), static_cast(3300)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto voltage = mDevice.BatteryPowerSourceCluster().GetBatVoltage(); + ASSERT_FALSE(voltage.IsNull()); + EXPECT_EQ(voltage.Value(), 3300U); +} + +// Test 5: Rejects unsupported attributes on PowerSource cluster (returns std::nullopt -> NOT_FOUND in registry) +TEST_F(TestBatteryPowerSourceAccessor, RejectUnsupportedAttribute) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, PowerSource::Id, PowerSource::Attributes::Order::Id), static_cast(1)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_ERROR_NOT_FOUND); +} + +// Test 6: Unhandled action, wrong cluster, or wrong endpoint +TEST_F(TestBatteryPowerSourceAccessor, UnhandledActionOrEndpoint) +{ + // Wrong action + uint8_t dummy[4] = { 0 }; + EXPECT_EQ(mRegistry.HandleAction("UnknownAction"_span, ByteSpan(dummy, sizeof(dummy))), CHIP_ERROR_NOT_FOUND); + + // Wrong cluster (OnOff) + { + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, Clusters::OnOff::Id, Clusters::OnOff::Attributes::OnOff::Id), true); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_ERROR_NOT_FOUND); + } + + // Wrong endpoint (endpoint 99) + { + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(99, PowerSource::Id, PowerSource::Attributes::BatPercentRemaining::Id), static_cast(50)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_ERROR_NOT_FOUND); + } +} + +} // namespace diff --git a/examples/all-devices-app/all-devices-common/oob-accessors/tests/TestSoilSensorAccessor.cpp b/examples/all-devices-app/all-devices-common/oob-accessors/tests/TestSoilSensorAccessor.cpp new file mode 100644 index 00000000000..f90207ac77a --- /dev/null +++ b/examples/all-devices-app/all-devices-common/oob-accessors/tests/TestSoilSensorAccessor.cpp @@ -0,0 +1,315 @@ +/* + * + * Copyright (c) 2026 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; + +namespace { + +const Globals::Structs::MeasurementAccuracyRangeStruct::Type kAccuracyRange[] = { + { .rangeMin = 0, .rangeMax = 100, .percentMax = MakeOptional(static_cast(10)) } +}; + +const SoilMeasurement::Attributes::SoilMoistureMeasurementLimits::TypeInfo::Type kDefaultMoistureLimits = { + .measurementType = Globals::MeasurementTypeEnum::kSoilMoisture, + .measured = true, + .minMeasuredValue = 0, + .maxMeasuredValue = 100, + .accuracyRanges = DataModel::List(kAccuracyRange), +}; + +const TemperatureMeasurementCluster::StartupConfiguration kDefaultTempConfig = { + .minMeasuredValue = DataModel::MakeNullable(static_cast(-1000)), + .maxMeasuredValue = DataModel::MakeNullable(static_cast(5000)), + .tolerance = 0, +}; + +template +DataModel::Nullable ReadMoisture(DeviceType & device) +{ + Testing::ClusterTester tester(device.SoilMeasurementCluster()); + SoilMeasurement::Attributes::SoilMoistureMeasuredValue::TypeInfo::DecodableType moisture; + EXPECT_TRUE(tester.ReadAttribute(SoilMeasurement::Attributes::SoilMoistureMeasuredValue::Id, moisture).IsSuccess()); + return moisture; +} + +template +DataModel::Nullable ReadTemperature(DeviceType & device) +{ + Testing::ClusterTester tester(device.TemperatureMeasurementCluster()); + TemperatureMeasurement::Attributes::MeasuredValue::TypeInfo::DecodableType temp; + EXPECT_TRUE(tester.ReadAttribute(TemperatureMeasurement::Attributes::MeasuredValue::Id, temp).IsSuccess()); + return temp; +} + +class TestSoilSensorAccessor : public ::testing::Test +{ +public: + static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } + static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } + +protected: + void SetUp() override + { + EXPECT_EQ(chip::DeviceLayer::PlatformMgr().InitChipStack(), CHIP_NO_ERROR); + EXPECT_EQ(mDevice.Register(1, mProvider), CHIP_NO_ERROR); + RegisterOOBAccessors(mDevice, mRegistry); + } + + void TearDown() override + { + mDevice.Unregister(mProvider); + chip::DeviceLayer::PlatformMgr().Shutdown(); + } + + DefaultTimerDelegate mTimerDelegate; + Testing::TestServerClusterContext mClusterContext; + CodeDrivenDataModelProvider mProvider{ mClusterContext.StorageDelegate(), mClusterContext.AttributePersistenceProvider() }; + SoilSensor mDevice{ mTimerDelegate, kDefaultMoistureLimits, kDefaultTempConfig }; + InMemoryOOBAccessorRegistry mRegistry; +}; + +// Test 1: Verify initial attribute values on SoilSensor +TEST_F(TestSoilSensorAccessor, InitialAttributeValues) +{ + auto moisture = ReadMoisture(mDevice); + EXPECT_TRUE(moisture.IsNull()); + + auto temp = ReadTemperature(mDevice); + EXPECT_TRUE(temp.IsNull()); + + ReadOnlyBufferBuilder builder; + EXPECT_EQ(mDevice.DeviceTypes(builder), CHIP_NO_ERROR); + auto types = builder.TakeBuffer(); + EXPECT_EQ(types.size(), 1U); + EXPECT_EQ(types[0].deviceTypeId, Device::Type::kSoilSensor.deviceTypeId); +} + +// Test 2: Setting soil moisture to a direct valid percentage +TEST_F(TestSoilSensorAccessor, SetMoisture_DirectValue42) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, SoilMeasurement::Id, SoilMeasurement::Attributes::SoilMoistureMeasuredValue::Id), + static_cast(42)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto moisture = ReadMoisture(mDevice); + ASSERT_FALSE(moisture.IsNull()); + EXPECT_EQ(moisture.Value(), 42); +} + +// Test 3: Setting soil moisture to null (e.g. sensor probe fault) +TEST_F(TestSoilSensorAccessor, SetMoisture_NullProbeFault) +{ + // Set a non-null moisture first to verify transition to null + auto setValidResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, SoilMeasurement::Id, SoilMeasurement::Attributes::SoilMoistureMeasuredValue::Id), + static_cast(42)); + ASSERT_FALSE(std::holds_alternative(setValidResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(setValidResult)), + CHIP_NO_ERROR); + EXPECT_EQ(ReadMoisture(mDevice).Value(), 42); + + ConcreteDataAttributePath path(1, SoilMeasurement::Id, SoilMeasurement::Attributes::SoilMoistureMeasuredValue::Id); + + uint8_t buffer[64]; + TLV::TLVWriter writer; + writer.Init(buffer); + EXPECT_EQ(writer.PutNull(TLV::AnonymousTag()), CHIP_NO_ERROR); + EXPECT_EQ(writer.Finalize(), CHIP_NO_ERROR); + + TLV::TLVReader reader; + reader.Init(buffer, writer.GetLengthWritten()); + EXPECT_EQ(reader.Next(), CHIP_NO_ERROR); + + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest(path, reader); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto moisture = ReadMoisture(mDevice); + EXPECT_TRUE(moisture.IsNull()); +} + +// Test 4: Setting temperature to a direct valid value (21.50°C -> 2150 in 0.01°C units) +TEST_F(TestSoilSensorAccessor, SetTemperature_DirectValue2150) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, TemperatureMeasurement::Id, TemperatureMeasurement::Attributes::MeasuredValue::Id), + static_cast(2150)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto temp = ReadTemperature(mDevice); + ASSERT_FALSE(temp.IsNull()); + EXPECT_EQ(temp.Value(), 2150); +} + +// Test 5: Setting temperature to null (e.g. thermistor sensor fault) +TEST_F(TestSoilSensorAccessor, SetTemperature_NullSensorFault) +{ + // Set a non-null temperature first to verify transition to null + auto setValidResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, TemperatureMeasurement::Id, TemperatureMeasurement::Attributes::MeasuredValue::Id), + static_cast(2150)); + ASSERT_FALSE(std::holds_alternative(setValidResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(setValidResult)), + CHIP_NO_ERROR); + EXPECT_EQ(ReadTemperature(mDevice).Value(), 2150); + + ConcreteDataAttributePath path(1, TemperatureMeasurement::Id, TemperatureMeasurement::Attributes::MeasuredValue::Id); + + uint8_t buffer[64]; + TLV::TLVWriter writer; + writer.Init(buffer); + EXPECT_EQ(writer.PutNull(TLV::AnonymousTag()), CHIP_NO_ERROR); + EXPECT_EQ(writer.Finalize(), CHIP_NO_ERROR); + + TLV::TLVReader reader; + reader.Init(buffer, writer.GetLengthWritten()); + EXPECT_EQ(reader.Next(), CHIP_NO_ERROR); + + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest(path, reader); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto temp = ReadTemperature(mDevice); + EXPECT_TRUE(temp.IsNull()); +} + +// Test 6: Setting out-of-range moisture or temperature returns ConstraintError +TEST_F(TestSoilSensorAccessor, OutOfRangeValuesReturnConstraintError) +{ + // Out-of-range moisture (> 100%) + auto moistureResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, SoilMeasurement::Id, SoilMeasurement::Attributes::SoilMoistureMeasuredValue::Id), + static_cast(101)); + ASSERT_FALSE(std::holds_alternative(moistureResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(moistureResult)), + CHIP_IM_GLOBAL_STATUS(ConstraintError)); + + // Temperature below minMeasuredValue (< -1000, i.e. < -10.00°C) + auto tempLowResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, TemperatureMeasurement::Id, TemperatureMeasurement::Attributes::MeasuredValue::Id), + static_cast(-1001)); + ASSERT_FALSE(std::holds_alternative(tempLowResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(tempLowResult)), + CHIP_IM_GLOBAL_STATUS(ConstraintError)); + + // Temperature above maxMeasuredValue (> 5000, i.e. > 50.00°C) + auto tempHighResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, TemperatureMeasurement::Id, TemperatureMeasurement::Attributes::MeasuredValue::Id), + static_cast(5001)); + ASSERT_FALSE(std::holds_alternative(tempHighResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(tempHighResult)), + CHIP_IM_GLOBAL_STATUS(ConstraintError)); +} + +// Test 7: Attribute update for unsupported cluster returns std::nullopt -> NOT_FOUND in registry +TEST_F(TestSoilSensorAccessor, RejectUnsupportedCluster) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, Identify::Id, static_cast(0)), static_cast(1)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_ERROR_NOT_FOUND); +} + +// Test 8: Unrecognized attribute returns std::nullopt -> NOT_FOUND in registry +TEST_F(TestSoilSensorAccessor, RejectUnsupportedAttribute) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, SoilMeasurement::Id, static_cast(0xFFFF)), static_cast(1)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_ERROR_NOT_FOUND); +} + +// Test 9: Unhandled action or wrong endpoint +TEST_F(TestSoilSensorAccessor, UnhandledActionOrEndpoint) +{ + uint8_t dummy[4] = { 0 }; + EXPECT_EQ(mRegistry.HandleAction("UnknownAction"_span, ByteSpan(dummy, sizeof(dummy))), CHIP_ERROR_NOT_FOUND); + + // Wrong endpoint + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(99, SoilMeasurement::Id, SoilMeasurement::Attributes::SoilMoistureMeasuredValue::Id), + static_cast(10)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_ERROR_NOT_FOUND); +} + +// Test 10: IncreasingMoistureSoilSensor registers, supports OOB accessors, and advances simulation from OOB state +TEST_F(TestSoilSensorAccessor, IncreasingMoistureSoilSensor_RegisterAndOOB) +{ + IncreasingMoistureSoilSensor sensor; + EXPECT_EQ(sensor.Register(2, mProvider), CHIP_NO_ERROR); + + InMemoryOOBAccessorRegistry registry; + RegisterOOBAccessors(sensor, registry); + + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(2, SoilMeasurement::Id, SoilMeasurement::Attributes::SoilMoistureMeasuredValue::Id), + static_cast(77)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(registry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto moisture = ReadMoisture(sensor); + ASSERT_FALSE(moisture.IsNull()); + EXPECT_EQ(moisture.Value(), 77); + + auto tempResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(2, TemperatureMeasurement::Id, TemperatureMeasurement::Attributes::MeasuredValue::Id), + static_cast(2150)); + ASSERT_FALSE(std::holds_alternative(tempResult)); + EXPECT_EQ(registry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(tempResult)), + CHIP_NO_ERROR); + EXPECT_EQ(ReadTemperature(sensor).Value(), 2150); + + // Verify TimerFired advances from current cluster values rather than resetting to private defaults + sensor.TimerFired(); + EXPECT_EQ(ReadMoisture(sensor).Value(), 78); + EXPECT_EQ(ReadTemperature(sensor).Value(), 2250); + + sensor.Unregister(mProvider); +} + +} // namespace diff --git a/examples/all-devices-app/all-devices-common/oob-accessors/tests/TestWaterValveAccessor.cpp b/examples/all-devices-app/all-devices-common/oob-accessors/tests/TestWaterValveAccessor.cpp new file mode 100644 index 00000000000..72c4106c093 --- /dev/null +++ b/examples/all-devices-app/all-devices-common/oob-accessors/tests/TestWaterValveAccessor.cpp @@ -0,0 +1,351 @@ +/* + * + * Copyright (c) 2026 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; + +namespace { + +class TestWaterValveAccessor : public ::testing::Test +{ +public: + static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } + static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } + +protected: + void SetUp() override + { + EXPECT_EQ(chip::DeviceLayer::PlatformMgr().InitChipStack(), CHIP_NO_ERROR); + EXPECT_EQ(mDevice.Register(1, mProvider), CHIP_NO_ERROR); + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().Startup(mClusterContext.Get()), CHIP_NO_ERROR); + EXPECT_EQ(mRegistry.Register(std::make_unique(mDevice)), CHIP_NO_ERROR); + } + + void TearDown() override + { + mDevice.ValveConfigurationAndControlCluster().Shutdown(ClusterShutdownType::kClusterShutdown); + mDevice.Unregister(mProvider); + chip::DeviceLayer::PlatformMgr().Shutdown(); + } + + DefaultTimerDelegate mTimerDelegate; + Testing::TestServerClusterContext mClusterContext; + CodeDrivenDataModelProvider mProvider{ mClusterContext.StorageDelegate(), mClusterContext.AttributePersistenceProvider() }; + LoggingWaterValve mDevice{ mTimerDelegate }; + InMemoryOOBAccessorRegistry mRegistry; +}; + +// Test 1: Setting CurrentState to kOpen +TEST_F(TestWaterValveAccessor, SetCurrentState_Open) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentState::Id), + static_cast(ValveConfigurationAndControl::ValveStateEnum::kOpen)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto state = mDevice.ValveConfigurationAndControlCluster().GetCurrentState(); + ASSERT_FALSE(state.IsNull()); + EXPECT_EQ(state.Value(), ValveConfigurationAndControl::ValveStateEnum::kOpen); +} + +// Test 2: Setting CurrentState to kClosed +TEST_F(TestWaterValveAccessor, SetCurrentState_Closed) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentState::Id), + static_cast(ValveConfigurationAndControl::ValveStateEnum::kClosed)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto state = mDevice.ValveConfigurationAndControlCluster().GetCurrentState(); + ASSERT_FALSE(state.IsNull()); + EXPECT_EQ(state.Value(), ValveConfigurationAndControl::ValveStateEnum::kClosed); +} + +// Test 3: Setting CurrentLevel to 60% +TEST_F(TestWaterValveAccessor, SetCurrentLevel_Level60) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentLevel::Id), + static_cast(60)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto level = mDevice.ValveConfigurationAndControlCluster().GetCurrentLevel(); + ASSERT_FALSE(level.IsNull()); + EXPECT_EQ(level.Value(), 60); + + auto state = mDevice.ValveConfigurationAndControlCluster().GetCurrentState(); + ASSERT_FALSE(state.IsNull()); + EXPECT_EQ(state.Value(), ValveConfigurationAndControl::ValveStateEnum::kOpen); +} + +// Test 4: Setting CurrentLevel to 0 closes valve +TEST_F(TestWaterValveAccessor, SetCurrentLevel_ZeroClosesValve) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentLevel::Id), + static_cast(0)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + + auto state = mDevice.ValveConfigurationAndControlCluster().GetCurrentState(); + ASSERT_FALSE(state.IsNull()); + EXPECT_EQ(state.Value(), ValveConfigurationAndControl::ValveStateEnum::kClosed); +} + +// Test 5: Reopening valve preserves LastOpenLevel +TEST_F(TestWaterValveAccessor, ReopenPreservesLastOpenLevel) +{ + // 1. Set level to 75 + { + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentLevel::Id), + static_cast(75)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + } + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentLevel().Value(), 75); + + // 2. Close valve via CurrentState + { + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentState::Id), + static_cast(ValveConfigurationAndControl::ValveStateEnum::kClosed)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + } + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentState().Value(), + ValveConfigurationAndControl::ValveStateEnum::kClosed); + + // 3. Reopen valve via CurrentState + { + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentState::Id), + static_cast(ValveConfigurationAndControl::ValveStateEnum::kOpen)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_NO_ERROR); + } + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentState().Value(), + ValveConfigurationAndControl::ValveStateEnum::kOpen); + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentLevel().Value(), 75); +} + +// Test 6: Rejects invalid CurrentState (e.g. kTransitioning) +TEST_F(TestWaterValveAccessor, RejectsInvalidCurrentState) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentState::Id), + static_cast(ValveConfigurationAndControl::ValveStateEnum::kTransitioning)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_IM_GLOBAL_STATUS(ConstraintError)); +} + +// Test 7: Rejects invalid CurrentLevel (null or > 100) +TEST_F(TestWaterValveAccessor, RejectsInvalidCurrentLevel) +{ + // Null level + { + ConcreteDataAttributePath path(1, ValveConfigurationAndControl::Id, + ValveConfigurationAndControl::Attributes::CurrentLevel::Id); + uint8_t buffer[64]; + TLV::TLVWriter writer; + writer.Init(buffer); + EXPECT_EQ(writer.PutNull(TLV::AnonymousTag()), CHIP_NO_ERROR); + EXPECT_EQ(writer.Finalize(), CHIP_NO_ERROR); + + TLV::TLVReader reader; + reader.Init(buffer, writer.GetLengthWritten()); + EXPECT_EQ(reader.Next(), CHIP_NO_ERROR); + + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest(path, reader); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + + // Level > 100 + { + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentLevel::Id), + static_cast(101)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } +} + +// Test 8: Unrecognized attribute returns std::nullopt -> NOT_FOUND in registry +TEST_F(TestWaterValveAccessor, RejectUnsupportedAttribute) +{ + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, static_cast(0xFFFF)), static_cast(1)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_ERROR_NOT_FOUND); +} + +// Test 9: Unhandled action or wrong endpoint +TEST_F(TestWaterValveAccessor, UnhandledActionOrEndpoint) +{ + uint8_t dummy[4] = { 0 }; + EXPECT_EQ(mRegistry.HandleAction("UnknownAction"_span, ByteSpan(dummy, sizeof(dummy))), CHIP_ERROR_NOT_FOUND); + + // Wrong cluster on valid endpoint + { + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, static_cast(0xFFFF), ValveConfigurationAndControl::Attributes::CurrentState::Id), + static_cast(ValveConfigurationAndControl::ValveStateEnum::kOpen)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_ERROR_NOT_FOUND); + } + + // Wrong endpoint + auto buildResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(99, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentState::Id), + static_cast(ValveConfigurationAndControl::ValveStateEnum::kOpen)); + ASSERT_FALSE(std::holds_alternative(buildResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(buildResult)), + CHIP_ERROR_NOT_FOUND); +} + +// Test 10: In-band OpenValve level is preserved when closed and reopened via OOB SetAttribute(CurrentState) +TEST_F(TestWaterValveAccessor, InBandOpenLevelPreservedOnOOBReopen) +{ + // Open in-band with 40% + EXPECT_EQ( + mDevice.ValveConfigurationAndControlCluster().OpenValve(DataModel::MakeNullable(40), DataModel::NullNullable), + CHIP_NO_ERROR); + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentState().Value(), + ValveConfigurationAndControl::ValveStateEnum::kOpen); + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentLevel().Value(), 40); + + // Close via OOB SetAttribute(CurrentState = kClosed) + auto closeResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentState::Id), + static_cast(ValveConfigurationAndControl::ValveStateEnum::kClosed)); + ASSERT_FALSE(std::holds_alternative(closeResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(closeResult)), + CHIP_NO_ERROR); + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentState().Value(), + ValveConfigurationAndControl::ValveStateEnum::kClosed); + + // Reopen via OOB CurrentState = kOpen + auto openResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentState::Id), + static_cast(ValveConfigurationAndControl::ValveStateEnum::kOpen)); + ASSERT_FALSE(std::holds_alternative(openResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(openResult)), + CHIP_NO_ERROR); + + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentState().Value(), + ValveConfigurationAndControl::ValveStateEnum::kOpen); + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentLevel().Value(), 40); +} + +// Test 11: In-band OpenValve level is preserved when closed via OOB CurrentLevel = 0 and reopened via OOB CurrentState = kOpen +TEST_F(TestWaterValveAccessor, InBandOpenLevelPreservedOnZeroLevelCloseAndReopen) +{ + // Open in-band with 60% + EXPECT_EQ( + mDevice.ValveConfigurationAndControlCluster().OpenValve(DataModel::MakeNullable(60), DataModel::NullNullable), + CHIP_NO_ERROR); + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentState().Value(), + ValveConfigurationAndControl::ValveStateEnum::kOpen); + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentLevel().Value(), 60); + + // Close via OOB SetAttribute(CurrentLevel = 0) + auto closeResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentLevel::Id), + static_cast(0)); + ASSERT_FALSE(std::holds_alternative(closeResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(closeResult)), + CHIP_NO_ERROR); + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentState().Value(), + ValveConfigurationAndControl::ValveStateEnum::kClosed); + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentLevel().Value(), 0); + + // Reopen via OOB CurrentState = kOpen + auto openResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentState::Id), + static_cast(ValveConfigurationAndControl::ValveStateEnum::kOpen)); + ASSERT_FALSE(std::holds_alternative(openResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(openResult)), + CHIP_NO_ERROR); + + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentState().Value(), + ValveConfigurationAndControl::ValveStateEnum::kOpen); + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetCurrentLevel().Value(), 60); +} + +// Test 12: DefaultOpenDuration is applied on OOB Open +TEST_F(TestWaterValveAccessor, DefaultOpenDurationAppliedOnOOBOpen) +{ + Testing::ClusterTester tester(mDevice.ValveConfigurationAndControlCluster()); + EXPECT_TRUE(tester + .WriteAttribute(ValveConfigurationAndControl::Attributes::DefaultOpenDuration::Id, + DataModel::MakeNullable(120)) + .IsSuccess()); + + // Reopen via OOB CurrentState = kOpen + auto openResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentState::Id), + static_cast(ValveConfigurationAndControl::ValveStateEnum::kOpen)); + ASSERT_FALSE(std::holds_alternative(openResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(openResult)), + CHIP_NO_ERROR); + + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetOpenDuration(), DataModel::MakeNullable(120)); + + // Also test OOB CurrentLevel open applies DefaultOpenDuration + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().CloseValve(), CHIP_NO_ERROR); + + auto levelResult = OOBDataSerializer::BuildSetAttributeRequest( + ConcreteAttributePath(1, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentLevel::Id), + static_cast(80)); + ASSERT_FALSE(std::holds_alternative(levelResult)); + EXPECT_EQ(mRegistry.HandleAction(OOBDataSerializer::kSetAttributeAction, std::get>(levelResult)), + CHIP_NO_ERROR); + + EXPECT_EQ(mDevice.ValveConfigurationAndControlCluster().GetOpenDuration(), DataModel::MakeNullable(120)); +} + +} // namespace diff --git a/examples/all-devices-app/docs/supported_clusters.md b/examples/all-devices-app/docs/supported_clusters.md index 88f1c5c3462..c4cda326bac 100644 --- a/examples/all-devices-app/docs/supported_clusters.md +++ b/examples/all-devices-app/docs/supported_clusters.md @@ -42,165 +42,165 @@ To update or validate this list manually, follow these steps: ## Cluster Implementation Details -| # | Cluster Name | ID | Code-Driven in SDK | Used in All-Devices | Notes | -| --------- | ---------------------------------------------------------- | ------------- | ------------------ | ------------------- | ------------------------------------- | -| 1 | AV Analysis | 1367 (0x0557) | No | No | | -| 2 | Access Control | 31 (0x001F) | Yes | Yes | | -| 3 | Account Login | 1294 (0x050E) | No | No | | -| 4 | Actions | 37 (0x0025) | Yes | No | | -| 5 | Activated Carbon Filter Monitoring | 114 (0x0072) | Yes | No | Alias of Resource Monitoring | -| 6 | Administrator Commissioning | 60 (0x003C) | Yes | Yes | | -| 7 | Air Quality | 91 (0x005B) | Yes | Yes | | -| 8 | Ambient Context Sensing | 1073 (0x0431) | Yes | Yes | | -| 9 | Ambient Sensing Union | 1074 (0x0432) | No | No | | -| 10 | Application Basic | 1293 (0x050D) | No | No | | -| 11 | Application Launcher | 1292 (0x050C) | No | No | | -| 12 | Audio Control | 1298 (0x0512) | No | No | | -| 13 | Audio Output | 1291 (0x050B) | No | No | | -| 14 | Ballast Configuration | 769 (0x0301) | No | No | | -| 15 | Basic Information | 40 (0x0028) | Yes | Yes | | -| 16 | Binding | 30 (0x001E) | Yes | Yes | | -| 17 | Boolean State | 69 (0x0045) | Yes | Yes | | -| 18 | Boolean State Configuration | 128 (0x0080) | Yes | No | | -| 19 | Bridged Device Basic Information | 57 (0x0039) | Yes | Yes | | -| 20 | Camera AV Settings User Level Management | 1362 (0x0552) | Yes | No | | -| 21 | Camera AV Stream Management | 1361 (0x0551) | Yes | No | | -| 22 | Carbon Dioxide Concentration Measurement | 1037 (0x040D) | Yes | Yes | Instance of Concentration Measurement | -| 23 | Carbon Monoxide Concentration Measurement | 1036 (0x040C) | Yes | Yes | Instance of Concentration Measurement | -| 24 | Channel | 1284 (0x0504) | No | No | | -| 25 | Chime | 1366 (0x0556) | Yes | Yes | | -| 26 | Closure Control | 260 (0x0104) | Yes | No | | -| 27 | Closure Dimension | 261 (0x0105) | Yes | No | | -| 28 | Color Control | 768 (0x0300) | No | No | | -| 29 | Commissioner Control | 1873 (0x0751) | Yes | No | | -| 30 | Commissioning Proxy | 1109 (0x0455) | Yes | Yes | | -| 31 | Commodity Metering | 2823 (0x0B07) | No | No | | -| 32 | Commodity Price | 149 (0x0095) | No | No | | -| 33 | Commodity Tariff | 1792 (0x0700) | No | No | | -| 34 | Content App Observer | 1296 (0x0510) | No | No | | -| 35 | Content Control | 1295 (0x050F) | No | No | | -| 36 | Content Launcher | 1290 (0x050A) | No | No | | -| 37 | Descriptor | 29 (0x001D) | Yes | Yes | | -| 38 | Device Energy Management | 152 (0x0098) | Yes | Yes | | -| 39 | Device Energy Management Mode | 159 (0x009F) | Yes | No | Instance of Mode Base | -| 40 | Diagnostic Logs | 50 (0x0032) | Yes | No | | -| 41 | Dishwasher Alarm | 93 (0x005D) | No | No | | -| 42 | Dishwasher Mode | 89 (0x0059) | Yes | Yes | Instance of Mode Base | -| 43 | Door Lock | 257 (0x0101) | No | No | | -| 44 | Dynamic Lighting | 773 (0x0305) | No | No | | -| 45 | Ecosystem Information | 1872 (0x0750) | No | No | | -| 46 | Electrical Alarm | 161 (0x00A1) | No | No | | -| 47 | Electrical Distribution | 162 (0x00A2) | No | No | | -| 48 | Electrical Energy Measurement | 145 (0x0091) | Yes | Yes | | -| 49 | Electrical Grid Conditions | 160 (0x00A0) | No | No | | -| 50 | Electrical Power Measurement | 144 (0x0090) | Yes | Yes | | -| 51 | Electrical Protection Alarm | 163 (0x00A3) | No | No | | -| 52 | Energy EVSE | 153 (0x0099) | Yes | No | | -| 53 | Energy EVSE Mode | 157 (0x009D) | Yes | No | Instance of Mode Base | -| 54 | Energy Preference | 155 (0x009B) | No | No | | -| 55 | Ethernet Network Diagnostics | 55 (0x0037) | Yes | No | | -| 56 | Fan Control | 514 (0x0202) | Yes | Yes | | -| 57 | Fixed Label | 64 (0x0040) | Yes | No | | -| 58 | Flow Measurement | 1028 (0x0404) | Yes | Yes | | -| 59 | Formaldehyde Concentration Measurement | 1067 (0x042B) | Yes | Yes | Instance of Concentration Measurement | -| 60 | General Commissioning | 48 (0x0030) | Yes | Yes | | -| 61 | General Diagnostics | 51 (0x0033) | Yes | Yes | | -| 62 | Group Key Management | 63 (0x003F) | Yes | Yes | | -| 63 | Groupcast | 101 (0x0065) | Yes | Yes | | -| 64 | Groups | 4 (0x0004) | Yes | Yes | | -| 65 | HEPA Filter Monitoring | 113 (0x0071) | Yes | No | Alias of Resource Monitoring | -| 66 | Humidistat | 517 (0x0205) | Yes | Yes | | -| 67 | ICD Management | 70 (0x0046) | Yes | Yes | | -| 68 | Identify | 3 (0x0003) | Yes | Yes | | -| 69 | Illuminance Measurement | 1024 (0x0400) | Yes | Yes | | -| 70 | Joint Fabric Administrator | 1875 (0x0753) | No | No | | -| 71 | Joint Fabric Datastore | 1874 (0x0752) | No | No | | -| 72 | Keypad Input | 1289 (0x0509) | No | No | | -| 73 | Laundry Dryer Controls | 74 (0x004A) | No | No | | -| 74 | Laundry Washer Controls | 83 (0x0053) | Yes | Yes | | -| 75 | Laundry Washer Mode | 81 (0x0051) | Yes | Yes | Instance of Mode Base | -| 76 | Leaf Wetness Measurement | 1031 (0x0407) | No | No | | -| 77 | Level Control | 8 (0x0008) | Yes | Yes | | -| 78 | Localization Configuration | 43 (0x002B) | Yes | No | | -| 79 | Low Power | 1288 (0x0508) | No | No | | -| 80 | Media File Management | 1297 (0x0511) | No | No | | -| 81 | Media Input | 1287 (0x0507) | No | No | | -| 82 | Media Playback | 1286 (0x0506) | No | No | | -| 83 | Messages | 151 (0x0097) | No | No | | -| 84 | Meter Identification | 2822 (0x0B06) | No | No | | -| 85 | Microwave Oven Control | 95 (0x005F) | Yes | Yes | | -| 86 | Microwave Oven Mode | 94 (0x005E) | Yes | Yes | Instance of Mode Base | -| 87 | Mode Select | 80 (0x0050) | Yes | Yes | | -| 88 | Network Commissioning | 49 (0x0031) | Yes | Yes | | -| 89 | Network Identity Management | 1104 (0x0450) | Yes | No | | -| 90 | Nitrogen Dioxide Concentration Measurement | 1043 (0x0413) | Yes | Yes | Instance of Concentration Measurement | -| 91 | OTA Software Update Provider | 41 (0x0029) | Yes | No | | -| 92 | OTA Software Update Requestor | 42 (0x002A) | Yes | No | | -| 93 | Occupancy Sensing | 1030 (0x0406) | Yes | Yes | | -| 94 | On/Off | 6 (0x0006) | Yes | Yes | | -| 95 | Operational Credentials | 62 (0x003E) | Yes | Yes | | -| 96 | Operational State | 96 (0x0060) | Yes | Yes | | -| 97 | Oven Cavity Operational State | 72 (0x0048) | Yes | Yes | | -| 98 | Oven Mode | 73 (0x0049) | Yes | No | Instance of Mode Base | -| 99 | Ozone Concentration Measurement | 1045 (0x0415) | Yes | Yes | Instance of Concentration Measurement | -| 100 | PM1 Concentration Measurement | 1068 (0x042C) | Yes | Yes | Instance of Concentration Measurement | -| 101 | PM10 Concentration Measurement | 1069 (0x042D) | Yes | Yes | Instance of Concentration Measurement | -| 102 | PM2.5 Concentration Measurement | 1066 (0x042A) | Yes | Yes | Instance of Concentration Measurement | -| 103 | Power Source | 47 (0x002F) | Yes | Yes | | -| 104 | Power Source Configuration | 46 (0x002E) | No | No | | -| 105 | Power Topology | 156 (0x009C) | Yes | Yes | | -| 106 | Pressure Measurement | 1027 (0x0403) | Yes | Yes | | -| 107 | Proximity Ranging | 1075 (0x0433) | Yes | Yes | | -| 108 | Proxy Configuration | 66 (0x0042) | No | No | | -| 109 | Proxy Discovery | 67 (0x0043) | No | No | | -| 110 | Pulse Width Modulation | 28 (0x001C) | No | No | | -| 111 | Pump Configuration and Control | 512 (0x0200) | No | No | | -| 112 | Push AV Stream Transport | 1365 (0x0555) | Yes | No | | -| 113 | RVC Clean Mode | 85 (0x0055) | Yes | Yes | Used by Robotic Vacuum Cleaner | -| 114 | RVC Operational State | 97 (0x0061) | Yes | Yes | | -| 115 | RVC Run Mode | 84 (0x0054) | Yes | Yes | Used by Robotic Vacuum Cleaner | -| 116 | Radon Concentration Measurement | 1071 (0x042F) | Yes | Yes | Instance of Concentration Measurement | -| 117 | Refrigerator Alarm | 87 (0x0057) | No | No | | -| 118 | Refrigerator And Temperature Controlled Cabinet Mode | 82 (0x0052) | Yes | No | Instance of Mode Base | -| 119 | Relative Humidity Measurement | 1029 (0x0405) | Yes | Yes | | -| 120 | Scenes | 5 (0x0005) | No | No | | -| 121 | Scenes Management | 98 (0x0062) | Yes | Yes | | -| 122 | Service Area | 336 (0x0150) | Yes | Yes | Used by Robotic Vacuum Cleaner | -| 123 | Smoke CO Alarm | 92 (0x005C) | Yes | Yes | | -| 124 | Software Diagnostics | 52 (0x0034) | Yes | Yes | | -| 125 | Soil Measurement | 1072 (0x0430) | Yes | Yes | | -| 126 | Soil Moisture Measurement | 1032 (0x0408) | No | No | | -| 127 | Switch | 59 (0x003B) | Yes | Yes | | -| 128 | TLS Certificate Management | 2049 (0x0801) | Yes | No | | -| 129 | TLS Client Management | 2050 (0x0802) | Yes | No | | -| 130 | Target Navigator | 1285 (0x0505) | No | No | | -| 131 | Temperature Alarm | 100 (0x0064) | No | No | | -| 132 | Temperature Control | 86 (0x0056) | Yes | Yes | | -| 133 | Temperature Controlled Cabinet Topology | 75 (0x004B) | No | No | | -| 134 | Temperature Measurement | 1026 (0x0402) | Yes | Yes | | -| 135 | Thermostat | 513 (0x0201) | Yes | Yes | Room Air Conditioner | -| 136 | Thermostat Mode | 99 (0x0063) | Yes | No | Instance of Mode Base | -| 137 | Thermostat User Interface Configuration | 516 (0x0204) | Yes | Yes | Room Air Conditioner | -| 138 | Thread Border Router Diagnostics | 1108 (0x0454) | No | No | | -| 139 | Thread Border Router Management | 1106 (0x0452) | Yes | Yes | | -| 140 | Thread Network Diagnostics | 53 (0x0035) | Yes | Yes | | -| 141 | Thread Network Directory | 1107 (0x0453) | Yes | Yes | | -| 142 | Time Format Localization | 44 (0x002C) | Yes | No | | -| 143 | Time Synchronization | 56 (0x0038) | Yes | No | | -| 144 | Total Volatile Organic Compounds Concentration Measurement | 1070 (0x042E) | Yes | Yes | Instance of Concentration Measurement | -| 145 | Unit Localization | 45 (0x002D) | Yes | No | | -| 146 | User Label | 65 (0x0041) | Yes | No | | -| 147 | Valid Proxies | 68 (0x0044) | No | No | | -| 148 | Valve Configuration and Control | 129 (0x0081) | Yes | Yes | Water Valve | -| 149 | Wake On LAN | 1283 (0x0503) | No | No | | -| 150 | Water Heater Management | 148 (0x0094) | Yes | No | | -| 151 | Water Heater Mode | 158 (0x009E) | Yes | No | Instance of Mode Base | -| 152 | Water Tank Level Monitoring | 121 (0x0079) | Yes | No | Alias of Resource Monitoring | -| 153 | WebRTC Transport Provider | 1363 (0x0553) | Yes | No | | -| 154 | WebRTC Transport Requestor | 1364 (0x0554) | Yes | No | | -| 155 | Wi-Fi Network Diagnostics | 54 (0x0036) | Yes | Yes | | -| 156 | Wi-Fi Network Management | 1105 (0x0451) | Yes | Yes | | -| 157 | Window Covering | 258 (0x0102) | Yes | Yes | window-covering | -| 158 | Zone Management | 1360 (0x0550) | Yes | No | | -| 159 | Smoke Concentration Measurement | 1076 (0x0434) | Yes | Yes | Instance of Concentration Measurement | -| **Total** | **159** | | **108** | **73** | | +| # | Cluster Name | ID | Code-Driven in SDK | Used in All-Devices | Notes | +| --------- | ---------------------------------------------------------- | ------------- | ------------------ | ------------------- | ---------------------------------------------------- | +| 1 | AV Analysis | 1367 (0x0557) | No | No | | +| 2 | Access Control | 31 (0x001F) | Yes | Yes | | +| 3 | Account Login | 1294 (0x050E) | No | No | | +| 4 | Actions | 37 (0x0025) | Yes | No | | +| 5 | Activated Carbon Filter Monitoring | 114 (0x0072) | Yes | No | Alias of Resource Monitoring | +| 6 | Administrator Commissioning | 60 (0x003C) | Yes | Yes | | +| 7 | Air Quality | 91 (0x005B) | Yes | Yes | | +| 8 | Ambient Context Sensing | 1073 (0x0431) | Yes | Yes | | +| 9 | Ambient Sensing Union | 1074 (0x0432) | No | No | | +| 10 | Application Basic | 1293 (0x050D) | No | No | | +| 11 | Application Launcher | 1292 (0x050C) | No | No | | +| 12 | Audio Control | 1298 (0x0512) | No | No | | +| 13 | Audio Output | 1291 (0x050B) | No | No | | +| 14 | Ballast Configuration | 769 (0x0301) | No | No | | +| 15 | Basic Information | 40 (0x0028) | Yes | Yes | | +| 16 | Binding | 30 (0x001E) | Yes | Yes | | +| 17 | Boolean State | 69 (0x0045) | Yes | Yes | | +| 18 | Boolean State Configuration | 128 (0x0080) | Yes | No | | +| 19 | Bridged Device Basic Information | 57 (0x0039) | Yes | Yes | | +| 20 | Camera AV Settings User Level Management | 1362 (0x0552) | Yes | No | | +| 21 | Camera AV Stream Management | 1361 (0x0551) | Yes | No | | +| 22 | Carbon Dioxide Concentration Measurement | 1037 (0x040D) | Yes | Yes | Instance of Concentration Measurement | +| 23 | Carbon Monoxide Concentration Measurement | 1036 (0x040C) | Yes | Yes | Instance of Concentration Measurement | +| 24 | Channel | 1284 (0x0504) | No | No | | +| 25 | Chime | 1366 (0x0556) | Yes | Yes | | +| 26 | Closure Control | 260 (0x0104) | Yes | No | | +| 27 | Closure Dimension | 261 (0x0105) | Yes | No | | +| 28 | Color Control | 768 (0x0300) | No | No | | +| 29 | Commissioner Control | 1873 (0x0751) | Yes | No | | +| 30 | Commissioning Proxy | 1109 (0x0455) | Yes | Yes | | +| 31 | Commodity Metering | 2823 (0x0B07) | No | No | | +| 32 | Commodity Price | 149 (0x0095) | No | No | | +| 33 | Commodity Tariff | 1792 (0x0700) | No | No | | +| 34 | Content App Observer | 1296 (0x0510) | No | No | | +| 35 | Content Control | 1295 (0x050F) | No | No | | +| 36 | Content Launcher | 1290 (0x050A) | No | No | | +| 37 | Descriptor | 29 (0x001D) | Yes | Yes | | +| 38 | Device Energy Management | 152 (0x0098) | Yes | Yes | | +| 39 | Device Energy Management Mode | 159 (0x009F) | Yes | No | Instance of Mode Base | +| 40 | Diagnostic Logs | 50 (0x0032) | Yes | No | | +| 41 | Dishwasher Alarm | 93 (0x005D) | No | No | | +| 42 | Dishwasher Mode | 89 (0x0059) | Yes | Yes | Instance of Mode Base | +| 43 | Door Lock | 257 (0x0101) | No | No | | +| 44 | Dynamic Lighting | 773 (0x0305) | No | No | | +| 45 | Ecosystem Information | 1872 (0x0750) | No | No | | +| 46 | Electrical Alarm | 161 (0x00A1) | No | No | | +| 47 | Electrical Distribution | 162 (0x00A2) | No | No | | +| 48 | Electrical Energy Measurement | 145 (0x0091) | Yes | Yes | | +| 49 | Electrical Grid Conditions | 160 (0x00A0) | No | No | | +| 50 | Electrical Power Measurement | 144 (0x0090) | Yes | Yes | | +| 51 | Electrical Protection Alarm | 163 (0x00A3) | No | No | | +| 52 | Energy EVSE | 153 (0x0099) | Yes | No | | +| 53 | Energy EVSE Mode | 157 (0x009D) | Yes | No | Instance of Mode Base | +| 54 | Energy Preference | 155 (0x009B) | No | No | | +| 55 | Ethernet Network Diagnostics | 55 (0x0037) | Yes | No | | +| 56 | Fan Control | 514 (0x0202) | Yes | Yes | | +| 57 | Fixed Label | 64 (0x0040) | Yes | No | | +| 58 | Flow Measurement | 1028 (0x0404) | Yes | Yes | | +| 59 | Formaldehyde Concentration Measurement | 1067 (0x042B) | Yes | Yes | Instance of Concentration Measurement | +| 60 | General Commissioning | 48 (0x0030) | Yes | Yes | | +| 61 | General Diagnostics | 51 (0x0033) | Yes | Yes | | +| 62 | Group Key Management | 63 (0x003F) | Yes | Yes | | +| 63 | Groupcast | 101 (0x0065) | Yes | Yes | | +| 64 | Groups | 4 (0x0004) | Yes | Yes | | +| 65 | HEPA Filter Monitoring | 113 (0x0071) | Yes | No | Alias of Resource Monitoring | +| 66 | Humidistat | 517 (0x0205) | Yes | Yes | | +| 67 | ICD Management | 70 (0x0046) | Yes | Yes | | +| 68 | Identify | 3 (0x0003) | Yes | Yes | | +| 69 | Illuminance Measurement | 1024 (0x0400) | Yes | Yes | | +| 70 | Joint Fabric Administrator | 1875 (0x0753) | No | No | | +| 71 | Joint Fabric Datastore | 1874 (0x0752) | No | No | | +| 72 | Keypad Input | 1289 (0x0509) | No | No | | +| 73 | Laundry Dryer Controls | 74 (0x004A) | No | No | | +| 74 | Laundry Washer Controls | 83 (0x0053) | Yes | Yes | | +| 75 | Laundry Washer Mode | 81 (0x0051) | Yes | Yes | Instance of Mode Base | +| 76 | Leaf Wetness Measurement | 1031 (0x0407) | No | No | | +| 77 | Level Control | 8 (0x0008) | Yes | Yes | | +| 78 | Localization Configuration | 43 (0x002B) | Yes | No | | +| 79 | Low Power | 1288 (0x0508) | No | No | | +| 80 | Media File Management | 1297 (0x0511) | No | No | | +| 81 | Media Input | 1287 (0x0507) | No | No | | +| 82 | Media Playback | 1286 (0x0506) | No | No | | +| 83 | Messages | 151 (0x0097) | No | No | | +| 84 | Meter Identification | 2822 (0x0B06) | No | No | | +| 85 | Microwave Oven Control | 95 (0x005F) | Yes | Yes | | +| 86 | Microwave Oven Mode | 94 (0x005E) | Yes | Yes | Instance of Mode Base | +| 87 | Mode Select | 80 (0x0050) | Yes | Yes | | +| 88 | Network Commissioning | 49 (0x0031) | Yes | Yes | | +| 89 | Network Identity Management | 1104 (0x0450) | Yes | No | | +| 90 | Nitrogen Dioxide Concentration Measurement | 1043 (0x0413) | Yes | Yes | Instance of Concentration Measurement | +| 91 | OTA Software Update Provider | 41 (0x0029) | Yes | No | | +| 92 | OTA Software Update Requestor | 42 (0x002A) | Yes | No | | +| 93 | Occupancy Sensing | 1030 (0x0406) | Yes | Yes | | +| 94 | On/Off | 6 (0x0006) | Yes | Yes | | +| 95 | Operational Credentials | 62 (0x003E) | Yes | Yes | | +| 96 | Operational State | 96 (0x0060) | Yes | Yes | | +| 97 | Oven Cavity Operational State | 72 (0x0048) | Yes | Yes | | +| 98 | Oven Mode | 73 (0x0049) | Yes | No | Instance of Mode Base | +| 99 | Ozone Concentration Measurement | 1045 (0x0415) | Yes | Yes | Instance of Concentration Measurement | +| 100 | PM1 Concentration Measurement | 1068 (0x042C) | Yes | Yes | Instance of Concentration Measurement | +| 101 | PM10 Concentration Measurement | 1069 (0x042D) | Yes | Yes | Instance of Concentration Measurement | +| 102 | PM2.5 Concentration Measurement | 1066 (0x042A) | Yes | Yes | Instance of Concentration Measurement | +| 103 | Power Source | 47 (0x002F) | Yes | Yes | | +| 104 | Power Source Configuration | 46 (0x002E) | No | No | | +| 105 | Power Topology | 156 (0x009C) | Yes | Yes | | +| 106 | Pressure Measurement | 1027 (0x0403) | Yes | Yes | | +| 107 | Proximity Ranging | 1075 (0x0433) | Yes | Yes | | +| 108 | Proxy Configuration | 66 (0x0042) | No | No | | +| 109 | Proxy Discovery | 67 (0x0043) | No | No | | +| 110 | Pulse Width Modulation | 28 (0x001C) | No | No | | +| 111 | Pump Configuration and Control | 512 (0x0200) | No | No | | +| 112 | Push AV Stream Transport | 1365 (0x0555) | Yes | No | | +| 113 | RVC Clean Mode | 85 (0x0055) | Yes | Yes | Used by Robotic Vacuum Cleaner | +| 114 | RVC Operational State | 97 (0x0061) | Yes | Yes | | +| 115 | RVC Run Mode | 84 (0x0054) | Yes | Yes | Used by Robotic Vacuum Cleaner | +| 116 | Radon Concentration Measurement | 1071 (0x042F) | Yes | Yes | Instance of Concentration Measurement | +| 117 | Refrigerator Alarm | 87 (0x0057) | No | No | | +| 118 | Refrigerator And Temperature Controlled Cabinet Mode | 82 (0x0052) | Yes | No | Instance of Mode Base | +| 119 | Relative Humidity Measurement | 1029 (0x0405) | Yes | Yes | | +| 120 | Scenes | 5 (0x0005) | No | No | | +| 121 | Scenes Management | 98 (0x0062) | Yes | Yes | | +| 122 | Service Area | 336 (0x0150) | Yes | Yes | Used by Robotic Vacuum Cleaner | +| 123 | Smoke CO Alarm | 92 (0x005C) | Yes | Yes | | +| 124 | Software Diagnostics | 52 (0x0034) | Yes | Yes | | +| 125 | Soil Measurement | 1072 (0x0430) | Yes | Yes | | +| 126 | Soil Moisture Measurement | 1032 (0x0408) | No | No | | +| 127 | Switch | 59 (0x003B) | Yes | Yes | | +| 128 | TLS Certificate Management | 2049 (0x0801) | Yes | No | | +| 129 | TLS Client Management | 2050 (0x0802) | Yes | No | | +| 130 | Target Navigator | 1285 (0x0505) | No | No | | +| 131 | Temperature Alarm | 100 (0x0064) | No | No | | +| 132 | Temperature Control | 86 (0x0056) | Yes | Yes | | +| 133 | Temperature Controlled Cabinet Topology | 75 (0x004B) | No | No | | +| 134 | Temperature Measurement | 1026 (0x0402) | Yes | Yes | | +| 135 | Thermostat | 513 (0x0201) | Yes | Yes | Room Air Conditioner, Thermostat | +| 136 | Thermostat Mode | 99 (0x0063) | Yes | No | Instance of Mode Base | +| 137 | Thermostat User Interface Configuration | 516 (0x0204) | Yes | Yes | Room Air Conditioner, Temperature Sensor, Thermostat | +| 138 | Thread Border Router Diagnostics | 1108 (0x0454) | No | No | | +| 139 | Thread Border Router Management | 1106 (0x0452) | Yes | Yes | | +| 140 | Thread Network Diagnostics | 53 (0x0035) | Yes | Yes | | +| 141 | Thread Network Directory | 1107 (0x0453) | Yes | Yes | | +| 142 | Time Format Localization | 44 (0x002C) | Yes | No | | +| 143 | Time Synchronization | 56 (0x0038) | Yes | No | | +| 144 | Total Volatile Organic Compounds Concentration Measurement | 1070 (0x042E) | Yes | Yes | Instance of Concentration Measurement | +| 145 | Unit Localization | 45 (0x002D) | Yes | No | | +| 146 | User Label | 65 (0x0041) | Yes | No | | +| 147 | Valid Proxies | 68 (0x0044) | No | No | | +| 148 | Valve Configuration and Control | 129 (0x0081) | Yes | Yes | Water Valve | +| 149 | Wake On LAN | 1283 (0x0503) | No | No | | +| 150 | Water Heater Management | 148 (0x0094) | Yes | No | | +| 151 | Water Heater Mode | 158 (0x009E) | Yes | No | Instance of Mode Base | +| 152 | Water Tank Level Monitoring | 121 (0x0079) | Yes | No | Alias of Resource Monitoring | +| 153 | WebRTC Transport Provider | 1363 (0x0553) | Yes | No | | +| 154 | WebRTC Transport Requestor | 1364 (0x0554) | Yes | No | | +| 155 | Wi-Fi Network Diagnostics | 54 (0x0036) | Yes | Yes | | +| 156 | Wi-Fi Network Management | 1105 (0x0451) | Yes | Yes | | +| 157 | Window Covering | 258 (0x0102) | Yes | Yes | window-covering | +| 158 | Zone Management | 1360 (0x0550) | Yes | No | | +| 159 | Smoke Concentration Measurement | 1076 (0x0434) | Yes | Yes | Instance of Concentration Measurement | +| **Total** | **159** | | **109** | **74** | | diff --git a/examples/all-devices-app/docs/supported_device_types.md b/examples/all-devices-app/docs/supported_device_types.md index 797600abf57..c62d57806a1 100644 --- a/examples/all-devices-app/docs/supported_device_types.md +++ b/examples/all-devices-app/docs/supported_device_types.md @@ -49,7 +49,7 @@ To update or validate this list manually, follow these steps: - **Blocked**: One or more mandatory clusters are missing (listed in the `Missing Clusters` column without an `[O]` suffix). -## Implemented Device Types (53 total) +## Implemented Device Types (54 total) | # | Device Type Name | ID | Missing Clusters | Notes | | --- | ------------------------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | @@ -100,14 +100,15 @@ To update or validate this list manually, follow these steps: | 45 | Soil Sensor | 69 (0x0045) | | | | 46 | Speaker | 34 (0x0022) | | | | 47 | Temperature Controlled Cabinet | 113 (0x0071) | Refrigerator And Temperature Controlled Cabinet Mode [O], Oven Mode [O], Temperature Alarm [O] | Part of Refrigerator / Oven | -| 48 | Temperature Sensor | 770 (0x0302) | | | -| 49 | Thread Border Router | 145 (0x0091) | Thread Border Router Diagnostics [O] | Minimally Ready | -| 50 | Water Freeze Detector | 65 (0x0041) | | | -| 51 | Water Leak Detector | 67 (0x0043) | | | -| 52 | Water Valve | 66 (0x0042) | | | -| 53 | Window Covering | 514 (0x0202) | | | +| 48 | Temperature Sensor | 770 (0x0302) | | UI configuration included | +| 49 | Thermostat | 769 (0x0301) | Thermostat Mode [O], Energy Preference [O] | Heating, Cooling and UI | +| 50 | Thread Border Router | 145 (0x0091) | Thread Border Router Diagnostics [O] | Minimally Ready | +| 51 | Water Freeze Detector | 65 (0x0041) | | | +| 52 | Water Leak Detector | 67 (0x0043) | | | +| 53 | Water Valve | 66 (0x0042) | | | +| 54 | Window Covering | 514 (0x0202) | | | -## Unimplemented Device Types (52 total) +## Unimplemented Device Types (51 total) | # | Device Type Name | ID | Ready Clusters | Missing Clusters | Notes | | --- | --------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------- | @@ -156,10 +157,9 @@ To update or validate this list manually, follow these steps: | 43 | Snapshot Camera | 325 (0x0145) | Camera AV Stream Management, Identify [O], Occupancy Sensing [O], Zone Management [O], Camera AV Settings User Level Management [O] | None | Ready | | 44 | Solar Power | 23 (0x0017) | Identify [O] | None | Ready | | 45 | Streaming Audio Player | 32 (0x0020) | Chime | Media Playback, Content Launcher | Blocked | -| 46 | Thermostat | 769 (0x0301) | Identify, Groups [O], Thermostat Mode [O], Thermostat, Thermostat User Interface Configuration [O] | Energy Preference [O] | Minimally Ready | -| 47 | Thermostat Controller | 778 (0x030A) | None | None | Ready | -| 48 | Thread Border Router | 145 (0x0091) | Thread Network Diagnostics, Thread Border Router Management, Thread Network Directory [O] | Thread Border Router Diagnostics [O] | Minimally Ready | -| 49 | Video Doorbell | 323 (0x0143) | None | None | Ready | -| 50 | Video Remote Control | 42 (0x002A) | None | None | Ready | -| 51 | Water Heater | 1295 (0x050F) | Water Heater Management, Identify [O], Water Heater Mode, Thermostat Mode [O], Thermostat, Thermostat User Interface Configuration [O] | None | Ready | -| 52 | Window Covering Controller | 515 (0x0203) | Identify [O] | None | Ready | +| 46 | Thermostat Controller | 778 (0x030A) | None | None | Ready | +| 47 | Thread Border Router | 145 (0x0091) | Thread Network Diagnostics, Thread Border Router Management, Thread Network Directory [O] | Thread Border Router Diagnostics [O] | Minimally Ready | +| 48 | Video Doorbell | 323 (0x0143) | None | None | Ready | +| 49 | Video Remote Control | 42 (0x002A) | None | None | Ready | +| 50 | Water Heater | 1295 (0x050F) | Water Heater Management, Identify [O], Water Heater Mode, Thermostat Mode [O], Thermostat, Thermostat User Interface Configuration [O] | None | Ready | +| 51 | Window Covering Controller | 515 (0x0203) | Identify [O] | None | Ready | diff --git a/examples/all-devices-app/posix/BUILD.gn b/examples/all-devices-app/posix/BUILD.gn index 8315ce06622..a9afa9e062e 100644 --- a/examples/all-devices-app/posix/BUILD.gn +++ b/examples/all-devices-app/posix/BUILD.gn @@ -103,6 +103,7 @@ source_set("device-instances") { "${chip_root}/examples/all-devices-app/all-devices-common/device/types/smoke-co-alarm", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/soil-sensor", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/speaker", + "${chip_root}/examples/all-devices-app/all-devices-common/device/types/thermostat", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/thread-border-router", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/water-valve", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/window-covering", diff --git a/examples/all-devices-app/silabs/BUILD.gn b/examples/all-devices-app/silabs/BUILD.gn index b66eea87918..8f2ea025a2b 100644 --- a/examples/all-devices-app/silabs/BUILD.gn +++ b/examples/all-devices-app/silabs/BUILD.gn @@ -160,6 +160,7 @@ silabs_executable("all_devices_app") { "${chip_root}/examples/all-devices-app/all-devices-common/device/types/soil-sensor", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/speaker", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/temperature-sensor", + "${chip_root}/examples/all-devices-app/all-devices-common/device/types/thermostat", "${chip_root}/examples/all-devices-app/all-devices-common/device/types/water-valve", # Providers diff --git a/examples/all-devices-app/silabs/src/AppTask.cpp b/examples/all-devices-app/silabs/src/AppTask.cpp index e77f87f3f1a..6f5577bfaab 100644 --- a/examples/all-devices-app/silabs/src/AppTask.cpp +++ b/examples/all-devices-app/silabs/src/AppTask.cpp @@ -48,7 +48,6 @@ #include #include #include -#include #include #include #include @@ -64,6 +63,7 @@ #include #if defined(SILABS_OTA_ENABLED) && SILABS_OTA_ENABLED +#include "OTAConfig.h" #include #endif @@ -281,23 +281,23 @@ CHIP_ERROR AppTask::InitCodeDrivenDataModel(chip::PersistentStorageDelegate & st #if CHIP_ENABLE_OPENTHREAD #if CHIP_CONFIG_ENABLE_ICD_SERVER using RootNodeType = chip::app::RootNodeWith; - sRootNode = std::make_unique(rootNodeContext, chip::app::ThreadFeature::Context{ .threadDriver = sThreadDriver }, - icdContext, otaContext); + auto rootNode = std::make_unique( + rootNodeContext, chip::app::ThreadFeature::Context{ .threadDriver = sThreadDriver }, icdContext, otaContext); #else using RootNodeType = chip::app::RootNodeWith; - sRootNode = std::make_unique(rootNodeContext, chip::app::ThreadFeature::Context{ .threadDriver = sThreadDriver }, - otaContext); + auto rootNode = std::make_unique(rootNodeContext, + chip::app::ThreadFeature::Context{ .threadDriver = sThreadDriver }, otaContext); #endif // CHIP_CONFIG_ENABLE_ICD_SERVER #elif defined(CHIP_DEVICE_CONFIG_ENABLE_WIFI) && CHIP_DEVICE_CONFIG_ENABLE_WIFI #if CHIP_CONFIG_ENABLE_ICD_SERVER using RootNodeType = chip::app::RootNodeWith; - sRootNode = std::make_unique( + auto rootNode = std::make_unique( rootNodeContext, chip::app::WifiFeature::Context{ .wifiDriver = *chip::DeviceLayer::NetworkCommissioning::SlWiFiDriver::GetInstance() }, icdContext, otaContext); #else using RootNodeType = chip::app::RootNodeWith; - sRootNode = std::make_unique( + auto rootNode = std::make_unique( rootNodeContext, chip::app::WifiFeature::Context{ .wifiDriver = *chip::DeviceLayer::NetworkCommissioning::SlWiFiDriver::GetInstance() }, otaContext); @@ -305,49 +305,54 @@ CHIP_ERROR AppTask::InitCodeDrivenDataModel(chip::PersistentStorageDelegate & st #else #if CHIP_CONFIG_ENABLE_ICD_SERVER using RootNodeType = chip::app::RootNodeWith; - sRootNode = std::make_unique(rootNodeContext, icdContext, otaContext); + auto rootNode = std::make_unique(rootNodeContext, icdContext, otaContext); #else using RootNodeType = chip::app::RootNodeWith; - sRootNode = std::make_unique(rootNodeContext, otaContext); + auto rootNode = std::make_unique(rootNodeContext, otaContext); #endif // CHIP_CONFIG_ENABLE_ICD_SERVER #endif #else // SILABS_OTA_ENABLED #if CHIP_ENABLE_OPENTHREAD #if CHIP_CONFIG_ENABLE_ICD_SERVER using RootNodeType = chip::app::RootNodeWith; - sRootNode = std::make_unique(rootNodeContext, chip::app::ThreadFeature::Context{ .threadDriver = sThreadDriver }, - icdContext); + auto rootNode = std::make_unique(rootNodeContext, + chip::app::ThreadFeature::Context{ .threadDriver = sThreadDriver }, icdContext); #else using RootNodeType = chip::app::RootNodeWith; - sRootNode = std::make_unique(rootNodeContext, chip::app::ThreadFeature::Context{ .threadDriver = sThreadDriver }); + auto rootNode = + std::make_unique(rootNodeContext, chip::app::ThreadFeature::Context{ .threadDriver = sThreadDriver }); #endif // CHIP_CONFIG_ENABLE_ICD_SERVER #elif defined(CHIP_DEVICE_CONFIG_ENABLE_WIFI) && CHIP_DEVICE_CONFIG_ENABLE_WIFI #if CHIP_CONFIG_ENABLE_ICD_SERVER using RootNodeType = chip::app::RootNodeWith; - sRootNode = std::make_unique( + auto rootNode = std::make_unique( rootNodeContext, chip::app::WifiFeature::Context{ .wifiDriver = *chip::DeviceLayer::NetworkCommissioning::SlWiFiDriver::GetInstance() }, icdContext); #else using RootNodeType = chip::app::RootNodeWith; - sRootNode = std::make_unique( + auto rootNode = std::make_unique( rootNodeContext, chip::app::WifiFeature::Context{ .wifiDriver = *chip::DeviceLayer::NetworkCommissioning::SlWiFiDriver::GetInstance() }); #endif // CHIP_CONFIG_ENABLE_ICD_SERVER #else #if CHIP_CONFIG_ENABLE_ICD_SERVER using RootNodeType = chip::app::RootNodeWith; - sRootNode = std::make_unique(rootNodeContext, icdContext); + auto rootNode = std::make_unique(rootNodeContext, icdContext); #else - sRootNode = std::make_unique(rootNodeContext); + auto rootNode = std::make_unique(rootNodeContext); #endif // CHIP_CONFIG_ENABLE_ICD_SERVER #endif #endif // SILABS_OTA_ENABLED - VerifyOrReturnError(sRootNode != nullptr, CHIP_ERROR_NO_MEMORY); + VerifyOrReturnError(rootNode != nullptr, CHIP_ERROR_NO_MEMORY); chip::app::ConsecutiveEndpointIdAllocator rootAllocator(kRootEndpointId); - ReturnErrorOnFailure(sRootNode->Register(rootAllocator, *sDataModelProvider)); + ReturnErrorOnFailure(rootNode->Register(rootAllocator, *sDataModelProvider)); +#if defined(SILABS_OTA_ENABLED) && SILABS_OTA_ENABLED + chip::SetDefaultOTARequestorEventGenerator(rootNode->GetFeature().Cluster()); +#endif + sRootNode = std::move(rootNode); chip::app::NoHooksDeviceFactory::GetInstance().Init(chip::app::NoHooksDeviceFactory::Context{ .groupDataProvider = *groupDataProvider, diff --git a/examples/all-devices-app/zephyr/Kconfig.devices b/examples/all-devices-app/zephyr/Kconfig.devices index f81c1738eff..011e9d01824 100644 --- a/examples/all-devices-app/zephyr/Kconfig.devices +++ b/examples/all-devices-app/zephyr/Kconfig.devices @@ -168,6 +168,8 @@ config ALL_DEVICES_DEVICE_SPEAKER config ALL_DEVICES_DEVICE_TEMPERATURE_SENSOR bool "Temperature sensor" +config ALL_DEVICES_DEVICE_THERMOSTAT + bool "Thermostat" config ALL_DEVICES_DEVICE_THREAD_BORDER_ROUTER bool "Thread border router" diff --git a/examples/all-devices-app/zephyr/include/AppTaskBase.h b/examples/all-devices-app/zephyr/include/AppTaskBase.h index 52dbcb7b371..b2787e4f13d 100644 --- a/examples/all-devices-app/zephyr/include/AppTaskBase.h +++ b/examples/all-devices-app/zephyr/include/AppTaskBase.h @@ -44,7 +44,6 @@ #include #include #include -#include #endif // CHIP_DEVICE_CONFIG_ENABLE_OTA_REQUESTOR #include @@ -101,9 +100,6 @@ class AppTaskBase /// Substitutes board-specific implementations CHIP_ERROR RegisterDeviceCreators(); - /// OTA Requestor cluster on the root endpoint, if enabled. - CHIP_ERROR RegisterOTACluster(); - /// Instantiates every type in DeviceFactory::SupportedDeviceTypes(). CHIP_ERROR RegisterAppDevices(); @@ -140,20 +136,19 @@ class AppTaskBase DeviceLayer::NetworkCommissioning::ZephyrWifiDriver::Instance(); #endif // CHIP_DEVICE_CONFIG_ENABLE_WIFI - std::unique_ptr mDataModelProvider; - std::unique_ptr mRootNode; - - std::unique_ptr mDevices[ALL_DEVICES_ENABLED_DEVICE_COUNT]; - size_t mDeviceCount = 0; - #if CHIP_DEVICE_CONFIG_ENABLE_OTA_REQUESTOR chip::DefaultOTARequestor mOTARequestorCore; chip::DefaultOTARequestorStorage mOTARequestorStorage; DeviceLayer::DefaultOTARequestorDriver mOTARequestorDriver; chip::BDXDownloader mOTADownloader; chip::OTARequestorAttributes mOTARequestorAttributes; - LazyRegisteredServerCluster mOTARequestorCluster; + Clusters::OTARequestorCluster * mOTARequestorCluster = nullptr; #endif // CHIP_DEVICE_CONFIG_ENABLE_OTA_REQUESTOR + + std::unique_ptr mRootNode; + std::unique_ptr mDevices[ALL_DEVICES_ENABLED_DEVICE_COUNT]; + size_t mDeviceCount = 0; + std::unique_ptr mDataModelProvider; }; } // namespace chip::app::AllDevices diff --git a/examples/all-devices-app/zephyr/include/AppTaskBase.ipp b/examples/all-devices-app/zephyr/include/AppTaskBase.ipp index b138f990652..7f3c3ebd3a1 100644 --- a/examples/all-devices-app/zephyr/include/AppTaskBase.ipp +++ b/examples/all-devices-app/zephyr/include/AppTaskBase.ipp @@ -69,7 +69,6 @@ CHIP_ERROR AppTaskBase::Run() ReturnErrorOnFailure(Self().InitPersistence()); ReturnErrorOnFailure(Self().InitRootNode()); ReturnErrorOnFailure(Self().RegisterDeviceCreators()); - ReturnErrorOnFailure(Self().RegisterOTACluster()); ReturnErrorOnFailure(Self().RegisterAppDevices()); ReturnErrorOnFailure(Self().InitServer()); ReturnErrorOnFailure(Self().InitBoardControls()); @@ -229,17 +228,22 @@ CHIP_ERROR AppTaskBase::InitRootNode() }; using RootNodeType = RootNodeWith; - mRootNode = std::make_unique(rootNodeContext, networkContext, otaContext); + auto rootNode = std::make_unique(rootNodeContext, networkContext, otaContext); #else using RootNodeType = RootNodeWith; - mRootNode = std::make_unique(rootNodeContext, networkContext); + auto rootNode = std::make_unique(rootNodeContext, networkContext); #endif - VerifyOrReturnError(mRootNode != nullptr, CHIP_ERROR_NO_MEMORY); + VerifyOrReturnError(rootNode != nullptr, CHIP_ERROR_NO_MEMORY); ConsecutiveEndpointIdAllocator rootAllocator(kRootEndpointId); - ReturnErrorOnFailure(mRootNode->Register(rootAllocator, *mDataModelProvider)); + ReturnErrorOnFailure(rootNode->Register(rootAllocator, *mDataModelProvider)); + +#if CHIP_DEVICE_CONFIG_ENABLE_OTA_REQUESTOR + mOTARequestorCluster = &rootNode->GetFeature().Cluster(); +#endif + mRootNode = std::move(rootNode); NoHooksDeviceFactory::GetInstance().Init(NoHooksDeviceFactory::Context{ .groupDataProvider = mGroupDataProvider, @@ -277,18 +281,6 @@ CHIP_ERROR AppTaskBase::RegisterDeviceCreators() return CHIP_NO_ERROR; } -template -CHIP_ERROR AppTaskBase::RegisterOTACluster() -{ -#if CHIP_DEVICE_CONFIG_ENABLE_OTA_REQUESTOR - // Must be registered before Server::Init() starts the provider. - mOTARequestorCluster.Create(kRootEndpointId, mOTARequestorCore, mOTARequestorAttributes, - Server::GetInstance().GetFabricTable()); - ReturnErrorOnFailure(mDataModelProvider->AddCluster(mOTARequestorCluster.Registration())); -#endif - return CHIP_NO_ERROR; -} - template CHIP_ERROR AppTaskBase::InitServer() { @@ -336,10 +328,11 @@ template CHIP_ERROR AppTaskBase::InitOTARequestor() { #if CHIP_DEVICE_CONFIG_ENABLE_OTA_REQUESTOR + VerifyOrReturnError(mOTARequestorCluster != nullptr, CHIP_ERROR_INCORRECT_STATE); SetRequestorInstance(&mOTARequestorCore); mOTARequestorStorage.Init(Server::GetInstance().GetPersistentStorage()); ReturnErrorOnFailure(mOTARequestorCore.Init(Server::GetInstance(), mOTARequestorStorage, mOTARequestorDriver, mOTADownloader, - mOTARequestorAttributes, mOTARequestorCluster.Cluster())); + mOTARequestorAttributes, *mOTARequestorCluster)); auto & imageProcessor = OTAImageProcessorImpl::GetDefaultInstance(); mOTARequestorDriver.Init(&mOTARequestorCore, &imageProcessor); diff --git a/examples/android/CHIPTool/app/build.gradle b/examples/android/CHIPTool/app/build.gradle index b180778eb9b..05ed525c461 100644 --- a/examples/android/CHIPTool/app/build.gradle +++ b/examples/android/CHIPTool/app/build.gradle @@ -2,6 +2,8 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-parcelize' +def enableGms = !project.hasProperty('matterEnableGms') || matterEnableGms.toBoolean() + android { namespace 'com.google.chip.chiptool' compileSdkVersion 34 @@ -78,6 +80,12 @@ android { } } + sourceSets { + main { + java.srcDirs += enableGms ? ['src/gms/java'] : ['src/nogms/java'] + } + } + if (matterSdkSourceBuild.toBoolean() && !matterBuildSrcDir.isEmpty()) { externalNativeBuild { cmake { @@ -131,6 +139,9 @@ dependencies { implementation "androidx.camera:camera-lifecycle:${camerax_version}" implementation "androidx.camera:camera-view:${camerax_version}" + if (enableGms) { + implementation 'com.google.android.gms:play-services-threadnetwork:16.2.1' + } implementation 'com.google.protobuf:protobuf-java:3.22.0' } diff --git a/examples/android/CHIPTool/app/src/gms/java/com/google/chip/chiptool/provisioning/GmsThreadDatasetHelper.kt b/examples/android/CHIPTool/app/src/gms/java/com/google/chip/chiptool/provisioning/GmsThreadDatasetHelper.kt new file mode 100644 index 00000000000..46a21b900f8 --- /dev/null +++ b/examples/android/CHIPTool/app/src/gms/java/com/google/chip/chiptool/provisioning/GmsThreadDatasetHelper.kt @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2026 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.google.chip.chiptool.provisioning + +import android.app.Activity +import android.content.Intent +import android.content.IntentSender +import android.util.Log +import androidx.fragment.app.Fragment +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.android.gms.threadnetwork.GetAllActiveCredentialsRequest +import com.google.android.gms.threadnetwork.ThreadNetwork +import com.google.android.gms.threadnetwork.ThreadNetworkClient +import com.google.android.gms.threadnetwork.ThreadNetworkCredentials + +/** GMSCore ThreadNetwork implementation enabled when `matterEnableGms=true`. */ +object GmsThreadDatasetHelper { + private const val TAG = "GmsThreadDatasetHelper" + private const val REQUEST_CODE_ACTIVE_CREDENTIALS = 1002 + private const val REQUEST_CODE_PREFERRED_CREDENTIALS = 1003 + const val IS_AVAILABLE = true + + /** + * Requests Thread network credentials via GMS ThreadNetwork: all active networks reachable + * through nearby border routers, or the preferred network if that is unavailable. The credentials + * are delivered through the consent intent result (see [parseConsentResult]); [onFallback] runs + * when Play services or the credentials are unavailable. Callbacks run on the main thread and may + * arrive after the fragment view is destroyed. + */ + fun fetchFromGms(fragment: Fragment, onFallback: () -> Unit) { + try { + val context = fragment.requireContext() + // Without Play services, GoogleApiManager keeps the call pending while it offers a + // "Get Google Play services" resolution, so the Task would never complete or fail. + if ( + GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != + ConnectionResult.SUCCESS + ) { + Log.d(TAG, "Google Play services unavailable, falling back to system service") + onFallback() + return + } + val client = ThreadNetwork.getClient(context) + client + .getAllActiveCredentials(GetAllActiveCredentialsRequest.newBuilder().build()) + .addOnSuccessListener { result -> + if (fragment.view == null) { + return@addOnSuccessListener + } + if (!launchConsent(fragment, result?.intentSender, REQUEST_CODE_ACTIVE_CREDENTIALS)) { + fetchPreferredCredentials(client, fragment, onFallback) + } + } + .addOnFailureListener { e -> + // For example LOCAL_NETWORK_NOT_CONNECTED when the phone is not on Wi-Fi. + Log.w(TAG, "GMS getAllActiveCredentials failed, trying getPreferredCredentials", e) + fetchPreferredCredentials(client, fragment, onFallback) + } + } catch (e: Exception) { + Log.d(TAG, "GMS ThreadNetwork API failed, falling back to system service", e) + onFallback() + } + } + + private fun fetchPreferredCredentials( + client: ThreadNetworkClient, + fragment: Fragment, + onFallback: () -> Unit, + ) { + try { + client.preferredCredentials + .addOnSuccessListener { result -> + if (fragment.view == null) { + return@addOnSuccessListener + } + if (!launchConsent(fragment, result?.intentSender, REQUEST_CODE_PREFERRED_CREDENTIALS)) { + onFallback() + } + } + .addOnFailureListener { e -> + Log.w(TAG, "GMS getPreferredCredentials failed, falling back to system service", e) + onFallback() + } + } catch (e: Exception) { + Log.e(TAG, "Error calling getPreferredCredentials", e) + onFallback() + } + } + + /** Launches the GMS consent prompt; returns false if there is nothing to launch or it failed. */ + private fun launchConsent(fragment: Fragment, sender: IntentSender?, requestCode: Int): Boolean { + if (sender == null) { + return false + } + return try { + @Suppress("DEPRECATION") + fragment.startIntentSenderForResult(sender, requestCode, null, 0, 0, 0, null) + true + } catch (e: Exception) { + Log.e(TAG, "Failed to launch GMS Thread consent intent", e) + false + } + } + + /** + * Returns the networks shared through a consent prompt launched by [fetchFromGms], an empty list + * if the user declined or nothing usable was shared, or null if [requestCode] is not ours. + */ + fun parseConsentResult( + requestCode: Int, + resultCode: Int, + data: Intent?, + ): List? { + if ( + requestCode != REQUEST_CODE_ACTIVE_CREDENTIALS && + requestCode != REQUEST_CODE_PREFERRED_CREDENTIALS + ) { + return null + } + if (resultCode != Activity.RESULT_OK || data == null) { + return emptyList() + } + return try { + val credentials = + if (requestCode == REQUEST_CODE_ACTIVE_CREDENTIALS) { + ThreadNetworkCredentials.parseListFromIntentSenderResultData(data) + } else { + listOf(ThreadNetworkCredentials.fromIntentSenderResultData(data)) + } + credentials + .filter { it.activeOperationalDataset.isNotEmpty() } + .map { + ThreadDatasetCandidate( + it.networkName, + it.extendedPanId, + it.channel, + it.activeOperationalDataset, + ) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to parse ThreadNetworkCredentials from intent result", e) + emptyList() + } + } +} diff --git a/examples/android/CHIPTool/app/src/main/AndroidManifest.xml b/examples/android/CHIPTool/app/src/main/AndroidManifest.xml index efd089817ef..6f5ffe5a672 100644 --- a/examples/android/CHIPTool/app/src/main/AndroidManifest.xml +++ b/examples/android/CHIPTool/app/src/main/AndroidManifest.xml @@ -17,6 +17,11 @@ + + + + + + diff --git a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/OtaProviderClientFragment.kt b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/OtaProviderClientFragment.kt index 3efdb47f139..2ead8075eaf 100644 --- a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/OtaProviderClientFragment.kt +++ b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/OtaProviderClientFragment.kt @@ -1,21 +1,39 @@ package com.google.chip.chiptool.clusterclient +import android.Manifest import android.app.Activity import android.app.AlertDialog +import android.content.Context import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.GradientDrawable import android.net.Uri +import android.net.wifi.WifiManager +import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.PowerManager +import android.os.SystemClock import android.provider.OpenableColumns import android.util.Log +import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.WindowManager import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.Button import android.widget.EditText +import android.widget.LinearLayout +import android.widget.ProgressBar import android.widget.Spinner +import android.widget.TextView import android.widget.Toast +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import chip.devicecontroller.ChipClusters @@ -59,6 +77,8 @@ class OtaProviderClientFragment : Fragment() { private var _binding: OtaProviderClientFragmentBinding? = null private var fileUri: Uri? = null + @Volatile private var cachedFirmwareFileSize: Long = 0L + @Volatile private var cachedTargetDeviceIds: List = emptyList() private val vendorId: Int get() = binding.vendorIdEd.text.toString().toInt() @@ -72,14 +92,204 @@ class OtaProviderClientFragment : Fragment() { private val attributeList = ClusterIDMapping.OtaSoftwareUpdateRequestor.Attribute.values() + // Power/radio locks are held while at least one BDX transfer is active. They are acquired in + // handleBDXTransferSessionBegin, which the native layer pairs with handleBDXTransferSessionEnd. + // OtaTransferService is started proactively when the user taps Announce while in the foreground + // so Android 12+ (API 31+) does not block FGS startup if BDX Init arrives after the user + // backgrounds the app. Active transfers and announced-waiting nodes are tracked as sets of node + // IDs so duplicate callbacks cannot skew the reference count. + private val transferLockGuard = Any() + private val activeTransferNodes = HashSet() + private val announcedWaitingNodes = HashSet() + private var wakeLock: PowerManager.WakeLock? = null + private var wifiLock: WifiManager.WifiLock? = null + private var lastLockRenewalMs = 0L + // Application context used to start OtaTransferService, kept so it can be stopped after detach. + private var transferServiceContext: Context? = null + private val lockHandler = Handler(Looper.getMainLooper()) + // Safety net in case an end callback is ever missed: the native BDX layer ends a stalled session + // after 5 minutes (and BDX Init timeout is 2 minutes), so if no progress occurs within + // LOCK_SAFETY_TIMEOUT_MS the locks and foreground service are dropped. + private val lockSafetyReleaser = Runnable { + Log.w(TAG, "No OTA transfer progress for $LOCK_SAFETY_TIMEOUT_MS ms, releasing transfer locks") + releaseAllTransferLocks() + renderMultiDeviceStatus() + } + + private fun onNodesAnnounced(nodeIds: Collection) { + synchronized(transferLockGuard) { + for (nodeId in nodeIds) { + if (!activeTransferNodes.contains(nodeId)) { + announcedWaitingNodes.add(nodeId) + } + } + if (announcedWaitingNodes.isNotEmpty() || activeTransferNodes.isNotEmpty()) { + val appContext = context?.applicationContext + if (appContext != null && transferServiceContext == null) { + val summary = + "Waiting for OTA QueryImage (${announcedWaitingNodes.size} node(s) announced)" + if (OtaTransferService.start(appContext, summary)) { + transferServiceContext = appContext + } + } + armSafetyTimeoutLocked() + } + } + } + + private fun onNodeQueryImageAccepted(nodeId: Long) { + synchronized(transferLockGuard) { + announcedWaitingNodes.add(nodeId) + if (activeTransferNodes.remove(nodeId) && activeTransferNodes.isEmpty()) { + releasePowerAndWifiLocksLocked() + } + val appContext = context?.applicationContext + if (appContext != null && transferServiceContext == null) { + val summary = "Waiting for OTA BDX Init (${announcedWaitingNodes.size} node(s) waiting)" + if (OtaTransferService.start(appContext, summary)) { + transferServiceContext = appContext + } + } + armSafetyTimeoutLocked() + } + } + + private fun onNodeAnnounceFinished(nodeId: Long) { + synchronized(transferLockGuard) { + if ( + announcedWaitingNodes.remove(nodeId) && + activeTransferNodes.isEmpty() && + announcedWaitingNodes.isEmpty() + ) { + releaseTransferLocksLocked() + } + } + } + + private fun onTransferSessionStarted(nodeId: Long) { + synchronized(transferLockGuard) { + announcedWaitingNodes.remove(nodeId) + if (activeTransferNodes.add(nodeId) && activeTransferNodes.size == 1) { + acquireTransferLocksLocked() + } + renewTransferLocksLocked() + } + } + + private fun onTransferSessionEnded(nodeId: Long) { + synchronized(transferLockGuard) { + announcedWaitingNodes.remove(nodeId) + val removedActive = activeTransferNodes.remove(nodeId) + if (removedActive && activeTransferNodes.isEmpty()) { + releasePowerAndWifiLocksLocked() + } + if (activeTransferNodes.isEmpty() && announcedWaitingNodes.isEmpty()) { + releaseTransferLocksLocked() + } + } + } + + private fun onTransferProgress() { + synchronized(transferLockGuard) { + if ( + activeTransferNodes.isNotEmpty() && + SystemClock.elapsedRealtime() - lastLockRenewalMs >= LOCK_RENEW_INTERVAL_MS + ) { + renewTransferLocksLocked() + } + } + } + + private fun releaseAllTransferLocks() { + synchronized(transferLockGuard) { + activeTransferNodes.clear() + announcedWaitingNodes.clear() + releaseTransferLocksLocked() + } + } + + private fun acquireTransferLocksLocked() { + val context = context?.applicationContext ?: return + if (wakeLock == null) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + wakeLock = + powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOCK_TAG).apply { + setReferenceCounted(false) + } + } + if (wifiLock == null) { + val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager + // Note: WIFI_MODE_FULL_LOW_LATENCY is effective while the screen is on and the app is in the + // foreground; when backgrounded or screen-off, OtaTransferService + PARTIAL_WAKE_LOCK keep + // the BDX transfer session alive and exempt from Doze network restrictions. + val mode = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + WifiManager.WIFI_MODE_FULL_LOW_LATENCY + } else { + @Suppress("DEPRECATION") WifiManager.WIFI_MODE_FULL_HIGH_PERF + } + wifiLock = wifiManager.createWifiLock(mode, LOCK_TAG).apply { setReferenceCounted(false) } + } + wifiLock?.acquire() + Log.d(TAG, "Acquired OTA transfer wake lock and Wi-Fi lock") + // Doze ignores wake locks of apps that are not in the foreground-service state. + if (transferServiceContext == null && OtaTransferService.start(context)) { + transferServiceContext = context + } + } + + private fun armSafetyTimeoutLocked() { + lockHandler.removeCallbacks(lockSafetyReleaser) + lockHandler.postDelayed(lockSafetyReleaser, LOCK_SAFETY_TIMEOUT_MS) + } + + private fun renewTransferLocksLocked() { + lastLockRenewalMs = SystemClock.elapsedRealtime() + // Re-acquiring a non-reference-counted wake lock just restarts its timeout. + wakeLock?.acquire(LOCK_SAFETY_TIMEOUT_MS) + armSafetyTimeoutLocked() + } + + private fun releasePowerAndWifiLocksLocked() { + wakeLock?.let { if (it.isHeld) it.release() } + wifiLock?.let { if (it.isHeld) it.release() } + Log.d(TAG, "Released OTA transfer wake lock and Wi-Fi lock") + } + + private fun releaseTransferLocksLocked() { + lockHandler.removeCallbacks(lockSafetyReleaser) + releasePowerAndWifiLocksLocked() + transferServiceContext?.let { OtaTransferService.stop(it) } + transferServiceContext = null + lastNotificationUpdateElapsedMs = 0L + } + + private fun ensureNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val ctx = context ?: return + if ( + ContextCompat.checkSelfPermission(ctx, Manifest.permission.POST_NOTIFICATIONS) != + PackageManager.PERMISSION_GRANTED + ) { + requestPermissions( + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + REQUEST_CODE_POST_NOTIFICATIONS, + ) + } + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { _binding = OtaProviderClientFragmentBinding.inflate(inflater, container, false) scope = viewLifecycleOwner.lifecycleScope + // Keep the screen on while this page is visible. CPU/Wi-Fi locks are taken per BDX transfer. + requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + deviceController.setCompletionListener(ChipControllerCallback()) addressUpdateFragment = @@ -88,8 +298,13 @@ class OtaProviderClientFragment : Fragment() { binding.selectFirmwareFileBtn.setOnClickListener { selectFirmwareFileBtnClick() } binding.updateOTAStatusBtn.setOnClickListener { updateOTAStatusBtnClick() } binding.announceOTAProviderBtn.setOnClickListener { + refreshCachedTargetDeviceIds() scope.launch { sendAnnounceOTAProviderBtnClick() } } + binding.retryFailedOtaBtn.setOnClickListener { + refreshCachedTargetDeviceIds() + scope.launch { retryFailedOTAUpdatesBtnClick() } + } binding.writeAclBtn.setOnClickListener { scope.launch { sendAclBtnClick() } } @@ -107,6 +322,7 @@ class OtaProviderClientFragment : Fragment() { binding.vendorIdEd.setText(ChipClient.VENDOR_ID.toString()) binding.delayActionTimeEd.setText("0") + renderMultiDeviceStatus() deviceController.startOTAProvider(otaProviderCallback) return binding.root } @@ -130,7 +346,7 @@ class OtaProviderClientFragment : Fragment() { override fun onError( attributePath: ChipAttributePath?, eventPath: ChipEventPath?, - e: Exception + e: Exception, ) { Log.d(TAG, "onError : ", e) showMessage("Error : $e") @@ -149,7 +365,7 @@ class OtaProviderClientFragment : Fragment() { }, devicePtr, listOf(attributePath), - 0 + 0, ) } @@ -171,7 +387,7 @@ class OtaProviderClientFragment : Fragment() { ArrayAdapter( requireContext(), android.R.layout.simple_spinner_dropdown_item, - GroupSettingFragment.Companion.AccessControlEntry.values() + GroupSettingFragment.Companion.AccessControlEntry.values(), ) val dialog = AlertDialog.Builder(requireContext()).apply { setView(dialogView) }.create() @@ -183,7 +399,7 @@ class OtaProviderClientFragment : Fragment() { GroupSettingFragment.Companion.AccessControlEntry.valueOf( accessControlEntrySp.selectedItem.toString() ) - .id + .id, ) requireActivity().runOnUiThread { dialog.dismiss() } } @@ -224,7 +440,7 @@ class OtaProviderClientFragment : Fragment() { listOf(nodeId), /* single nodeId as subject. */ null, /* null (empty) targets: wildcard access to everything */ Optional.empty(), /* no auxiliaryType (does not apply to writes) */ - deviceController.fabricIndex.toUInt() + deviceController.fabricIndex.toUInt(), ) newEntry.toTlv(AnonymousTag, tlvWriter) tlvWriter.endArray() @@ -256,11 +472,11 @@ class OtaProviderClientFragment : Fragment() { 0, ClusterIDMapping.AccessControl.ID, ClusterIDMapping.AccessControl.Attribute.Acl.id, - tlvWriter.getEncoded() + tlvWriter.getEncoded(), ) ), 0, - 0 + 0, ) } @@ -283,13 +499,13 @@ class OtaProviderClientFragment : Fragment() { override fun onError( attributePath: ChipAttributePath?, eventPath: ChipEventPath?, - e: Exception + e: Exception, ) { requireActivity().runOnUiThread { Toast.makeText( requireActivity(), R.string.ota_provider_invalid_attribute, - Toast.LENGTH_SHORT + Toast.LENGTH_SHORT, ) .show() } @@ -310,7 +526,7 @@ class OtaProviderClientFragment : Fragment() { }, devicePtr, listOf(path), - 0 + 0, ) } @@ -321,7 +537,7 @@ class OtaProviderClientFragment : Fragment() { Toast.makeText( requireActivity(), R.string.ota_provider_invalid_attribute, - Toast.LENGTH_SHORT + Toast.LENGTH_SHORT, ) .show() } @@ -344,7 +560,7 @@ class OtaProviderClientFragment : Fragment() { sendWriteDefaultOTAProviders( providerNodeIdEd.text.toString().toULong(), endpointIdEd.text.toString().toUInt(), - fabricIndexEd.text.toString().toUInt() + fabricIndexEd.text.toString().toUInt(), ) requireActivity().runOnUiThread { dialog.dismiss() } } @@ -355,7 +571,7 @@ class OtaProviderClientFragment : Fragment() { private suspend fun sendWriteDefaultOTAProviders( providerNodeId: ULong, endpointId: UInt, - fabricIndex: UInt + fabricIndex: UInt, ) { val endpoint = OTA_REQUESTER_ENDPOINT_ID val clusterId = ClusterIDMapping.OtaSoftwareUpdateRequestor.ID @@ -397,7 +613,7 @@ class OtaProviderClientFragment : Fragment() { devicePtr, listOf(writeRequest), 0, - 0 + 0, ) } @@ -433,7 +649,7 @@ class OtaProviderClientFragment : Fragment() { addCategory(Intent.CATEGORY_OPENABLE) type = "*/*" }, - REQUEST_CODE_OPEN_SAF + REQUEST_CODE_OPEN_SAF, ) } @@ -444,6 +660,43 @@ class OtaProviderClientFragment : Fragment() { return requireContext().contentResolver.openInputStream(uri) } + /** Skips [count] bytes, stopping early only at end of stream. */ + private fun skipFully(stream: InputStream, count: Long) { + var remaining = count + while (remaining > 0) { + val skipped = stream.skip(remaining) + if (skipped > 0) { + remaining -= skipped + } else if (stream.read() >= 0) { + // skip() may return 0 before end of stream; read() distinguishes the two cases. + remaining-- + } else { + return + } + } + } + + /** Fills [buffer] unless end of stream is reached first; returns the number of bytes read. */ + private fun readFully(stream: InputStream, buffer: ByteArray): Int { + var total = 0 + while (total < buffer.size) { + val read = stream.read(buffer, total, buffer.size - total) + if (read < 0) { + break + } + total += read + } + return total + } + + private fun InputStream.closeQuietly() { + try { + close() + } catch (e: IOException) { + Log.d(TAG, "Failed to close firmware stream", e) + } + } + private fun queryName(uri: Uri?): String? { if (uri == null) { return null @@ -471,6 +724,7 @@ class OtaProviderClientFragment : Fragment() { } val filename = queryName(uri) fileUri = uri + cachedFirmwareFileSize = getFileSize(uri) requireActivity().runOnUiThread { binding.firmwareFileTv.text = filename } } @@ -483,11 +737,17 @@ class OtaProviderClientFragment : Fragment() { when (binding.titleStatusSp.selectedItem.toString()) { QueryImageResponseStatusEnum.UpdateAvailable.name -> - otaProviderCallback.setOTAFile(version, versionString, filename, fileUri) + otaProviderCallback.setOTAFile( + version, + versionString, + filename, + fileUri, + cachedFirmwareFileSize, + ) QueryImageResponseStatusEnum.Busy.name -> otaProviderCallback.setOTABusyError( binding.delayActionTimeEd.text.toString().toUInt(), - binding.titleUserConsentNeededSp.selectedItem.toString().toBoolean() + binding.titleUserConsentNeededSp.selectedItem.toString().toBoolean(), ) QueryImageResponseStatusEnum.NotAvailable.name -> otaProviderCallback.setOTANotAvailableError( @@ -496,43 +756,546 @@ class OtaProviderClientFragment : Fragment() { } } - private suspend fun sendAnnounceOTAProviderBtnClick() { - requireActivity().runOnUiThread { updateOTAStatusBtnClick() } + private fun getFileSize(uri: Uri?): Long { + if (uri == null) return 0L + try { + val cursor = requireContext().contentResolver.query(uri, null, null, null, null) + cursor?.use { c -> + val sizeIndex = c.getColumnIndex(OpenableColumns.SIZE) + if (sizeIndex != -1 && c.moveToFirst()) { + return c.getLong(sizeIndex) + } + } + } catch (e: Exception) { + Log.d(TAG, "Failed to query size from contentResolver cursor", e) + } + try { + requireContext().contentResolver.openFileDescriptor(uri, "r")?.use { pfd -> + return pfd.statSize + } + } catch (e: Exception) { + Log.d(TAG, "Failed to get statSize from fileDescriptor", e) + } + return 0L + } - val devicePtr = - try { - ChipClient.getConnectedDevicePointer(requireContext(), addressUpdateFragment.deviceId) - } catch (e: IllegalStateException) { - Log.d(TAG, "getConnectedDevicePointer exception", e) - showMessage("Get DevicePointer fail!") - return + private fun formatTimeDuration(durationMs: Long): String { + val totalSec = durationMs / 1000 + val hours = totalSec / 3600 + val minutes = (totalSec % 3600) / 60 + val seconds = totalSec % 60 + return if (hours > 0) { + String.format("%02d:%02d:%02d", hours, minutes, seconds) + } else { + String.format("%02d:%02d", minutes, seconds) + } + } + + data class DeviceOtaStatus( + val nodeId: Long, + var status: String = "Idle", + var blockIndex: Long = 0L, + var startTimeMs: Long = 0L, + var totalBytesTransferred: Long = 0L, + var totalFileSize: Long = 0L, + var transferSpeedKBps: Double = 0.0, + var isComplete: Boolean = false, + var endTimeMs: Long = 0L, + ) + + private class NodeCardViewHolder( + val rootView: LinearLayout, + val titleTv: TextView, + val badgeTv: TextView, + val progressBar: ProgressBar, + val metricsTv: TextView, + val statusTv: TextView, + val retryBtn: Button, + ) + + private val deviceStatusMap = java.util.concurrent.ConcurrentHashMap() + private val deviceInputStreams = + java.util.concurrent.ConcurrentHashMap() + private val nodeCardViews = HashMap() + private val badgeBackgroundCache = HashMap() + @Volatile private var lastUiRenderElapsedMs = 0L + @Volatile private var lastNotificationUpdateElapsedMs = 0L + + private fun dpToPx(context: Context, dp: Int): Int = + (dp * context.resources.displayMetrics.density).toInt() + + private fun createRoundedBackground( + fillColor: Int, + strokeColor: Int, + radiusDp: Int, + ctx: Context, + ) = + GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = dpToPx(ctx, radiusDp).toFloat() + setColor(fillColor) + if (strokeColor != Color.TRANSPARENT) { + setStroke(dpToPx(ctx, 1), strokeColor) + } + } + + private fun getOrCreateBadgeBackground(badgeBgHex: String, ctx: Context): GradientDrawable = + badgeBackgroundCache.getOrPut(badgeBgHex) { + createRoundedBackground(Color.parseColor(badgeBgHex), Color.TRANSPARENT, 12, ctx) + } + + private fun getOrCreateNodeCard( + context: Context, + container: LinearLayout, + nodeId: Long, + ): NodeCardViewHolder { + nodeCardViews[nodeId]?.let { + return it + } + + val pad12 = dpToPx(context, 12) + val pad8 = dpToPx(context, 8) + val pad4 = dpToPx(context, 4) + + val cardLayout = + LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setPadding(pad12, pad12, pad12, pad12) + background = + createRoundedBackground( + Color.parseColor("#FFFFFF"), + Color.parseColor("#CBD5E1"), + 8, + context, + ) + layoutParams = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + .apply { bottomMargin = pad8 } + } + + val headerRow = + LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + layoutParams = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + } + + val titleTv = + TextView(context).apply { + text = "Node $nodeId" + textSize = 15f + setTypeface(typeface, Typeface.BOLD) + setTextColor(Color.parseColor("#0F172A")) + layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) + } + + val badgeTv = + TextView(context).apply { + textSize = 12f + setTypeface(typeface, Typeface.BOLD) + setPadding(pad8, pad4, pad8, pad4) + } + + headerRow.addView(titleTv) + headerRow.addView(badgeTv) + + val progressBar = + ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal).apply { + max = 100 + progress = 0 + isIndeterminate = false + layoutParams = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + .apply { + topMargin = pad8 + bottomMargin = pad4 + } } - val otaRequestCluster = - ChipClusters.OtaSoftwareUpdateRequestorCluster(devicePtr, OTA_REQUESTER_ENDPOINT_ID) - otaRequestCluster.announceOTAProvider( - object : DefaultClusterCallback { - override fun onSuccess() { - showMessage("announceOTAProvider command success") - Log.e(TAG, "announceOTAProvider command success") + val metricsTv = + TextView(context).apply { + textSize = 12f + typeface = Typeface.MONOSPACE + setTextColor(Color.parseColor("#334155")) + visibility = View.GONE + } + + val footerRow = + LinearLayout(context).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + layoutParams = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + .apply { topMargin = pad4 } + } + + val statusTv = + TextView(context).apply { + textSize = 13f + setTextColor(Color.parseColor("#475569")) + layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) + } + + val retryBtn = + Button(context).apply { + setText(R.string.ota_provider_retry_node_btn_text) + isAllCaps = false + textSize = 12f + minHeight = dpToPx(context, 48) + visibility = View.GONE + setOnClickListener { scope.launch { sendAnnounceOTAProviderBtnClick(listOf(nodeId)) } } + } + + footerRow.addView(statusTv) + footerRow.addView(retryBtn) + + cardLayout.addView(headerRow) + cardLayout.addView(progressBar) + cardLayout.addView(metricsTv) + cardLayout.addView(footerRow) + + container.addView(cardLayout) + return NodeCardViewHolder( + cardLayout, + titleTv, + badgeTv, + progressBar, + metricsTv, + statusTv, + retryBtn, + ) + .also { nodeCardViews[nodeId] = it } + } + + private fun refreshCachedTargetDeviceIds(): List { + val input = _binding?.targetDeviceIdsEd?.text?.toString()?.trim().orEmpty() + val ids = + if (input.isNotBlank()) { + input + .split(",") + .mapNotNull { it.trim().toLongOrNull() } + .ifEmpty { listOf(addressUpdateFragment.deviceId) } + } else { + listOf(addressUpdateFragment.deviceId) + } + cachedTargetDeviceIds = ids + return ids + } + + private fun getTargetDeviceIds(): List { + if (Looper.myLooper() == Looper.getMainLooper()) { + return refreshCachedTargetDeviceIds() + } + return cachedTargetDeviceIds + } + + private fun isNodeStatusFailed(statusText: String): Boolean = + statusText.contains("fail", ignoreCase = true) || + statusText.contains("error", ignoreCase = true) + + private fun isNodeEligibleForRetry(nodeId: Long): Boolean { + val status = deviceStatusMap[nodeId]?.status.orEmpty() + return isNodeStatusFailed(status) || status.isEmpty() || status == "Announcing OTA Provider..." + } + + private fun updateDeviceStatus( + nodeId: Long, + status: String, + blockIndex: Long = 0L, + throttleUi: Boolean = false, + ) { + val deviceStatus = deviceStatusMap.getOrPut(nodeId) { DeviceOtaStatus(nodeId) } + deviceStatus.status = status + if (blockIndex > 0) { + deviceStatus.blockIndex = blockIndex + } + val nowElapsed = SystemClock.elapsedRealtime() + if (throttleUi && nowElapsed - lastUiRenderElapsedMs < UI_UPDATE_THROTTLE_MS) { + return + } + lastUiRenderElapsedMs = nowElapsed + renderMultiDeviceStatus() + } + + private fun renderMultiDeviceStatus() { + val activity = activity ?: return + activity.runOnUiThread { + val currentBinding = _binding ?: return@runOnUiThread + val ctx = context ?: return@runOnUiThread + + val activeCount: Int + val waitingCount: Int + val wakeHeld: Boolean + val wifiHeld: Boolean + val fgsActive: Boolean + synchronized(transferLockGuard) { + activeCount = activeTransferNodes.size + waitingCount = announcedWaitingNodes.size + wakeHeld = wakeLock?.isHeld == true + wifiHeld = wifiLock?.isHeld == true + fgsActive = transferServiceContext != null + } + + var aggregateSpeedKBps = 0.0 + var completedCount = 0 + var failedCount = 0 + var activePercentSum = 0.0 + var activePercentNodes = 0 + + val sortedEntries = deviceStatusMap.entries.sortedBy { it.key } + if (sortedEntries.isEmpty() && nodeCardViews.isNotEmpty()) { + currentBinding.deviceProgressContainer.removeAllViews() + nodeCardViews.clear() + } + + for ((nodeId, status) in sortedEntries) { + val isFailed = isNodeStatusFailed(status.status) + val isDone = + status.isComplete || + status.status.contains("Completed", ignoreCase = true) || + status.status.contains("Finished", ignoreCase = true) + val isApplying = status.status.contains("Applying", ignoreCase = true) + val isTransferring = + !isDone && + !isFailed && + (status.blockIndex > 0 || + status.status.contains("Sending", ignoreCase = true) || + status.status.contains("Started", ignoreCase = true)) + + if (isDone) completedCount++ + if (isFailed) failedCount++ + if (isTransferring) { + aggregateSpeedKBps += status.transferSpeedKBps + if (status.totalFileSize > 0) { + val nodePct = + ((status.totalBytesTransferred.toDouble() / status.totalFileSize.toDouble()) * 100.0) + .coerceIn(0.0, 100.0) + activePercentSum += nodePct + activePercentNodes++ + } + } + + val holder = getOrCreateNodeCard(ctx, currentBinding.deviceProgressContainer, nodeId) + holder.titleTv.text = "Node $nodeId" + + val (badgeLabel, badgeTextHex, badgeBgHex) = + when { + isDone -> Triple("✓ Completed", "#166534", "#DCFCE7") + isFailed -> Triple("✕ Failed", "#991B1B", "#FEE2E2") + isApplying -> Triple("⚙ Applying", "#5B21B6", "#EDE9FE") + isTransferring -> Triple("↑ Transferring", "#1E40AF", "#DBEAFE") + status.status.contains("Announc", ignoreCase = true) || + status.status.contains("QueryImage", ignoreCase = true) -> + Triple("⏳ Waiting", "#92400E", "#FEF3C7") + else -> Triple("• Idle", "#334155", "#F1F5F9") + } + holder.badgeTv.text = badgeLabel + holder.badgeTv.setTextColor(Color.parseColor(badgeTextHex)) + holder.badgeTv.background = getOrCreateBadgeBackground(badgeBgHex, ctx) + + val percentDouble = + if (status.totalFileSize > 0) { + ((status.totalBytesTransferred.toDouble() / status.totalFileSize.toDouble()) * 100.0) + .coerceIn(0.0, 100.0) + } else if (isDone) { + 100.0 + } else { + 0.0 + } + holder.progressBar.progress = percentDouble.toInt() + + if (status.blockIndex > 0 || status.totalFileSize > 0) { + val transferredKB = status.totalBytesTransferred / 1024.0 + val totalKB = status.totalFileSize / 1024.0 + val metricsBuilder = StringBuilder() + if (status.totalFileSize > 0) { + metricsBuilder.append( + String.format("%.1f / %.1f KB (%.1f%%)", transferredKB, totalKB, percentDouble) + ) + } else if (transferredKB > 0) { + metricsBuilder.append(String.format("%.1f KB", transferredKB)) + } + if (status.blockIndex > 0) { + if (metricsBuilder.isNotEmpty()) metricsBuilder.append(" · ") + metricsBuilder.append("Block #").append(status.blockIndex) + } + if (status.transferSpeedKBps > 0) { + if (metricsBuilder.isNotEmpty()) metricsBuilder.append(" · ") + metricsBuilder.append(String.format("%.1f KB/s", status.transferSpeedKBps)) + } + holder.metricsTv.text = metricsBuilder.toString() + holder.metricsTv.visibility = View.VISIBLE + } else { + holder.metricsTv.visibility = View.GONE } - override fun onError(ex: java.lang.Exception?) { - showMessage("announceOTAProvider command failure $ex") - Log.e(TAG, "announceOTAProvider command failure", ex) + holder.statusTv.text = status.status + holder.retryBtn.visibility = if (isFailed) View.VISIBLE else View.GONE + } + + currentBinding.otaProviderSummaryTv.text = + String.format( + "Serving %d / %d concurrent · Aggregate: %.1f KB/s · Waiting: %d · Done: %d · Failed: %d", + activeCount, + MAX_CONCURRENT_BDX_SESSIONS, + aggregateSpeedKBps, + waitingCount, + completedCount, + failedCount, + ) + currentBinding.otaLockStatusTv.text = + String.format( + "CPU WakeLock: %s · Wi-Fi Lock: %s · Foreground Service: %s", + if (wakeHeld) "HELD" else "IDLE", + if (wifiHeld) "HELD" else "IDLE", + if (fgsActive) "ACTIVE" else "IDLE", + ) + + if (fgsActive && activeCount > 0) { + val avgProgress = + if (activePercentNodes > 0) (activePercentSum / activePercentNodes).toInt() else 0 + val nowElapsed = SystemClock.elapsedRealtime() + if ( + avgProgress >= 100 || + nowElapsed - lastNotificationUpdateElapsedMs >= NOTIFICATION_UPDATE_THROTTLE_MS + ) { + lastNotificationUpdateElapsedMs = nowElapsed + synchronized(transferLockGuard) { + val liveActiveCount = activeTransferNodes.size + val svcContext = transferServiceContext + if (svcContext != null && liveActiveCount > 0) { + val notifSummary = + String.format( + "Serving %d node(s) · %d%% (%.1f KB/s)", + liveActiveCount, + avgProgress, + aggregateSpeedKBps, + ) + OtaTransferService.updateProgress( + svcContext, + liveActiveCount, + avgProgress, + notifSummary, + ) + } + } } - }, - deviceController.controllerNodeId.toULong().toLong(), - vendorId, - 0 /* AnnounceReason */, - Optional.empty(), - OTA_PROVIDER_ENDPOINT_ID - ) + } + } + } + + private suspend fun sendAnnounceOTAProviderBtnClick(nodesToAnnounce: List? = null) { + ensureNotificationPermission() + requireActivity().runOnUiThread { + refreshCachedTargetDeviceIds() + updateOTAStatusBtnClick() + } + + val activeSnapshot = synchronized(transferLockGuard) { activeTransferNodes.toSet() } + val requestedNodeIds = nodesToAnnounce ?: getTargetDeviceIds() + val targetNodeIds = + if (nodesToAnnounce == null) { + requestedNodeIds.filterNot { activeSnapshot.contains(it) } + } else { + requestedNodeIds + } + if (nodesToAnnounce == null) { + val staleKeys = deviceStatusMap.keys.filterNot { activeSnapshot.contains(it) } + for (staleKey in staleKeys) { + deviceStatusMap.remove(staleKey) + } + activity?.runOnUiThread { + val container = _binding?.deviceProgressContainer + val iter = nodeCardViews.entries.iterator() + while (iter.hasNext()) { + val entry = iter.next() + if (!activeSnapshot.contains(entry.key)) { + container?.removeView(entry.value.rootView) + iter.remove() + } + } + } + } + for (nodeId in targetNodeIds) { + deviceStatusMap[nodeId] = DeviceOtaStatus(nodeId, "Announcing OTA Provider...") + } + onNodesAnnounced(targetNodeIds) + renderMultiDeviceStatus() + + for (nodeId in targetNodeIds) { + val devicePtr = + try { + ChipClient.getConnectedDevicePointer(requireContext(), nodeId) + } catch (e: IllegalStateException) { + Log.d(TAG, "getConnectedDevicePointer exception for Node $nodeId", e) + onNodeAnnounceFinished(nodeId) + updateDeviceStatus(nodeId, "Connection failed: ${e.message}") + continue + } + + val otaRequestCluster = + ChipClusters.OtaSoftwareUpdateRequestorCluster(devicePtr, OTA_REQUESTER_ENDPOINT_ID) + otaRequestCluster.announceOTAProvider( + object : DefaultClusterCallback { + override fun onSuccess() { + Log.i(TAG, "announceOTAProvider command success for Node $nodeId") + updateDeviceStatus(nodeId, "Announced. Waiting for QueryImage...") + } + + override fun onError(ex: java.lang.Exception?) { + Log.e(TAG, "announceOTAProvider command failure for Node $nodeId", ex) + onNodeAnnounceFinished(nodeId) + updateDeviceStatus(nodeId, "Announce failed: ${ex?.message}") + } + }, + deviceController.controllerNodeId.toULong().toLong(), + vendorId, + 0 /* AnnounceReason */, + Optional.empty(), + OTA_PROVIDER_ENDPOINT_ID, + ) + } + } + + private suspend fun retryFailedOTAUpdatesBtnClick() { + val failedNodeIds = getTargetDeviceIds().filter { isNodeEligibleForRetry(it) } + + if (failedNodeIds.isEmpty()) { + requireActivity().runOnUiThread { + Toast.makeText(requireContext(), "No failed OTA nodes to retry", Toast.LENGTH_SHORT).show() + } + return + } + + sendAnnounceOTAProviderBtnClick(failedNodeIds) } override fun onDestroyView() { super.onDestroyView() + // Finish the provider first: it ends every active BDX session (delivering + // handleBDXTransferSessionEnd synchronously) and guarantees no further delegate callbacks. deviceController.finishOTAProvider() + for ((_, stream) in deviceInputStreams) { + stream.closeQuietly() + } + deviceInputStreams.clear() + nodeCardViews.clear() + badgeBackgroundCache.clear() + releaseAllTransferLocks() + activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) _binding = null } @@ -541,9 +1304,7 @@ class OtaProviderClientFragment : Fragment() { private var version: Long = 0 private var versionString: String? = null private var uri: Uri? = null - - private var inputStream: InputStream? = null - private var bufferedInputStream: BufferedInputStream? = null + private var fileSize: Long = 0L private var status: QueryImageResponseStatusEnum? = null private var delayedTime: UInt? = null @@ -554,13 +1315,15 @@ class OtaProviderClientFragment : Fragment() { versionString: String, fileName: String, uri: Uri?, - userConsentNeeded: Boolean? = null + fileSize: Long = 0L, + userConsentNeeded: Boolean? = null, ) { this.status = QueryImageResponseStatusEnum.UpdateAvailable this.version = version this.versionString = versionString this.fileName = fileName this.uri = uri + this.fileSize = fileSize this.delayedTime = null this.userConsentNeeded = userConsentNeeded } @@ -578,18 +1341,52 @@ class OtaProviderClientFragment : Fragment() { } override fun handleQueryImage( + nodeId: Long, vendorId: Int, productId: Int, softwareVersion: Long, hardwareVersion: Int?, location: String?, requestorCanConsent: Boolean?, - metadataForProvider: ByteArray? + metadataForProvider: ByteArray?, ): OTAProviderDelegate.QueryImageResponse? { Log.d( TAG, - "handleQueryImage, $vendorId, $productId, $softwareVersion, $hardwareVersion, $location" + "handleQueryImage, nodeId=$nodeId, $vendorId, $productId, " + + "$softwareVersion, $hardwareVersion, $location", ) + val resolvedNodeId = + if (nodeId != 0L) { + nodeId + } else { + val announcedNodeId = + getTargetDeviceIds().firstOrNull { + deviceStatusMap[it]?.status?.startsWith("Announced") == true + } + announcedNodeId ?: 0L + } + + if (resolvedNodeId != 0L) { + deviceInputStreams.remove(resolvedNodeId)?.closeQuietly() + deviceStatusMap[resolvedNodeId]?.apply { + blockIndex = 0L + totalBytesTransferred = 0L + transferSpeedKBps = 0.0 + isComplete = false + startTimeMs = 0L + endTimeMs = 0L + } + if (status == QueryImageResponseStatusEnum.UpdateAvailable) { + onNodeQueryImageAccepted(resolvedNodeId) + updateDeviceStatus(resolvedNodeId, "QueryImage received (SW Ver: $softwareVersion)") + } else { + onNodeAnnounceFinished(resolvedNodeId) + updateDeviceStatus( + resolvedNodeId, + "QueryImage received (SW Ver: $softwareVersion, response: $status)", + ) + } + } return when (status) { QueryImageResponseStatusEnum.UpdateAvailable -> @@ -598,7 +1395,7 @@ class OtaProviderClientFragment : Fragment() { OTAProviderDelegate.QueryImageResponse( status, delayedTime?.toLong() ?: 0, - userConsentNeeded ?: false + userConsentNeeded ?: false, ) QueryImageResponseStatusEnum.NotAvailable -> OTAProviderDelegate.QueryImageResponse(status, userConsentNeeded ?: false) @@ -606,82 +1403,179 @@ class OtaProviderClientFragment : Fragment() { } } - override fun handleOTAQueryFailure(error: Int) { - Log.d(TAG, "handleOTAQueryFailure, $error") - showMessage("handleOTAQueryFailure : $error") + override fun handleOTAQueryFailure(nodeId: Long, error: Int) { + Log.d(TAG, "handleOTAQueryFailure, nodeId=$nodeId, error=$error") + if (nodeId != 0L) { + deviceInputStreams.remove(nodeId)?.closeQuietly() + onTransferSessionEnded(nodeId) + updateDeviceStatus(nodeId, "OTA QueryImage failed (status: $error)") + } + showMessage("handleOTAQueryFailure (node $nodeId): $error") } override fun handleApplyUpdateRequest( nodeId: Long, - newVersion: Long + newVersion: Long, ): OTAProviderDelegate.ApplyUpdateResponse { Log.d(TAG, "handleApplyUpdateRequest, $nodeId, $newVersion") + updateDeviceStatus(nodeId, "Applying Update (Target Version: $newVersion)") return OTAProviderDelegate.ApplyUpdateResponse( OTAProviderDelegate.ApplyUpdateActionEnum.Proceed, - APPLY_WAITING_TIME + APPLY_WAITING_TIME, ) } override fun handleNotifyUpdateApplied(nodeId: Long) { Log.d(TAG, "handleNotifyUpdateApplied, $nodeId") - showMessage("Finish Firmware Update : $nodeId") + updateDeviceStatus(nodeId, "Firmware Update Completed Successfully!") } override fun handleBDXTransferSessionBegin( nodeId: Long, fileDesignator: String?, - offset: Long + offset: Long, ) { Log.d(TAG, "handleBDXTransferSessionBegin, $nodeId, $fileDesignator, $offset") + onTransferSessionStarted(nodeId) + // A redundant QueryImage/BDX Init from the same node supersedes its previous session. + deviceInputStreams.remove(nodeId)?.closeQuietly() + // Reset startTimeMs before opening the firmware stream so a failed begin cannot reuse a + // stale timestamp from a prior run when handleBDXTransferSessionEnd runs. + deviceStatusMap[nodeId]?.startTimeMs = 0L + var bufferedInputStream: BufferedInputStream? = null try { - inputStream = getInputStream(uri) + val inputStream = getInputStream(uri) ?: throw IOException("No firmware file selected") bufferedInputStream = BufferedInputStream(inputStream) - } catch (e: IOException) { - Log.d(TAG, "exception", e) - inputStream?.close() - bufferedInputStream?.close() - inputStream = null - bufferedInputStream = null + skipFully(bufferedInputStream, offset) + deviceInputStreams[nodeId] = bufferedInputStream + val deviceStatus = deviceStatusMap.getOrPut(nodeId) { DeviceOtaStatus(nodeId) } + deviceStatus.startTimeMs = System.currentTimeMillis() + deviceStatus.totalBytesTransferred = 0L + deviceStatus.totalFileSize = fileSize + deviceStatus.transferSpeedKBps = 0.0 + deviceStatus.isComplete = false + deviceStatus.endTimeMs = 0L + updateDeviceStatus(nodeId, "BDX Transfer Started (offset: $offset)") + } catch (e: Exception) { + // IOException, or SecurityException if access to the selected file was revoked. The first + // block query will find no stream and abort, which triggers handleBDXTransferSessionEnd + // after the native layer sends the abort StatusReport. + Log.d(TAG, "exception for Node $nodeId", e) + updateDeviceStatus(nodeId, "BDX Session Error: ${e.message}") + bufferedInputStream?.closeQuietly() return } } override fun handleBDXTransferSessionEnd(errorCode: Long, nodeId: Long) { Log.d(TAG, "handleBDXTransferSessionEnd, $errorCode, $nodeId") - inputStream?.close() - bufferedInputStream?.close() - inputStream = null - bufferedInputStream = null - showMessage("BDXTransfer End! - ErrorCode: $errorCode") + deviceInputStreams.remove(nodeId)?.closeQuietly() + onTransferSessionEnded(nodeId) + val statusObj = deviceStatusMap[nodeId] + if (statusObj != null) { + statusObj.endTimeMs = System.currentTimeMillis() + statusObj.isComplete = (errorCode == 0L) + if (statusObj.startTimeMs == 0L) { + val msg = + if (errorCode == 0L) statusObj.status else "${statusObj.status} (BDX error $errorCode)" + updateDeviceStatus(nodeId, msg) + return + } + val totalMs = statusObj.endTimeMs - statusObj.startTimeMs + val lastingTimeStr = formatTimeDuration(totalMs) + val finalSpeedMsg = + if (statusObj.transferSpeedKBps > 0) { + String.format( + " (Avg: %.1f KB/s / %.1f kbps, Total OTA Time: %s)", + statusObj.transferSpeedKBps, + statusObj.transferSpeedKBps * 8.0, + lastingTimeStr, + ) + } else " (Total OTA Time: $lastingTimeStr)" + val msg = + if (errorCode == 0L) "BDX Transfer Finished$finalSpeedMsg" + else "BDX Transfer Ended with error $errorCode (Total OTA Time: $lastingTimeStr)" + updateDeviceStatus(nodeId, msg) + } else { + val msg = + if (errorCode == 0L) "BDX Transfer Finished" + else "BDX Transfer Ended with error $errorCode" + updateDeviceStatus(nodeId, msg) + } } override fun handleBDXQuery( nodeId: Long, blockSize: Int, blockIndex: Long, - bytesToSkip: Long + bytesToSkip: Long, ): OTAProviderDelegate.BDXData? { - // This code is just example code. This code doesn't check blockIndex and bytesToSkip - // variable. Log.d(TAG, "handleBDXQuery, $nodeId, $blockSize, $blockIndex, $bytesToSkip") - showMessage("sending.. $blockIndex") + val bufferedInputStream = deviceInputStreams[nodeId] if (bufferedInputStream == null) { - return OTAProviderDelegate.BDXData(ByteArray(0), true) + // Returning null makes the native layer abort the transfer rather than report a bogus EOF. + Log.e(TAG, "No firmware stream for Node $nodeId, aborting BDX transfer") + return null } val packet = ByteArray(blockSize) - val len = bufferedInputStream!!.read(packet) - - val sendPacket = - if (len < blockSize) { - packet.copyOf(len) - } else if (len < 0) { - ByteArray(0) - } else { - packet.clone() + val len = + try { + skipFully(bufferedInputStream, bytesToSkip) + readFully(bufferedInputStream, packet) + } catch (e: IOException) { + // Native BdxOTASession aborts on null return and invokes handleBDXTransferSessionEnd + // once the abort StatusReport is sent, which releases the session's transfer locks. + Log.e(TAG, "Failed to read firmware for Node $nodeId", e) + updateDeviceStatus(nodeId, "BDX Read Error: ${e.message}") + deviceInputStreams.remove(nodeId)?.closeQuietly() + return null } + onTransferProgress() + + val sendPacket = if (len < blockSize) packet.copyOf(len) else packet val isEOF = len < blockSize + val deviceStatus = deviceStatusMap.getOrPut(nodeId) { DeviceOtaStatus(nodeId) } + if (sendPacket.isNotEmpty()) { + deviceStatus.totalBytesTransferred += sendPacket.size + val elapsedTimeSec = (System.currentTimeMillis() - deviceStatus.startTimeMs) / 1000.0 + if (elapsedTimeSec > 0) { + deviceStatus.transferSpeedKBps = + (deviceStatus.totalBytesTransferred / 1024.0) / elapsedTimeSec + } + } + + val now = System.currentTimeMillis() + val lastingMs = if (deviceStatus.startTimeMs > 0) now - deviceStatus.startTimeMs else 0L + val lastingText = formatTimeDuration(lastingMs) + + var etaText = "" + if (deviceStatus.totalFileSize > 0 && deviceStatus.transferSpeedKBps > 0) { + val remainingBytes = deviceStatus.totalFileSize - deviceStatus.totalBytesTransferred + if (remainingBytes > 0) { + val speedBytesPerSec = deviceStatus.transferSpeedKBps * 1024.0 + val remainingMs = ((remainingBytes / speedBytesPerSec) * 1000).toLong() + etaText = ", ETA: ${formatTimeDuration(remainingMs)}" + } else if (isEOF) { + etaText = ", ETA: 00:00" + } + } + + val speedText = + if (deviceStatus.transferSpeedKBps > 0) { + val kbps = deviceStatus.transferSpeedKBps * 8.0 + String.format("%.1f KB/s [%.1f kbps]", deviceStatus.transferSpeedKBps, kbps) + } else "" + + val statusMsg = + if (speedText.isNotEmpty()) { + "Sending firmware... Speed: $speedText (Elapsed: $lastingText$etaText)" + } else { + "Sending firmware... (Elapsed: $lastingText)" + } + updateDeviceStatus(nodeId, statusMsg, blockIndex, throttleUi = !isEOF) + return OTAProviderDelegate.BDXData(sendPacket, isEOF) } } @@ -706,7 +1600,9 @@ class OtaProviderClientFragment : Fragment() { } private fun showMessage(msg: String) { - requireActivity().runOnUiThread { binding.commandStatusTv.text = msg } + // Also called from the CHIP thread, possibly after the view has been destroyed. + val activity = activity ?: return + activity.runOnUiThread { _binding?.commandStatusTv?.text = msg } } companion object { @@ -715,9 +1611,19 @@ class OtaProviderClientFragment : Fragment() { fun newInstance(): OtaProviderClientFragment = OtaProviderClientFragment() private const val REQUEST_CODE_OPEN_SAF = 100 + private const val REQUEST_CODE_POST_NOTIFICATIONS = 101 private const val APPLY_WAITING_TIME = 10L private const val OTA_PROVIDER_ENDPOINT_ID = 0 private const val OTA_REQUESTER_ENDPOINT_ID = 0 + + private const val MAX_CONCURRENT_BDX_SESSIONS = 8 + private const val UI_UPDATE_THROTTLE_MS = 250L + private const val NOTIFICATION_UPDATE_THROTTLE_MS = 1000L + + private const val LOCK_TAG = "CHIPTool:OtaTransfer" + // Longer than the 5 minute BDX transfer timeout, so a live session never loses its locks. + private const val LOCK_SAFETY_TIMEOUT_MS = 10 * 60 * 1000L + private const val LOCK_RENEW_INTERVAL_MS = 30 * 1000L } } diff --git a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/OtaTransferService.kt b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/OtaTransferService.kt new file mode 100644 index 00000000000..9420d24f576 --- /dev/null +++ b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/OtaTransferService.kt @@ -0,0 +1,172 @@ +package com.google.chip.chiptool.clusterclient + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import android.util.Log +import com.google.chip.chiptool.R + +/** + * Foreground service that runs while the OTA provider has active BDX transfers. + * + * It holds no locks itself. Putting the app process in the foreground-service state is what keeps + * the wake lock and Wi-Fi lock held by [OtaProviderClientFragment] (same UID) effective in Doze and + * keeps network access when CHIPTool is not the foreground app. + */ +class OtaTransferService : Service() { + private var isForeground = false + + override fun onCreate() { + super.onCreate() + getSystemService(NotificationManager::class.java) + .createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + getString(R.string.ota_transfer_channel_name), + NotificationManager.IMPORTANCE_LOW, + ) + ) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + ACTION_STOP -> { + // Stopping through onStartCommand (rather than stopService) guarantees startForeground was + // already called for every earlier startForegroundService request. + isForeground = false + stopForeground(STOP_FOREGROUND_REMOVE) + stopSelfResult(startId) + } + ACTION_UPDATE_PROGRESS -> { + if (isForeground) { + val activeCount = intent.getIntExtra(EXTRA_ACTIVE_COUNT, 0) + val progressPercent = intent.getIntExtra(EXTRA_PROGRESS_PERCENT, -1) + val summaryText = intent.getStringExtra(EXTRA_SUMMARY_TEXT) + getSystemService(NotificationManager::class.java) + .notify(NOTIFICATION_ID, buildNotification(activeCount, progressPercent, summaryText)) + } else { + // Ignore late progress updates queued after ACTION_STOP so the notification is not + // recreated after the last transfer finishes. + stopSelfResult(startId) + } + } + else -> { + isForeground = true + val activeCount = intent?.getIntExtra(EXTRA_ACTIVE_COUNT, 0) ?: 0 + val progressPercent = intent?.getIntExtra(EXTRA_PROGRESS_PERCENT, -1) ?: -1 + val summaryText = intent?.getStringExtra(EXTRA_SUMMARY_TEXT) + startInForeground(activeCount, progressPercent, summaryText) + } + } + return START_NOT_STICKY + } + + override fun onDestroy() { + isForeground = false + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? = null + + private fun buildNotification( + activeCount: Int, + progressPercent: Int, + summaryText: String?, + ): Notification { + val builder = + Notification.Builder(this, CHANNEL_ID) + .setSmallIcon(android.R.drawable.stat_sys_upload) + .setContentTitle(getString(R.string.ota_transfer_notification_title)) + .setOngoing(true) + .setOnlyAlertOnce(true) + if (!summaryText.isNullOrEmpty()) { + builder.setContentText(summaryText) + } + if (activeCount > 0 && progressPercent >= 0) { + builder.setProgress(100, progressPercent.coerceIn(0, 100), false) + } + return builder.build() + } + + private fun startInForeground(activeCount: Int, progressPercent: Int, summaryText: String?) { + val notification = buildNotification(activeCount, progressPercent, summaryText) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE, + ) + } else { + startForeground(NOTIFICATION_ID, notification) + } + } + + companion object { + private const val TAG = "OtaTransferService" + private const val ACTION_START = "com.google.chip.chiptool.action.START_OTA_TRANSFER_SERVICE" + private const val ACTION_STOP = "com.google.chip.chiptool.action.STOP_OTA_TRANSFER_SERVICE" + private const val ACTION_UPDATE_PROGRESS = + "com.google.chip.chiptool.action.UPDATE_OTA_TRANSFER_PROGRESS" + private const val EXTRA_ACTIVE_COUNT = "extra_active_count" + private const val EXTRA_PROGRESS_PERCENT = "extra_progress_percent" + private const val EXTRA_SUMMARY_TEXT = "extra_summary_text" + private const val CHANNEL_ID = "ota_transfer" + private const val NOTIFICATION_ID = 0x07A + + /** Starts the foreground service; returns false if disallowed by the platform. */ + fun start(context: Context, summaryText: String? = null): Boolean = + try { + val intent = + Intent(context, OtaTransferService::class.java).apply { + action = ACTION_START + if (!summaryText.isNullOrEmpty()) { + putExtra(EXTRA_SUMMARY_TEXT, summaryText) + } + } + context.startForegroundService(intent) + true + } catch (e: Exception) { + // For example ForegroundServiceStartNotAllowedException when the app is in the background + // on API 31+. Transfers still run, but without the foreground-service Doze exemption. + Log.w(TAG, "Could not start OTA transfer foreground service", e) + false + } + + /** Updates the ongoing foreground notification progress if the service is active. */ + fun updateProgress( + context: Context, + activeCount: Int, + progressPercent: Int, + summaryText: String, + ) { + val intent = + Intent(context, OtaTransferService::class.java) + .setAction(ACTION_UPDATE_PROGRESS) + .putExtra(EXTRA_ACTIVE_COUNT, activeCount) + .putExtra(EXTRA_PROGRESS_PERCENT, progressPercent) + .putExtra(EXTRA_SUMMARY_TEXT, summaryText) + try { + context.startService(intent) + } catch (e: Exception) { + Log.w(TAG, "Could not update OTA transfer service progress", e) + } + } + + /** Requests the OTA transfer foreground service to remove its notification and stop. */ + fun stop(context: Context) { + val intent = Intent(context, OtaTransferService::class.java).setAction(ACTION_STOP) + try { + context.startService(intent) + } catch (e: Exception) { + Log.w(TAG, "Could not deliver stop to OTA transfer service, stopping directly", e) + context.stopService(Intent(context, OtaTransferService::class.java)) + } + } + } +} diff --git a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/provisioning/EnterNetworkFragment.kt b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/provisioning/EnterNetworkFragment.kt index 860db9febca..88d365058be 100644 --- a/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/provisioning/EnterNetworkFragment.kt +++ b/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/provisioning/EnterNetworkFragment.kt @@ -17,22 +17,34 @@ */ package com.google.chip.chiptool.provisioning +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.EditText import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import com.google.chip.chiptool.NetworkCredentialsParcelable import com.google.chip.chiptool.R import com.google.chip.chiptool.util.FragmentUtil +import java.lang.reflect.InvocationHandler +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Proxy +import java.util.concurrent.Executor /** * Fragment to collect Wi-Fi network information from user and send it to device being provisioned. */ class EnterNetworkFragment : Fragment() { + /** Unregisters the pending ThreadNetworkController dataset callback, if any. */ + private var unregisterDatasetCallback: (() -> Unit)? = null + private val networkType: ProvisionNetworkType get() = requireNotNull( @@ -46,7 +58,7 @@ class EnterNetworkFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View? { val layoutRes = when (networkType) { @@ -55,8 +67,18 @@ class EnterNetworkFragment : Fragment() { } return inflater.inflate(layoutRes, container, false).apply { + val fragmentView = this val saveNetworkBtn: Button = findViewById(R.id.saveNetworkBtn) - saveNetworkBtn.setOnClickListener { onSaveNetworkClicked(this) } + saveNetworkBtn.setOnClickListener { onSaveNetworkClicked(fragmentView) } + if (networkType == ProvisionNetworkType.THREAD) { + findViewById