diff --git a/.ai/instructions.md b/.ai/instructions.md index 681829bae6..994d517f75 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -276,12 +276,12 @@ This document provides essential context for AI models interacting with this pro ## 7. Specific Instructions for AI Collaboration * **Contribution Workflow (Pull Request Process):** - 1. **Fork & Branch:** Create a new branch in your fork. + 1. **Fork & Branch:** Create a new branch based on the `dev` branch (always use `git checkout -b dev` to ensure you're branching from `dev`, not the currently checked out branch). 2. **Make Changes:** Adhere to all coding conventions and patterns. 3. **Test:** Create component tests for all supported platforms and run the full test suite locally. 4. **Lint:** Run `pre-commit` to ensure code is compliant. 5. **Commit:** Commit your changes. There is no strict format for commit messages. - 6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made with the PULL_REQUEST_TEMPLATE.md template filled out correctly. + 6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template. * **Documentation Contributions:** * Documentation is hosted in the separate `esphome/esphome-docs` repository. @@ -402,35 +402,45 @@ This document provides essential context for AI models interacting with this pro _use_feature = True ``` - **Good Pattern (CORE.data with Helpers):** + **Bad Pattern (Flat Keys):** ```python + # Don't do this - keys should be namespaced under component domain + MY_FEATURE_KEY = "my_component_feature" + CORE.data[MY_FEATURE_KEY] = True + ``` + + **Good Pattern (dataclass):** + ```python + from dataclasses import dataclass, field from esphome.core import CORE - # Keys for CORE.data storage - COMPONENT_STATE_KEY = "my_component_state" - USE_FEATURE_KEY = "my_component_use_feature" + DOMAIN = "my_component" - def _get_component_state() -> list: - """Get component state from CORE.data.""" - return CORE.data.setdefault(COMPONENT_STATE_KEY, []) + @dataclass + class MyComponentData: + feature_enabled: bool = False + item_count: int = 0 + items: list[str] = field(default_factory=list) - def _get_use_feature() -> bool | None: - """Get feature flag from CORE.data.""" - return CORE.data.get(USE_FEATURE_KEY) + def _get_data() -> MyComponentData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = MyComponentData() + return CORE.data[DOMAIN] - def _set_use_feature(value: bool) -> None: - """Set feature flag in CORE.data.""" - CORE.data[USE_FEATURE_KEY] = value + def request_feature() -> None: + _get_data().feature_enabled = True - def enable_feature(): - _set_use_feature(True) + def add_item(item: str) -> None: + _get_data().items.append(item) ``` + If you need a real-world example, search for components that use `@dataclass` with `CORE.data` in the codebase. Note: Some components may use `TypedDict` for dictionary-based storage; both patterns are acceptable depending on your needs. + **Why this matters:** - Module-level globals persist between compilation runs if the dashboard doesn't fork/exec - `CORE.data` automatically clears between runs - - Typed helper functions provide better IDE support and maintainability - - Encapsulation makes state management explicit and testable + - Namespacing under `DOMAIN` prevents key collisions between components + - `@dataclass` provides type safety and cleaner attribute access * **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys. diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 3ade00f0cd..0a71b6859f 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c +191a0e6ab5842d153dd77a2023bc5742f9d4333c334de8d81b57f2b8d4d4b65e diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 28437e6302..41dd02458e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,7 @@ - [ ] Bugfix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Developer breaking change (an API change that could break external components) - [ ] Code quality improvements to existing code or addition of tests - [ ] Other diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index f314e79ad9..75586fd854 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -17,12 +17,12 @@ runs: steps: - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: venv # yamllint disable-line rule:line-length diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index dd1bc29d83..8e96297cc0 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -22,11 +22,11 @@ jobs: if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Generate a token id: generate-token - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2 + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 with: app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} @@ -68,6 +68,7 @@ jobs: 'bugfix', 'new-feature', 'breaking-change', + 'developer-breaking-change', 'code-quality' ]; @@ -367,6 +368,7 @@ jobs: { pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' }, { pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' }, { pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' }, + { pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' }, { pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' } ]; diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index 400373679f..4c4bbf9981 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -21,9 +21,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11" @@ -62,7 +62,7 @@ jobs: run: git diff - if: failure() name: Archive artifacts - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: generated-proto-files path: | diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 78d1c2b87f..94068c19d6 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -21,10 +21,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11" diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 7111c61dda..84d79cda17 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -43,13 +43,13 @@ jobs: - "docker" # - "lint" steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Set TAG run: | diff --git a/.github/workflows/ci-memory-impact-comment.yml b/.github/workflows/ci-memory-impact-comment.yml index eea1d2c148..7e81e1184d 100644 --- a/.github/workflows/ci-memory-impact-comment.yml +++ b/.github/workflows/ci-memory-impact-comment.yml @@ -49,7 +49,7 @@ jobs: - name: Check out code from base repository if: steps.pr.outputs.skip != 'true' - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: # Always check out from the base repository (esphome/esphome), never from forks # Use the PR's target branch to ensure we run trusted code from the main repo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16837b3186..434aa388f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,18 +36,18 @@ jobs: cache-key: ${{ steps.cache-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Generate cache-key id: cache-key run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: venv # yamllint disable-line rule:line-length @@ -70,7 +70,7 @@ jobs: if: needs.determine-jobs.outputs.python-linters == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -91,7 +91,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -132,7 +132,7 @@ jobs: - common steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Restore Python id: restore-python uses: ./.github/actions/restore-python @@ -152,12 +152,12 @@ jobs: . venv/bin/activate pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ - name: Upload coverage to Codecov - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: venv key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -183,7 +183,7 @@ jobs: component-test-batches: ${{ steps.determine.outputs.component-test-batches }} steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: # Fetch enough history to find the merge base fetch-depth: 2 @@ -193,7 +193,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Restore components graph cache - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} @@ -223,7 +223,7 @@ jobs: echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT - name: Save components graph cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} @@ -237,15 +237,15 @@ jobs: if: needs.determine-jobs.outputs.integration-tests == 'true' steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Set up Python 3.13 id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -273,7 +273,7 @@ jobs: if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Restore Python uses: ./.github/actions/restore-python @@ -321,7 +321,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -334,14 +334,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} @@ -400,7 +400,7 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -413,14 +413,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} @@ -489,7 +489,7 @@ jobs: steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: # Need history for HEAD~1 to work for checking changed files fetch-depth: 2 @@ -502,14 +502,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} @@ -577,7 +577,7 @@ jobs: version: 1.0 - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: @@ -662,13 +662,13 @@ jobs: if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') steps: - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - - uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache + - uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache env: SKIP: pylint,clang-tidy-hash - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 @@ -688,7 +688,7 @@ jobs: skip: ${{ steps.check-script.outputs.skip }} steps: - name: Check out target branch - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: ref: ${{ github.base_ref }} @@ -735,7 +735,7 @@ jobs: - name: Restore cached memory analysis id: cache-memory-analysis if: steps.check-script.outputs.skip != 'true' - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} @@ -759,7 +759,7 @@ jobs: - name: Cache platformio if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} @@ -800,7 +800,7 @@ jobs: - name: Save memory analysis to cache if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} @@ -821,7 +821,7 @@ jobs: fi - name: Upload memory analysis JSON - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: memory-analysis-target path: memory-analysis-target.json @@ -840,14 +840,14 @@ jobs: flash_usage: ${{ steps.extract.outputs.flash_usage }} steps: - name: Check out PR branch - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} @@ -885,7 +885,7 @@ jobs: --platform "$platform" - name: Upload memory analysis JSON - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: memory-analysis-pr path: memory-analysis-pr.json @@ -908,20 +908,20 @@ jobs: GH_TOKEN: ${{ github.token }} steps: - name: Check out code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Restore Python uses: ./.github/actions/restore-python with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Download target analysis JSON - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: memory-analysis-target path: ./memory-analysis continue-on-error: true - name: Download PR analysis JSON - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: memory-analysis-pr path: ./memory-analysis @@ -959,13 +959,13 @@ jobs: - memory-impact-comment if: always() steps: - - name: Success - if: ${{ !(contains(needs.*.result, 'failure')) }} - run: exit 0 - - name: Failure - if: ${{ contains(needs.*.result, 'failure') }} + - name: Check job results env: - JSON_DOC: ${{ toJSON(needs) }} + NEEDS_JSON: ${{ toJSON(needs) }} run: | - echo $JSON_DOC | jq - exit 1 + # memory-impact-target-branch is allowed to fail without blocking CI. + # This job builds the target branch (dev/beta/release) which may fail because: + # 1. The target branch has a build issue independent of this PR + # 2. This PR fixes a build issue on the target branch + # In either case, we only care that the PR branch builds successfully. + echo "$NEEDS_JSON" | jq -e 'del(.["memory-impact-target-branch"]) | all(.result != "failure")' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2273975328..e63c3075dd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,11 +54,11 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 + uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 + uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75d88abf29..b41b118504 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: branch_build: ${{ steps.tag.outputs.branch_build }} deploy_env: ${{ steps.tag.outputs.deploy_env }} steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Get tag id: tag # yamllint disable rule:line-length @@ -60,9 +60,9 @@ jobs: contents: read id-token: write steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.x" - name: Build @@ -92,14 +92,14 @@ jobs: os: "ubuntu-24.04-arm" steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.11" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Log in to docker hub uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 @@ -138,7 +138,7 @@ jobs: # version: ${{ needs.init.outputs.tag }} - name: Upload digests - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: digests-${{ matrix.platform.arch }} path: /tmp/digests @@ -168,17 +168,17 @@ jobs: - ghcr - dockerhub steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Download digests - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: pattern: digests-* path: /tmp/digests merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Log in to docker hub if: matrix.registry == 'dockerhub' @@ -219,10 +219,19 @@ jobs: - init - deploy-manifest steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + with: + app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} + private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} + owner: esphome + repositories: home-assistant-addon + - name: Trigger Workflow uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: - github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} + github-token: ${{ steps.generate-token.outputs.token }} script: | let description = "ESPHome"; if (context.eventName == "release") { @@ -245,10 +254,19 @@ jobs: needs: [init] environment: ${{ needs.init.outputs.deploy_env }} steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + with: + app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} + private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} + owner: esphome + repositories: esphome-schema + - name: Trigger Workflow uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: - github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }} + github-token: ${{ steps.generate-token.outputs.token }} script: | github.rest.actions.createWorkflowDispatch({ owner: "esphome", @@ -259,3 +277,34 @@ jobs: version: "${{ needs.init.outputs.tag }}", } }) + + version-notifier: + if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false' + runs-on: ubuntu-latest + needs: + - init + - deploy-manifest + steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + with: + app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} + private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} + owner: esphome + repositories: version-notifier + + - name: Trigger Workflow + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + github.rest.actions.createWorkflowDispatch({ + owner: "esphome", + repo: "version-notifier", + workflow_id: "notify.yml", + ref: "main", + inputs: { + version: "${{ needs.init.outputs.tag }}", + } + }) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 5843b3a5e0..7e03e2a5f9 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Stale - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 + uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch remove-stale-when-updated: true diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 9479645ccc..8c830d99c7 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -13,16 +13,16 @@ jobs: if: github.repository == 'esphome/esphome' steps: - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Checkout Home Assistant - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: repository: home-assistant/core path: lib/home-assistant - name: Setup Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: 3.13 @@ -41,7 +41,7 @@ jobs: python script/run-in-env.py pre-commit run --all-files - name: Commit changes - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot diff --git a/.gitignore b/.gitignore index 390d1ab45b..da568d9b83 100644 --- a/.gitignore +++ b/.gitignore @@ -91,6 +91,10 @@ venv-*/ # mypy .mypy_cache/ +# nix +/default.nix +/shell.nix + .pioenvs .piolibdeps .pio diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b86d00f2aa..de7d30cfa2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.5 + rev: v0.14.10 hooks: # Run the linter. - id: ruff diff --git a/CODEOWNERS b/CODEOWNERS index e6970af47c..bdcc86ef0c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -21,6 +21,7 @@ esphome/components/adc128s102/* @DeerMaximum esphome/components/addressable_light/* @justfalter esphome/components/ade7880/* @kpfleming esphome/components/ade7953/* @angelnu +esphome/components/ade7953_base/* @angelnu esphome/components/ade7953_i2c/* @angelnu esphome/components/ade7953_spi/* @angelnu esphome/components/ads1118/* @solomondg1 @@ -41,6 +42,7 @@ esphome/components/animation/* @syndlex esphome/components/anova/* @buxtronix esphome/components/apds9306/* @aodrenah esphome/components/api/* @esphome/core +esphome/components/aqi/* @freekode @jasstrong @ximex esphome/components/as5600/* @ammmze esphome/components/as5600/sensor/* @ammmze esphome/components/as7341/* @mrgnr @@ -72,6 +74,7 @@ esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/ble_nus/* @tomaszduda23 esphome/components/bluetooth_proxy/* @bdraco @jesserockz +esphome/components/bm8563/* @abmantis esphome/components/bme280_base/* @esphome/core esphome/components/bme280_spi/* @apbodrov esphome/components/bme680_bsec/* @trvrnrth @@ -88,6 +91,7 @@ esphome/components/bmp3xx_spi/* @latonita esphome/components/bmp581/* @kahrendt esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid +esphome/components/bthome_mithermometer/* @nagyrobi esphome/components/button/* @esphome/core esphome/components/bytebuffer/* @clydebarrow esphome/components/camera/* @bdraco @DT-art1 @@ -95,6 +99,7 @@ esphome/components/camera_encoder/* @DT-art1 esphome/components/canbus/* @danielschramm @mvturnho esphome/components/cap1188/* @mreditor97 esphome/components/captive_portal/* @esphome/core +esphome/components/cc1101/* @gabest11 @lygris esphome/components/ccs811/* @habbie esphome/components/cd74hc4067/* @asoehlke esphome/components/ch422g/* @clydebarrow @jesterret @@ -130,7 +135,7 @@ esphome/components/display_menu_base/* @numo68 esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee esphome/components/ds2484/* @mrk-its -esphome/components/dsmr/* @glmnet @zuidwijk +esphome/components/dsmr/* @glmnet @PolarGoose @zuidwijk esphome/components/duty_time/* @dudanov esphome/components/ee895/* @Stock-M esphome/components/ektf2232/touchscreen/* @jesserockz @@ -188,6 +193,7 @@ esphome/components/gps/* @coogle @ximex esphome/components/graph/* @synco esphome/components/graphical_display_menu/* @MrMDavidson esphome/components/gree/* @orestismers +esphome/components/gree/switch/* @nagyrobi esphome/components/grove_gas_mc_v2/* @YorkshireIoT esphome/components/grove_tb6612fng/* @max246 esphome/components/growatt_solar/* @leeuwte @@ -202,13 +208,16 @@ esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/switch/* @dwmw2 +esphome/components/hc8/* @omartijn esphome/components/hdc2010/* @optimusprimespace @ssieb esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hlk_fm22x/* @OnFreund +esphome/components/hlw8032/* @rici4kubicek esphome/components/hm3301/* @freekode esphome/components/hmac_md5/* @dwmw2 +esphome/components/hmac_sha256/* @dwmw2 esphome/components/homeassistant/* @esphome/core @OttoWinter esphome/components/homeassistant/number/* @landonr esphome/components/homeassistant/switch/* @Links2004 @@ -222,6 +231,7 @@ esphome/components/hte501/* @Stock-M esphome/components/http_request/ota/* @oarcher esphome/components/http_request/update/* @jesserockz esphome/components/htu31d/* @betterengineering +esphome/components/hub75/* @stuartparmenter esphome/components/hydreon_rgxx/* @functionpointer esphome/components/hyt271/* @Philippe12 esphome/components/i2c/* @esphome/core @@ -301,7 +311,7 @@ esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core esphome/components/media_player/* @jesserockz esphome/components/micro_wake_word/* @jesserockz @kahrendt -esphome/components/micronova/* @jorre05 +esphome/components/micronova/* @edenhaus @jorre05 esphome/components/microphone/* @jesserockz @kahrendt esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov @@ -385,6 +395,7 @@ esphome/components/radon_eye_rd200/* @jeffeb3 esphome/components/rc522/* @glmnet esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_spi/* @glmnet +esphome/components/rd03d/* @jasstrong esphome/components/resampler/speaker/* @kahrendt esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz @@ -460,6 +471,7 @@ esphome/components/st7735/* @SenexCrenshaw esphome/components/st7789v/* @kbx81 esphome/components/st7920/* @marsjan155 esphome/components/statsd/* @Links2004 +esphome/components/stts22h/* @B48D81EFCC esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/sun_gtil2/* @Mat931 @@ -481,6 +493,7 @@ esphome/components/template/datetime/* @rfdarter esphome/components/template/event/* @nohat esphome/components/template/fan/* @ssieb esphome/components/text/* @mauritskorse +esphome/components/thermopro_ble/* @sittner esphome/components/thermostat/* @kbx81 esphome/components/time/* @esphome/core esphome/components/tinyusb/* @kbx81 @@ -508,6 +521,7 @@ esphome/components/tuya/switch/* @jesserockz esphome/components/tuya/text_sensor/* @dentra esphome/components/uart/* @esphome/core esphome/components/uart/button/* @ssieb +esphome/components/uart/event/* @eoasmxd esphome/components/uart/packet_transport/* @clydebarrow esphome/components/udp/* @clydebarrow esphome/components/ufire_ec/* @pvizeli @@ -515,6 +529,7 @@ esphome/components/ufire_ise/* @pvizeli esphome/components/ultrasonic/* @OttoWinter esphome/components/update/* @jesserockz esphome/components/uponor_smatrix/* @kroimon +esphome/components/usb_cdc_acm/* @kbx81 esphome/components/usb_host/* @clydebarrow esphome/components/usb_uart/* @clydebarrow esphome/components/valve/* @esphome/core @@ -525,6 +540,7 @@ esphome/components/version/* @esphome/core esphome/components/voice_assistant/* @jesserockz @kahrendt esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/watchdog/* @oarcher +esphome/components/water_heater/* @dhoeben esphome/components/waveshare_epaper/* @clydebarrow esphome/components/web_server/ota/* @esphome/core esphome/components/web_server_base/* @esphome/core @@ -560,5 +576,6 @@ esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xxtea/* @clydebarrow esphome/components/zephyr/* @tomaszduda23 esphome/components/zhlt01/* @cfeenstra1024 +esphome/components/zigbee/* @tomaszduda23 esphome/components/zio_ultrasonic/* @kahrendt esphome/components/zwave_proxy/* @kbx81 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 303b548310..66ad3ed599 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ We welcome contributions to the ESPHome suite of code and documentation! -Please read our [contributing guide](https://esphome.io/guides/contributing.html) if you wish to contribute to the +Please read our [contributing guide](https://developers.esphome.io/contributing/code/) if you wish to contribute to the project and be sure to join us on [Discord](https://discord.gg/KhAMKrd). **See also:** diff --git a/Doxyfile b/Doxyfile index a19120b9da..503979b61e 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.12.0-dev +PROJECT_NUMBER = 2026.1.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/MANIFEST.in b/MANIFEST.in index 45d5e86672..ed65edc656 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include LICENSE include README.md include requirements.txt +recursive-include esphome *.yaml recursive-include esphome *.cpp *.h *.tcc *.c recursive-include esphome *.py.script recursive-include esphome LICENSE.txt diff --git a/README.md b/README.md index 0439b1bc06..b8ce8d091d 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ - - ESPHome Logo + + ESPHome Logo diff --git a/docker/Dockerfile b/docker/Dockerfile index 64ce67e819..348a503bc8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,6 +11,16 @@ FROM base-source-${BUILD_TYPE} AS base RUN git config --system --add safe.directory "*" +# Install build tools for Python packages that require compilation +# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager) +RUN if command -v apk > /dev/null; then \ + apk add --no-cache build-base; \ + else \ + apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && rm -rf /var/lib/apt/lists/*; \ + fi + ENV PIP_DISABLE_PIP_VERSION_CHECK=1 RUN pip install --no-cache-dir -U pip uv==0.6.14 diff --git a/esphome/__main__.py b/esphome/__main__.py index b0c081a34f..3849a585ca 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -62,6 +62,9 @@ from esphome.util import ( _LOGGER = logging.getLogger(__name__) +# Maximum buffer size for serial log reading to prevent unbounded memory growth +SERIAL_BUFFER_MAX_SIZE = 65536 + # Special non-component keys that appear in configs _NON_COMPONENT_KEYS = frozenset( { @@ -431,25 +434,37 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: while tries < 5: try: with ser: + buffer = b"" + ser.timeout = 0.1 # 100ms timeout for non-blocking reads while True: try: - raw = ser.readline() + # Read all available data and timestamp it + chunk = ser.read(ser.in_waiting or 1) + if not chunk: + continue + time_ = datetime.now() + milliseconds = time_.microsecond // 1000 + time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{milliseconds:03}]" + + # Add to buffer and process complete lines + # Limit buffer size to prevent unbounded memory growth + # if device sends data without newlines + buffer += chunk + if len(buffer) > SERIAL_BUFFER_MAX_SIZE: + buffer = buffer[-SERIAL_BUFFER_MAX_SIZE:] + while b"\n" in buffer: + raw_line, buffer = buffer.split(b"\n", 1) + line = raw_line.replace(b"\r", b"").decode( + "utf8", "backslashreplace" + ) + safe_print(parser.parse_line(line, time_str)) + + backtrace_state = platformio_api.process_stacktrace( + config, line, backtrace_state=backtrace_state + ) except serial.SerialException: _LOGGER.error("Serial port closed!") return 0 - line = ( - raw.replace(b"\r", b"") - .replace(b"\n", b"") - .decode("utf8", "backslashreplace") - ) - time_ = datetime.now() - nanoseconds = time_.microsecond // 1000 - time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]" - safe_print(parser.parse_line(line, time_str)) - - backtrace_state = platformio_api.process_stacktrace( - config, line, backtrace_state=backtrace_state - ) except serial.SerialException: tries += 1 time.sleep(1) @@ -518,10 +533,49 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int: rc = platformio_api.run_compile(config, CORE.verbose) if rc != 0: return rc + + # Check if firmware was rebuilt and emit build_info + create manifest + _check_and_emit_build_info() + idedata = platformio_api.get_idedata(config) return 0 if idedata is not None else 1 +def _check_and_emit_build_info() -> None: + """Check if firmware was rebuilt and emit build_info.""" + import json + + firmware_path = CORE.firmware_bin + build_info_json_path = CORE.relative_build_path("build_info.json") + + # Check if both files exist + if not firmware_path.exists() or not build_info_json_path.exists(): + return + + # Check if firmware is newer than build_info (indicating a relink occurred) + if firmware_path.stat().st_mtime <= build_info_json_path.stat().st_mtime: + return + + # Read build_info from JSON + try: + with open(build_info_json_path, encoding="utf-8") as f: + build_info = json.load(f) + except (OSError, json.JSONDecodeError) as e: + _LOGGER.debug("Failed to read build_info: %s", e) + return + + config_hash = build_info.get("config_hash") + build_time_str = build_info.get("build_time_str") + + if config_hash is None or build_time_str is None: + return + + # Emit build_info with human-readable time + _LOGGER.info( + "Build Info: config_hash=0x%08x build_time_str=%s", config_hash, build_time_str + ) + + def upload_using_esptool( config: ConfigType, port: str, file: str, speed: int ) -> str | int: @@ -750,7 +804,13 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: exit_code = compile_program(args, config) if exit_code != 0: return exit_code - _LOGGER.info("Successfully compiled program.") + if CORE.is_host: + from esphome.platformio_api import get_idedata + + program_path = str(get_idedata(config).firmware_elf_path) + _LOGGER.info("Successfully compiled program to path '%s'", program_path) + else: + _LOGGER.info("Successfully compiled program.") return 0 @@ -800,10 +860,8 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: if CORE.is_host: from esphome.platformio_api import get_idedata - idedata = get_idedata(config) - if idedata is None: - return 1 - program_path = idedata.raw["prog_path"] + program_path = str(get_idedata(config).firmware_elf_path) + _LOGGER.info("Running program from path '%s'", program_path) return run_external_process(program_path) # Get devices, resolving special identifiers like OTA @@ -944,6 +1002,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: """ from esphome import platformio_api from esphome.analyze_memory.cli import MemoryAnalyzerCLI + from esphome.analyze_memory.ram_strings import RamStringsAnalyzer # Always compile to ensure fresh data (fast if no changes - just relinks) exit_code = write_cpp(config) @@ -966,21 +1025,39 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int: external_components = detect_external_components(config) _LOGGER.debug("Detected external components: %s", external_components) - # Perform memory analysis + # Perform component memory analysis _LOGGER.info("Analyzing memory usage...") analyzer = MemoryAnalyzerCLI( str(firmware_elf), idedata.objdump_path, idedata.readelf_path, external_components, + idedata=idedata, ) analyzer.analyze() - # Generate and display report + # Generate and display component report report = analyzer.generate_report() print() print(report) + # Perform RAM strings analysis + _LOGGER.info("Analyzing RAM strings...") + try: + ram_analyzer = RamStringsAnalyzer( + str(firmware_elf), + objdump_path=idedata.objdump_path, + platform=CORE.target_platform, + ) + ram_analyzer.analyze() + + # Generate and display RAM strings report + ram_report = ram_analyzer.generate_report() + print() + print(ram_report) + except Exception as e: # pylint: disable=broad-except + _LOGGER.warning("RAM strings analysis failed: %s", e) + return 0 @@ -1319,7 +1396,7 @@ def parse_args(argv): "clean-all", help="Clean all build and platform files." ) parser_clean_all.add_argument( - "configuration", help="Your YAML configuration directory.", nargs="*" + "configuration", help="Your YAML file or configuration directory.", nargs="*" ) parser_dashboard = subparsers.add_parser( diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 71e86e3788..9c935c78fa 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -15,27 +15,20 @@ from .const import ( SECTION_TO_ATTR, SYMBOL_PATTERNS, ) +from .demangle import batch_demangle from .helpers import ( get_component_class_patterns, get_esphome_components, map_section_name, parse_symbol_line, ) +from .toolchain import find_tool, run_tool if TYPE_CHECKING: from esphome.platformio_api import IDEData _LOGGER = logging.getLogger(__name__) -# GCC global constructor/destructor prefix annotations -_GCC_PREFIX_ANNOTATIONS = { - "_GLOBAL__sub_I_": "global constructor for", - "_GLOBAL__sub_D_": "global destructor for", -} - -# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2) -_GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)") - # C++ runtime patterns for categorization _CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"]) @@ -61,6 +54,9 @@ _NAMESPACE_STD = "std::" # Type alias for symbol information: (symbol_name, size, component) SymbolInfoType = tuple[str, int, str] +# RAM sections - symbols in these sections consume RAM +RAM_SECTIONS = frozenset([".data", ".bss"]) + @dataclass class MemorySection: @@ -68,7 +64,20 @@ class MemorySection: name: str symbols: list[SymbolInfoType] = field(default_factory=list) - total_size: int = 0 + total_size: int = 0 # Actual section size from ELF headers + symbol_size: int = 0 # Sum of symbol sizes (may be less than total_size) + + +@dataclass +class SDKSymbol: + """Represents a symbol from an SDK library that's not in the ELF symbol table.""" + + name: str + size: int + library: str # Name of the .a file (e.g., "libpp.a") + section: str # ".bss" or ".data" + is_local: bool # True if static/local symbol (lowercase in nm output) + demangled: str = "" # Demangled name (populated after analysis) @dataclass @@ -126,6 +135,10 @@ class MemoryAnalyzer: self.objdump_path = objdump_path or "objdump" self.readelf_path = readelf_path or "readelf" self.external_components = external_components or set() + self._idedata = idedata + + # Derive nm path from objdump path using shared toolchain utility + self.nm_path = find_tool("nm", self.objdump_path) self.sections: dict[str, MemorySection] = {} self.components: dict[str, ComponentMemory] = defaultdict( @@ -136,15 +149,25 @@ class MemoryAnalyzer: self._esphome_core_symbols: list[ tuple[str, str, int] ] = [] # Track core symbols - self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict( + # Track symbols for all components: (symbol_name, demangled, size, section) + self._component_symbols: dict[str, list[tuple[str, str, int, str]]] = ( + defaultdict(list) + ) + # Track RAM symbols separately for detailed analysis: (symbol_name, demangled, size, section) + self._ram_symbols: dict[str, list[tuple[str, str, int, str]]] = defaultdict( list - ) # Track symbols for all components + ) + # Track ELF symbol names for SDK cross-reference + self._elf_symbol_names: set[str] = set() + # SDK symbols not in ELF (static/local symbols from closed-source libs) + self._sdk_symbols: list[SDKSymbol] = [] def analyze(self) -> dict[str, ComponentMemory]: """Analyze the ELF file and return component memory usage.""" self._parse_sections() self._parse_symbols() self._categorize_symbols() + self._analyze_sdk_libraries() return dict(self.components) def _parse_sections(self) -> None: @@ -198,6 +221,8 @@ class MemoryAnalyzer: continue self.sections[section].symbols.append((name, size, "")) + self.sections[section].symbol_size += size + self._elf_symbol_names.add(name) seen_addresses.add(address) def _categorize_symbols(self) -> None: @@ -241,8 +266,13 @@ class MemoryAnalyzer: if size > 0: demangled = self._demangle_symbol(symbol_name) self._component_symbols[component].append( - (symbol_name, demangled, size) + (symbol_name, demangled, size, section_name) ) + # Track RAM symbols separately for detailed RAM analysis + if section_name in RAM_SECTIONS: + self._ram_symbols[component].append( + (symbol_name, demangled, size, section_name) + ) def _identify_component(self, symbol_name: str) -> str: """Identify which component a symbol belongs to.""" @@ -312,168 +342,9 @@ class MemoryAnalyzer: if not symbols: return - # Try to find the appropriate c++filt for the platform - cppfilt_cmd = "c++filt" - _LOGGER.info("Demangling %d symbols", len(symbols)) - _LOGGER.debug("objdump_path = %s", self.objdump_path) - - # Check if we have a toolchain-specific c++filt - if self.objdump_path and self.objdump_path != "objdump": - # Replace objdump with c++filt in the path - potential_cppfilt = self.objdump_path.replace("objdump", "c++filt") - _LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt) - if Path(potential_cppfilt).exists(): - cppfilt_cmd = potential_cppfilt - _LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd) - else: - _LOGGER.info( - "✗ Toolchain c++filt not found at %s, using system c++filt", - potential_cppfilt, - ) - else: - _LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path) - - # Strip GCC optimization suffixes and prefixes before demangling - # Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt - # Prefixes like _GLOBAL__sub_I_ need to be removed and tracked - symbols_stripped: list[str] = [] - symbols_prefixes: list[str] = [] # Track removed prefixes - for symbol in symbols: - # Remove GCC optimization markers - stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol) - - # Handle GCC global constructor/initializer prefixes - # _GLOBAL__sub_I_ -> extract for demangling - prefix = "" - for gcc_prefix in _GCC_PREFIX_ANNOTATIONS: - if stripped.startswith(gcc_prefix): - prefix = gcc_prefix - stripped = stripped[len(prefix) :] - break - - symbols_stripped.append(stripped) - symbols_prefixes.append(prefix) - - try: - # Send all symbols to c++filt at once - result = subprocess.run( - [cppfilt_cmd], - input="\n".join(symbols_stripped), - capture_output=True, - text=True, - check=False, - ) - except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e: - # On error, cache originals - _LOGGER.warning("Failed to batch demangle symbols: %s", e) - for symbol in symbols: - self._demangle_cache[symbol] = symbol - return - - if result.returncode != 0: - _LOGGER.warning( - "c++filt exited with code %d: %s", - result.returncode, - result.stderr[:200] if result.stderr else "(no error output)", - ) - # Cache originals on failure - for symbol in symbols: - self._demangle_cache[symbol] = symbol - return - - # Process demangled output - self._process_demangled_output( - symbols, symbols_stripped, symbols_prefixes, result.stdout, cppfilt_cmd - ) - - def _process_demangled_output( - self, - symbols: list[str], - symbols_stripped: list[str], - symbols_prefixes: list[str], - demangled_output: str, - cppfilt_cmd: str, - ) -> None: - """Process demangled symbol output and populate cache. - - Args: - symbols: Original symbol names - symbols_stripped: Stripped symbol names sent to c++filt - symbols_prefixes: Removed prefixes to restore - demangled_output: Output from c++filt - cppfilt_cmd: Path to c++filt command (for logging) - """ - demangled_lines = demangled_output.strip().split("\n") - failed_count = 0 - - for original, stripped, prefix, demangled in zip( - symbols, symbols_stripped, symbols_prefixes, demangled_lines - ): - # Add back any prefix that was removed - demangled = self._restore_symbol_prefix(prefix, stripped, demangled) - - # If we stripped a suffix, add it back to the demangled name for clarity - if original != stripped and not prefix: - demangled = self._restore_symbol_suffix(original, demangled) - - self._demangle_cache[original] = demangled - - # Log symbols that failed to demangle (stayed the same as stripped version) - if stripped == demangled and stripped.startswith("_Z"): - failed_count += 1 - if failed_count <= 5: # Only log first 5 failures - _LOGGER.warning("Failed to demangle: %s", original) - - if failed_count == 0: - _LOGGER.info("Successfully demangled all %d symbols", len(symbols)) - return - - _LOGGER.warning( - "Failed to demangle %d/%d symbols using %s", - failed_count, - len(symbols), - cppfilt_cmd, - ) - - @staticmethod - def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str: - """Restore prefix that was removed before demangling. - - Args: - prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_") - stripped: Stripped symbol name - demangled: Demangled symbol name - - Returns: - Demangled name with prefix restored/annotated - """ - if not prefix: - return demangled - - # Successfully demangled - add descriptive prefix - if demangled != stripped and ( - annotation := _GCC_PREFIX_ANNOTATIONS.get(prefix) - ): - return f"[{annotation}: {demangled}]" - - # Failed to demangle - restore original prefix - return prefix + demangled - - @staticmethod - def _restore_symbol_suffix(original: str, demangled: str) -> str: - """Restore GCC optimization suffix that was removed before demangling. - - Args: - original: Original symbol name with suffix - demangled: Demangled symbol name without suffix - - Returns: - Demangled name with suffix annotation - """ - if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original): - return f"{demangled} [{suffix_match.group(1)}]" - return demangled + self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path) + _LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache)) def _demangle_symbol(self, symbol: str) -> str: """Get demangled C++ symbol name from cache.""" @@ -495,6 +366,247 @@ class MemoryAnalyzer: return "Other Core" + def get_unattributed_ram(self) -> tuple[int, int, int]: + """Get unattributed RAM sizes (SDK/framework overhead). + + Returns: + Tuple of (unattributed_bss, unattributed_data, total_unattributed) + These are bytes in RAM sections that have no corresponding symbols. + """ + bss_section = self.sections.get(".bss") + data_section = self.sections.get(".data") + + unattributed_bss = 0 + unattributed_data = 0 + + if bss_section: + unattributed_bss = max(0, bss_section.total_size - bss_section.symbol_size) + if data_section: + unattributed_data = max( + 0, data_section.total_size - data_section.symbol_size + ) + + return unattributed_bss, unattributed_data, unattributed_bss + unattributed_data + + def _find_sdk_library_dirs(self) -> list[Path]: + """Find SDK library directories based on platform. + + Returns: + List of paths to SDK library directories containing .a files. + """ + sdk_dirs: list[Path] = [] + + if self._idedata is None: + return sdk_dirs + + # Get the CC path to determine the framework location + cc_path = getattr(self._idedata, "cc_path", None) + if not cc_path: + return sdk_dirs + + cc_path = Path(cc_path) + + # For ESP8266 Arduino framework + # CC is like: ~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-gcc + # SDK libs are in: ~/.platformio/packages/framework-arduinoespressif8266/tools/sdk/lib/ + if "xtensa-lx106" in str(cc_path): + platformio_dir = cc_path.parent.parent.parent + esp8266_sdk = ( + platformio_dir + / "framework-arduinoespressif8266" + / "tools" + / "sdk" + / "lib" + ) + if esp8266_sdk.exists(): + sdk_dirs.append(esp8266_sdk) + # Also check for NONOSDK subdirectories (closed-source libs) + sdk_dirs.extend( + subdir + for subdir in esp8266_sdk.iterdir() + if subdir.is_dir() and subdir.name.startswith("NONOSDK") + ) + + # For ESP32 IDF framework + # CC is like: ~/.platformio/packages/toolchain-xtensa-esp-elf/bin/xtensa-esp32-elf-gcc + # or: ~/.platformio/packages/toolchain-riscv32-esp/bin/riscv32-esp-elf-gcc + elif "xtensa-esp" in str(cc_path) or "riscv32-esp" in str(cc_path): + # Detect ESP32 variant from CC path or defines + variant = self._detect_esp32_variant() + if variant: + platformio_dir = cc_path.parent.parent.parent + espidf_dir = platformio_dir / "framework-espidf" / "components" + if espidf_dir.exists(): + # Find all directories named after the variant that contain .a files + # This handles various ESP-IDF library layouts: + # - components/*/lib// + # - components/*// + # - components/*/lib/lib// + # - components/*/*/lib_*// + sdk_dirs.extend( + variant_dir + for variant_dir in espidf_dir.rglob(variant) + if variant_dir.is_dir() and any(variant_dir.glob("*.a")) + ) + + return sdk_dirs + + def _detect_esp32_variant(self) -> str | None: + """Detect ESP32 variant from idedata defines. + + Returns: + Variant string like 'esp32', 'esp32s2', 'esp32c3', etc. or None. + """ + if self._idedata is None: + return None + + defines = getattr(self._idedata, "defines", []) + if not defines: + return None + + # ESPHome always adds USE_ESP32_VARIANT_xxx defines + variant_prefix = "USE_ESP32_VARIANT_" + for define in defines: + if define.startswith(variant_prefix): + # Extract variant name and convert to lowercase + # USE_ESP32_VARIANT_ESP32 -> esp32 + # USE_ESP32_VARIANT_ESP32S3 -> esp32s3 + return define[len(variant_prefix) :].lower() + + return None + + def _parse_sdk_library( + self, lib_path: Path + ) -> tuple[list[tuple[str, int, str, bool]], set[str]]: + """Parse a single SDK library for symbols. + + Args: + lib_path: Path to the .a library file + + Returns: + Tuple of: + - List of BSS/DATA symbols: (symbol_name, size, section, is_local) + - Set of global BSS/DATA symbol names (for checking if RAM is linked) + """ + ram_symbols: list[tuple[str, int, str, bool]] = [] + global_ram_symbols: set[str] = set() + + result = run_tool([self.nm_path, "--size-sort", str(lib_path)], timeout=10) + if result is None: + return ram_symbols, global_ram_symbols + + for line in result.stdout.splitlines(): + parts = line.split() + if len(parts) < 3: + continue + + try: + size = int(parts[0], 16) + sym_type = parts[1] + name = parts[2] + + # Only collect BSS (b/B) and DATA (d/D) for RAM analysis + if sym_type in ("b", "B"): + section = ".bss" + is_local = sym_type == "b" + ram_symbols.append((name, size, section, is_local)) + # Track global RAM symbols (B/D) for linking check + if sym_type == "B": + global_ram_symbols.add(name) + elif sym_type in ("d", "D"): + section = ".data" + is_local = sym_type == "d" + ram_symbols.append((name, size, section, is_local)) + if sym_type == "D": + global_ram_symbols.add(name) + except (ValueError, IndexError): + continue + + return ram_symbols, global_ram_symbols + + def _analyze_sdk_libraries(self) -> None: + """Analyze SDK libraries to find symbols not in the ELF. + + This finds static/local symbols from closed-source SDK libraries + that consume RAM but don't appear in the final ELF symbol table. + Only includes symbols from libraries that have RAM actually linked + (at least one global BSS/DATA symbol in the ELF). + """ + sdk_dirs = self._find_sdk_library_dirs() + if not sdk_dirs: + _LOGGER.debug("No SDK library directories found") + return + + _LOGGER.debug("Analyzing SDK libraries in %d directories", len(sdk_dirs)) + + # Track seen symbols to avoid duplicates from multiple SDK versions + seen_symbols: set[str] = set() + + for sdk_dir in sdk_dirs: + for lib_path in sorted(sdk_dir.glob("*.a")): + lib_name = lib_path.name + ram_symbols, global_ram_symbols = self._parse_sdk_library(lib_path) + + # Check if this library's RAM is actually linked by seeing if any + # of its global BSS/DATA symbols appear in the ELF + if not global_ram_symbols & self._elf_symbol_names: + # No RAM from this library is in the ELF - skip it + continue + + for name, size, section, is_local in ram_symbols: + # Skip if already in ELF or already seen from another lib + if name in self._elf_symbol_names or name in seen_symbols: + continue + + # Only track symbols with non-zero size + if size > 0: + self._sdk_symbols.append( + SDKSymbol( + name=name, + size=size, + library=lib_name, + section=section, + is_local=is_local, + ) + ) + seen_symbols.add(name) + + # Demangle SDK symbols for better readability + if self._sdk_symbols: + sdk_names = [sym.name for sym in self._sdk_symbols] + demangled_map = batch_demangle(sdk_names, objdump_path=self.objdump_path) + for sym in self._sdk_symbols: + sym.demangled = demangled_map.get(sym.name, sym.name) + + # Sort by size descending for reporting + self._sdk_symbols.sort(key=lambda s: s.size, reverse=True) + + total_sdk_ram = sum(s.size for s in self._sdk_symbols) + _LOGGER.debug( + "Found %d SDK symbols not in ELF, totaling %d bytes", + len(self._sdk_symbols), + total_sdk_ram, + ) + + def get_sdk_ram_symbols(self) -> list[SDKSymbol]: + """Get SDK symbols that consume RAM but aren't in the ELF symbol table. + + Returns: + List of SDKSymbol objects sorted by size descending. + """ + return self._sdk_symbols + + def get_sdk_ram_by_library(self) -> dict[str, list[SDKSymbol]]: + """Get SDK RAM symbols grouped by library. + + Returns: + Dictionary mapping library name to list of symbols. + """ + by_lib: dict[str, list[SDKSymbol]] = defaultdict(list) + for sym in self._sdk_symbols: + by_lib[sym.library].append(sym) + return dict(by_lib) + if __name__ == "__main__": from .cli import main diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 44ade221f8..a77e17afce 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -1,16 +1,24 @@ """CLI interface for memory analysis with report generation.""" +from __future__ import annotations + from collections import defaultdict +from collections.abc import Callable import sys +from typing import TYPE_CHECKING from . import ( _COMPONENT_API, _COMPONENT_CORE, _COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL, + RAM_SECTIONS, MemoryAnalyzer, ) +if TYPE_CHECKING: + from . import ComponentMemory + class MemoryAnalyzerCLI(MemoryAnalyzer): """Memory analyzer with CLI-specific report generation.""" @@ -19,6 +27,8 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): SYMBOL_SIZE_THRESHOLD: int = ( 100 # Show symbols larger than this in detailed analysis ) + # Lower threshold for RAM symbols (RAM is more constrained) + RAM_SYMBOL_SIZE_THRESHOLD: int = 24 # Column width constants COL_COMPONENT: int = 29 @@ -83,6 +93,60 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): COL_CORE_PERCENT, ) + def _add_section_header(self, lines: list[str], title: str) -> None: + """Add a section header with title centered between separator lines.""" + lines.append("") + lines.append("=" * self.TABLE_WIDTH) + lines.append(title.center(self.TABLE_WIDTH)) + lines.append("=" * self.TABLE_WIDTH) + lines.append("") + + def _add_top_consumers( + self, + lines: list[str], + title: str, + components: list[tuple[str, ComponentMemory]], + get_size: Callable[[ComponentMemory], int], + total: int, + memory_type: str, + limit: int = 25, + ) -> None: + """Add a formatted list of top memory consumers to the report. + + Args: + lines: List of report lines to append the output to. + title: Section title to print before the list. + components: Sequence of (name, ComponentMemory) tuples to analyze. + get_size: Callable that takes a ComponentMemory and returns the + size in bytes to use for ranking and display. + total: Total size in bytes for computing percentage usage. + memory_type: Label for the memory region (e.g., "flash" or "RAM"). + limit: Maximum number of components to include in the list. + """ + lines.append("") + lines.append(f"{title}:") + for i, (name, mem) in enumerate(components[:limit]): + size = get_size(mem) + if size > 0: + percentage = (size / total * 100) if total > 0 else 0 + lines.append( + f"{i + 1}. {name} ({size:,} B) - {percentage:.1f}% of analyzed {memory_type}" + ) + + def _format_symbol_with_section( + self, demangled: str, size: int, section: str | None = None + ) -> str: + """Format a symbol entry, optionally adding a RAM section label. + + If section is one of the RAM sections (.data or .bss), a label like + " [data]" or " [bss]" is appended. For non-RAM sections or when + section is None, no section label is added. + """ + section_label = "" + if section in RAM_SECTIONS: + section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss] + return f"{demangled} ({size:,} B){section_label}" + def generate_report(self, detailed: bool = False) -> str: """Generate a formatted memory report.""" components = sorted( @@ -123,43 +187,70 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B" ) - # Top consumers - lines.append("") - lines.append("Top Flash Consumers:") - for i, (name, mem) in enumerate(components[:25]): - if mem.flash_total > 0: - percentage = ( - (mem.flash_total / total_flash * 100) if total_flash > 0 else 0 - ) - lines.append( - f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash" - ) - - lines.append("") - lines.append("Top RAM Consumers:") - ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True) - for i, (name, mem) in enumerate(ram_components[:25]): - if mem.ram_total > 0: - percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0 - lines.append( - f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM" - ) - - lines.append("") - lines.append( - "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." + # Show unattributed RAM (SDK/framework overhead) + unattributed_bss, unattributed_data, unattributed_total = ( + self.get_unattributed_ram() + ) + if unattributed_total > 0: + lines.append("") + lines.append( + f"Unattributed RAM: {unattributed_total:,} B (SDK/framework overhead)" + ) + if unattributed_bss > 0 and unattributed_data > 0: + lines.append( + f" .bss: {unattributed_bss:,} B | .data: {unattributed_data:,} B" + ) + + # Show SDK symbol breakdown if available + sdk_by_lib = self.get_sdk_ram_by_library() + if sdk_by_lib: + lines.append("") + lines.append("SDK library breakdown (static symbols not in ELF):") + # Sort libraries by total size + lib_totals = [ + (lib, sum(s.size for s in syms), syms) + for lib, syms in sdk_by_lib.items() + ] + lib_totals.sort(key=lambda x: x[1], reverse=True) + + for lib_name, lib_total, syms in lib_totals: + if lib_total == 0: + continue + lines.append(f" {lib_name}: {lib_total:,} B") + # Show top symbols from this library + for sym in sorted(syms, key=lambda s: s.size, reverse=True)[:3]: + section_label = sym.section.lstrip(".") + # Use demangled name (falls back to original if not demangled) + display_name = sym.demangled or sym.name + if len(display_name) > 50: + display_name = f"{display_name[:47]}..." + lines.append( + f" {sym.size:>6,} B [{section_label}] {display_name}" + ) + + # Top consumers + self._add_top_consumers( + lines, + "Top Flash Consumers", + components, + lambda m: m.flash_total, + total_flash, + "flash", + ) + + ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True) + self._add_top_consumers( + lines, + "Top RAM Consumers", + ram_components, + lambda m: m.ram_total, + total_ram, + "RAM", ) - lines.append("=" * self.TABLE_WIDTH) # Add ESPHome core detailed analysis if there are core symbols if self._esphome_core_symbols: - lines.append("") - lines.append("=" * self.TABLE_WIDTH) - lines.append( - f"{_COMPONENT_CORE} Detailed Analysis".center(self.TABLE_WIDTH) - ) - lines.append("=" * self.TABLE_WIDTH) - lines.append("") + self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis") # Group core symbols by subcategory core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict( @@ -211,7 +302,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):" ) for i, (symbol, demangled, size) in enumerate(large_core_symbols): - lines.append(f"{i + 1}. {demangled} ({size:,} B)") + # Core symbols only track (symbol, demangled, size) without section info, + # so we don't show section labels here + lines.append( + f"{i + 1}. {self._format_symbol_with_section(demangled, size)}" + ) lines.append("=" * self.TABLE_WIDTH) @@ -267,11 +362,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): for comp_name, comp_mem in components_to_analyze: if not (comp_symbols := self._component_symbols.get(comp_name, [])): continue - lines.append("") - lines.append("=" * self.TABLE_WIDTH) - lines.append(f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH)) - lines.append("=" * self.TABLE_WIDTH) - lines.append("") + self._add_section_header(lines, f"{comp_name} Detailed Analysis") # Sort symbols by size sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True) @@ -282,19 +373,69 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): # Show all symbols above threshold for better visibility large_symbols = [ - (sym, dem, size) - for sym, dem, size in sorted_symbols + (sym, dem, size, sec) + for sym, dem, size, sec in sorted_symbols if size > self.SYMBOL_SIZE_THRESHOLD ] lines.append( f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" ) - for i, (symbol, demangled, size) in enumerate(large_symbols): - lines.append(f"{i + 1}. {demangled} ({size:,} B)") + for i, (symbol, demangled, size, section) in enumerate(large_symbols): + lines.append( + f"{i + 1}. {self._format_symbol_with_section(demangled, size, section)}" + ) lines.append("=" * self.TABLE_WIDTH) + # Detailed RAM analysis by component (at end, before RAM strings analysis) + self._add_section_header(lines, "RAM Symbol Analysis by Component") + + # Show top 15 RAM consumers with their large symbols + for name, mem in ram_components[:15]: + if mem.ram_total == 0: + continue + ram_syms = self._ram_symbols.get(name, []) + if not ram_syms: + continue + + # Sort by size descending + sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True) + large_ram_syms = [ + s for s in sorted_ram_syms if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD + ] + + lines.append(f"{name} ({mem.ram_total:,} B total RAM):") + + # Show breakdown by section type + data_size = sum(s[2] for s in ram_syms if s[3] == ".data") + bss_size = sum(s[2] for s in ram_syms if s[3] == ".bss") + lines.append(f" .data (initialized): {data_size:,} B") + lines.append(f" .bss (uninitialized): {bss_size:,} B") + + if large_ram_syms: + lines.append( + f" Symbols > {self.RAM_SYMBOL_SIZE_THRESHOLD} B ({len(large_ram_syms)}):" + ) + for symbol, demangled, size, section in large_ram_syms[:10]: + # Format section label consistently by stripping leading dot + section_label = section.lstrip(".") if section else "" + # Add ellipsis if name is truncated + demangled_display = ( + f"{demangled[:70]}..." if len(demangled) > 70 else demangled + ) + lines.append( + f" {size:>6,} B [{section_label}] {demangled_display}" + ) + if len(large_ram_syms) > 10: + lines.append(f" ... and {len(large_ram_syms) - 10} more") + lines.append("") + + lines.append( + "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." + ) + lines.append("=" * self.TABLE_WIDTH) + return "\n".join(lines) def dump_uncategorized_symbols(self, output_file: str | None = None) -> None: diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 78af82059f..9933bd77fd 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -7,11 +7,13 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::") # Section mapping for ELF file sections # Maps standard section names to their various platform-specific variants +# Note: Order matters! More specific patterns (.bss) must come before general ones (.dram) +# because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise SECTION_MAPPING = { ".text": frozenset([".text", ".iram"]), ".rodata": frozenset([".rodata"]), + ".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss" ".data": frozenset([".data", ".dram"]), - ".bss": frozenset([".bss"]), } # Section to ComponentMemory attribute mapping @@ -88,6 +90,77 @@ SYMBOL_PATTERNS = { "sys_mbox_new", "sys_arch_mbox_tryfetch", ], + # LibreTiny/Beken BK7231 radio calibration + "bk_radio_cal": [ + "bk7011_", + "calibration_main", + "gcali_", + "rwnx_cal", + ], + # LibreTiny/Beken WiFi MAC layer + "bk_wifi_mac": [ + "rxu_", # RX upper layer + "txu_", # TX upper layer + "txl_", # TX lower layer + "rxl_", # RX lower layer + "scanu_", # Scan unit + "mm_hw_", # MAC management hardware + "mm_bcn", # MAC management beacon + "mm_tim", # MAC management TIM + "mm_check", # MAC management checks + "sm_connect", # Station management + "me_beacon", # Management entity beacon + "me_build", # Management entity build + "hapd_", # Host AP daemon + "chan_pre_", # Channel management + "handle_probe_", # Probe handling + ], + # LibreTiny/Beken system control + "bk_system": [ + "sctrl_", # System control + "icu_ctrl", # Interrupt control unit + "gdma_ctrl", # DMA control + "mpb_ctrl", # MPB control + "uf2_", # UF2 OTA + "bkreg_", # Beken registers + ], + # LibreTiny/Beken BLE stack + "bk_ble": [ + "gapc_", # GAP client + "gattc_", # GATT client + "attc_", # ATT client + "attmdb_", # ATT database + "atts_", # ATT server + "l2cc_", # L2CAP + "prf_env", # Profile environment + ], + # LibreTiny/Beken scheduler + "bk_scheduler": [ + "sch_plan_", # Scheduler plan + "sch_prog_", # Scheduler program + "sch_arb_", # Scheduler arbiter + ], + # LibreTiny/Beken DMA descriptors + "bk_dma": [ + "rx_payload_desc", + "rx_dma_hdrdesc", + "tx_hw_desc", + "host_event_data", + "host_cmd_data", + ], + # ARM EABI compiler runtime (LibreTiny uses ARM Cortex-M) + "arm_runtime": [ + "__aeabi_", + "__adddf3", + "__subdf3", + "__muldf3", + "__divdf3", + "__addsf3", + "__subsf3", + "__mulsf3", + "__divsf3", + "__gnu_unwind", + ], "xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"], "heap": ["heap_", "multi_heap"], "spi_flash": ["spi_flash"], @@ -782,7 +855,22 @@ SYMBOL_PATTERNS = { "math_internal": ["__mdiff", "__lshift", "__mprec_tens", "quorem"], "character_class": ["__chclass"], "camellia": ["camellia_", "camellia_feistel"], - "crypto_tables": ["FSb", "FSb2", "FSb3", "FSb4"], + "crypto_tables": [ + "FSb", + "FSb2", + "FSb3", + "FSb4", + "Te0", # AES encryption table + "Td0", # AES decryption table + "crc32_table", # CRC32 lookup table + "crc_tab", # CRC lookup table + ], + "crypto_hash": [ + "SHA1Transform", # SHA1 hash function + "MD5Transform", # MD5 hash function + "SHA256", + "SHA512", + ], "event_buffer": ["g_eb_list_desc", "eb_space"], "base_node": ["base_node_", "base_node_add_handler"], "file_descriptor": ["s_fd_table"], diff --git a/esphome/analyze_memory/demangle.py b/esphome/analyze_memory/demangle.py new file mode 100644 index 0000000000..8999108b51 --- /dev/null +++ b/esphome/analyze_memory/demangle.py @@ -0,0 +1,182 @@ +"""Symbol demangling utilities for memory analysis. + +This module provides functions for demangling C++ symbol names using c++filt. +""" + +from __future__ import annotations + +import logging +import re +import subprocess + +from .toolchain import find_tool + +_LOGGER = logging.getLogger(__name__) + +# GCC global constructor/destructor prefix annotations +GCC_PREFIX_ANNOTATIONS = { + "_GLOBAL__sub_I_": "global constructor for", + "_GLOBAL__sub_D_": "global destructor for", +} + +# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2) +GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)") + + +def _strip_gcc_annotations(symbol: str) -> tuple[str, str]: + """Strip GCC optimization suffixes and prefixes from a symbol. + + Args: + symbol: The mangled symbol name + + Returns: + Tuple of (stripped_symbol, removed_prefix) + """ + # Remove GCC optimization markers + stripped = GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol) + + # Handle GCC global constructor/initializer prefixes + prefix = "" + for gcc_prefix in GCC_PREFIX_ANNOTATIONS: + if stripped.startswith(gcc_prefix): + prefix = gcc_prefix + stripped = stripped[len(prefix) :] + break + + return stripped, prefix + + +def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str: + """Restore prefix that was removed before demangling. + + Args: + prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_") + stripped: Stripped symbol name + demangled: Demangled symbol name + + Returns: + Demangled name with prefix restored/annotated + """ + if not prefix: + return demangled + + # Successfully demangled - add descriptive prefix + if demangled != stripped and (annotation := GCC_PREFIX_ANNOTATIONS.get(prefix)): + return f"[{annotation}: {demangled}]" + + # Failed to demangle - restore original prefix + return prefix + demangled + + +def _restore_symbol_suffix(original: str, demangled: str) -> str: + """Restore GCC optimization suffix that was removed before demangling. + + Args: + original: Original symbol name with suffix + demangled: Demangled symbol name without suffix + + Returns: + Demangled name with suffix annotation + """ + if suffix_match := GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original): + return f"{demangled} [{suffix_match.group(1)}]" + return demangled + + +def batch_demangle( + symbols: list[str], + cppfilt_path: str | None = None, + objdump_path: str | None = None, +) -> dict[str, str]: + """Batch demangle C++ symbol names. + + Args: + symbols: List of symbol names to demangle + cppfilt_path: Path to c++filt binary (auto-detected if not provided) + objdump_path: Path to objdump binary to derive c++filt path from + + Returns: + Dictionary mapping original symbol names to demangled names + """ + cache: dict[str, str] = {} + + if not symbols: + return cache + + # Find c++filt tool + cppfilt_cmd = cppfilt_path or find_tool("c++filt", objdump_path) + if not cppfilt_cmd: + _LOGGER.warning("Could not find c++filt, symbols will not be demangled") + return {s: s for s in symbols} + + _LOGGER.debug("Demangling %d symbols using %s", len(symbols), cppfilt_cmd) + + # Strip GCC optimization suffixes and prefixes before demangling + symbols_stripped: list[str] = [] + symbols_prefixes: list[str] = [] + for symbol in symbols: + stripped, prefix = _strip_gcc_annotations(symbol) + symbols_stripped.append(stripped) + symbols_prefixes.append(prefix) + + try: + result = subprocess.run( + [cppfilt_cmd], + input="\n".join(symbols_stripped), + capture_output=True, + text=True, + check=False, + ) + except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e: + _LOGGER.warning("Failed to batch demangle symbols: %s", e) + return {s: s for s in symbols} + + if result.returncode != 0: + _LOGGER.warning( + "c++filt exited with code %d: %s", + result.returncode, + result.stderr[:200] if result.stderr else "(no error output)", + ) + return {s: s for s in symbols} + + # Process demangled output + demangled_lines = result.stdout.strip().split("\n") + + # Check for output length mismatch + if len(demangled_lines) != len(symbols): + _LOGGER.warning( + "c++filt output mismatch: expected %d lines, got %d", + len(symbols), + len(demangled_lines), + ) + return {s: s for s in symbols} + + failed_count = 0 + + for original, stripped, prefix, demangled in zip( + symbols, symbols_stripped, symbols_prefixes, demangled_lines + ): + # Add back any prefix that was removed + demangled = _restore_symbol_prefix(prefix, stripped, demangled) + + # If we stripped a suffix, add it back to the demangled name for clarity + if original != stripped and not prefix: + demangled = _restore_symbol_suffix(original, demangled) + + cache[original] = demangled + + # Count symbols that failed to demangle + if stripped == demangled and stripped.startswith("_Z"): + failed_count += 1 + if failed_count <= 5: + _LOGGER.debug("Failed to demangle: %s", original) + + if failed_count > 0: + _LOGGER.debug( + "Failed to demangle %d/%d symbols using %s", + failed_count, + len(symbols), + cppfilt_cmd, + ) + + return cache diff --git a/esphome/analyze_memory/ram_strings.py b/esphome/analyze_memory/ram_strings.py new file mode 100644 index 0000000000..fbcbeeca61 --- /dev/null +++ b/esphome/analyze_memory/ram_strings.py @@ -0,0 +1,493 @@ +"""Analyzer for RAM-stored strings in ESP8266/ESP32 firmware ELF files. + +This module identifies strings that are stored in RAM sections (.data, .bss, .rodata) +rather than in flash sections (.irom0.text, .irom.text), which is important for +memory-constrained platforms like ESP8266. +""" + +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass +import logging +from pathlib import Path +import re +import subprocess + +from .demangle import batch_demangle +from .toolchain import find_tool + +_LOGGER = logging.getLogger(__name__) + +# ESP8266: .rodata is in RAM (DRAM), not flash +# ESP32: .rodata is in flash, mapped to data bus +ESP8266_RAM_SECTIONS = frozenset([".data", ".rodata", ".bss"]) +ESP8266_FLASH_SECTIONS = frozenset([".irom0.text", ".irom.text", ".text"]) + +# ESP32: .rodata is memory-mapped from flash +ESP32_RAM_SECTIONS = frozenset([".data", ".bss", ".dram0.data", ".dram0.bss"]) +ESP32_FLASH_SECTIONS = frozenset([".text", ".rodata", ".flash.text", ".flash.rodata"]) + +# nm symbol types for data symbols (D=global data, d=local data, R=rodata, B=bss) +DATA_SYMBOL_TYPES = frozenset(["D", "d", "R", "r", "B", "b"]) + + +@dataclass +class SectionInfo: + """Information about an ELF section.""" + + name: str + address: int + size: int + + +@dataclass +class RamString: + """A string found in RAM.""" + + section: str + address: int + content: str + + @property + def size(self) -> int: + """Size in bytes including null terminator.""" + return len(self.content) + 1 + + +@dataclass +class RamSymbol: + """A symbol found in RAM.""" + + name: str + sym_type: str + address: int + size: int + section: str + demangled: str = "" # Demangled name, set after batch demangling + + +class RamStringsAnalyzer: + """Analyzes ELF files to find strings stored in RAM.""" + + def __init__( + self, + elf_path: str, + objdump_path: str | None = None, + min_length: int = 8, + platform: str = "esp32", + ) -> None: + """Initialize the RAM strings analyzer. + + Args: + elf_path: Path to the ELF file to analyze + objdump_path: Path to objdump binary (used to find other tools) + min_length: Minimum string length to report (default: 8) + platform: Platform name ("esp8266", "esp32", etc.) for section mapping + """ + self.elf_path = Path(elf_path) + if not self.elf_path.exists(): + raise FileNotFoundError(f"ELF file not found: {elf_path}") + + self.objdump_path = objdump_path + self.min_length = min_length + self.platform = platform + + # Set RAM/flash sections based on platform + if self.platform == "esp8266": + self.ram_sections = ESP8266_RAM_SECTIONS + self.flash_sections = ESP8266_FLASH_SECTIONS + else: + # ESP32 and other platforms + self.ram_sections = ESP32_RAM_SECTIONS + self.flash_sections = ESP32_FLASH_SECTIONS + + self.sections: dict[str, SectionInfo] = {} + self.ram_strings: list[RamString] = [] + self.ram_symbols: list[RamSymbol] = [] + + def _run_command(self, cmd: list[str]) -> str: + """Run a command and return its output.""" + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return result.stdout + except subprocess.CalledProcessError as e: + _LOGGER.debug("Command failed: %s - %s", " ".join(cmd), e.stderr) + raise + except FileNotFoundError: + _LOGGER.warning("Command not found: %s", cmd[0]) + raise + + def analyze(self) -> None: + """Perform the full RAM analysis.""" + self._parse_sections() + self._extract_strings() + self._analyze_symbols() + self._demangle_symbols() + + def _parse_sections(self) -> None: + """Parse section headers from ELF file.""" + objdump = find_tool("objdump", self.objdump_path) + if not objdump: + _LOGGER.error("Could not find objdump command") + return + + try: + output = self._run_command([objdump, "-h", str(self.elf_path)]) + except (subprocess.CalledProcessError, FileNotFoundError): + return + + # Parse section headers + # Format: Idx Name Size VMA LMA File off Algn + section_pattern = re.compile( + r"^\s*\d+\s+(\S+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)" + ) + + for line in output.split("\n"): + if match := section_pattern.match(line): + name = match.group(1) + size = int(match.group(2), 16) + vma = int(match.group(3), 16) + self.sections[name] = SectionInfo(name, vma, size) + + def _extract_strings(self) -> None: + """Extract strings from RAM sections.""" + objdump = find_tool("objdump", self.objdump_path) + if not objdump: + return + + for section_name in self.ram_sections: + if section_name not in self.sections: + continue + + try: + output = self._run_command( + [objdump, "-s", "-j", section_name, str(self.elf_path)] + ) + except subprocess.CalledProcessError: + # Section may exist but have no content (e.g., .bss) + continue + except FileNotFoundError: + continue + + strings = self._parse_hex_dump(output, section_name) + self.ram_strings.extend(strings) + + def _parse_hex_dump(self, output: str, section_name: str) -> list[RamString]: + """Parse hex dump output to extract strings. + + Args: + output: Output from objdump -s + section_name: Name of the section being parsed + + Returns: + List of RamString objects + """ + strings: list[RamString] = [] + current_string = bytearray() + string_start_addr = 0 + + for line in output.split("\n"): + # Lines look like: " 3ffef8a0 00000000 00000000 00000000 00000000 ................" + match = re.match(r"^\s+([0-9a-fA-F]+)\s+((?:[0-9a-fA-F]{2,8}\s*)+)", line) + if not match: + continue + + addr = int(match.group(1), 16) + hex_data = match.group(2).strip() + + # Convert hex to bytes + hex_bytes = hex_data.split() + byte_offset = 0 + for hex_chunk in hex_bytes: + # Handle both byte-by-byte and word formats + for i in range(0, len(hex_chunk), 2): + byte_val = int(hex_chunk[i : i + 2], 16) + if 0x20 <= byte_val <= 0x7E: # Printable ASCII + if not current_string: + string_start_addr = addr + byte_offset + current_string.append(byte_val) + else: + if byte_val == 0 and len(current_string) >= self.min_length: + # Found null terminator + strings.append( + RamString( + section=section_name, + address=string_start_addr, + content=current_string.decode( + "ascii", errors="ignore" + ), + ) + ) + current_string = bytearray() + byte_offset += 1 + + return strings + + def _analyze_symbols(self) -> None: + """Analyze symbols in RAM sections.""" + nm = find_tool("nm", self.objdump_path) + if not nm: + return + + try: + output = self._run_command([nm, "-S", "--size-sort", str(self.elf_path)]) + except (subprocess.CalledProcessError, FileNotFoundError): + return + + for line in output.split("\n"): + parts = line.split() + if len(parts) < 4: + continue + + try: + addr = int(parts[0], 16) + size = int(parts[1], 16) if parts[1] != "?" else 0 + except ValueError: + continue + + sym_type = parts[2] + name = " ".join(parts[3:]) + + # Filter for data symbols + if sym_type not in DATA_SYMBOL_TYPES: + continue + + # Check if symbol is in a RAM section + for section_name in self.ram_sections: + if section_name not in self.sections: + continue + + section = self.sections[section_name] + if section.address <= addr < section.address + section.size: + self.ram_symbols.append( + RamSymbol( + name=name, + sym_type=sym_type, + address=addr, + size=size, + section=section_name, + ) + ) + break + + def _demangle_symbols(self) -> None: + """Batch demangle all RAM symbol names.""" + if not self.ram_symbols: + return + + # Collect all symbol names and demangle them + symbol_names = [s.name for s in self.ram_symbols] + demangle_cache = batch_demangle(symbol_names, objdump_path=self.objdump_path) + + # Assign demangled names to symbols + for symbol in self.ram_symbols: + symbol.demangled = demangle_cache.get(symbol.name, symbol.name) + + def _get_sections_size(self, section_names: frozenset[str]) -> int: + """Get total size of specified sections.""" + return sum( + section.size + for name, section in self.sections.items() + if name in section_names + ) + + def get_total_ram_usage(self) -> int: + """Get total RAM usage from RAM sections.""" + return self._get_sections_size(self.ram_sections) + + def get_total_flash_usage(self) -> int: + """Get total flash usage from flash sections.""" + return self._get_sections_size(self.flash_sections) + + def get_total_string_bytes(self) -> int: + """Get total bytes used by strings in RAM.""" + return sum(s.size for s in self.ram_strings) + + def get_repeated_strings(self) -> list[tuple[str, int]]: + """Find strings that appear multiple times. + + Returns: + List of (string, count) tuples sorted by potential savings + """ + string_counts: dict[str, int] = defaultdict(int) + for ram_string in self.ram_strings: + string_counts[ram_string.content] += 1 + + return sorted( + [(s, c) for s, c in string_counts.items() if c > 1], + key=lambda x: x[1] * (len(x[0]) + 1), + reverse=True, + ) + + def get_long_strings(self, min_len: int = 20) -> list[RamString]: + """Get strings longer than the specified length. + + Args: + min_len: Minimum string length + + Returns: + List of RamString objects sorted by length + """ + return sorted( + [s for s in self.ram_strings if len(s.content) >= min_len], + key=lambda x: len(x.content), + reverse=True, + ) + + def get_largest_symbols(self, min_size: int = 100) -> list[RamSymbol]: + """Get RAM symbols larger than the specified size. + + Args: + min_size: Minimum symbol size in bytes + + Returns: + List of RamSymbol objects sorted by size + """ + return sorted( + [s for s in self.ram_symbols if s.size >= min_size], + key=lambda x: x.size, + reverse=True, + ) + + def generate_report(self, show_all_sections: bool = False) -> str: + """Generate a formatted RAM strings analysis report. + + Args: + show_all_sections: If True, show all sections, not just RAM + + Returns: + Formatted report string + """ + lines: list[str] = [] + table_width = 80 + + lines.append("=" * table_width) + lines.append( + f"RAM Strings Analysis ({self.platform.upper()})".center(table_width) + ) + lines.append("=" * table_width) + lines.append("") + + # Section Analysis + lines.append("SECTION ANALYSIS") + lines.append("-" * table_width) + lines.append(f"{'Section':<20} {'Address':<12} {'Size':<12} {'Location'}") + lines.append("-" * table_width) + + total_ram_usage = 0 + total_flash_usage = 0 + + for name, section in sorted(self.sections.items(), key=lambda x: x[1].address): + if name in self.ram_sections: + location = "RAM" + total_ram_usage += section.size + elif name in self.flash_sections: + location = "FLASH" + total_flash_usage += section.size + else: + location = "OTHER" + + if show_all_sections or name in self.ram_sections: + lines.append( + f"{name:<20} 0x{section.address:08x} {section.size:>8} B {location}" + ) + + lines.append("-" * table_width) + lines.append(f"Total RAM sections size: {total_ram_usage:,} bytes") + lines.append(f"Total Flash sections size: {total_flash_usage:,} bytes") + + # Strings in RAM + lines.append("") + lines.append("=" * table_width) + lines.append("STRINGS IN RAM SECTIONS") + lines.append("=" * table_width) + lines.append( + "Note: .bss sections contain uninitialized data (no strings to extract)" + ) + + # Group strings by section + strings_by_section: dict[str, list[RamString]] = defaultdict(list) + for ram_string in self.ram_strings: + strings_by_section[ram_string.section].append(ram_string) + + for section_name in sorted(strings_by_section.keys()): + section_strings = strings_by_section[section_name] + lines.append(f"\nSection: {section_name}") + lines.append("-" * 40) + for ram_string in sorted(section_strings, key=lambda x: x.address): + clean_string = ram_string.content[:100] + ( + "..." if len(ram_string.content) > 100 else "" + ) + lines.append( + f' 0x{ram_string.address:08x}: "{clean_string}" (len={len(ram_string.content)})' + ) + + # Large RAM symbols + lines.append("") + lines.append("=" * table_width) + lines.append("LARGE DATA SYMBOLS IN RAM (>= 50 bytes)") + lines.append("=" * table_width) + + largest_symbols = self.get_largest_symbols(50) + lines.append(f"\n{'Symbol':<50} {'Type':<6} {'Size':<10} {'Section'}") + lines.append("-" * table_width) + + for symbol in largest_symbols: + # Use demangled name if available, otherwise raw name + display_name = symbol.demangled or symbol.name + name_display = display_name[:49] if len(display_name) > 49 else display_name + lines.append( + f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}" + ) + + # Summary + lines.append("") + lines.append("=" * table_width) + lines.append("SUMMARY") + lines.append("=" * table_width) + lines.append(f"Total strings found in RAM: {len(self.ram_strings)}") + total_string_bytes = self.get_total_string_bytes() + lines.append(f"Total bytes used by strings: {total_string_bytes:,}") + + # Optimization targets + lines.append("") + lines.append("=" * table_width) + lines.append("POTENTIAL OPTIMIZATION TARGETS") + lines.append("=" * table_width) + + # Repeated strings + repeated = self.get_repeated_strings()[:10] + if repeated: + lines.append("\nRepeated strings (could be deduplicated):") + for string, count in repeated: + savings = (count - 1) * (len(string) + 1) + clean_string = string[:50] + ("..." if len(string) > 50 else "") + lines.append( + f' "{clean_string}" - appears {count} times (potential savings: {savings} bytes)' + ) + + # Long strings - platform-specific advice + long_strings = self.get_long_strings(20)[:10] + if long_strings: + if self.platform == "esp8266": + lines.append( + "\nLong strings that could be moved to PROGMEM (>= 20 chars):" + ) + else: + # ESP32: strings in DRAM are typically there for a reason + # (interrupt handlers, pre-flash-init code, etc.) + lines.append("\nLong strings in DRAM (>= 20 chars):") + lines.append( + "Note: ESP32 DRAM strings may be required for interrupt/early-boot contexts" + ) + for ram_string in long_strings: + clean_string = ram_string.content[:60] + ( + "..." if len(ram_string.content) > 60 else "" + ) + lines.append( + f' {ram_string.section} @ 0x{ram_string.address:08x}: "{clean_string}" ({len(ram_string.content)} bytes)' + ) + + lines.append("") + return "\n".join(lines) diff --git a/esphome/analyze_memory/toolchain.py b/esphome/analyze_memory/toolchain.py new file mode 100644 index 0000000000..23d85e9700 --- /dev/null +++ b/esphome/analyze_memory/toolchain.py @@ -0,0 +1,93 @@ +"""Toolchain utilities for memory analysis.""" + +from __future__ import annotations + +import logging +from pathlib import Path +import subprocess +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Sequence + +_LOGGER = logging.getLogger(__name__) + +# Platform-specific toolchain prefixes +TOOLCHAIN_PREFIXES = [ + "xtensa-lx106-elf-", # ESP8266 + "xtensa-esp32-elf-", # ESP32 + "xtensa-esp-elf-", # ESP32 (newer IDF) + "", # System default (no prefix) +] + + +def find_tool( + tool_name: str, + objdump_path: str | None = None, +) -> str | None: + """Find a toolchain tool by name. + + First tries to derive the tool path from objdump_path (if provided), + then falls back to searching for platform-specific tools. + + Args: + tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt") + objdump_path: Path to objdump binary to derive other tool paths from + + Returns: + Path to the tool or None if not found + """ + # Try to derive from objdump path first (most reliable) + if objdump_path and objdump_path != "objdump": + objdump_file = Path(objdump_path) + # Replace just the filename portion, preserving any prefix (e.g., xtensa-esp32-elf-) + new_name = objdump_file.name.replace("objdump", tool_name) + potential_path = str(objdump_file.with_name(new_name)) + if Path(potential_path).exists(): + _LOGGER.debug("Found %s at: %s", tool_name, potential_path) + return potential_path + + # Try platform-specific tools + for prefix in TOOLCHAIN_PREFIXES: + cmd = f"{prefix}{tool_name}" + try: + subprocess.run([cmd, "--version"], capture_output=True, check=True) + _LOGGER.debug("Found %s: %s", tool_name, cmd) + return cmd + except (subprocess.CalledProcessError, FileNotFoundError): + continue + + _LOGGER.warning("Could not find %s tool", tool_name) + return None + + +def run_tool( + cmd: Sequence[str], + timeout: int = 30, +) -> subprocess.CompletedProcess[str] | None: + """Run a toolchain command and return the result. + + Args: + cmd: Command and arguments to run + timeout: Timeout in seconds + + Returns: + CompletedProcess on success, None on failure + """ + try: + return subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired: + _LOGGER.warning("Command timed out: %s", " ".join(cmd)) + return None + except FileNotFoundError: + _LOGGER.warning("Command not found: %s", cmd[0]) + return None + except OSError as e: + _LOGGER.warning("Failed to run command %s: %s", cmd[0], e) + return None diff --git a/esphome/components/a01nyub/a01nyub.cpp b/esphome/components/a01nyub/a01nyub.cpp index d0bc89a0c9..210c3557b3 100644 --- a/esphome/components/a01nyub/a01nyub.cpp +++ b/esphome/components/a01nyub/a01nyub.cpp @@ -30,7 +30,9 @@ void A01nyubComponent::check_buffer_() { ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters); this->publish_state(meters); } else { - ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); + char hex_buf[format_hex_pretty_size(4)]; + ESP_LOGW(TAG, "Invalid data read from sensor: %s", + format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_.size())); } } else { ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); diff --git a/esphome/components/a02yyuw/a02yyuw.cpp b/esphome/components/a02yyuw/a02yyuw.cpp index ee378c3283..a2aad0cef1 100644 --- a/esphome/components/a02yyuw/a02yyuw.cpp +++ b/esphome/components/a02yyuw/a02yyuw.cpp @@ -29,7 +29,9 @@ void A02yyuwComponent::check_buffer_() { ESP_LOGV(TAG, "Distance from sensor: %f mm", distance); this->publish_state(distance); } else { - ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); + char hex_buf[format_hex_pretty_size(4)]; + ESP_LOGW(TAG, "Invalid data read from sensor: %s", + format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_.size())); } } else { ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 2c5603ee3d..b13fcd519a 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -87,16 +87,19 @@ void AbsoluteHumidityComponent::loop() { break; default: this->publish_state(NAN); - this->status_set_error("Invalid saturation vapor pressure equation selection!"); + this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!")); return; } - ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); // Calculate absolute humidity const float absolute_humidity = vapor_density(es, hr, temperature_k); + ESP_LOGD(TAG, + "Saturation vapor pressure %f kPa\n" + "Publishing absolute humidity %f g/m³", + es, absolute_humidity); + // Publish absolute humidity - ESP_LOGD(TAG, "Publishing absolute humidity %f g/m³", absolute_humidity); this->status_clear_warning(); this->publish_state(absolute_humidity); } @@ -163,7 +166,7 @@ float AbsoluteHumidityComponent::es_wobus(float t) { } // From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/ -// H/T to https://esphome.io/cookbook/bme280_environment.html +// H/T to https://esphome.io/cookbook/bme280_environment/ // H/T to https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/ float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) { // es = saturated vapor pressure (kPa) diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index e6f7a1214a..04c01948c8 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -211,13 +211,13 @@ void AcDimmer::write_state(float state) { this->store_.value = new_value; } void AcDimmer::dump_config() { - ESP_LOGCONFIG(TAG, "AcDimmer:"); - LOG_PIN(" Output Pin: ", this->gate_pin_); - LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_); ESP_LOGCONFIG(TAG, + "AcDimmer:\n" " Min Power: %.1f%%\n" " Init with half cycle: %s", this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_)); + LOG_PIN(" Output Pin: ", this->gate_pin_); + LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_); if (method_ == DIM_METHOD_LEADING_PULSE) { ESP_LOGCONFIG(TAG, " Method: leading pulse"); } else if (method_ == DIM_METHOD_LEADING) { diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py index 5e24779510..9f9afb6d80 100644 --- a/esphome/components/ac_dimmer/output.py +++ b/esphome/components/ac_dimmer/output.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import output import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_METHOD, CONF_MIN_POWER +from esphome.core import CORE CODEOWNERS = ["@glmnet"] @@ -36,6 +37,12 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + if CORE.is_esp8266: + # ac_dimmer uses setTimer1Callback which requires the waveform generator + from esphome.components.esp8266.const import require_waveform + + require_waveform() + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 15dc447b6c..96c8334a6d 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -1,15 +1,17 @@ from esphome import pins import esphome.codegen as cg -from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant -from esphome.components.esp32.const import ( +from esphome.components.esp32 import ( VARIANT_ESP32, VARIANT_ESP32C2, VARIANT_ESP32C3, VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + get_esp32_variant, ) import esphome.config_validation as cv from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 @@ -99,6 +101,13 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { 5: adc_channel_t.ADC_CHANNEL_5, 6: adc_channel_t.ADC_CHANNEL_6, }, + # https://docs.espressif.com/projects/esp-idf/en/latest/esp32c61/api-reference/peripherals/gpio.html + VARIANT_ESP32C61: { + 1: adc_channel_t.ADC_CHANNEL_0, + 3: adc_channel_t.ADC_CHANNEL_1, + 4: adc_channel_t.ADC_CHANNEL_2, + 5: adc_channel_t.ADC_CHANNEL_3, + }, # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h VARIANT_ESP32H2: { 1: adc_channel_t.ADC_CHANNEL_0, @@ -107,6 +116,17 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { 4: adc_channel_t.ADC_CHANNEL_3, 5: adc_channel_t.ADC_CHANNEL_4, }, + # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h + VARIANT_ESP32P4: { + 16: adc_channel_t.ADC_CHANNEL_0, + 17: adc_channel_t.ADC_CHANNEL_1, + 18: adc_channel_t.ADC_CHANNEL_2, + 19: adc_channel_t.ADC_CHANNEL_3, + 20: adc_channel_t.ADC_CHANNEL_4, + 21: adc_channel_t.ADC_CHANNEL_5, + 22: adc_channel_t.ADC_CHANNEL_6, + 23: adc_channel_t.ADC_CHANNEL_7, + }, # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h VARIANT_ESP32S2: { 1: adc_channel_t.ADC_CHANNEL_0, @@ -133,16 +153,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { 9: adc_channel_t.ADC_CHANNEL_8, 10: adc_channel_t.ADC_CHANNEL_9, }, - VARIANT_ESP32P4: { - 16: adc_channel_t.ADC_CHANNEL_0, - 17: adc_channel_t.ADC_CHANNEL_1, - 18: adc_channel_t.ADC_CHANNEL_2, - 19: adc_channel_t.ADC_CHANNEL_3, - 20: adc_channel_t.ADC_CHANNEL_4, - 21: adc_channel_t.ADC_CHANNEL_5, - 22: adc_channel_t.ADC_CHANNEL_6, - 23: adc_channel_t.ADC_CHANNEL_7, - }, } # pin to adc2 channel mapping @@ -173,8 +183,19 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { VARIANT_ESP32C5: {}, # no ADC2 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h VARIANT_ESP32C6: {}, # no ADC2 + # ESP32-C61 has no ADC2 + VARIANT_ESP32C61: {}, # no ADC2 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h VARIANT_ESP32H2: {}, # no ADC2 + # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h + VARIANT_ESP32P4: { + 49: adc_channel_t.ADC_CHANNEL_0, + 50: adc_channel_t.ADC_CHANNEL_1, + 51: adc_channel_t.ADC_CHANNEL_2, + 52: adc_channel_t.ADC_CHANNEL_3, + 53: adc_channel_t.ADC_CHANNEL_4, + 54: adc_channel_t.ADC_CHANNEL_5, + }, # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h VARIANT_ESP32S2: { 11: adc_channel_t.ADC_CHANNEL_0, @@ -201,14 +222,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { 19: adc_channel_t.ADC_CHANNEL_8, 20: adc_channel_t.ADC_CHANNEL_9, }, - VARIANT_ESP32P4: { - 49: adc_channel_t.ADC_CHANNEL_0, - 50: adc_channel_t.ADC_CHANNEL_1, - 51: adc_channel_t.ADC_CHANNEL_2, - 52: adc_channel_t.ADC_CHANNEL_3, - 53: adc_channel_t.ADC_CHANNEL_4, - 54: adc_channel_t.ADC_CHANNEL_5, - }, } diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index ab6a89fce0..ea1263db5f 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -42,10 +42,11 @@ void ADCSensor::setup() { adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize init_config.unit_id = this->adc_unit_; init_config.ulp_mode = ADC_ULP_MODE_DISABLE; -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2 +#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || - // USE_ESP32_VARIANT_ESP32H2 + // USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); if (err != ESP_OK) { ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); @@ -74,7 +75,7 @@ void ADCSensor::setup() { adc_cali_handle_t handle = nullptr; #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 // RISC-V variants and S3 use curve fitting calibration adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) @@ -111,7 +112,7 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2 +#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 } this->setup_flags_.init_complete = true; @@ -120,23 +121,21 @@ void ADCSensor::setup() { void ADCSensor::dump_config() { LOG_SENSOR("", "ADC Sensor", this); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, - " Channel: %d\n" - " Unit: %s\n" - " Attenuation: %s\n" - " Samples: %i\n" - " Sampling mode: %s", - this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), - this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_, - LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_))); - ESP_LOGCONFIG( TAG, + " Channel: %d\n" + " Unit: %s\n" + " Attenuation: %s\n" + " Samples: %i\n" + " Sampling mode: %s\n" " Setup Status:\n" " Handle Init: %s\n" " Config: %s\n" " Calibration: %s\n" " Overall Init: %s", + this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), + this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_, + LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)), this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED", this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED"); @@ -186,11 +185,11 @@ float ADCSensor::sample_fixed_attenuation_() { ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); if (this->calibration_handle_ != nullptr) { #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else // Other ESP32 variants use line fitting calibration adc_cali_delete_scheme_line_fitting(this->calibration_handle_); -#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2 +#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 this->calibration_handle_ = nullptr; } } @@ -219,7 +218,7 @@ float ADCSensor::sample_autorange_() { if (this->calibration_handle_ != nullptr) { // Delete old calibration handle #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else adc_cali_delete_scheme_line_fitting(this->calibration_handle_); @@ -231,7 +230,7 @@ float ADCSensor::sample_autorange_() { adc_cali_handle_t handle = nullptr; #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_curve_fitting_config_t cali_config = {}; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) cali_config.chan = this->channel_; @@ -266,7 +265,7 @@ float ADCSensor::sample_autorange_() { ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); if (handle != nullptr) { #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(handle); #else adc_cali_delete_scheme_line_fitting(handle); @@ -288,7 +287,7 @@ float ADCSensor::sample_autorange_() { } // Clean up calibration handle #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 + USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 adc_cali_delete_scheme_curve_fitting(handle); #else adc_cali_delete_scheme_line_fitting(handle); diff --git a/esphome/components/addressable_light/addressable_light_display.h b/esphome/components/addressable_light/addressable_light_display.h index f47389fd05..53f8604b7d 100644 --- a/esphome/components/addressable_light/addressable_light_display.h +++ b/esphome/components/addressable_light/addressable_light_display.h @@ -25,11 +25,13 @@ class AddressableLightDisplay : public display::DisplayBuffer { if (enabled_ && !enabled) { // enabled -> disabled // - Tell the parent light to refresh, effectively wiping the display. Also // restores the previous effect (if any). - light_state_->make_call().set_effect(this->last_effect_).perform(); + if (this->last_effect_index_.has_value()) { + light_state_->make_call().set_effect(*this->last_effect_index_).perform(); + } } else if (!enabled_ && enabled) { // disabled -> enabled - // - Save the current effect. - this->last_effect_ = light_state_->get_effect_name(); + // - Save the current effect index. + this->last_effect_index_ = light_state_->get_current_effect_index(); // - Disable any current effect. light_state_->make_call().set_effect(0).perform(); } @@ -56,7 +58,7 @@ class AddressableLightDisplay : public display::DisplayBuffer { int32_t width_; int32_t height_; std::vector addressable_light_buffer_; - optional last_effect_; + optional last_effect_index_; optional> pixel_mapper_f_; }; } // namespace addressable_light diff --git a/esphome/components/ade7880/ade7880.cpp b/esphome/components/ade7880/ade7880.cpp index fd560e0676..f6a15190cd 100644 --- a/esphome/components/ade7880/ade7880.cpp +++ b/esphome/components/ade7880/ade7880.cpp @@ -162,11 +162,13 @@ void ADE7880::update() { } void ADE7880::dump_config() { - ESP_LOGCONFIG(TAG, "ADE7880:"); + ESP_LOGCONFIG(TAG, + "ADE7880:\n" + " Frequency: %.0f Hz", + this->frequency_); LOG_PIN(" IRQ0 Pin: ", this->irq0_pin_); LOG_PIN(" IRQ1 Pin: ", this->irq1_pin_); LOG_PIN(" RESET Pin: ", this->reset_pin_); - ESP_LOGCONFIG(TAG, " Frequency: %.0f Hz", this->frequency_); if (this->channel_a_ != nullptr) { ESP_LOGCONFIG(TAG, " Phase A:"); diff --git a/esphome/components/ade7880/sensor.py b/esphome/components/ade7880/sensor.py index 39dbeb225f..beb74d7310 100644 --- a/esphome/components/ade7880/sensor.py +++ b/esphome/components/ade7880/sensor.py @@ -227,7 +227,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(ADE7880), cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All( - cv.frequency, cv.Range(min=45.0, max=66.0) + cv.frequency, cv.float_range(min=45.0, max=66.0) ), cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema, diff --git a/esphome/components/ade7953_base/__init__.py b/esphome/components/ade7953_base/__init__.py index 42b6c8ba24..4fc35352f9 100644 --- a/esphome/components/ade7953_base/__init__.py +++ b/esphome/components/ade7953_base/__init__.py @@ -24,6 +24,8 @@ from esphome.const import ( UNIT_WATT, ) +CODEOWNERS = ["@angelnu"] + CONF_CURRENT_A = "current_a" CONF_CURRENT_B = "current_b" CONF_ACTIVE_POWER_A = "active_power_a" diff --git a/esphome/components/ade7953_base/ade7953_base.cpp b/esphome/components/ade7953_base/ade7953_base.cpp index 5f5fdd27ee..821e4a3105 100644 --- a/esphome/components/ade7953_base/ade7953_base.cpp +++ b/esphome/components/ade7953_base/ade7953_base.cpp @@ -25,7 +25,8 @@ void ADE7953::setup() { this->ade_write_8(PGA_V_8, pga_v_); this->ade_write_8(PGA_IA_8, pga_ia_); this->ade_write_8(PGA_IB_8, pga_ib_); - this->ade_write_32(AVGAIN_32, vgain_); + this->ade_write_32(AVGAIN_32, avgain_); + this->ade_write_32(BVGAIN_32, bvgain_); this->ade_write_32(AIGAIN_32, aigain_); this->ade_write_32(BIGAIN_32, bigain_); this->ade_write_32(AWGAIN_32, awgain_); @@ -34,7 +35,8 @@ void ADE7953::setup() { this->ade_read_8(PGA_V_8, &pga_v_); this->ade_read_8(PGA_IA_8, &pga_ia_); this->ade_read_8(PGA_IB_8, &pga_ib_); - this->ade_read_32(AVGAIN_32, &vgain_); + this->ade_read_32(AVGAIN_32, &avgain_); + this->ade_read_32(BVGAIN_32, &bvgain_); this->ade_read_32(AIGAIN_32, &aigain_); this->ade_read_32(BIGAIN_32, &bigain_); this->ade_read_32(AWGAIN_32, &awgain_); @@ -63,13 +65,14 @@ void ADE7953::dump_config() { " PGA_V_8: 0x%X\n" " PGA_IA_8: 0x%X\n" " PGA_IB_8: 0x%X\n" - " VGAIN_32: 0x%08jX\n" + " AVGAIN_32: 0x%08jX\n" + " BVGAIN_32: 0x%08jX\n" " AIGAIN_32: 0x%08jX\n" " BIGAIN_32: 0x%08jX\n" " AWGAIN_32: 0x%08jX\n" " BWGAIN_32: 0x%08jX", - this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) vgain_, (uintmax_t) aigain_, - (uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_); + this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) avgain_, (uintmax_t) bvgain_, + (uintmax_t) aigain_, (uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_); } #define ADE_PUBLISH_(name, val, factor) \ diff --git a/esphome/components/ade7953_base/ade7953_base.h b/esphome/components/ade7953_base/ade7953_base.h index d711a5c6be..bcafddca4e 100644 --- a/esphome/components/ade7953_base/ade7953_base.h +++ b/esphome/components/ade7953_base/ade7953_base.h @@ -46,7 +46,12 @@ class ADE7953 : public PollingComponent, public sensor::Sensor { void set_pga_ib(uint8_t pga_ib) { pga_ib_ = pga_ib; } // Set input gains - void set_vgain(uint32_t vgain) { vgain_ = vgain; } + void set_vgain(uint32_t vgain) { + // Datasheet says: "to avoid discrepancies in other registers, + // if AVGAIN is set then BVGAIN should be set to the same value." + avgain_ = vgain; + bvgain_ = vgain; + } void set_aigain(uint32_t aigain) { aigain_ = aigain; } void set_bigain(uint32_t bigain) { bigain_ = bigain; } void set_awgain(uint32_t awgain) { awgain_ = awgain; } @@ -100,7 +105,8 @@ class ADE7953 : public PollingComponent, public sensor::Sensor { uint8_t pga_v_; uint8_t pga_ia_; uint8_t pga_ib_; - uint32_t vgain_; + uint32_t avgain_; + uint32_t bvgain_; uint32_t aigain_; uint32_t bigain_; uint32_t awgain_; diff --git a/esphome/components/ads1115/sensor/ads1115_sensor.cpp b/esphome/components/ads1115/sensor/ads1115_sensor.cpp index 6de95f1d12..fac6b60d0a 100644 --- a/esphome/components/ads1115/sensor/ads1115_sensor.cpp +++ b/esphome/components/ads1115/sensor/ads1115_sensor.cpp @@ -21,10 +21,12 @@ void ADS1115Sensor::update() { void ADS1115Sensor::dump_config() { LOG_SENSOR(" ", "ADS1115 Sensor", this); - ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_); - ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_); - ESP_LOGCONFIG(TAG, " Resolution: %u", this->resolution_); - ESP_LOGCONFIG(TAG, " Sample rate: %u", this->samplerate_); + ESP_LOGCONFIG(TAG, + " Multiplexer: %u\n" + " Gain: %u\n" + " Resolution: %u\n" + " Sample rate: %u", + this->multiplexer_, this->gain_, this->resolution_, this->samplerate_); } } // namespace ads1115 diff --git a/esphome/components/ads1118/sensor/ads1118_sensor.cpp b/esphome/components/ads1118/sensor/ads1118_sensor.cpp index c3ce3bdc9c..7193c3c880 100644 --- a/esphome/components/ads1118/sensor/ads1118_sensor.cpp +++ b/esphome/components/ads1118/sensor/ads1118_sensor.cpp @@ -9,8 +9,10 @@ static const char *const TAG = "ads1118.sensor"; void ADS1118Sensor::dump_config() { LOG_SENSOR(" ", "ADS1118 Sensor", this); - ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_); - ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_); + ESP_LOGCONFIG(TAG, + " Multiplexer: %u\n" + " Gain: %u", + this->multiplexer_, this->gain_); } float ADS1118Sensor::sample() { diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 53c712a7a7..03d9d9cd9e 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -83,7 +83,7 @@ void AHT10Component::setup() { void AHT10Component::restart_read_() { if (this->read_count_ == AHT10_ATTEMPTS) { this->read_count_ = 0; - this->status_set_error("Reading timed out"); + this->status_set_error(LOG_STR("Reading timed out")); return; } this->read_count_++; diff --git a/esphome/components/airthings_ble/airthings_listener.cpp b/esphome/components/airthings_ble/airthings_listener.cpp index a36d614df5..58faf923f5 100644 --- a/esphome/components/airthings_ble/airthings_listener.cpp +++ b/esphome/components/airthings_ble/airthings_listener.cpp @@ -20,7 +20,8 @@ bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &devic sn |= ((uint32_t) it.data[2] << 16); sn |= ((uint32_t) it.data[3] << 24); - ESP_LOGD(TAG, "Found AirThings device Serial:%" PRIu32 " (MAC: %s)", sn, device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGD(TAG, "Found AirThings device Serial:%" PRIu32 " (MAC: %s)", sn, device.address_str_to(addr_buf)); return true; } } diff --git a/esphome/components/airthings_wave_base/airthings_wave_base.cpp b/esphome/components/airthings_wave_base/airthings_wave_base.cpp index 16789ff454..e4c7d2a81d 100644 --- a/esphome/components/airthings_wave_base/airthings_wave_base.cpp +++ b/esphome/components/airthings_wave_base/airthings_wave_base.cpp @@ -1,4 +1,5 @@ #include "airthings_wave_base.h" +#include "esphome/components/esp32_ble/ble_uuid.h" // All information related to reading battery information came from the sensors.airthings_wave // project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave) @@ -93,8 +94,10 @@ void AirthingsWaveBase::update() { bool AirthingsWaveBase::request_read_values_() { auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_); if (chr == nullptr) { - ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), - this->sensors_data_characteristic_uuid_.to_string().c_str()); + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_str(service_buf), + this->sensors_data_characteristic_uuid_.to_str(char_buf)); return false; } @@ -117,17 +120,20 @@ bool AirthingsWaveBase::request_battery_() { auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->access_control_point_characteristic_uuid_); if (chr == nullptr) { + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; ESP_LOGW(TAG, "No access control point characteristic found at service %s char %s", - this->service_uuid_.to_string().c_str(), - this->access_control_point_characteristic_uuid_.to_string().c_str()); + this->service_uuid_.to_str(service_buf), this->access_control_point_characteristic_uuid_.to_str(char_buf)); return false; } auto *descr = this->parent()->get_descriptor(this->service_uuid_, this->access_control_point_characteristic_uuid_, CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID); if (descr == nullptr) { - ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_string().c_str(), - this->access_control_point_characteristic_uuid_.to_string().c_str()); + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; + ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_str(service_buf), + this->access_control_point_characteristic_uuid_.to_str(char_buf)); return false; } diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index c29e02c8ef..89c0908a74 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -8,8 +8,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { static const char *const TAG = "alarm_control_panel"; @@ -35,26 +34,12 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)), LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; + // Single state callback - triggers check get_state() for specific states this->state_callback_.call(); #if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_alarm_control_panel_update(this); #endif - if (state == ACP_STATE_TRIGGERED) { - this->triggered_callback_.call(); - } else if (state == ACP_STATE_ARMING) { - this->arming_callback_.call(); - } else if (state == ACP_STATE_PENDING) { - this->pending_callback_.call(); - } else if (state == ACP_STATE_ARMED_HOME) { - this->armed_home_callback_.call(); - } else if (state == ACP_STATE_ARMED_NIGHT) { - this->armed_night_callback_.call(); - } else if (state == ACP_STATE_ARMED_AWAY) { - this->armed_away_callback_.call(); - } else if (state == ACP_STATE_DISARMED) { - this->disarmed_callback_.call(); - } - + // Cleared fires when leaving TRIGGERED state if (prev_state == ACP_STATE_TRIGGERED) { this->cleared_callback_.call(); } @@ -69,34 +54,6 @@ void AlarmControlPanel::add_on_state_callback(std::function &&callback) this->state_callback_.add(std::move(callback)); } -void AlarmControlPanel::add_on_triggered_callback(std::function &&callback) { - this->triggered_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_arming_callback(std::function &&callback) { - this->arming_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_armed_home_callback(std::function &&callback) { - this->armed_home_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_armed_night_callback(std::function &&callback) { - this->armed_night_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_armed_away_callback(std::function &&callback) { - this->armed_away_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_pending_callback(std::function &&callback) { - this->pending_callback_.add(std::move(callback)); -} - -void AlarmControlPanel::add_on_disarmed_callback(std::function &&callback) { - this->disarmed_callback_.add(std::move(callback)); -} - void AlarmControlPanel::add_on_cleared_callback(std::function &&callback) { this->cleared_callback_.add(std::move(callback)); } @@ -157,5 +114,4 @@ void AlarmControlPanel::disarm(optional code) { call.perform(); } -} // namespace alarm_control_panel -} // namespace esphome +} // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index 85c2b2148e..340f15bcd6 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "alarm_control_panel_call.h" #include "alarm_control_panel_state.h" @@ -9,8 +7,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/log.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { enum AlarmControlPanelFeature : uint8_t { // Matches Home Assistant values @@ -35,54 +32,13 @@ class AlarmControlPanel : public EntityBase { */ void publish_state(AlarmControlPanelState state); - /** Add a callback for when the state of the alarm_control_panel changes + /** Add a callback for when the state of the alarm_control_panel changes. + * Triggers can check get_state() to determine the new state. * * @param callback The callback function */ void add_on_state_callback(std::function &&callback); - /** Add a callback for when the state of the alarm_control_panel chanes to triggered - * - * @param callback The callback function - */ - void add_on_triggered_callback(std::function &&callback); - - /** Add a callback for when the state of the alarm_control_panel chanes to arming - * - * @param callback The callback function - */ - void add_on_arming_callback(std::function &&callback); - - /** Add a callback for when the state of the alarm_control_panel changes to pending - * - * @param callback The callback function - */ - void add_on_pending_callback(std::function &&callback); - - /** Add a callback for when the state of the alarm_control_panel changes to armed_home - * - * @param callback The callback function - */ - void add_on_armed_home_callback(std::function &&callback); - - /** Add a callback for when the state of the alarm_control_panel changes to armed_night - * - * @param callback The callback function - */ - void add_on_armed_night_callback(std::function &&callback); - - /** Add a callback for when the state of the alarm_control_panel changes to armed_away - * - * @param callback The callback function - */ - void add_on_armed_away_callback(std::function &&callback); - - /** Add a callback for when the state of the alarm_control_panel changes to disarmed - * - * @param callback The callback function - */ - void add_on_disarmed_callback(std::function &&callback); - /** Add a callback for when the state of the alarm_control_panel clears from triggered * * @param callback The callback function @@ -172,29 +128,14 @@ class AlarmControlPanel : public EntityBase { uint32_t last_update_; // the call control function virtual void control(const AlarmControlPanelCall &call) = 0; - // state callback - CallbackManager state_callback_{}; - // trigger callback - CallbackManager triggered_callback_{}; - // arming callback - CallbackManager arming_callback_{}; - // pending callback - CallbackManager pending_callback_{}; - // armed_home callback - CallbackManager armed_home_callback_{}; - // armed_night callback - CallbackManager armed_night_callback_{}; - // armed_away callback - CallbackManager armed_away_callback_{}; - // disarmed callback - CallbackManager disarmed_callback_{}; - // clear callback - CallbackManager cleared_callback_{}; + // state callback - triggers check get_state() for specific state + LazyCallbackManager state_callback_{}; + // clear callback - fires when leaving TRIGGERED state + LazyCallbackManager cleared_callback_{}; // chime callback - CallbackManager chime_callback_{}; + LazyCallbackManager chime_callback_{}; // ready callback - CallbackManager ready_callback_{}; + LazyCallbackManager ready_callback_{}; }; -} // namespace alarm_control_panel -} // namespace esphome +} // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp index 7bb9b9989c..5e98d58368 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { static const char *const TAG = "alarm_control_panel"; @@ -99,5 +98,4 @@ void AlarmControlPanelCall::perform() { } } -} // namespace alarm_control_panel -} // namespace esphome +} // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.h b/esphome/components/alarm_control_panel/alarm_control_panel_call.h index 034e3142da..cff00900dd 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_call.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.h @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { class AlarmControlPanel; @@ -36,5 +35,4 @@ class AlarmControlPanelCall { void validate_(); }; -} // namespace alarm_control_panel -} // namespace esphome +} // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp index abe6f51995..862c620497 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp @@ -1,7 +1,6 @@ #include "alarm_control_panel_state.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { switch (state) { @@ -30,5 +29,4 @@ const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState stat } } -} // namespace alarm_control_panel -} // namespace esphome +} // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.h b/esphome/components/alarm_control_panel/alarm_control_panel_state.h index ad16222dc0..dd0b91f064 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_state.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.h @@ -3,8 +3,7 @@ #include #include "esphome/core/log.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { enum AlarmControlPanelState : uint8_t { ACP_STATE_DISARMED = 0, @@ -25,5 +24,4 @@ enum AlarmControlPanelState : uint8_t { */ const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state); -} // namespace alarm_control_panel -} // namespace esphome +} // namespace esphome::alarm_control_panel diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index db2ef78158..ce5ceadb47 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -3,9 +3,9 @@ #include "esphome/core/automation.h" #include "alarm_control_panel.h" -namespace esphome { -namespace alarm_control_panel { +namespace esphome::alarm_control_panel { +/// Trigger on any state change class StateTrigger : public Trigger<> { public: explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { @@ -13,55 +13,30 @@ class StateTrigger : public Trigger<> { } }; -class TriggeredTrigger : public Trigger<> { +/// Template trigger that fires when entering a specific state +template class StateEnterTrigger : public Trigger<> { public: - explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); }); + explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { + alarm_control_panel->add_on_state_callback([this]() { + if (this->alarm_control_panel_->get_state() == State) + this->trigger(); + }); } + + protected: + AlarmControlPanel *alarm_control_panel_; }; -class ArmingTrigger : public Trigger<> { - public: - explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_arming_callback([this]() { this->trigger(); }); - } -}; - -class PendingTrigger : public Trigger<> { - public: - explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_pending_callback([this]() { this->trigger(); }); - } -}; - -class ArmedHomeTrigger : public Trigger<> { - public: - explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_armed_home_callback([this]() { this->trigger(); }); - } -}; - -class ArmedNightTrigger : public Trigger<> { - public: - explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_armed_night_callback([this]() { this->trigger(); }); - } -}; - -class ArmedAwayTrigger : public Trigger<> { - public: - explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_armed_away_callback([this]() { this->trigger(); }); - } -}; - -class DisarmedTrigger : public Trigger<> { - public: - explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) { - alarm_control_panel->add_on_disarmed_callback([this]() { this->trigger(); }); - } -}; +// Type aliases for state-specific triggers +using TriggeredTrigger = StateEnterTrigger; +using ArmingTrigger = StateEnterTrigger; +using PendingTrigger = StateEnterTrigger; +using ArmedHomeTrigger = StateEnterTrigger; +using ArmedNightTrigger = StateEnterTrigger; +using ArmedAwayTrigger = StateEnterTrigger; +using DisarmedTrigger = StateEnterTrigger; +/// Trigger when leaving TRIGGERED state (alarm cleared) class ClearedTrigger : public Trigger<> { public: explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { @@ -69,6 +44,7 @@ class ClearedTrigger : public Trigger<> { } }; +/// Trigger on chime event (zone opened while disarmed) class ChimeTrigger : public Trigger<> { public: explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { @@ -76,6 +52,7 @@ class ChimeTrigger : public Trigger<> { } }; +/// Trigger on ready state change class ReadyTrigger : public Trigger<> { public: explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { @@ -187,5 +164,4 @@ template class AlarmControlPanelCondition : public Conditionresponse_offset_ >= this->response_length_) { - ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str()); if (length < GENI_RESPONSE_HEADER_LENGTH) { - ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] response too short", this->parent_->address_str()); return; } if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) { - ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(), + ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str(), response[0], response[1], response[2], response[3], response[4]); return; } @@ -77,11 +77,11 @@ void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { }; if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) { - ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str()); extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F); extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F); } else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) { - ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str()); extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F); extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F); extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F); @@ -100,7 +100,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc if (param->open.status == ESP_GATT_OK) { this->response_offset_ = 0; this->response_length_ = 0; - ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str()); + ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str()); } break; } @@ -132,7 +132,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID); if (chr == nullptr) { - ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str()); + ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str()); break; } auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), @@ -164,12 +164,12 @@ void Alpha3::send_request_(uint8_t *request, size_t len) { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len, request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } void Alpha3::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str()); return; } diff --git a/esphome/components/alpha3/alpha3.h b/esphome/components/alpha3/alpha3.h index 7189ecbc33..19d8e99331 100644 --- a/esphome/components/alpha3/alpha3.h +++ b/esphome/components/alpha3/alpha3.h @@ -15,10 +15,8 @@ namespace alpha3 { namespace espbt = esphome::esp32_ble_tracker; static const espbt::ESPBTUUID ALPHA3_GENI_SERVICE_UUID = espbt::ESPBTUUID::from_uint16(0xfe5d); -static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID = - espbt::ESPBTUUID::from_raw({static_cast(0xa9), 0x7b, static_cast(0xb8), static_cast(0x85), 0x0, - 0x1a, 0x28, static_cast(0xaa), 0x2a, 0x43, 0x6e, 0x3, static_cast(0xd1), - static_cast(0xff), static_cast(0x9c), static_cast(0x85)}); +static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID = espbt::ESPBTUUID::from_raw( + {0xa9, 0x7b, 0xb8, 0x85, 0x00, 0x1a, 0x28, 0xaa, 0x2a, 0x43, 0x6e, 0x03, 0xd1, 0xff, 0x9c, 0x85}); static const int16_t GENI_RESPONSE_HEADER_LENGTH = 13; static const size_t GENI_RESPONSE_TYPE_LENGTH = 8; diff --git a/esphome/components/am43/sensor/am43_sensor.cpp b/esphome/components/am43/sensor/am43_sensor.cpp index 4cc99001ae..b2bc3254e2 100644 --- a/esphome/components/am43/sensor/am43_sensor.cpp +++ b/esphome/components/am43/sensor/am43_sensor.cpp @@ -44,11 +44,9 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); if (chr == nullptr) { if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { - ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", - this->parent_->address_str().c_str()); + ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->parent_->address_str()); } else { - ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", - this->parent_->address_str().c_str()); + ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->parent_->address_str()); } break; } @@ -82,8 +80,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i this->char_handle_, packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), - status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } this->current_sensor_ = 0; @@ -97,7 +94,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i void Am43::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str()); return; } if (this->current_sensor_ == 0) { @@ -107,7 +104,7 @@ void Am43::update() { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } this->current_sensor_++; diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp b/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp index f83f2aff08..0b3bd0e472 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp @@ -12,10 +12,11 @@ void AnalogThresholdBinarySensor::setup() { // TRUE state is defined to be when sensor is >= threshold // so when undefined sensor value initialize to FALSE if (std::isnan(sensor_value)) { + this->raw_state_ = false; this->publish_initial_state(false); } else { - this->publish_initial_state(sensor_value >= - (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f); + this->raw_state_ = sensor_value >= (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f; + this->publish_initial_state(this->raw_state_); } } @@ -25,8 +26,10 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) { this->sensor_->add_on_state_callback([this](float sensor_value) { // if there is an invalid sensor reading, ignore the change and keep the current state if (!std::isnan(sensor_value)) { - this->publish_state(sensor_value >= - (this->state ? this->lower_threshold_.value() : this->upper_threshold_.value())); + // Use raw_state_ for hysteresis logic, not this->state which is post-filter + this->raw_state_ = + sensor_value >= (this->raw_state_ ? this->lower_threshold_.value() : this->upper_threshold_.value()); + this->publish_state(this->raw_state_); } }); } diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h index 55d6b15c36..9ea95d8570 100644 --- a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -20,6 +20,7 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina sensor::Sensor *sensor_{nullptr}; TemplatableValue upper_threshold_{}; TemplatableValue lower_threshold_{}; + bool raw_state_{false}; // Pre-filter state for hysteresis logic }; } // namespace analog_threshold diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index d0e8f6827f..5054488089 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -42,7 +42,7 @@ void Anova::control(const ClimateCall &call) { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } if (call.get_target_temperature().has_value()) { @@ -51,7 +51,7 @@ void Anova::control(const ClimateCall &call) { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } } @@ -67,8 +67,10 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); if (chr == nullptr) { - ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str()); - ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str()); + ESP_LOGW(TAG, + "[%s] No control service found at device, not an Anova..?\n" + "[%s] Note, this component does not currently support Anova Nano.", + this->get_name().c_str(), this->get_name().c_str()); break; } this->char_handle_ = chr->handle; @@ -124,8 +126,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), - status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } } } @@ -150,7 +151,7 @@ void Anova::update() { esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } this->current_request_++; } diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index a9286c531f..0e2c612279 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -27,12 +27,13 @@ from esphome.const import ( CONF_SERVICE, CONF_SERVICES, CONF_TAG, + CONF_THEN, CONF_TRIGGER_ID, CONF_VARIABLES, ) -from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority -from esphome.cpp_generator import TemplateArgsType -from esphome.types import ConfigType +from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigFragmentType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -63,17 +64,21 @@ HomeAssistantActionResponseTrigger = api_ns.class_( "HomeAssistantActionResponseTrigger", automation.Trigger ) APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition) +APIRespondAction = api_ns.class_("APIRespondAction", automation.Action) +APIUnregisterServiceCallAction = api_ns.class_( + "APIUnregisterServiceCallAction", automation.Action +) UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger) ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument") -SERVICE_ARG_NATIVE_TYPES = { - "bool": bool, +SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = { + "bool": cg.bool_, "int": cg.int32, - "float": float, + "float": cg.float_, "string": cg.std_string, - "bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"), + "bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"), "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), - "float[]": cg.FixedVector.template(float).operator("const").operator("ref"), + "float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"), "string[]": cg.FixedVector.template(cg.std_string) .operator("const") .operator("ref"), @@ -85,6 +90,7 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" CONF_HOMEASSISTANT_STATES = "homeassistant_states" CONF_LISTEN_BACKLOG = "listen_backlog" CONF_MAX_SEND_QUEUE = "max_send_queue" +CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only" def validate_encryption_key(value): @@ -101,6 +107,85 @@ def validate_encryption_key(value): return value +CONF_SUPPORTS_RESPONSE = "supports_response" + +# Enum values in api::enums namespace +enums_ns = api_ns.namespace("enums") +SUPPORTS_RESPONSE_OPTIONS = { + "none": enums_ns.SUPPORTS_RESPONSE_NONE, + "optional": enums_ns.SUPPORTS_RESPONSE_OPTIONAL, + "only": enums_ns.SUPPORTS_RESPONSE_ONLY, + "status": enums_ns.SUPPORTS_RESPONSE_STATUS, +} + + +def _auto_detect_supports_response(config: ConfigType) -> ConfigType: + """Auto-detect supports_response based on api.respond usage in the action's then block. + + - If api.respond with data found: set to "optional" (unless user explicitly set) + - If api.respond without data found: set to "status" (unless user explicitly set) + - If no api.respond found: set to "none" (unless user explicitly set) + """ + + def scan_actions(items: ConfigFragmentType) -> tuple[bool, bool]: + """Recursively scan actions for api.respond. + + Returns: (found, has_data) tuple - has_data is True if ANY api.respond has data + """ + found_any = False + has_data_any = False + + if isinstance(items, list): + for item in items: + found, has_data = scan_actions(item) + if found: + found_any = True + has_data_any = has_data_any or has_data + elif isinstance(items, dict): + # Check if this is an api.respond action + if "api.respond" in items: + respond_config = items["api.respond"] + has_data = isinstance(respond_config, dict) and "data" in respond_config + return True, has_data + # Recursively check all values + for value in items.values(): + found, has_data = scan_actions(value) + if found: + found_any = True + has_data_any = has_data_any or has_data + + return found_any, has_data_any + + then = config.get(CONF_THEN, []) + action_name = config.get(CONF_ACTION) + found, has_data = scan_actions(then) + + # If user explicitly set supports_response, validate and use that + if CONF_SUPPORTS_RESPONSE in config: + user_value = config[CONF_SUPPORTS_RESPONSE] + # Validate: "only" requires api.respond with data + if user_value == "only" and not has_data: + raise cv.Invalid( + f"Action '{action_name}' has supports_response=only but no api.respond " + "action with 'data:' was found. Use 'status' for responses without data, " + "or add 'data:' to your api.respond action." + ) + return config + + # Auto-detect based on api.respond usage + if found: + config[CONF_SUPPORTS_RESPONSE] = "optional" if has_data else "status" + else: + config[CONF_SUPPORTS_RESPONSE] = "none" + + return config + + +def _validate_supports_response(value): + """Validate supports_response after auto-detection has set the value.""" + return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value) + + ACTIONS_SCHEMA = automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), @@ -111,10 +196,20 @@ ACTIONS_SCHEMA = automation.validate_automation( cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True), } ), + # No default - auto-detected by _auto_detect_supports_response + cv.Optional(CONF_SUPPORTS_RESPONSE): cv.enum( + SUPPORTS_RESPONSE_OPTIONS, lower=True + ), }, cv.All( cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), cv.rename_key(CONF_SERVICE, CONF_ACTION), + _auto_detect_supports_response, + # Re-validate supports_response after auto-detection sets it + cv.Schema( + {cv.Required(CONF_SUPPORTS_RESPONSE): _validate_supports_response}, + extra=cv.ALLOW_EXTRA, + ), ), ) @@ -131,32 +226,6 @@ def _encryption_schema(config): return ENCRYPTION_SCHEMA(config) -def _validate_api_config(config: ConfigType) -> ConfigType: - """Validate API configuration with mutual exclusivity check and deprecation warning.""" - # Check if both password and encryption are configured - has_password = CONF_PASSWORD in config and config[CONF_PASSWORD] - has_encryption = CONF_ENCRYPTION in config - - if has_password and has_encryption: - raise cv.Invalid( - "The 'password' and 'encryption' options are mutually exclusive. " - "The API client only supports one authentication method at a time. " - "Please remove one of them. " - "Note: 'password' authentication is deprecated and will be removed in version 2026.1.0. " - "We strongly recommend using 'encryption' instead for better security." - ) - - # Warn about password deprecation - if has_password: - _LOGGER.warning( - "API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. " - "Please migrate to the 'encryption' configuration. " - "See https://esphome.io/components/api.html#configuration-variables" - ) - - return config - - def _consume_api_sockets(config: ConfigType) -> ConfigType: """Register socket needs for API component.""" from esphome.components import socket @@ -173,7 +242,17 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(APIServer), cv.Optional(CONF_PORT, default=6053): cv.port, - cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, + # Removed in 2026.1.0 - kept to provide helpful error message + cv.Optional(CONF_PASSWORD): cv.invalid( + "The 'password' option has been removed in ESPHome 2026.1.0.\n" + "Password authentication was deprecated in May 2022.\n" + "Please migrate to encryption for secure API communication:\n\n" + "api:\n" + " encryption:\n" + " key: !secret api_encryption_key\n\n" + "Generate a key with: openssl rand -base64 32\n" + "Or visit https://esphome.io/components/api/#configuration-variables" + ), cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" ): cv.positive_time_period_milliseconds, @@ -235,13 +314,12 @@ CONFIG_SCHEMA = cv.All( } ).extend(cv.COMPONENT_SCHEMA), cv.rename_key(CONF_SERVICES, CONF_ACTIONS), - _validate_api_config, _consume_api_sockets, ) @coroutine_with_priority(CoroPriority.WEB) -async def to_code(config): +async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -249,9 +327,6 @@ async def to_code(config): CORE.register_controller() cg.add(var.set_port(config[CONF_PORT])) - if config[CONF_PASSWORD]: - cg.add_define("USE_API_PASSWORD") - cg.add(var.set_password(config[CONF_PASSWORD])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) if CONF_LISTEN_BACKLOG in config: @@ -260,9 +335,9 @@ async def to_code(config): cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS])) cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE]) - # Set USE_API_SERVICES if any services are enabled + # Set USE_API_USER_DEFINED_ACTIONS if any services are enabled if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: - cg.add_define("USE_API_SERVICES") + cg.add_define("USE_API_USER_DEFINED_ACTIONS") # Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration if config[CONF_CUSTOM_SERVICES]: @@ -278,20 +353,61 @@ async def to_code(config): # Collect all triggers first, then register all at once with initializer_list triggers: list[cg.Pvariable] = [] for conf in actions: - template_args = [] - func_args = [] - service_arg_names = [] + func_args: list[tuple[MockObj, str]] = [] + service_template_args: list[MockObj] = [] # User service argument types + + # Determine supports_response mode + # cv.enum returns the key with enum_value attribute containing the MockObj + supports_response_key = conf[CONF_SUPPORTS_RESPONSE] + supports_response = supports_response_key.enum_value + is_none = supports_response_key == "none" + is_optional = supports_response_key == "optional" + + # Add call_id and return_response based on supports_response mode + # These must match the C++ Trigger template arguments + # - none: no extra args + # - status: call_id only (for reporting success/error without data) + # - only: call_id only (response always expected with data) + # - optional: call_id + return_response (client decides) + if not is_none: + # call_id is present for "optional", "only", and "status" + func_args.append((cg.uint32, "call_id")) + # return_response only present for "optional" + if is_optional: + func_args.append((cg.bool_, "return_response")) + + service_arg_names: list[str] = [] for name, var_ in conf[CONF_VARIABLES].items(): native = SERVICE_ARG_NATIVE_TYPES[var_] - template_args.append(native) + service_template_args.append(native) func_args.append((native, name)) service_arg_names.append(name) - templ = cg.TemplateArguments(*template_args) + # Template args: supports_response mode, then user service arg types + templ = cg.TemplateArguments(supports_response, *service_template_args) trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names + conf[CONF_TRIGGER_ID], + templ, + conf[CONF_ACTION], + service_arg_names, ) triggers.append(trigger) - await automation.build_automation(trigger, func_args, conf) + auto = await automation.build_automation(trigger, func_args, conf) + + # For non-none response modes, automatically append unregister action + # This ensures the call is unregistered after all actions complete (including async ones) + if not is_none: + arg_types = [arg[0] for arg in func_args] + action_templ = cg.TemplateArguments(*arg_types) + unregister_id = ID( + f"{conf[CONF_TRIGGER_ID]}__unregister", + is_declaration=True, + type=APIUnregisterServiceCallAction.template(action_templ), + ) + unregister_action = cg.new_Pvariable( + unregister_id, + var, + ) + cg.add(auto.add_actions([unregister_action])) # Register all services at once - single allocation, no reallocations cg.add(var.initialize_user_services(triggers)) @@ -537,9 +653,98 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg return var -@automation.register_condition("api.connected", APIConnectedCondition, {}) +CONF_SUCCESS = "success" +CONF_ERROR_MESSAGE = "error_message" + + +def _validate_api_respond_data(config): + """Set flag during validation so AUTO_LOAD can include json component.""" + if CONF_DATA in config: + CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True + return config + + +API_RESPOND_ACTION_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.use_id(APIServer), + cv.Optional(CONF_SUCCESS, default=True): cv.templatable(cv.boolean), + cv.Optional(CONF_ERROR_MESSAGE, default=""): cv.templatable(cv.string), + cv.Optional(CONF_DATA): cv.lambda_, + } + ), + _validate_api_respond_data, +) + + +@automation.register_action( + "api.respond", + APIRespondAction, + API_RESPOND_ACTION_SCHEMA, +) +async def api_respond_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + # Validate that api.respond is used inside an API action context. + # We can't easily validate this at config time since the schema validation + # doesn't have access to the parent action context. Validating here in to_code + # is still much better than a cryptic C++ compile error. + has_call_id = any(name == "call_id" for _, name in args) + if not has_call_id: + raise EsphomeError( + "api.respond can only be used inside an API action's 'then:' block. " + "The 'call_id' variable is required to send a response." + ) + + cg.add_define("USE_API_USER_DEFINED_ACTION_RESPONSES") + serv = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, serv) + + # Check if we're in optional mode (has return_response arg) + is_optional = any(name == "return_response" for _, name in args) + if is_optional: + cg.add(var.set_is_optional_mode(True)) + + templ = await cg.templatable(config[CONF_SUCCESS], args, cg.bool_) + cg.add(var.set_success(templ)) + + templ = await cg.templatable(config[CONF_ERROR_MESSAGE], args, cg.std_string) + cg.add(var.set_error_message(templ)) + + if CONF_DATA in config: + cg.add_define("USE_API_USER_DEFINED_ACTION_RESPONSES_JSON") + # Lambda populates the JsonObject root - no return value needed + lambda_ = await cg.process_lambda( + config[CONF_DATA], + args + [(cg.JsonObject, "root")], + return_type=cg.void, + ) + cg.add(var.set_data(lambda_)) + + return var + + +API_CONNECTED_CONDITION_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(APIServer), + cv.Optional(CONF_STATE_SUBSCRIPTION_ONLY, default=False): cv.templatable( + cv.boolean + ), + } +) + + +@automation.register_condition( + "api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA +) async def api_connected_to_code(config, condition_id, template_arg, args): - return cg.new_Pvariable(condition_id, template_arg) + var = cg.new_Pvariable(condition_id, template_arg) + templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_) + cg.add(var.set_state_subscription_only(templ)) + return var def FILTER_SOURCE_FILES() -> list[str]: diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e115e4630d..652b456850 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -7,10 +7,7 @@ service APIConnection { option (needs_setup_connection) = false; option (needs_authentication) = false; } - rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse) { - option (needs_setup_connection) = false; - option (needs_authentication) = false; - } + // REMOVED in ESPHome 2026.1.0: rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse) rpc disconnect (DisconnectRequest) returns (DisconnectResponse) { option (needs_setup_connection) = false; option (needs_authentication) = false; @@ -82,14 +79,13 @@ service APIConnection { // * VarInt denoting the type of message. // * The message object encoded as a ProtoBuf message -// The connection is established in 4 steps: +// The connection is established in 2 steps: // * First, the client connects to the server and sends a "Hello Request" identifying itself -// * The server responds with a "Hello Response" and selects the protocol version -// * After receiving this message, the client attempts to authenticate itself using -// the password and a "Connect Request" -// * The server responds with a "Connect Response" and notifies of invalid password. +// * The server responds with a "Hello Response" and the connection is authenticated // If anything in this initial process fails, the connection must immediately closed // by both sides and _no_ disconnection message is to be sent. +// Note: Password authentication via AuthenticationRequest/AuthenticationResponse (message IDs 3, 4) +// was removed in ESPHome 2026.1.0. Those message IDs are reserved and should not be reused. // Message sent at the beginning of each connection // Can only be sent by the client and only at the beginning of the connection @@ -102,7 +98,7 @@ message HelloRequest { // For example "Home Assistant" // Not strictly necessary to send but nice for debugging // purposes. - string client_info = 1 [(pointer_to_buffer) = true]; + string client_info = 1; uint32 api_version_major = 2; uint32 api_version_minor = 3; } @@ -130,25 +126,23 @@ message HelloResponse { string name = 4; } -// Message sent at the beginning of each connection to authenticate the client -// Can only be sent by the client and only at the beginning of the connection +// DEPRECATED in ESPHome 2026.1.0 - Password authentication is no longer supported. +// These messages are kept for protocol documentation but are not processed by the server. +// Use noise encryption instead: https://esphome.io/components/api/#configuration-variables message AuthenticationRequest { option (id) = 3; option (source) = SOURCE_CLIENT; option (no_delay) = true; - option (ifdef) = "USE_API_PASSWORD"; + option deprecated = true; - // The password to log in with - string password = 1 [(pointer_to_buffer) = true]; + string password = 1; } -// Confirmation of successful connection. After this the connection is available for all traffic. -// Can only be sent by the server and only at the beginning of the connection message AuthenticationResponse { option (id) = 4; option (source) = SOURCE_SERVER; option (no_delay) = true; - option (ifdef) = "USE_API_PASSWORD"; + option deprecated = true; bool invalid_password = 1; } @@ -205,7 +199,9 @@ message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; - bool uses_password = 1 [(field_ifdef) = "USE_API_PASSWORD"]; + // Deprecated in ESPHome 2026.1.0, but kept for backward compatibility + // with older ESPHome versions that still send this field. + bool uses_password = 1 [deprecated = true]; // The name of the node, given by "App.set_name()" string name = 2; @@ -518,7 +514,7 @@ message ListEntitiesLightResponse { bool legacy_supports_color_temperature = 8 [deprecated=true]; float min_mireds = 9; float max_mireds = 10; - repeated string effects = 11; + repeated string effects = 11 [(container_pointer_no_template) = "FixedVector"]; bool disabled_by_default = 13; string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 15; @@ -589,6 +585,7 @@ enum SensorStateClass { STATE_CLASS_MEASUREMENT = 1; STATE_CLASS_TOTAL_INCREASING = 2; STATE_CLASS_TOTAL = 3; + STATE_CLASS_MEASUREMENT_ANGLE = 4; } // Deprecated in API version 1.5 @@ -795,7 +792,7 @@ message HomeassistantActionResponse { uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest bool success = 2; // Whether the service call succeeded string error_message = 3; // Error message if success = false - bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; + bytes response_data = 4 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"]; } // ==================== IMPORT HOME ASSISTANT STATES ==================== @@ -840,7 +837,7 @@ message GetTimeResponse { option (no_delay) = true; fixed32 epoch_seconds = 1; - string timezone = 2 [(pointer_to_buffer) = true]; + string timezone = 2; } // ==================== USER-DEFINES SERVICES ==================== @@ -854,22 +851,31 @@ enum ServiceArgType { SERVICE_ARG_TYPE_FLOAT_ARRAY = 6; SERVICE_ARG_TYPE_STRING_ARRAY = 7; } +enum SupportsResponseType { + SUPPORTS_RESPONSE_NONE = 0; + SUPPORTS_RESPONSE_OPTIONAL = 1; + SUPPORTS_RESPONSE_ONLY = 2; + // Status-only response - reports success/error without data payload + // Value is higher to avoid conflicts with future Home Assistant values + SUPPORTS_RESPONSE_STATUS = 100; +} message ListEntitiesServicesArgument { - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; ServiceArgType type = 2; } message ListEntitiesServicesResponse { option (id) = 41; option (source) = SOURCE_SERVER; - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; fixed32 key = 2; repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true]; + SupportsResponseType supports_response = 4; } message ExecuteServiceArgument { - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; bool bool_ = 1; int32 legacy_int = 2; float float_ = 3; @@ -885,10 +891,25 @@ message ExecuteServiceRequest { option (id) = 42; option (source) = SOURCE_CLIENT; option (no_delay) = true; - option (ifdef) = "USE_API_SERVICES"; + option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; fixed32 key = 1; repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true]; + uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"]; + bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"]; +} + +// Message sent by ESPHome to Home Assistant with service execution response data +message ExecuteServiceResponse { + option (id) = 131; + option (source) = SOURCE_SERVER; + option (no_delay) = true; + option (ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"; + + uint32 call_id = 1; // Matches the call_id from ExecuteServiceRequest + bool success = 2; // Whether the service execution succeeded + string error_message = 3; // Error message if success = false + bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES_JSON"]; } // ==================== CAMERA ==================== @@ -1076,6 +1097,85 @@ message ClimateCommandRequest { uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"]; } +// ==================== WATER_HEATER ==================== +enum WaterHeaterMode { + WATER_HEATER_MODE_OFF = 0; + WATER_HEATER_MODE_ECO = 1; + WATER_HEATER_MODE_ELECTRIC = 2; + WATER_HEATER_MODE_PERFORMANCE = 3; + WATER_HEATER_MODE_HIGH_DEMAND = 4; + WATER_HEATER_MODE_HEAT_PUMP = 5; + WATER_HEATER_MODE_GAS = 6; +} + +message ListEntitiesWaterHeaterResponse { + option (id) = 132; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_WATER_HEATER"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; + bool disabled_by_default = 5; + EntityCategory entity_category = 6; + uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; + float min_temperature = 8; + float max_temperature = 9; + float target_temperature_step = 10; + repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"]; + // Bitmask of WaterHeaterFeature flags + uint32 supported_features = 12; +} + +message WaterHeaterStateResponse { + option (id) = 133; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_WATER_HEATER"; + option (no_delay) = true; + + fixed32 key = 1; + float current_temperature = 2; + float target_temperature = 3; + WaterHeaterMode mode = 4; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; + // Bitmask of current state flags (bit 0 = away, bit 1 = on) + uint32 state = 6; + float target_temperature_low = 7; + float target_temperature_high = 8; +} + +// Bitmask for WaterHeaterCommandRequest.has_fields +enum WaterHeaterCommandHasField { + WATER_HEATER_COMMAND_HAS_NONE = 0; + WATER_HEATER_COMMAND_HAS_MODE = 1; + WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2; + WATER_HEATER_COMMAND_HAS_STATE = 4; + WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8; + WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16; +} + +message WaterHeaterCommandRequest { + option (id) = 134; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_WATER_HEATER"; + option (no_delay) = true; + option (base_class) = "CommandProtoMessage"; + + fixed32 key = 1; + // Bitmask of which fields are set (see WaterHeaterCommandHasField) + uint32 has_fields = 2; + WaterHeaterMode mode = 3; + float target_temperature = 4; + uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"]; + // State flags bitmask (bit 0 = away, bit 1 = on) + uint32 state = 6; + float target_temperature_low = 7; + float target_temperature_high = 8; +} + // ==================== NUMBER ==================== enum NumberMode { NUMBER_MODE_AUTO = 0; @@ -1188,7 +1288,7 @@ message ListEntitiesSirenResponse { string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; bool disabled_by_default = 6; - repeated string tones = 7; + repeated string tones = 7 [(container_pointer_no_template) = "FixedVector"]; bool supports_duration = 8; bool supports_volume = 9; EntityCategory entity_category = 10; @@ -1588,7 +1688,7 @@ message BluetoothGATTWriteRequest { uint32 handle = 2; bool response = 3; - bytes data = 4 [(pointer_to_buffer) = true]; + bytes data = 4; } message BluetoothGATTReadDescriptorRequest { @@ -1608,7 +1708,7 @@ message BluetoothGATTWriteDescriptorRequest { uint64 address = 1; uint32 handle = 2; - bytes data = 3 [(pointer_to_buffer) = true]; + bytes data = 3; } message BluetoothGATTNotifyRequest { @@ -1833,7 +1933,7 @@ message VoiceAssistantAudio { option (source) = SOURCE_BOTH; option (ifdef) = "USE_VOICE_ASSISTANT"; - bytes data = 1; + bytes data = 1 [(pointer_to_buffer) = true]; bool end = 2; } @@ -2321,7 +2421,7 @@ message ZWaveProxyFrame { option (ifdef) = "USE_ZWAVE_PROXY"; option (no_delay) = true; - bytes data = 1 [(pointer_to_buffer) = true]; + bytes data = 1; } enum ZWaveProxyRequestType { @@ -2335,5 +2435,5 @@ message ZWaveProxyRequest { option (ifdef) = "USE_ZWAVE_PROXY"; ZWaveProxyRequestType type = 1; - bytes data = 2 [(pointer_to_buffer) = true]; + bytes data = 2; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4acd2fc15c..fb3548d117 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -6,11 +6,18 @@ #ifdef USE_API_PLAINTEXT #include "api_frame_helper_plaintext.h" #endif +#ifdef USE_API_USER_DEFINED_ACTIONS +#include "user_services.h" +#endif #include #include #include #include +#include #include +#ifdef USE_ESP8266 +#include +#endif #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/entity_base.h" @@ -36,6 +43,9 @@ #ifdef USE_ZWAVE_PROXY #include "esphome/components/zwave_proxy/zwave_proxy.h" #endif +#ifdef USE_WATER_HEATER +#include "esphome/components/water_heater/water_heater.h" +#endif namespace esphome::api { @@ -87,21 +97,18 @@ static const int CAMERA_STOP_STREAM = 5000; return; #endif // USE_DEVICES -APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) - : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { +APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) : parent_(parent) { #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) - auto noise_ctx = parent->get_noise_ctx(); - if (noise_ctx->has_psk()) { - this->helper_ = - std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)}; + auto &noise_ctx = parent->get_noise_ctx(); + if (noise_ctx.has_psk()) { + this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), noise_ctx)}; } else { - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; } #elif defined(USE_API_PLAINTEXT) - this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)}; + this->helper_ = std::unique_ptr{new APIPlaintextFrameHelper(std::move(sock))}; #elif defined(USE_API_NOISE) - this->helper_ = std::unique_ptr{ - new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx(), &this->client_info_)}; + this->helper_ = std::unique_ptr{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; #else #error "No frame helper defined" #endif @@ -122,11 +129,13 @@ void APIConnection::start() { this->fatal_error_with_log_(LOG_STR("Helper init failed"), err); return; } - this->client_info_.peername = helper_->getpeername(); - this->client_info_.name = this->client_info_.peername; + // Initialize client name with peername (IP address) until Hello message provides actual name + const char *peername = this->helper_->get_client_peername(); + this->helper_->set_client_name(peername, strlen(peername)); } APIConnection::~APIConnection() { + this->destroy_active_iterator_(); #ifdef USE_BLUETOOTH_PROXY if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) { bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); @@ -139,6 +148,32 @@ APIConnection::~APIConnection() { #endif } +void APIConnection::destroy_active_iterator_() { + switch (this->active_iterator_) { + case ActiveIterator::LIST_ENTITIES: + this->iterator_storage_.list_entities.~ListEntitiesIterator(); + break; + case ActiveIterator::INITIAL_STATE: + this->iterator_storage_.initial_state.~InitialStateIterator(); + break; + case ActiveIterator::NONE: + break; + } + this->active_iterator_ = ActiveIterator::NONE; +} + +void APIConnection::begin_iterator_(ActiveIterator type) { + this->destroy_active_iterator_(); + this->active_iterator_ = type; + if (type == ActiveIterator::LIST_ENTITIES) { + new (&this->iterator_storage_.list_entities) ListEntitiesIterator(this); + this->iterator_storage_.list_entities.begin(); + } else { + new (&this->iterator_storage_.initial_state) InitialStateIterator(this); + this->iterator_storage_.initial_state.begin(); + } +} + void APIConnection::loop() { if (this->flags_.next_close) { // requested a disconnect @@ -169,8 +204,7 @@ void APIConnection::loop() { } else { this->last_traffic_ = now; // read a packet - this->read_message(buffer.data_len, buffer.type, - buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr); + this->read_message(buffer.data_len, buffer.type, buffer.data); if (this->flags_.remove) return; } @@ -182,28 +216,42 @@ void APIConnection::loop() { this->process_batch_(); } - if (!this->list_entities_iterator_.completed()) { - this->process_iterator_batch_(this->list_entities_iterator_); - } else if (!this->initial_state_iterator_.completed()) { - this->process_iterator_batch_(this->initial_state_iterator_); - - // If we've completed initial states, process any remaining and clear the flag - if (this->initial_state_iterator_.completed()) { - // Process any remaining batched messages immediately - if (!this->deferred_batch_.empty()) { - this->process_batch_(); + switch (this->active_iterator_) { + case ActiveIterator::LIST_ENTITIES: + if (this->iterator_storage_.list_entities.completed()) { + this->destroy_active_iterator_(); + if (this->flags_.state_subscription) { + this->begin_iterator_(ActiveIterator::INITIAL_STATE); + } + } else { + this->process_iterator_batch_(this->iterator_storage_.list_entities); } - // Now that everything is sent, enable immediate sending for future state changes - this->flags_.should_try_send_immediately = true; - } + break; + case ActiveIterator::INITIAL_STATE: + if (this->iterator_storage_.initial_state.completed()) { + this->destroy_active_iterator_(); + // Process any remaining batched messages immediately + if (!this->deferred_batch_.empty()) { + this->process_batch_(); + } + // Now that everything is sent, enable immediate sending for future state changes + this->flags_.should_try_send_immediately = true; + // Release excess memory from buffers that grew during initial sync + this->deferred_batch_.release_buffer(); + this->helper_->release_buffers(); + } else { + this->process_iterator_batch_(this->iterator_storage_.initial_state); + } + break; + case ActiveIterator::NONE: + break; } if (this->flags_.sent_ping) { // Disconnect if not responded within 2.5*keepalive if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { on_fatal_error(); - ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(), - this->client_info_.peername.c_str()); + this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting")); } } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { // Only send ping if we're not disconnecting @@ -220,40 +268,24 @@ void APIConnection::loop() { } } -#ifdef USE_CAMERA - if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) { - uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available()); - bool done = this->image_reader_->available() == to_send; - - CameraImageResponse msg; - msg.key = camera::Camera::instance()->get_object_id_hash(); - msg.set_data(this->image_reader_->peek_data_buffer(), to_send); - msg.done = done; -#ifdef USE_DEVICES - msg.device_id = camera::Camera::instance()->get_device_id(); -#endif - - if (this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { - this->image_reader_->consume_data(to_send); - if (done) { - this->image_reader_->return_image(); - } - } - } -#endif - #ifdef USE_API_HOMEASSISTANT_STATES if (state_subs_at_ >= 0) { this->process_state_subscriptions_(); } #endif + +#ifdef USE_CAMERA + // Process camera last - state updates are higher priority + // (missing a frame is fine, missing a state update is not) + this->try_send_camera_image_(); +#endif } bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { // remote initiated disconnect_client // don't close yet, we still need to send the disconnect response // close will happen on next loop - ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); + this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected")); this->flags_.next_close = true; DisconnectResponse resp; return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); @@ -344,7 +376,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne bool is_single) { auto *binary_sensor = static_cast(entity); ListEntitiesBinarySensorResponse msg; - msg.set_device_class(binary_sensor->get_device_class_ref()); + msg.device_class = binary_sensor->get_device_class_ref(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single); @@ -376,7 +408,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_position = traits.get_supports_position(); msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); - msg.set_device_class(cover->get_device_class_ref()); + msg.device_class = cover->get_device_class_ref(); return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -411,7 +443,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co if (traits.supports_direction()) msg.direction = static_cast(fan->direction); if (traits.supports_preset_modes() && fan->has_preset_mode()) - msg.set_preset_mode(StringRef(fan->get_preset_mode())); + msg.preset_mode = StringRef(fan->get_preset_mode()); return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, @@ -439,7 +471,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { if (msg.has_direction) call.set_direction(static_cast(msg.direction)); if (msg.has_preset_mode) - call.set_preset_mode(msg.preset_mode); + call.set_preset_mode(msg.preset_mode.c_str(), msg.preset_mode.size()); call.perform(); } #endif @@ -467,7 +499,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * resp.cold_white = values.get_cold_white(); resp.warm_white = values.get_warm_white(); if (light->supports_effects()) { - resp.set_effect(light->get_effect_name_ref()); + resp.effect = light->get_effect_name_ref(); } return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -484,12 +516,16 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c msg.min_mireds = traits.get_min_mireds(); msg.max_mireds = traits.get_max_mireds(); } + FixedVector effects_list; if (light->supports_effects()) { - msg.effects.emplace_back("None"); - for (auto *effect : light->get_effects()) { - msg.effects.emplace_back(effect->get_name()); + auto &light_effects = light->get_effects(); + effects_list.init(light_effects.size() + 1); + effects_list.push_back("None"); + for (auto *effect : light_effects) { + effects_list.push_back(effect->get_name()); } } + msg.effects = &effects_list; return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -521,7 +557,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) { if (msg.has_flash_length) call.set_flash_length(msg.flash_length); if (msg.has_effect) - call.set_effect(msg.effect); + call.set_effect(msg.effect.c_str(), msg.effect.size()); call.perform(); } #endif @@ -545,10 +581,10 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * bool is_single) { auto *sensor = static_cast(entity); ListEntitiesSensorResponse msg; - msg.set_unit_of_measurement(sensor->get_unit_of_measurement_ref()); + msg.unit_of_measurement = sensor->get_unit_of_measurement_ref(); msg.accuracy_decimals = sensor->get_accuracy_decimals(); msg.force_update = sensor->get_force_update(); - msg.set_device_class(sensor->get_device_class_ref()); + msg.device_class = sensor->get_device_class_ref(); msg.state_class = static_cast(sensor->get_state_class()); return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single); @@ -575,7 +611,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection * auto *a_switch = static_cast(entity); ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); - msg.set_device_class(a_switch->get_device_class_ref()); + msg.device_class = a_switch->get_device_class_ref(); return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -600,7 +636,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec bool is_single) { auto *text_sensor = static_cast(entity); TextSensorStateResponse resp; - resp.set_state(StringRef(text_sensor->state)); + resp.state = StringRef(text_sensor->state); resp.missing_state = !text_sensor->has_state(); return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); @@ -609,7 +645,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect bool is_single) { auto *text_sensor = static_cast(entity); ListEntitiesTextSensorResponse msg; - msg.set_device_class(text_sensor->get_device_class_ref()); + msg.device_class = text_sensor->get_device_class_ref(); return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -639,13 +675,13 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) { - resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode())); + resp.custom_fan_mode = StringRef(climate->get_custom_fan_mode()); } if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); } if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) { - resp.set_custom_preset(StringRef(climate->get_custom_preset())); + resp.custom_preset = StringRef(climate->get_custom_preset()); } if (traits.get_supports_swing_modes()) resp.swing_mode = static_cast(climate->swing_mode); @@ -700,11 +736,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { if (msg.has_fan_mode) call.set_fan_mode(static_cast(msg.fan_mode)); if (msg.has_custom_fan_mode) - call.set_fan_mode(msg.custom_fan_mode); + call.set_fan_mode(msg.custom_fan_mode.c_str(), msg.custom_fan_mode.size()); if (msg.has_preset) call.set_preset(static_cast(msg.preset)); if (msg.has_custom_preset) - call.set_preset(msg.custom_preset); + call.set_preset(msg.custom_preset.c_str(), msg.custom_preset.size()); if (msg.has_swing_mode) call.set_swing_mode(static_cast(msg.swing_mode)); call.perform(); @@ -730,9 +766,9 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * bool is_single) { auto *number = static_cast(entity); ListEntitiesNumberResponse msg; - msg.set_unit_of_measurement(number->traits.get_unit_of_measurement_ref()); + msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref(); msg.mode = static_cast(number->traits.get_mode()); - msg.set_device_class(number->traits.get_device_class_ref()); + msg.device_class = number->traits.get_device_class_ref(); msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); @@ -845,7 +881,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c bool is_single) { auto *text = static_cast(entity); TextStateResponse resp; - resp.set_state(StringRef(text->state)); + resp.state = StringRef(text->state); resp.missing_state = !text->has_state(); return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -857,7 +893,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co msg.mode = static_cast(text->traits.get_mode()); msg.min_length = text->traits.get_min_length(); msg.max_length = text->traits.get_max_length(); - msg.set_pattern(text->traits.get_pattern_ref()); + msg.pattern = text->traits.get_pattern_ref(); return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -878,7 +914,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection bool is_single) { auto *select = static_cast(entity); SelectStateResponse resp; - resp.set_state(StringRef(select->current_option())); + resp.state = StringRef(select->current_option()); resp.missing_state = !select->has_state(); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -893,7 +929,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection * } void APIConnection::select_command(const SelectCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) - call.set_option(msg.state); + call.set_option(msg.state.c_str(), msg.state.size()); call.perform(); } #endif @@ -903,7 +939,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection * bool is_single) { auto *button = static_cast(entity); ListEntitiesButtonResponse msg; - msg.set_device_class(button->get_device_class_ref()); + msg.device_class = button->get_device_class_ref(); return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -972,7 +1008,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c auto *valve = static_cast(entity); ListEntitiesValveResponse msg; auto traits = valve->get_traits(); - msg.set_device_class(valve->get_device_class_ref()); + msg.device_class = valve->get_device_class_ref(); msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); @@ -1017,7 +1053,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec for (auto &supported_format : traits.get_supported_formats()) { msg.supported_formats.emplace_back(); auto &media_format = msg.supported_formats.back(); - media_format.set_format(StringRef(supported_format.format)); + media_format.format = StringRef(supported_format.format); media_format.sample_rate = supported_format.sample_rate; media_format.num_channels = supported_format.num_channels; media_format.purpose = static_cast(supported_format.purpose); @@ -1045,6 +1081,36 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { #endif #ifdef USE_CAMERA +void APIConnection::try_send_camera_image_() { + if (!this->image_reader_) + return; + + // Send as many chunks as possible without blocking + while (this->image_reader_->available()) { + if (!this->helper_->can_write_without_blocking()) + return; + + uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available()); + bool done = this->image_reader_->available() == to_send; + + CameraImageResponse msg; + msg.key = camera::Camera::instance()->get_object_id_hash(); + msg.set_data(this->image_reader_->peek_data_buffer(), to_send); + msg.done = done; +#ifdef USE_DEVICES + msg.device_id = camera::Camera::instance()->get_device_id(); +#endif + + if (!this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { + return; // Send failed, try again later + } + this->image_reader_->consume_data(to_send); + if (done) { + this->image_reader_->return_image(); + return; + } + } +} void APIConnection::set_camera_state(std::shared_ptr image) { if (!this->flags_.state_subscription) return; @@ -1052,8 +1118,11 @@ void APIConnection::set_camera_state(std::shared_ptr image) return; if (this->image_reader_->available()) return; - if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) + if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) { this->image_reader_->set_image(std::move(image)); + // Try to send immediately to reduce latency + this->try_send_camera_image_(); + } } uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { @@ -1082,9 +1151,8 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { if (homeassistant::global_homeassistant_time != nullptr) { homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); #ifdef USE_TIME_TIMEZONE - if (value.timezone_len > 0) { - homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast(value.timezone), - value.timezone_len); + if (!value.timezone.empty()) { + homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size()); } #endif } @@ -1195,8 +1263,8 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA for (auto &wake_word : config.available_wake_words) { resp.available_wake_words.emplace_back(); auto &resp_wake_word = resp.available_wake_words.back(); - resp_wake_word.set_id(StringRef(wake_word.id)); - resp_wake_word.set_wake_word(StringRef(wake_word.wake_word)); + resp_wake_word.id = StringRef(wake_word.id); + resp_wake_word.wake_word = StringRef(wake_word.wake_word); for (const auto &lang : wake_word.trained_languages) { resp_wake_word.trained_languages.push_back(lang); } @@ -1211,8 +1279,8 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA resp.available_wake_words.emplace_back(); auto &resp_wake_word = resp.available_wake_words.back(); - resp_wake_word.set_id(StringRef(wake_word.id)); - resp_wake_word.set_wake_word(StringRef(wake_word.wake_word)); + resp_wake_word.id = StringRef(wake_word.id); + resp_wake_word.wake_word = StringRef(wake_word.wake_word); for (const auto &lang : wake_word.trained_languages) { resp_wake_word.trained_languages.push_back(lang); } @@ -1294,6 +1362,57 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe } #endif +#ifdef USE_WATER_HEATER +bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_heater) { + return this->send_message_smart_(water_heater, &APIConnection::try_send_water_heater_state, + WaterHeaterStateResponse::MESSAGE_TYPE, WaterHeaterStateResponse::ESTIMATED_SIZE); +} +uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single) { + auto *wh = static_cast(entity); + WaterHeaterStateResponse resp; + resp.mode = static_cast(wh->get_mode()); + resp.current_temperature = wh->get_current_temperature(); + resp.target_temperature = wh->get_target_temperature(); + resp.target_temperature_low = wh->get_target_temperature_low(); + resp.target_temperature_high = wh->get_target_temperature_high(); + resp.state = wh->get_state(); + resp.key = wh->get_object_id_hash(); + + return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); +} +uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single) { + auto *wh = static_cast(entity); + ListEntitiesWaterHeaterResponse msg; + auto traits = wh->get_traits(); + msg.min_temperature = traits.get_min_temperature(); + msg.max_temperature = traits.get_max_temperature(); + msg.target_temperature_step = traits.get_target_temperature_step(); + msg.supported_modes = &traits.get_supported_modes(); + msg.supported_features = traits.get_feature_flags(); + return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); +} + +void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) { + ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater) + if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE) + call.set_mode(static_cast(msg.mode)); + if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE) + call.set_target_temperature(msg.target_temperature); + if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW) + call.set_target_temperature_low(msg.target_temperature_low); + if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH) + call.set_target_temperature_high(msg.target_temperature_high); + if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) { + call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0); + call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0); + } + call.perform(); +} +#endif + #ifdef USE_EVENT void APIConnection::send_event(event::Event *event, const char *event_type) { this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, @@ -1302,7 +1421,7 @@ void APIConnection::send_event(event::Event *event, const char *event_type) { uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { EventResponse resp; - resp.set_event_type(StringRef(event_type)); + resp.event_type = StringRef(event_type); return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1310,7 +1429,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c bool is_single) { auto *event = static_cast(entity); ListEntitiesEventResponse msg; - msg.set_device_class(event->get_device_class_ref()); + msg.device_class = event->get_device_class_ref(); msg.event_types = &event->get_event_types(); return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); @@ -1333,11 +1452,11 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.has_progress = true; resp.progress = update->update_info.progress; } - resp.set_current_version(StringRef(update->update_info.current_version)); - resp.set_latest_version(StringRef(update->update_info.latest_version)); - resp.set_title(StringRef(update->update_info.title)); - resp.set_release_summary(StringRef(update->update_info.summary)); - resp.set_release_url(StringRef(update->update_info.release_url)); + resp.current_version = StringRef(update->update_info.current_version); + resp.latest_version = StringRef(update->update_info.latest_version); + resp.title = StringRef(update->update_info.title); + resp.release_summary = StringRef(update->update_info.summary); + resp.release_url = StringRef(update->update_info.release_url); } return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1345,7 +1464,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection * bool is_single) { auto *update = static_cast(entity); ListEntitiesUpdateResponse msg; - msg.set_device_class(update->get_device_class_ref()); + msg.device_class = update->get_device_class_ref(); return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1383,9 +1502,10 @@ void APIConnection::complete_authentication_() { } this->flags_.connection_state = static_cast(ConnectionState::AUTHENTICATED); - ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); + this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected")); #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername); + this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()), + std::string(this->helper_->get_client_peername())); #endif #ifdef USE_HOMEASSISTANT_TIME if (homeassistant::global_homeassistant_time != nullptr) { @@ -1400,41 +1520,25 @@ void APIConnection::complete_authentication_() { } bool APIConnection::send_hello_response(const HelloRequest &msg) { - this->client_info_.name.assign(reinterpret_cast(msg.client_info), msg.client_info_len); - this->client_info_.peername = this->helper_->getpeername(); + // Copy client name with truncation if needed (set_client_name handles truncation) + this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size()); this->client_api_version_major_ = msg.api_version_major; this->client_api_version_minor_ = msg.api_version_minor; - ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(), - this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_); + ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(), + this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_); HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 13; + resp.api_version_minor = 14; // Send only the version string - the client only logs this for debugging and doesn't use it otherwise - resp.set_server_info(ESPHOME_VERSION_REF); - resp.set_name(StringRef(App.get_name())); + resp.server_info = ESPHOME_VERSION_REF; + resp.name = StringRef(App.get_name()); -#ifdef USE_API_PASSWORD - // Password required - wait for authentication - this->flags_.connection_state = static_cast(ConnectionState::CONNECTED); -#else - // No password configured - auto-authenticate + // Auto-authenticate - password auth was removed in ESPHome 2026.1.0 this->complete_authentication_(); -#endif return this->send_message(resp, HelloResponse::MESSAGE_TYPE); } -#ifdef USE_API_PASSWORD -bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) { - AuthenticationResponse resp; - // bool invalid_password = 1; - resp.invalid_password = !this->parent_->check_password(msg.password, msg.password_len); - if (!resp.invalid_password) { - this->complete_authentication_(); - } - return this->send_message(resp, AuthenticationResponse::MESSAGE_TYPE); -} -#endif // USE_API_PASSWORD bool APIConnection::send_ping_response(const PingRequest &msg) { PingResponse resp; @@ -1443,59 +1547,92 @@ bool APIConnection::send_ping_response(const PingRequest &msg) { bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { DeviceInfoResponse resp{}; -#ifdef USE_API_PASSWORD - resp.uses_password = true; -#endif - resp.set_name(StringRef(App.get_name())); - resp.set_friendly_name(StringRef(App.get_friendly_name())); + resp.name = StringRef(App.get_name()); + resp.friendly_name = StringRef(App.get_friendly_name()); #ifdef USE_AREAS - resp.set_suggested_area(StringRef(App.get_area())); + resp.suggested_area = StringRef(App.get_area()); #endif - // mac_address must store temporary string - will be valid during send_message call - std::string mac_address = get_mac_address_pretty(); - resp.set_mac_address(StringRef(mac_address)); + // Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) + char mac_address[18]; + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_upper(mac, mac_address); + resp.mac_address = StringRef(mac_address); - resp.set_esphome_version(ESPHOME_VERSION_REF); + resp.esphome_version = ESPHOME_VERSION_REF; - resp.set_compilation_time(App.get_compilation_time_ref()); + // Stack buffer for build time string + char build_time_str[Application::BUILD_TIME_STR_SIZE]; + App.get_build_time_string(build_time_str); + resp.compilation_time = StringRef(build_time_str); - // Compile-time StringRef constants for manufacturers + // Manufacturer string - define once, handle ESP8266 PROGMEM separately #if defined(USE_ESP8266) || defined(USE_ESP32) - static constexpr auto MANUFACTURER = StringRef::from_lit("Espressif"); +#define ESPHOME_MANUFACTURER "Espressif" #elif defined(USE_RP2040) - static constexpr auto MANUFACTURER = StringRef::from_lit("Raspberry Pi"); +#define ESPHOME_MANUFACTURER "Raspberry Pi" #elif defined(USE_BK72XX) - static constexpr auto MANUFACTURER = StringRef::from_lit("Beken"); +#define ESPHOME_MANUFACTURER "Beken" #elif defined(USE_LN882X) - static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning"); +#define ESPHOME_MANUFACTURER "Lightning" #elif defined(USE_NRF52) - static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor"); +#define ESPHOME_MANUFACTURER "Nordic Semiconductor" #elif defined(USE_RTL87XX) - static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek"); +#define ESPHOME_MANUFACTURER "Realtek" #elif defined(USE_HOST) - static constexpr auto MANUFACTURER = StringRef::from_lit("Host"); +#define ESPHOME_MANUFACTURER "Host" #endif - resp.set_manufacturer(MANUFACTURER); +#ifdef USE_ESP8266 + // ESP8266 requires PROGMEM for flash storage, copy to stack for memcpy compatibility + static const char MANUFACTURER_PROGMEM[] PROGMEM = ESPHOME_MANUFACTURER; + char manufacturer_buf[sizeof(MANUFACTURER_PROGMEM)]; + memcpy_P(manufacturer_buf, MANUFACTURER_PROGMEM, sizeof(MANUFACTURER_PROGMEM)); + resp.manufacturer = StringRef(manufacturer_buf, sizeof(MANUFACTURER_PROGMEM) - 1); +#else + static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER); + resp.manufacturer = MANUFACTURER; +#endif +#undef ESPHOME_MANUFACTURER + +#ifdef USE_ESP8266 + static const char MODEL_PROGMEM[] PROGMEM = ESPHOME_BOARD; + char model_buf[sizeof(MODEL_PROGMEM)]; + memcpy_P(model_buf, MODEL_PROGMEM, sizeof(MODEL_PROGMEM)); + resp.model = StringRef(model_buf, sizeof(MODEL_PROGMEM) - 1); +#else static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD); - resp.set_model(MODEL); + resp.model = MODEL; +#endif #ifdef USE_DEEP_SLEEP resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; #endif #ifdef ESPHOME_PROJECT_NAME +#ifdef USE_ESP8266 + static const char PROJECT_NAME_PROGMEM[] PROGMEM = ESPHOME_PROJECT_NAME; + static const char PROJECT_VERSION_PROGMEM[] PROGMEM = ESPHOME_PROJECT_VERSION; + char project_name_buf[sizeof(PROJECT_NAME_PROGMEM)]; + char project_version_buf[sizeof(PROJECT_VERSION_PROGMEM)]; + memcpy_P(project_name_buf, PROJECT_NAME_PROGMEM, sizeof(PROJECT_NAME_PROGMEM)); + memcpy_P(project_version_buf, PROJECT_VERSION_PROGMEM, sizeof(PROJECT_VERSION_PROGMEM)); + resp.project_name = StringRef(project_name_buf, sizeof(PROJECT_NAME_PROGMEM) - 1); + resp.project_version = StringRef(project_version_buf, sizeof(PROJECT_VERSION_PROGMEM) - 1); +#else static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME); static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION); - resp.set_project_name(PROJECT_NAME); - resp.set_project_version(PROJECT_VERSION); + resp.project_name = PROJECT_NAME; + resp.project_version = PROJECT_VERSION; +#endif #endif #ifdef USE_WEBSERVER resp.webserver_port = USE_WEBSERVER_PORT; #endif #ifdef USE_BLUETOOTH_PROXY resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); - // bt_mac must store temporary string - will be valid during send_message call - std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(); - resp.set_bluetooth_mac_address(StringRef(bluetooth_mac)); + // Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) + char bluetooth_mac[18]; + bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac); + resp.bluetooth_mac_address = StringRef(bluetooth_mac); #endif #ifdef USE_VOICE_ASSISTANT resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags(); @@ -1514,7 +1651,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { break; auto &device_info = resp.devices[device_index++]; device_info.device_id = device->get_device_id(); - device_info.set_name(StringRef(device->get_name())); + device_info.name = StringRef(device->get_name()); device_info.area_id = device->get_area_id(); } #endif @@ -1525,7 +1662,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { break; auto &area_info = resp.areas[area_index++]; area_info.area_id = area->get_area_id(); - area_info.set_name(StringRef(area->get_name())); + area_info.name = StringRef(area->get_name()); } #endif @@ -1534,25 +1671,92 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { #ifdef USE_API_HOMEASSISTANT_STATES void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { + // Skip if entity_id is empty (invalid message) + if (msg.entity_id.empty()) { + return; + } + for (auto &it : this->parent_->get_state_subs()) { - if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) { - it.callback(msg.state); + // Compare entity_id: check length matches and content matches + size_t entity_id_len = strlen(it.entity_id); + if (entity_id_len != msg.entity_id.size() || + memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) { + continue; } + + // Compare attribute: either both have matching attribute, or both have none + size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0; + if (sub_attr_len != msg.attribute.size() || + (sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) { + continue; + } + + // Create null-terminated state for callback (parse_number needs null-termination) + // HA state max length is 255, so 256 byte buffer covers all cases + char state_buf[256]; + size_t copy_len = msg.state.size(); + if (copy_len >= sizeof(state_buf)) { + copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator + } + if (copy_len > 0) { + memcpy(state_buf, msg.state.c_str(), copy_len); + } + state_buf[copy_len] = '\0'; + it.callback(StringRef(state_buf, copy_len)); } } #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void APIConnection::execute_service(const ExecuteServiceRequest &msg) { bool found = false; +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + // Register the call and get a unique server-generated action_call_id + // This avoids collisions when multiple clients use the same call_id + uint32_t action_call_id = 0; + if (msg.call_id != 0) { + action_call_id = this->parent_->register_active_action_call(msg.call_id, this); + } + // Use the overload that passes action_call_id separately (avoids copying msg) + for (auto *service : this->parent_->get_user_services()) { + if (service->execute_service(msg, action_call_id)) { + found = true; + } + } +#else for (auto *service : this->parent_->get_user_services()) { if (service->execute_service(msg)) { found = true; } } +#endif if (!found) { ESP_LOGV(TAG, "Could not find service"); } + // Note: For services with supports_response != none, the call is unregistered + // by an automatically appended APIUnregisterServiceCallAction at the end of + // the action list. This ensures async actions (delays, waits) complete first. } +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES +void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message) { + ExecuteServiceResponse resp; + resp.call_id = call_id; + resp.success = success; + resp.error_message = error_message; + this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); +} +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON +void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message, + const uint8_t *response_data, size_t response_data_len) { + ExecuteServiceResponse resp; + resp.call_id = call_id; + resp.success = success; + resp.error_message = error_message; + resp.response_data = response_data; + resp.response_data_len = response_data_len; + this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE); +} +#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON +#endif // USE_API_USER_DEFINED_ACTION_RESPONSES #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -1574,13 +1778,13 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption resp.success = false; psk_t psk{}; - if (msg.key.empty()) { + if (msg.key_len == 0) { if (this->parent_->clear_noise_psk(true)) { resp.success = true; } else { ESP_LOGW(TAG, "Failed to clear encryption key"); } - } else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { + } else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) { ESP_LOGW(TAG, "Invalid encryption key length"); } else if (!this->parent_->save_noise_psk(psk, true)) { ESP_LOGW(TAG, "Failed to save encryption key"); @@ -1615,10 +1819,30 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { return false; } bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { - if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) { // SubscribeLogsResponse + const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); + + if (!this->try_to_clear_buffer(!is_log_message)) { return false; } + // Toggle Nagle's algorithm based on message type to prevent log messages from + // filling the TCP send buffer and crowding out important state updates. + // + // This honors the `no_delay` proto option - SubscribeLogsResponse is the only + // message with `option (no_delay) = false;` in api.proto, indicating it should + // allow Nagle coalescing. This option existed since 2019 but was never implemented. + // + // - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce + // into fewer, larger packets. They flush naturally via TCP delayed ACK timer + // (~200ms), buffer filling, or when a state update triggers a flush. + // + // - All other messages (state updates, responses): Disable Nagle (NODELAY=true) + // for immediate delivery. These are time-sensitive and should not be delayed. + // + // This must be done proactively BEFORE the buffer fills up - checking buffer + // state here would be too late since we'd already be in a degraded state. + this->helper_->set_nodelay(!is_log_message); + APIError err = this->helper_->write_protobuf_packet(message_type, buffer); if (err == APIError::WOULD_BLOCK) return false; @@ -1629,15 +1853,9 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { // Do not set last_traffic_ on send return true; } -#ifdef USE_API_PASSWORD -void APIConnection::on_unauthenticated_access() { - this->on_fatal_error(); - ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); -} -#endif void APIConnection::on_no_setup_connection() { this->on_fatal_error(); - ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str()); + this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup")); } void APIConnection::on_fatal_error() { this->helper_->close(); @@ -1652,13 +1870,13 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c for (auto &item : items) { if (item.entity == entity && item.message_type == message_type) { // Replace with new creator - item.creator = std::move(creator); + item.creator = creator; return; } } // No existing item found, add new one - items.emplace_back(entity, std::move(creator), message_type, estimated_size); + items.emplace_back(entity, creator, message_type, estimated_size); } void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, @@ -1667,7 +1885,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCre // This avoids expensive vector::insert which shifts all elements // Note: We only ever have one high-priority message at a time (ping OR disconnect) // If we're disconnecting, pings are blocked, so this simple swap is sufficient - items.emplace_back(entity, std::move(creator), message_type, estimated_size); + items.emplace_back(entity, creator, message_type, estimated_size); if (items.size() > 1) { // Swap the new high-priority item to the front std::swap(items.front(), items.back()); @@ -1683,9 +1901,9 @@ bool APIConnection::schedule_batch_() { } void APIConnection::process_batch_() { - // Ensure PacketInfo remains trivially destructible for our placement new approach - static_assert(std::is_trivially_destructible::value, - "PacketInfo must remain trivially destructible with this placement-new approach"); + // Ensure MessageInfo remains trivially destructible for our placement new approach + static_assert(std::is_trivially_destructible::value, + "MessageInfo must remain trivially destructible with this placement-new approach"); if (this->deferred_batch_.empty()) { this->flags_.batch_scheduled = false; @@ -1725,12 +1943,12 @@ void APIConnection::process_batch_() { return; } - size_t packets_to_process = std::min(num_items, MAX_PACKETS_PER_BATCH); + size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH); - // Stack-allocated array for packet info - alignas(PacketInfo) char packet_info_storage[MAX_PACKETS_PER_BATCH * sizeof(PacketInfo)]; - PacketInfo *packet_info = reinterpret_cast(packet_info_storage); - size_t packet_count = 0; + // Stack-allocated array for message info + alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)]; + MessageInfo *message_info = reinterpret_cast(message_info_storage); + size_t message_count = 0; // Cache these values to avoid repeated virtual calls const uint8_t header_padding = this->helper_->frame_header_padding(); @@ -1761,7 +1979,7 @@ void APIConnection::process_batch_() { uint32_t current_offset = 0; // Process items and encode directly to buffer (up to our limit) - for (size_t i = 0; i < packets_to_process; i++) { + for (size_t i = 0; i < messages_to_process; i++) { const auto &item = this->deferred_batch_[i]; // Try to encode message // The creator will calculate overhead to determine if the message fits @@ -1775,11 +1993,11 @@ void APIConnection::process_batch_() { // Message was encoded successfully // payload_size is header_padding + actual payload size + footer_size uint16_t proto_payload_size = payload_size - header_padding - footer_size; - // Use placement new to construct PacketInfo in pre-allocated stack array - // This avoids default-constructing all MAX_PACKETS_PER_BATCH elements - // Explicit destruction is not needed because PacketInfo is trivially destructible, + // Use placement new to construct MessageInfo in pre-allocated stack array + // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements + // Explicit destruction is not needed because MessageInfo is trivially destructible, // as ensured by the static_assert in its definition. - new (&packet_info[packet_count++]) PacketInfo(item.message_type, current_offset, proto_payload_size); + new (&message_info[message_count++]) MessageInfo(item.message_type, current_offset, proto_payload_size); // Update tracking variables items_processed++; @@ -1803,9 +2021,9 @@ void APIConnection::process_batch_() { shared_buf.resize(shared_buf.size() + footer_size); } - // Send all collected packets - APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf}, - std::span(packet_info, packet_count)); + // Send all collected messages + APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, + std::span(message_info, message_count)); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); } @@ -1873,10 +2091,10 @@ void APIConnection::process_state_subscriptions_() { const auto &it = subs[this->state_subs_at_]; SubscribeHomeAssistantStateResponse resp; - resp.set_entity_id(StringRef(it.entity_id)); + resp.entity_id = StringRef(it.entity_id); - // Avoid string copy by directly using the optional's value if it exists - resp.set_attribute(it.attribute.has_value() ? StringRef(it.attribute.value()) : StringRef("")); + // Avoid string copy by using the const char* pointer if it exists + resp.attribute = it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""); resp.once = it.once; if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { @@ -1885,8 +2103,13 @@ void APIConnection::process_state_subscriptions_() { } #endif // USE_API_HOMEASSISTANT_STATES +void APIConnection::log_client_(int level, const LogString *message) { + esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(), + this->helper_->get_client_peername(), LOG_STR_ARG(message)); +} + void APIConnection::log_warning_(const LogString *message, APIError err) { - ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(), + ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(), LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 6cfd108927..15d79a25ec 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -9,32 +9,23 @@ #include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" +#include "esphome/core/string_ref.h" #include #include namespace esphome::api { -// Client information structure -struct ClientInfo { - std::string name; // Client name from Hello message - std::string peername; // IP:port from socket -}; - // Keepalive timeout in milliseconds static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; // Maximum number of entities to process in a single batch during initial state/info sending -// This was increased from 20 to 24 after removing the unique_id field from entity info messages, -// which reduced message sizes allowing more entities per batch without exceeding packet limits -static constexpr size_t MAX_INITIAL_PER_BATCH = 24; -// Maximum number of packets to process in a single batch (platform-dependent) -// This limit exists to prevent stack overflow from the PacketInfo array in process_batch_ -// Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes -#if defined(USE_ESP32) || defined(USE_HOST) -static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HOST has plenty -#else -static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks -#endif +// API 1.14+ clients compute object_id client-side, so messages are smaller and we can fit more per batch +// TODO: Remove MAX_INITIAL_PER_BATCH_LEGACY before 2026.7.0 - all clients should support API 1.14 by then +static constexpr size_t MAX_INITIAL_PER_BATCH_LEGACY = 24; // For clients < API 1.14 (includes object_id) +static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= API 1.14 (no object_id) +// Verify MAX_MESSAGES_PER_BATCH (defined in api_frame_helper.h) can hold the initial batch +static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH, + "MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH"); class APIConnection final : public APIServerConnection { public: @@ -176,6 +167,11 @@ class APIConnection final : public APIServerConnection { void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; #endif +#ifdef USE_WATER_HEATER + bool send_water_heater_state(water_heater::WaterHeater *water_heater); + void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; +#endif + #ifdef USE_EVENT void send_event(event::Event *event, const char *event_type); #endif @@ -197,16 +193,17 @@ class APIConnection final : public APIServerConnection { void on_get_time_response(const GetTimeResponse &value) override; #endif bool send_hello_response(const HelloRequest &msg) override; -#ifdef USE_API_PASSWORD - bool send_authenticate_response(const AuthenticationRequest &msg) override; -#endif bool send_disconnect_response(const DisconnectRequest &msg) override; bool send_ping_response(const PingRequest &msg) override; bool send_device_info_response(const DeviceInfoRequest &msg) override; - void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } + void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } void subscribe_states(const SubscribeStatesRequest &msg) override { this->flags_.state_subscription = true; - this->initial_state_iterator_.begin(); + // Start initial state iterator only if no iterator is active + // If list_entities is running, we'll start initial_state when it completes + if (this->active_iterator_ == ActiveIterator::NONE) { + this->begin_iterator_(ActiveIterator::INITIAL_STATE); + } } void subscribe_logs(const SubscribeLogsRequest &msg) override { this->flags_.log_subscription = msg.level; @@ -221,8 +218,15 @@ class APIConnection final : public APIServerConnection { #ifdef USE_API_HOMEASSISTANT_STATES void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void execute_service(const ExecuteServiceRequest &msg) override; +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message); +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON + void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message, + const uint8_t *response_data, size_t response_data_len); +#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON +#endif // USE_API_USER_DEFINED_ACTION_RESPONSES #endif #ifdef USE_API_NOISE bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override; @@ -244,9 +248,6 @@ class APIConnection final : public APIServerConnection { } void on_fatal_error() override; -#ifdef USE_API_PASSWORD - void on_unauthenticated_access() override; -#endif void on_no_setup_connection() override; ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { // FIXME: ensure no recursive writes can happen @@ -273,13 +274,18 @@ class APIConnection final : public APIServerConnection { bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; - const std::string &get_name() const { return this->client_info_.name; } - const std::string &get_peername() const { return this->client_info_.peername; } + const char *get_name() const { return this->helper_->get_client_name(); } + /// Get peer name (IP address) - cached at connection init time + const char *get_peername() const { return this->helper_->get_client_peername(); } protected: // Helper function to handle authentication completion void complete_authentication_(); +#ifdef USE_CAMERA + void try_send_camera_image_(); +#endif + #ifdef USE_API_HOMEASSISTANT_STATES void process_state_subscriptions_(); #endif @@ -303,25 +309,24 @@ class APIConnection final : public APIServerConnection { APIConnection *conn, uint32_t remaining_size, bool is_single) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); - // Try to use static reference first to avoid allocation - StringRef static_ref = entity->get_object_id_ref_for_api_(); - // Store dynamic string outside the if-else to maintain lifetime - std::string object_id; - if (!static_ref.empty()) { - msg.set_object_id(static_ref); - } else { - // Dynamic case - need to allocate - object_id = entity->get_object_id(); - msg.set_object_id(StringRef(object_id)); + + // API 1.14+ clients compute object_id client-side from the entity name + // For older clients, we must send object_id for backward compatibility + // See: https://github.com/esphome/backlog/issues/76 + // TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then + // Buffer must remain in scope until encode_message_to_buffer is called + char object_id_buf[OBJECT_ID_MAX_LEN]; + if (!conn->client_supports_api_version(1, 14)) { + msg.object_id = entity->get_object_id_to(object_id_buf); } if (entity->has_own_name()) { - msg.set_name(entity->get_name()); + msg.name = entity->get_name(); } // Set common EntityBase properties #ifdef USE_ENTITY_ICON - msg.set_icon(entity->get_icon_ref()); + msg.icon = entity->get_icon_ref(); #endif msg.disabled_by_default = entity->is_disabled_by_default(); msg.entity_category = static_cast(entity->get_entity_category()); @@ -336,16 +341,24 @@ class APIConnection final : public APIServerConnection { inline bool check_voice_assistant_api_connection_() const; #endif + // Get the max batch size based on client API version + // API 1.14+ clients don't receive object_id, so messages are smaller and more fit per batch + // TODO: Remove this method before 2026.7.0 and use MAX_INITIAL_PER_BATCH directly + size_t get_max_batch_size_() const { + return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY; + } + // Helper method to process multiple entities from an iterator in a batch template void process_iterator_batch_(Iterator &iterator) { size_t initial_size = this->deferred_batch_.size(); - while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) { + size_t max_batch = this->get_max_batch_size_(); + while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) { iterator.advance(); } // If the batch is full, process it immediately // Note: iterator.advance() already calls schedule_batch_() via schedule_message_() - if (this->deferred_batch_.size() >= MAX_INITIAL_PER_BATCH) { + if (this->deferred_batch_.size() >= max_batch) { this->process_batch_(); } } @@ -449,6 +462,12 @@ class APIConnection final : public APIServerConnection { static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); #endif +#ifdef USE_WATER_HEATER + static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single); + static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single); +#endif #ifdef USE_EVENT static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single); @@ -483,18 +502,27 @@ class APIConnection final : public APIServerConnection { std::unique_ptr helper_; APIServer *parent_; - // Group 2: Larger objects (must be 4-byte aligned) - // These contain vectors/pointers internally, so putting them early ensures good alignment - InitialStateIterator initial_state_iterator_; - ListEntitiesIterator list_entities_iterator_; + // Group 2: Iterator union (saves ~16 bytes vs separate iterators) + // These iterators are never active simultaneously - list_entities runs to completion + // before initial_state begins, so we use a union with explicit construction/destruction. + enum class ActiveIterator : uint8_t { NONE, LIST_ENTITIES, INITIAL_STATE }; + + union IteratorUnion { + ListEntitiesIterator list_entities; + InitialStateIterator initial_state; + // Constructor/destructor do nothing - use placement new/explicit destructor + IteratorUnion() {} + ~IteratorUnion() {} + } iterator_storage_; + + // Helper methods for iterator lifecycle management + void destroy_active_iterator_(); + void begin_iterator_(ActiveIterator type); #ifdef USE_CAMERA std::unique_ptr image_reader_; #endif - // Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each) - ClientInfo client_info_; - - // Group 4: 4-byte types + // Group 3: 4-byte types uint32_t last_traffic_; #ifdef USE_API_HOMEASSISTANT_STATES int state_subs_at_ = -1; @@ -505,28 +533,9 @@ class APIConnection final : public APIServerConnection { class MessageCreator { public: - // Constructor for function pointer MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; } - - // Constructor for const char * (Event types - no allocation needed) explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; } - // Delete copy operations - MessageCreator should only be moved - MessageCreator(const MessageCreator &other) = delete; - MessageCreator &operator=(const MessageCreator &other) = delete; - - // Move constructor - MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; } - - // Move assignment - MessageCreator &operator=(MessageCreator &&other) noexcept { - if (this != &other) { - data_ = other.data_; - other.data_.function_ptr = nullptr; - } - return *this; - } - // Call operator - uses message_type to determine union type uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const; @@ -535,7 +544,7 @@ class APIConnection final : public APIServerConnection { union Data { MessageCreatorPtr function_ptr; const char *const_char_ptr; - } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before + } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit }; // Generic batching mechanism for both state updates and entity info @@ -548,16 +557,14 @@ class APIConnection final : public APIServerConnection { // Constructor for creating BatchItem BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) - : entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {} + : entity(entity), creator(creator), message_type(message_type), estimated_size(estimated_size) {} }; std::vector items; uint32_t batch_start_time{0}; - DeferredBatch() { - // Pre-allocate capacity for typical batch sizes to avoid reallocation - items.reserve(8); - } + // No pre-allocation - log connections never use batching, and for + // connections that do, buffers are released after initial sync anyway // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); @@ -576,6 +583,15 @@ class APIConnection final : public APIServerConnection { bool empty() const { return items.empty(); } size_t size() const { return items.size(); } const BatchItem &operator[](size_t index) const { return items[index]; } + // Release excess capacity - only releases if items already empty + void release_buffer() { + // Safe to call: batch is processed before release_buffer is called, + // and if any items remain (partial processing), we must not clear them. + // Use swap trick since shrink_to_fit() is non-binding and may be ignored. + if (items.empty()) { + std::vector().swap(items); + } + } }; // DeferredBatch here (16 bytes, 4-byte aligned) @@ -613,7 +629,9 @@ class APIConnection final : public APIServerConnection { // 2-byte types immediately after flags_ (no padding between them) uint16_t client_api_version_major_{0}; uint16_t client_api_version_minor_{0}; - // Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary + // 1-byte type to fill padding + ActiveIterator active_iterator_{ActiveIterator::NONE}; + // Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary uint32_t get_batch_delay_ms_() const; // Message will use 8 more bytes than the minimum size, and typical @@ -709,12 +727,12 @@ class APIConnection final : public APIServerConnection { } // Fall back to scheduled batching - return this->schedule_message_(entity, std::move(creator), message_type, estimated_size); + return this->schedule_message_(entity, creator, message_type, estimated_size); } // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { - this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size); + this->deferred_batch_.add_item(entity, creator, message_type, estimated_size); return this->schedule_batch_(); } @@ -731,6 +749,8 @@ class APIConnection final : public APIServerConnection { return this->schedule_batch_(); } + // Helper function to log client messages with name and peername + void log_client_(int level, const LogString *message); // Helper function to log API errors with errno void log_warning_(const LogString *message, APIError err); // Helper to handle fatal errors with logging diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 20f8fcaf61..dd44fe9e17 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -1,6 +1,5 @@ #include "api_frame_helper.h" #ifdef USE_API -#include "api_connection.h" // For ClientInfo struct #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -13,12 +12,29 @@ namespace esphome::api { static const char *const TAG = "api.frame_helper"; -#define HELPER_LOG(msg, ...) \ - ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) +// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) +static constexpr size_t API_MAX_LOG_BYTES = 168; + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#else +#define HELPER_LOG(msg, ...) ((void) 0) +#endif #ifdef HELPER_LOG_PACKETS -#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) -#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) +#define LOG_PACKET_RECEIVED(buffer) \ + do { \ + char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ + ESP_LOGVV(TAG, "Received frame: %s", \ + format_hex_pretty_to(hex_buf_, (buffer).data(), \ + (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \ + } while (0) +#define LOG_PACKET_SENDING(data, len) \ + do { \ + char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ + ESP_LOGVV(TAG, "Sending raw: %s", \ + format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \ + } while (0) #else #define LOG_PACKET_RECEIVED(buffer) ((void) 0) #define LOG_PACKET_SENDING(data, len) ((void) 0) @@ -229,6 +245,8 @@ APIError APIFrameHelper::init_common_() { HELPER_LOG("Bad state for init %d", (int) state_); return APIError::BAD_STATE; } + // Cache peername now while socket is valid - needed for error logging after socket failure + this->socket_->getpeername_to(this->client_peername_); int err = this->socket_->setblocking(false); if (err != 0) { state_ = State::FAILED; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 9aaada3cf7..27ec1ff915 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -29,25 +29,28 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266 static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms #endif -// Forward declaration -struct ClientInfo; +// Maximum number of messages to batch in a single write operation +// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there) +static constexpr size_t MAX_MESSAGES_PER_BATCH = 34; class ProtoWriteBuffer; +// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars) +static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32; + struct ReadPacketBuffer { - std::vector container; - uint16_t type; - uint16_t data_offset; + const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call) uint16_t data_len; + uint16_t type; }; -// Packed packet info structure to minimize memory usage -struct PacketInfo { +// Packed message info structure to minimize memory usage +struct MessageInfo { uint16_t offset; // Offset in buffer where message starts uint16_t payload_size; // Size of the message payload uint8_t message_type; // Message type (0-255) - PacketInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {} + MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {} }; enum class APIError : uint16_t { @@ -83,16 +86,23 @@ const LogString *api_error_to_logstr(APIError err); class APIFrameHelper { public: APIFrameHelper() = default; - explicit APIFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) - : socket_owned_(std::move(socket)), client_info_(client_info) { - socket_ = socket_owned_.get(); + explicit APIFrameHelper(std::unique_ptr socket) : socket_(std::move(socket)) {} + + // Get client name (null-terminated) + const char *get_client_name() const { return this->client_name_; } + // Get client peername/IP (null-terminated, cached at init time for availability after socket failure) + const char *get_client_peername() const { return this->client_peername_; } + // Set client name from buffer with length (truncates if needed) + void set_client_name(const char *name, size_t len) { + size_t copy_len = std::min(len, sizeof(this->client_name_) - 1); + memcpy(this->client_name_, name, copy_len); + this->client_name_[copy_len] = '\0'; } virtual ~APIFrameHelper() = default; virtual APIError init() = 0; virtual APIError loop(); virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } - std::string getpeername() { return socket_->getpeername(); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } APIError close() { state_ = State::CLOSED; @@ -110,17 +120,48 @@ class APIFrameHelper { } return APIError::OK; } + /// Toggle TCP_NODELAY socket option to control Nagle's algorithm. + /// + /// This is used to allow log messages to coalesce (Nagle enabled) while keeping + /// state updates low-latency (NODELAY enabled). Without this, many small log + /// packets fill the TCP send buffer, crowding out important state updates. + /// + /// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040) + /// this is just a boolean assignment; on other platforms it's a lightweight syscall. + /// + /// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle + /// @return true if successful or already in desired state + bool set_nodelay(bool enable) { + if (this->nodelay_enabled_ == enable) + return true; + int val = enable ? 1 : 0; + int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int)); + if (err == 0) { + this->nodelay_enabled_ = enable; + } + return err == 0; + } virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; - // Write multiple protobuf packets in a single operation - // packets contains (message_type, offset, length) for each message in the buffer + // Write multiple protobuf messages in a single operation + // messages contains (message_type, offset, length) for each message in the buffer // The buffer contains all messages with appropriate padding before each - virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) = 0; + virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) = 0; // Get the frame header padding required by this protocol uint8_t frame_header_padding() const { return frame_header_padding_; } // Get the frame footer size required by this protocol uint8_t frame_footer_size() const { return frame_footer_size_; } // Check if socket has data ready to read bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } + // Release excess memory from internal buffers after initial sync + void release_buffers() { + // rx_buf_: Safe to clear only if no partial read in progress. + // rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame + // and clearing would lose partially received data. + if (this->rx_buf_len_ == 0) { + // Use swap trick since shrink_to_fit() is non-binding and may be ignored + std::vector().swap(this->rx_buf_); + } + } protected: // Buffer containing data to be sent @@ -149,9 +190,8 @@ class APIFrameHelper { APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector &tx_buf, const std::string &info, StateEnum &state, StateEnum failed_state); - // Pointers first (4 bytes each) - socket::Socket *socket_{nullptr}; - std::unique_ptr socket_owned_; + // Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) + std::unique_ptr socket_; // Common state enum for all frame helpers // Note: Not all states are used by all implementations @@ -174,12 +214,12 @@ class APIFrameHelper { // Containers (size varies, but typically 12+ bytes on 32-bit) std::array, API_MAX_SEND_QUEUE> tx_buf_; - std::vector reusable_iovs_; std::vector rx_buf_; - // Pointer to client info (4 bytes on 32-bit) - // Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance. - const ClientInfo *client_info_{nullptr}; + // Client name buffer - stores name from Hello message or initial peername + char client_name_[CLIENT_INFO_NAME_MAX_LEN]{}; + // Cached peername/IP address - captured at init time for availability after socket failure + char client_peername_[socket::SOCKADDR_STR_LEN]{}; // Group smaller types together uint16_t rx_buf_len_ = 0; @@ -189,7 +229,10 @@ class APIFrameHelper { uint8_t tx_buf_head_{0}; uint8_t tx_buf_tail_{0}; uint8_t tx_buf_count_{0}; - // 8 bytes total, 0 bytes padding + // Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true + // since init_common_() enables NODELAY. Used by set_nodelay() to allow log + // messages to coalesce while keeping state updates low-latency. + bool nodelay_enabled_{true}; // Common initialization for both plaintext and noise protocols APIError init_common_(); diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 633b07a7fa..21b0463dfe 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -24,12 +24,29 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit"; #endif static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") -#define HELPER_LOG(msg, ...) \ - ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) +// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) +static constexpr size_t API_MAX_LOG_BYTES = 168; + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#else +#define HELPER_LOG(msg, ...) ((void) 0) +#endif #ifdef HELPER_LOG_PACKETS -#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) -#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) +#define LOG_PACKET_RECEIVED(buffer) \ + do { \ + char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ + ESP_LOGVV(TAG, "Received frame: %s", \ + format_hex_pretty_to(hex_buf_, (buffer).data(), \ + (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \ + } while (0) +#define LOG_PACKET_SENDING(data, len) \ + do { \ + char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ + ESP_LOGVV(TAG, "Sending raw: %s", \ + format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \ + } while (0) #else #define LOG_PACKET_RECEIVED(buffer) ((void) 0) #define LOG_PACKET_SENDING(data, len) ((void) 0) @@ -239,12 +256,13 @@ APIError APINoiseFrameHelper::state_action_() { } if (state_ == State::SERVER_HELLO) { // send server hello + constexpr size_t mac_len = 13; // 12 hex chars + null terminator const std::string &name = App.get_name(); - const std::string &mac = get_mac_address(); + char mac[mac_len]; + get_mac_address_into_buffer(mac); // Calculate positions and sizes size_t name_len = name.size() + 1; // including null terminator - size_t mac_len = mac.size() + 1; // including null terminator size_t name_offset = 1; size_t mac_offset = name_offset + name_len; size_t total_size = 1 + name_len + mac_len; @@ -257,7 +275,7 @@ APIError APINoiseFrameHelper::state_action_() { // node name, terminated by null byte std::memcpy(msg.get() + name_offset, name.c_str(), name_len); // node mac, terminated by null byte - std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len); + std::memcpy(msg.get() + mac_offset, mac, mac_len); aerr = write_frame_(msg.get(), total_size); if (aerr != APIError::OK) @@ -406,8 +424,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::BAD_DATA_PACKET; } - buffer->container = std::move(this->rx_buf_); - buffer->data_offset = 4; + buffer->data = msg_data + 4; // Skip 4-byte header (type + length) buffer->data_len = data_len; buffer->type = type; return APIError::OK; @@ -415,12 +432,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { // Resize to include MAC space (required for Noise encryption) buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); - PacketInfo packet{type, 0, - static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; - return write_protobuf_packets(buffer, std::span(&packet, 1)); + MessageInfo msg{type, 0, + static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; + return write_protobuf_messages(buffer, std::span(&msg, 1)); } -APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { +APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) { APIError aerr = state_action_(); if (aerr != APIError::OK) { return aerr; @@ -430,20 +447,20 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st return APIError::WOULD_BLOCK; } - if (packets.empty()) { + if (messages.empty()) { return APIError::OK; } uint8_t *buffer_data = buffer.get_buffer()->data(); - this->reusable_iovs_.clear(); - this->reusable_iovs_.reserve(packets.size()); + // Stack-allocated iovec array - no heap allocation + StaticVector iovs; uint16_t total_write_len = 0; - // We need to encrypt each packet in place - for (const auto &packet : packets) { + // We need to encrypt each message in place + for (const auto &msg : messages) { // The buffer already has padding at offset - uint8_t *buf_start = buffer_data + packet.offset; + uint8_t *buf_start = buffer_data + msg.offset; // Write noise header buf_start[0] = 0x01; // indicator @@ -451,10 +468,10 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st // Write message header (to be encrypted) const uint8_t msg_offset = 3; - buf_start[msg_offset] = static_cast(packet.message_type >> 8); // type high byte - buf_start[msg_offset + 1] = static_cast(packet.message_type); // type low byte - buf_start[msg_offset + 2] = static_cast(packet.payload_size >> 8); // data_len high byte - buf_start[msg_offset + 3] = static_cast(packet.payload_size); // data_len low byte + buf_start[msg_offset] = static_cast(msg.message_type >> 8); // type high byte + buf_start[msg_offset + 1] = static_cast(msg.message_type); // type low byte + buf_start[msg_offset + 2] = static_cast(msg.payload_size >> 8); // data_len high byte + buf_start[msg_offset + 3] = static_cast(msg.payload_size); // data_len low byte // payload data is already in the buffer starting at offset + 7 // Make sure we have space for MAC @@ -463,8 +480,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st // Encrypt the message in place NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size, - 4 + packet.payload_size + frame_footer_size_); + noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + msg.payload_size, + 4 + msg.payload_size + frame_footer_size_); int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); APIError aerr = @@ -476,14 +493,14 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st buf_start[1] = static_cast(mbuf.size >> 8); buf_start[2] = static_cast(mbuf.size); - // Add iovec for this encrypted packet - size_t packet_len = static_cast(3 + mbuf.size); // indicator + size + encrypted data - this->reusable_iovs_.push_back({buf_start, packet_len}); - total_write_len += packet_len; + // Add iovec for this encrypted message + size_t msg_len = static_cast(3 + mbuf.size); // indicator + size + encrypted data + iovs.push_back({buf_start, msg_len}); + total_write_len += msg_len; } - // Send all encrypted packets in one writev call - return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); + // Send all encrypted messages in one writev call + return this->write_raw_(iovs.data(), iovs.size(), total_write_len); } APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { @@ -527,7 +544,7 @@ APIError APINoiseFrameHelper::init_handshake_() { if (aerr != APIError::OK) return aerr; - const auto &psk = ctx_->get_psk(); + const auto &psk = this->ctx_.get_psk(); err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"), APIError::HANDSHAKESTATE_SETUP_FAILED); @@ -539,7 +556,8 @@ APIError APINoiseFrameHelper::init_handshake_() { if (aerr != APIError::OK) return aerr; // set_prologue copies it into handshakestate, so we can get rid of it now - prologue_ = {}; + // Use swap idiom to actually release memory (= {} only clears size, not capacity) + std::vector().swap(prologue_); err = noise_handshakestate_start(handshake_); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index e3243e4fa5..183b8c8a51 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -9,9 +9,8 @@ namespace esphome::api { class APINoiseFrameHelper final : public APIFrameHelper { public: - APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx, - const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) { + APINoiseFrameHelper(std::unique_ptr socket, APINoiseContext &ctx) + : APIFrameHelper(std::move(socket)), ctx_(ctx) { // Noise header structure: // Pos 0: indicator (0x01) // Pos 1-2: encrypted payload size (16-bit big-endian) @@ -24,7 +23,7 @@ class APINoiseFrameHelper final : public APIFrameHelper { APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; - APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; + APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) override; protected: APIError state_action_(); @@ -41,8 +40,8 @@ class APINoiseFrameHelper final : public APIFrameHelper { NoiseCipherState *send_cipher_{nullptr}; NoiseCipherState *recv_cipher_{nullptr}; - // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) - std::shared_ptr ctx_; + // Reference to noise context (4 bytes on 32-bit) + APINoiseContext &ctx_; // Vector (12 bytes on 32-bit) std::vector prologue_; diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index dcbd35aa32..3dfd683929 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -1,7 +1,6 @@ #include "api_frame_helper_plaintext.h" #ifdef USE_API #ifdef USE_API_PLAINTEXT -#include "api_connection.h" // For ClientInfo struct #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -18,12 +17,29 @@ namespace esphome::api { static const char *const TAG = "api.plaintext"; -#define HELPER_LOG(msg, ...) \ - ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__) +// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512) +static constexpr size_t API_MAX_LOG_BYTES = 168; + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) +#else +#define HELPER_LOG(msg, ...) ((void) 0) +#endif #ifdef HELPER_LOG_PACKETS -#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) -#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str()) +#define LOG_PACKET_RECEIVED(buffer) \ + do { \ + char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ + ESP_LOGVV(TAG, "Received frame: %s", \ + format_hex_pretty_to(hex_buf_, (buffer).data(), \ + (buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \ + } while (0) +#define LOG_PACKET_SENDING(data, len) \ + do { \ + char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \ + ESP_LOGVV(TAG, "Sending raw: %s", \ + format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \ + } while (0) #else #define LOG_PACKET_RECEIVED(buffer) ((void) 0) #define LOG_PACKET_SENDING(data, len) ((void) 0) @@ -210,36 +226,36 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { return aerr; } - buffer->container = std::move(this->rx_buf_); - buffer->data_offset = 0; + buffer->data = this->rx_buf_.data(); buffer->data_len = this->rx_header_parsed_len_; buffer->type = this->rx_header_parsed_type_; return APIError::OK; } APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) { - PacketInfo packet{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_)}; - return write_protobuf_packets(buffer, std::span(&packet, 1)); + MessageInfo msg{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_)}; + return write_protobuf_messages(buffer, std::span(&msg, 1)); } -APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { +APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, + std::span messages) { if (state_ != State::DATA) { return APIError::BAD_STATE; } - if (packets.empty()) { + if (messages.empty()) { return APIError::OK; } uint8_t *buffer_data = buffer.get_buffer()->data(); - this->reusable_iovs_.clear(); - this->reusable_iovs_.reserve(packets.size()); + // Stack-allocated iovec array - no heap allocation + StaticVector iovs; uint16_t total_write_len = 0; - for (const auto &packet : packets) { + for (const auto &msg : messages) { // Calculate varint sizes for header layout - uint8_t size_varint_len = api::ProtoSize::varint(static_cast(packet.payload_size)); - uint8_t type_varint_len = api::ProtoSize::varint(static_cast(packet.message_type)); + uint8_t size_varint_len = api::ProtoSize::varint(static_cast(msg.payload_size)); + uint8_t type_varint_len = api::ProtoSize::varint(static_cast(msg.message_type)); uint8_t total_header_len = 1 + size_varint_len + type_varint_len; // Calculate where to start writing the header @@ -267,25 +283,25 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer // // The message starts at offset + frame_header_padding_ // So we write the header starting at offset + frame_header_padding_ - total_header_len - uint8_t *buf_start = buffer_data + packet.offset; + uint8_t *buf_start = buffer_data + msg.offset; uint32_t header_offset = frame_header_padding_ - total_header_len; // Write the plaintext header buf_start[header_offset] = 0x00; // indicator // Encode varints directly into buffer - ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); - ProtoVarInt(packet.message_type) + ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); + ProtoVarInt(msg.message_type) .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); - // Add iovec for this packet (header + payload) - size_t packet_len = static_cast(total_header_len + packet.payload_size); - this->reusable_iovs_.push_back({buf_start + header_offset, packet_len}); - total_write_len += packet_len; + // Add iovec for this message (header + payload) + size_t msg_len = static_cast(total_header_len + msg.payload_size); + iovs.push_back({buf_start + header_offset, msg_len}); + total_write_len += msg_len; } - // Send all packets in one writev call - return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len); + // Send all messages in one writev call + return write_raw_(iovs.data(), iovs.size(), total_write_len); } } // namespace esphome::api diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index bba981d26b..96d47e9c7b 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -7,8 +7,7 @@ namespace esphome::api { class APIPlaintextFrameHelper final : public APIFrameHelper { public: - APIPlaintextFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) - : APIFrameHelper(std::move(socket), client_info) { + explicit APIPlaintextFrameHelper(std::unique_ptr socket) : APIFrameHelper(std::move(socket)) { // Plaintext header structure (worst case): // Pos 0: indicator (0x00) // Pos 1-3: payload size varint (up to 3 bytes) @@ -21,7 +20,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper { APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; - APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; + APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span messages) override; protected: APIError try_read_frame_(); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 0a073fb662..03a6639b5e 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -23,9 +23,7 @@ bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { - // Use raw data directly to avoid allocation - this->client_info = value.data(); - this->client_info_len = value.size(); + this->client_info = StringRef(reinterpret_cast(value.data()), value.size()); break; } default: @@ -36,71 +34,51 @@ bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) void HelloResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->api_version_major); buffer.encode_uint32(2, this->api_version_minor); - buffer.encode_string(3, this->server_info_ref_); - buffer.encode_string(4, this->name_ref_); + buffer.encode_string(3, this->server_info); + buffer.encode_string(4, this->name); } void HelloResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->api_version_major); size.add_uint32(1, this->api_version_minor); - size.add_length(1, this->server_info_ref_.size()); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->server_info.size()); + size.add_length(1, this->name.size()); } -#ifdef USE_API_PASSWORD -bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { - switch (field_id) { - case 1: { - // Use raw data directly to avoid allocation - this->password = value.data(); - this->password_len = value.size(); - break; - } - default: - return false; - } - return true; -} -void AuthenticationResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); } -void AuthenticationResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->invalid_password); } -#endif #ifdef USE_AREAS void AreaInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->area_id); - buffer.encode_string(2, this->name_ref_); + buffer.encode_string(2, this->name); } void AreaInfo::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->area_id); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); } #endif #ifdef USE_DEVICES void DeviceInfo::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, this->device_id); - buffer.encode_string(2, this->name_ref_); + buffer.encode_string(2, this->name); buffer.encode_uint32(3, this->area_id); } void DeviceInfo::calculate_size(ProtoSize &size) const { size.add_uint32(1, this->device_id); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); size.add_uint32(1, this->area_id); } #endif void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { -#ifdef USE_API_PASSWORD - buffer.encode_bool(1, this->uses_password); -#endif - buffer.encode_string(2, this->name_ref_); - buffer.encode_string(3, this->mac_address_ref_); - buffer.encode_string(4, this->esphome_version_ref_); - buffer.encode_string(5, this->compilation_time_ref_); - buffer.encode_string(6, this->model_ref_); + buffer.encode_string(2, this->name); + buffer.encode_string(3, this->mac_address); + buffer.encode_string(4, this->esphome_version); + buffer.encode_string(5, this->compilation_time); + buffer.encode_string(6, this->model); #ifdef USE_DEEP_SLEEP buffer.encode_bool(7, this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - buffer.encode_string(8, this->project_name_ref_); + buffer.encode_string(8, this->project_name); #endif #ifdef ESPHOME_PROJECT_NAME - buffer.encode_string(9, this->project_version_ref_); + buffer.encode_string(9, this->project_version); #endif #ifdef USE_WEBSERVER buffer.encode_uint32(10, this->webserver_port); @@ -108,28 +86,28 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #ifdef USE_BLUETOOTH_PROXY buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags); #endif - buffer.encode_string(12, this->manufacturer_ref_); - buffer.encode_string(13, this->friendly_name_ref_); + buffer.encode_string(12, this->manufacturer); + buffer.encode_string(13, this->friendly_name); #ifdef USE_VOICE_ASSISTANT buffer.encode_uint32(17, this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - buffer.encode_string(16, this->suggested_area_ref_); + buffer.encode_string(16, this->suggested_area); #endif #ifdef USE_BLUETOOTH_PROXY - buffer.encode_string(18, this->bluetooth_mac_address_ref_); + buffer.encode_string(18, this->bluetooth_mac_address); #endif #ifdef USE_API_NOISE buffer.encode_bool(19, this->api_encryption_supported); #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - buffer.encode_message(20, it, true); + buffer.encode_message(20, it); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - buffer.encode_message(21, it, true); + buffer.encode_message(21, it); } #endif #ifdef USE_AREAS @@ -143,22 +121,19 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { #endif } void DeviceInfoResponse::calculate_size(ProtoSize &size) const { -#ifdef USE_API_PASSWORD - size.add_bool(1, this->uses_password); -#endif - size.add_length(1, this->name_ref_.size()); - size.add_length(1, this->mac_address_ref_.size()); - size.add_length(1, this->esphome_version_ref_.size()); - size.add_length(1, this->compilation_time_ref_.size()); - size.add_length(1, this->model_ref_.size()); + size.add_length(1, this->name.size()); + size.add_length(1, this->mac_address.size()); + size.add_length(1, this->esphome_version.size()); + size.add_length(1, this->compilation_time.size()); + size.add_length(1, this->model.size()); #ifdef USE_DEEP_SLEEP size.add_bool(1, this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - size.add_length(1, this->project_name_ref_.size()); + size.add_length(1, this->project_name.size()); #endif #ifdef ESPHOME_PROJECT_NAME - size.add_length(1, this->project_version_ref_.size()); + size.add_length(1, this->project_version.size()); #endif #ifdef USE_WEBSERVER size.add_uint32(1, this->webserver_port); @@ -166,16 +141,16 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const { #ifdef USE_BLUETOOTH_PROXY size.add_uint32(1, this->bluetooth_proxy_feature_flags); #endif - size.add_length(1, this->manufacturer_ref_.size()); - size.add_length(1, this->friendly_name_ref_.size()); + size.add_length(1, this->manufacturer.size()); + size.add_length(1, this->friendly_name.size()); #ifdef USE_VOICE_ASSISTANT size.add_uint32(2, this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - size.add_length(2, this->suggested_area_ref_.size()); + size.add_length(2, this->suggested_area.size()); #endif #ifdef USE_BLUETOOTH_PROXY - size.add_length(2, this->bluetooth_mac_address_ref_.size()); + size.add_length(2, this->bluetooth_mac_address.size()); #endif #ifdef USE_API_NOISE size.add_bool(2, this->api_encryption_supported); @@ -202,14 +177,14 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const { } #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); - buffer.encode_string(5, this->device_class_ref_); + buffer.encode_string(3, this->name); + buffer.encode_string(5, this->device_class); buffer.encode_bool(6, this->is_status_binary_sensor); buffer.encode_bool(7, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(8, this->icon_ref_); + buffer.encode_string(8, this->icon); #endif buffer.encode_uint32(9, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -217,14 +192,14 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesBinarySensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); - size.add_length(1, this->device_class_ref_.size()); + size.add_length(1, this->name.size()); + size.add_length(1, this->device_class.size()); size.add_bool(1, this->is_status_binary_sensor); size.add_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -250,16 +225,16 @@ void BinarySensorStateResponse::calculate_size(ProtoSize &size) const { #endif #ifdef USE_COVER void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); buffer.encode_bool(5, this->assumed_state); buffer.encode_bool(6, this->supports_position); buffer.encode_bool(7, this->supports_tilt); - buffer.encode_string(8, this->device_class_ref_); + buffer.encode_string(8, this->device_class); buffer.encode_bool(9, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(10, this->icon_ref_); + buffer.encode_string(10, this->icon); #endif buffer.encode_uint32(11, static_cast(this->entity_category)); buffer.encode_bool(12, this->supports_stop); @@ -268,16 +243,16 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesCoverResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); size.add_bool(1, this->assumed_state); size.add_bool(1, this->supports_position); size.add_bool(1, this->supports_tilt); - size.add_length(1, this->device_class_ref_.size()); + size.add_length(1, this->device_class.size()); size.add_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_uint32(1, static_cast(this->entity_category)); size.add_bool(1, this->supports_stop); @@ -343,16 +318,16 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_FAN void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); buffer.encode_bool(7, this->supports_direction); buffer.encode_int32(8, this->supported_speed_count); buffer.encode_bool(9, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(10, this->icon_ref_); + buffer.encode_string(10, this->icon); #endif buffer.encode_uint32(11, static_cast(this->entity_category)); for (const char *it : *this->supported_preset_modes) { @@ -363,16 +338,16 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); size.add_bool(1, this->supports_oscillation); size.add_bool(1, this->supports_speed); size.add_bool(1, this->supports_direction); size.add_int32(1, this->supported_speed_count); size.add_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_uint32(1, static_cast(this->entity_category)); if (!this->supported_preset_modes->empty()) { @@ -390,7 +365,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->oscillating); buffer.encode_uint32(5, static_cast(this->direction)); buffer.encode_int32(6, this->speed_level); - buffer.encode_string(7, this->preset_mode_ref_); + buffer.encode_string(7, this->preset_mode); #ifdef USE_DEVICES buffer.encode_uint32(8, this->device_id); #endif @@ -401,7 +376,7 @@ void FanStateResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->oscillating); size.add_uint32(1, static_cast(this->direction)); size.add_int32(1, this->speed_level); - size.add_length(1, this->preset_mode_ref_.size()); + size.add_length(1, this->preset_mode.size()); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); #endif @@ -447,9 +422,10 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 13: - this->preset_mode = value.as_string(); + case 13: { + this->preset_mode = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -468,20 +444,20 @@ bool FanCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_LIGHT void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); for (const auto &it : *this->supported_color_modes) { buffer.encode_uint32(12, static_cast(it), true); } buffer.encode_float(9, this->min_mireds); buffer.encode_float(10, this->max_mireds); - for (auto &it : this->effects) { - buffer.encode_string(11, it, true); + for (const char *it : *this->effects) { + buffer.encode_string(11, it, strlen(it), true); } buffer.encode_bool(13, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(14, this->icon_ref_); + buffer.encode_string(14, this->icon); #endif buffer.encode_uint32(15, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -489,9 +465,9 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); if (!this->supported_color_modes->empty()) { for (const auto &it : *this->supported_color_modes) { size.add_uint32_force(1, static_cast(it)); @@ -499,14 +475,14 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const { } size.add_float(1, this->min_mireds); size.add_float(1, this->max_mireds); - if (!this->effects.empty()) { - for (const auto &it : this->effects) { - size.add_length_force(1, it.size()); + if (!this->effects->empty()) { + for (const char *it : *this->effects) { + size.add_length_force(1, strlen(it)); } } size.add_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -526,7 +502,7 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(8, this->color_temperature); buffer.encode_float(12, this->cold_white); buffer.encode_float(13, this->warm_white); - buffer.encode_string(9, this->effect_ref_); + buffer.encode_string(9, this->effect); #ifdef USE_DEVICES buffer.encode_uint32(14, this->device_id); #endif @@ -544,7 +520,7 @@ void LightStateResponse::calculate_size(ProtoSize &size) const { size.add_float(1, this->color_temperature); size.add_float(1, this->cold_white); size.add_float(1, this->warm_white); - size.add_length(1, this->effect_ref_.size()); + size.add_length(1, this->effect.size()); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); #endif @@ -611,9 +587,10 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 19: - this->effect = value.as_string(); + case 19: { + this->effect = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -659,16 +636,16 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SENSOR void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif - buffer.encode_string(6, this->unit_of_measurement_ref_); + buffer.encode_string(6, this->unit_of_measurement); buffer.encode_int32(7, this->accuracy_decimals); buffer.encode_bool(8, this->force_update); - buffer.encode_string(9, this->device_class_ref_); + buffer.encode_string(9, this->device_class); buffer.encode_uint32(10, static_cast(this->state_class)); buffer.encode_bool(12, this->disabled_by_default); buffer.encode_uint32(13, static_cast(this->entity_category)); @@ -677,16 +654,16 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesSensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif - size.add_length(1, this->unit_of_measurement_ref_.size()); + size.add_length(1, this->unit_of_measurement.size()); size.add_int32(1, this->accuracy_decimals); size.add_bool(1, this->force_update); - size.add_length(1, this->device_class_ref_.size()); + size.add_length(1, this->device_class.size()); size.add_uint32(1, static_cast(this->state_class)); size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); @@ -713,31 +690,31 @@ void SensorStateResponse::calculate_size(ProtoSize &size) const { #endif #ifdef USE_SWITCH void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->assumed_state); buffer.encode_bool(7, this->disabled_by_default); buffer.encode_uint32(8, static_cast(this->entity_category)); - buffer.encode_string(9, this->device_class_ref_); + buffer.encode_string(9, this->device_class); #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); #endif } void ListEntitiesSwitchResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->assumed_state); size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class_ref_.size()); + size.add_length(1, this->device_class.size()); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); #endif @@ -784,36 +761,36 @@ bool SwitchCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_TEXT_SENSOR void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class_ref_); + buffer.encode_string(8, this->device_class); #ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); #endif } void ListEntitiesTextSensorResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class_ref_.size()); + size.add_length(1, this->device_class.size()); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); #endif } void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_string(2, this->state_ref_); + buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); @@ -821,7 +798,7 @@ void TextSensorStateResponse::encode(ProtoWriteBuffer buffer) const { } void TextSensorStateResponse::calculate_size(ProtoSize &size) const { size.add_fixed32(1, this->key); - size.add_length(1, this->state_ref_.size()); + size.add_length(1, this->state.size()); size.add_bool(1, this->missing_state); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); @@ -852,9 +829,11 @@ void SubscribeLogsResponse::calculate_size(ProtoSize &size) const { #ifdef USE_API_NOISE bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->key = value.as_string(); + case 1: { + this->key = value.data(); + this->key_len = value.size(); break; + } default: return false; } @@ -865,23 +844,23 @@ void NoiseEncryptionSetKeyResponse::calculate_size(ProtoSize &size) const { size #endif #ifdef USE_API_HOMEASSISTANT_SERVICES void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->key_ref_); + buffer.encode_string(1, this->key); buffer.encode_string(2, this->value); } void HomeassistantServiceMap::calculate_size(ProtoSize &size) const { - size.add_length(1, this->key_ref_.size()); + size.add_length(1, this->key.size()); size.add_length(1, this->value.size()); } void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->service_ref_); + buffer.encode_string(1, this->service); for (auto &it : this->data) { - buffer.encode_message(2, it, true); + buffer.encode_message(2, it); } for (auto &it : this->data_template) { - buffer.encode_message(3, it, true); + buffer.encode_message(3, it); } for (auto &it : this->variables) { - buffer.encode_message(4, it, true); + buffer.encode_message(4, it); } buffer.encode_bool(5, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -895,7 +874,7 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { #endif } void HomeassistantActionRequest::calculate_size(ProtoSize &size) const { - size.add_length(1, this->service_ref_.size()); + size.add_length(1, this->service.size()); size.add_repeated_message(1, this->data); size.add_repeated_message(1, this->data_template); size.add_repeated_message(1, this->variables); @@ -927,12 +906,12 @@ bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt v } bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 3: - this->error_message = value.as_string(); + case 3: { + this->error_message = StringRef(reinterpret_cast(value.data()), value.size()); break; + } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON case 4: { - // Use raw data directly to avoid allocation this->response_data = value.data(); this->response_data_len = value.size(); break; @@ -946,26 +925,29 @@ bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDe #endif #ifdef USE_API_HOMEASSISTANT_STATES void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->entity_id_ref_); - buffer.encode_string(2, this->attribute_ref_); + buffer.encode_string(1, this->entity_id); + buffer.encode_string(2, this->attribute); buffer.encode_bool(3, this->once); } void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->entity_id_ref_.size()); - size.add_length(1, this->attribute_ref_.size()); + size.add_length(1, this->entity_id.size()); + size.add_length(1, this->attribute.size()); size.add_bool(1, this->once); } bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->entity_id = value.as_string(); + case 1: { + this->entity_id = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 2: - this->state = value.as_string(); + } + case 2: { + this->state = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 3: - this->attribute = value.as_string(); + } + case 3: { + this->attribute = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -975,9 +957,7 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { - // Use raw data directly to avoid allocation - this->timezone = value.data(); - this->timezone_len = value.size(); + this->timezone = StringRef(reinterpret_cast(value.data()), value.size()); break; } default: @@ -995,26 +975,28 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } return true; } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->name_ref_); + buffer.encode_string(1, this->name); buffer.encode_uint32(2, static_cast(this->type)); } void ListEntitiesServicesArgument::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); size.add_uint32(1, static_cast(this->type)); } void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->name_ref_); + buffer.encode_string(1, this->name); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { - buffer.encode_message(3, it, true); + buffer.encode_message(3, it); } + buffer.encode_uint32(4, static_cast(this->supports_response)); } void ListEntitiesServicesResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); size.add_fixed32(1, this->key); size.add_repeated_message(1, this->args); + size.add_uint32(1, static_cast(this->supports_response)); } bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -1040,9 +1022,10 @@ bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) } bool ExecuteServiceArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: - this->string_ = value.as_string(); + case 4: { + this->string_ = StringRef(reinterpret_cast(value.data()), value.size()); break; + } case 9: this->string_array.push_back(value.as_string()); break; @@ -1075,6 +1058,23 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) { this->string_array.init(count_string_array); ProtoDecodableMessage::decode(buffer, length); } +bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + case 3: + this->call_id = value.as_uint32(); + break; +#endif +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + case 4: + this->return_response = value.as_bool(); + break; +#endif + default: + return false; + } + return true; +} bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: @@ -1102,14 +1102,32 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) { ProtoDecodableMessage::decode(buffer, length); } #endif +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES +void ExecuteServiceResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->call_id); + buffer.encode_bool(2, this->success); + buffer.encode_string(3, this->error_message); +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON + buffer.encode_bytes(4, this->response_data, this->response_data_len); +#endif +} +void ExecuteServiceResponse::calculate_size(ProtoSize &size) const { + size.add_uint32(1, this->call_id); + size.add_bool(1, this->success); + size.add_length(1, this->error_message.size()); +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON + size.add_length(1, this->response_data_len); +#endif +} +#endif #ifdef USE_CAMERA void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); buffer.encode_bool(5, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(6, this->icon_ref_); + buffer.encode_string(6, this->icon); #endif buffer.encode_uint32(7, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -1117,12 +1135,12 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesCameraResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); size.add_bool(1, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_uint32(1, static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -1161,9 +1179,9 @@ bool CameraImageRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { #endif #ifdef USE_CLIMATE void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); buffer.encode_bool(5, this->supports_current_temperature); buffer.encode_bool(6, this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { @@ -1190,7 +1208,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { } buffer.encode_bool(18, this->disabled_by_default); #ifdef USE_ENTITY_ICON - buffer.encode_string(19, this->icon_ref_); + buffer.encode_string(19, this->icon); #endif buffer.encode_uint32(20, static_cast(this->entity_category)); buffer.encode_float(21, this->visual_current_temperature_step); @@ -1204,9 +1222,9 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(27, this->feature_flags); } void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); size.add_bool(1, this->supports_current_temperature); size.add_bool(1, this->supports_two_point_target_temperature); if (!this->supported_modes->empty()) { @@ -1245,7 +1263,7 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { } size.add_bool(2, this->disabled_by_default); #ifdef USE_ENTITY_ICON - size.add_length(2, this->icon_ref_.size()); + size.add_length(2, this->icon.size()); #endif size.add_uint32(2, static_cast(this->entity_category)); size.add_float(2, this->visual_current_temperature_step); @@ -1268,9 +1286,9 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(8, static_cast(this->action)); buffer.encode_uint32(9, static_cast(this->fan_mode)); buffer.encode_uint32(10, static_cast(this->swing_mode)); - buffer.encode_string(11, this->custom_fan_mode_ref_); + buffer.encode_string(11, this->custom_fan_mode); buffer.encode_uint32(12, static_cast(this->preset)); - buffer.encode_string(13, this->custom_preset_ref_); + buffer.encode_string(13, this->custom_preset); buffer.encode_float(14, this->current_humidity); buffer.encode_float(15, this->target_humidity); #ifdef USE_DEVICES @@ -1287,9 +1305,9 @@ void ClimateStateResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, static_cast(this->action)); size.add_uint32(1, static_cast(this->fan_mode)); size.add_uint32(1, static_cast(this->swing_mode)); - size.add_length(1, this->custom_fan_mode_ref_.size()); + size.add_length(1, this->custom_fan_mode.size()); size.add_uint32(1, static_cast(this->preset)); - size.add_length(1, this->custom_preset_ref_.size()); + size.add_length(1, this->custom_preset.size()); size.add_float(1, this->current_humidity); size.add_float(1, this->target_humidity); #ifdef USE_DEVICES @@ -1352,12 +1370,14 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) } bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 17: - this->custom_fan_mode = value.as_string(); + case 17: { + this->custom_fan_mode = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 21: - this->custom_preset = value.as_string(); + } + case 21: { + this->custom_preset = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -1386,41 +1406,149 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { return true; } #endif +#ifdef USE_WATER_HEATER +void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); +#ifdef USE_ENTITY_ICON + buffer.encode_string(4, this->icon); +#endif + buffer.encode_bool(5, this->disabled_by_default); + buffer.encode_uint32(6, static_cast(this->entity_category)); +#ifdef USE_DEVICES + buffer.encode_uint32(7, this->device_id); +#endif + buffer.encode_float(8, this->min_temperature); + buffer.encode_float(9, this->max_temperature); + buffer.encode_float(10, this->target_temperature_step); + for (const auto &it : *this->supported_modes) { + buffer.encode_uint32(11, static_cast(it), true); + } + buffer.encode_uint32(12, this->supported_features); +} +void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif + size.add_float(1, this->min_temperature); + size.add_float(1, this->max_temperature); + size.add_float(1, this->target_temperature_step); + if (!this->supported_modes->empty()) { + for (const auto &it : *this->supported_modes) { + size.add_uint32_force(1, static_cast(it)); + } + } + size.add_uint32(1, this->supported_features); +} +void WaterHeaterStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_float(2, this->current_temperature); + buffer.encode_float(3, this->target_temperature); + buffer.encode_uint32(4, static_cast(this->mode)); +#ifdef USE_DEVICES + buffer.encode_uint32(5, this->device_id); +#endif + buffer.encode_uint32(6, this->state); + buffer.encode_float(7, this->target_temperature_low); + buffer.encode_float(8, this->target_temperature_high); +} +void WaterHeaterStateResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->key); + size.add_float(1, this->current_temperature); + size.add_float(1, this->target_temperature); + size.add_uint32(1, static_cast(this->mode)); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif + size.add_uint32(1, this->state); + size.add_float(1, this->target_temperature_low); + size.add_float(1, this->target_temperature_high); +} +bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: + this->has_fields = value.as_uint32(); + break; + case 3: + this->mode = static_cast(value.as_uint32()); + break; +#ifdef USE_DEVICES + case 5: + this->device_id = value.as_uint32(); + break; +#endif + case 6: + this->state = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: + this->key = value.as_fixed32(); + break; + case 4: + this->target_temperature = value.as_float(); + break; + case 7: + this->target_temperature_low = value.as_float(); + break; + case 8: + this->target_temperature_high = value.as_float(); + break; + default: + return false; + } + return true; +} +#endif #ifdef USE_NUMBER void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_float(6, this->min_value); buffer.encode_float(7, this->max_value); buffer.encode_float(8, this->step); buffer.encode_bool(9, this->disabled_by_default); buffer.encode_uint32(10, static_cast(this->entity_category)); - buffer.encode_string(11, this->unit_of_measurement_ref_); + buffer.encode_string(11, this->unit_of_measurement); buffer.encode_uint32(12, static_cast(this->mode)); - buffer.encode_string(13, this->device_class_ref_); + buffer.encode_string(13, this->device_class); #ifdef USE_DEVICES buffer.encode_uint32(14, this->device_id); #endif } void ListEntitiesNumberResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_float(1, this->min_value); size.add_float(1, this->max_value); size.add_float(1, this->step); size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->unit_of_measurement_ref_.size()); + size.add_length(1, this->unit_of_measurement.size()); size.add_uint32(1, static_cast(this->mode)); - size.add_length(1, this->device_class_ref_.size()); + size.add_length(1, this->device_class.size()); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); #endif @@ -1469,11 +1597,11 @@ bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SELECT void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif for (const char *it : *this->options) { buffer.encode_string(6, it, strlen(it), true); @@ -1485,11 +1613,11 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif if (!this->options->empty()) { for (const char *it : *this->options) { @@ -1504,7 +1632,7 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { } void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_string(2, this->state_ref_); + buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); @@ -1512,7 +1640,7 @@ void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { } void SelectStateResponse::calculate_size(ProtoSize &size) const { size.add_fixed32(1, this->key); - size.add_length(1, this->state_ref_.size()); + size.add_length(1, this->state.size()); size.add_bool(1, this->missing_state); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); @@ -1532,9 +1660,10 @@ bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: - this->state = value.as_string(); + case 2: { + this->state = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -1553,15 +1682,15 @@ bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_SIREN void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); - for (auto &it : this->tones) { - buffer.encode_string(7, it, true); + for (const char *it : *this->tones) { + buffer.encode_string(7, it, strlen(it), true); } buffer.encode_bool(8, this->supports_duration); buffer.encode_bool(9, this->supports_volume); @@ -1571,16 +1700,16 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesSirenResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); - if (!this->tones.empty()) { - for (const auto &it : this->tones) { - size.add_length_force(1, it.size()); + if (!this->tones->empty()) { + for (const char *it : *this->tones) { + size.add_length_force(1, strlen(it)); } } size.add_bool(1, this->supports_duration); @@ -1636,9 +1765,10 @@ bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } bool SirenCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 5: - this->tone = value.as_string(); + case 5: { + this->tone = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -1660,35 +1790,35 @@ bool SirenCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_LOCK void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_bool(8, this->assumed_state); buffer.encode_bool(9, this->supports_open); buffer.encode_bool(10, this->requires_code); - buffer.encode_string(11, this->code_format_ref_); + buffer.encode_string(11, this->code_format); #ifdef USE_DEVICES buffer.encode_uint32(12, this->device_id); #endif } void ListEntitiesLockResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); size.add_bool(1, this->assumed_state); size.add_bool(1, this->supports_open); size.add_bool(1, this->requires_code); - size.add_length(1, this->code_format_ref_.size()); + size.add_length(1, this->code_format.size()); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); #endif @@ -1727,9 +1857,10 @@ bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } bool LockCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 4: - this->code = value.as_string(); + case 4: { + this->code = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -1748,29 +1879,29 @@ bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_BUTTON void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class_ref_); + buffer.encode_string(8, this->device_class); #ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); #endif } void ListEntitiesButtonResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class_ref_.size()); + size.add_length(1, this->device_class.size()); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); #endif @@ -1800,31 +1931,31 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_MEDIA_PLAYER void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->format_ref_); + buffer.encode_string(1, this->format); buffer.encode_uint32(2, this->sample_rate); buffer.encode_uint32(3, this->num_channels); buffer.encode_uint32(4, static_cast(this->purpose)); buffer.encode_uint32(5, this->sample_bytes); } void MediaPlayerSupportedFormat::calculate_size(ProtoSize &size) const { - size.add_length(1, this->format_ref_.size()); + size.add_length(1, this->format.size()); size.add_uint32(1, this->sample_rate); size.add_uint32(1, this->num_channels); size.add_uint32(1, static_cast(this->purpose)); size.add_uint32(1, this->sample_bytes); } void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_bool(8, this->supports_pause); for (auto &it : this->supported_formats) { - buffer.encode_message(9, it, true); + buffer.encode_message(9, it); } #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); @@ -1832,11 +1963,11 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(11, this->feature_flags); } void ListEntitiesMediaPlayerResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); @@ -1897,9 +2028,10 @@ bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt val } bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 7: - this->media_url = value.as_string(); + case 7: { + this->media_url = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -1944,7 +2076,7 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { - buffer.encode_message(1, this->advertisements[i], true); + buffer.encode_message(1, this->advertisements[i]); } } void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { @@ -2017,7 +2149,7 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->properties); for (auto &it : this->descriptors) { - buffer.encode_message(4, it, true); + buffer.encode_message(4, it); } buffer.encode_uint32(5, this->short_uuid); } @@ -2038,7 +2170,7 @@ void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const { } buffer.encode_uint32(2, this->handle); for (auto &it : this->characteristics) { - buffer.encode_message(3, it, true); + buffer.encode_message(3, it); } buffer.encode_uint32(4, this->short_uuid); } @@ -2054,7 +2186,7 @@ void BluetoothGATTService::calculate_size(ProtoSize &size) const { void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint64(1, this->address); for (auto &it : this->services) { - buffer.encode_message(2, it, true); + buffer.encode_message(2, it); } } void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { @@ -2107,7 +2239,6 @@ bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt val bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 4: { - // Use raw data directly to avoid allocation this->data = value.data(); this->data_len = value.size(); break; @@ -2146,7 +2277,6 @@ bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, Proto bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 3: { - // Use raw data directly to avoid allocation this->data = value.data(); this->data_len = value.size(); break; @@ -2303,17 +2433,17 @@ void VoiceAssistantAudioSettings::calculate_size(ProtoSize &size) const { } void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->start); - buffer.encode_string(2, this->conversation_id_ref_); + buffer.encode_string(2, this->conversation_id); buffer.encode_uint32(3, this->flags); buffer.encode_message(4, this->audio_settings); - buffer.encode_string(5, this->wake_word_phrase_ref_); + buffer.encode_string(5, this->wake_word_phrase); } void VoiceAssistantRequest::calculate_size(ProtoSize &size) const { size.add_bool(1, this->start); - size.add_length(1, this->conversation_id_ref_.size()); + size.add_length(1, this->conversation_id.size()); size.add_uint32(1, this->flags); size.add_message_object(1, this->audio_settings); - size.add_length(1, this->wake_word_phrase_ref_.size()); + size.add_length(1, this->wake_word_phrase.size()); } bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { @@ -2330,12 +2460,14 @@ bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value) } bool VoiceAssistantEventData::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->name = value.as_string(); + case 1: { + this->name = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 2: - this->value = value.as_string(); + } + case 2: { + this->value = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2374,20 +2506,22 @@ bool VoiceAssistantAudio::decode_varint(uint32_t field_id, ProtoVarInt value) { } bool VoiceAssistantAudio::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->data = value.as_string(); + case 1: { + this->data = value.data(); + this->data_len = value.size(); break; + } default: return false; } return true; } void VoiceAssistantAudio::encode(ProtoWriteBuffer buffer) const { - buffer.encode_bytes(1, this->data_ptr_, this->data_len_); + buffer.encode_bytes(1, this->data, this->data_len); buffer.encode_bool(2, this->end); } void VoiceAssistantAudio::calculate_size(ProtoSize &size) const { - size.add_length(1, this->data_len_); + size.add_length(1, this->data_len); size.add_bool(1, this->end); } bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -2411,12 +2545,14 @@ bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVar } bool VoiceAssistantTimerEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: - this->timer_id = value.as_string(); + case 2: { + this->timer_id = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 3: - this->name = value.as_string(); + } + case 3: { + this->name = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2434,15 +2570,18 @@ bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt } bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->media_id = value.as_string(); + case 1: { + this->media_id = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 2: - this->text = value.as_string(); + } + case 2: { + this->text = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 3: - this->preannounce_media_id = value.as_string(); + } + case 3: { + this->preannounce_media_id = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2451,15 +2590,15 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } void VoiceAssistantAnnounceFinished::calculate_size(ProtoSize &size) const { size.add_bool(1, this->success); } void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->id_ref_); - buffer.encode_string(2, this->wake_word_ref_); + buffer.encode_string(1, this->id); + buffer.encode_string(2, this->wake_word); for (auto &it : this->trained_languages) { buffer.encode_string(3, it, true); } } void VoiceAssistantWakeWord::calculate_size(ProtoSize &size) const { - size.add_length(1, this->id_ref_.size()); - size.add_length(1, this->wake_word_ref_.size()); + size.add_length(1, this->id.size()); + size.add_length(1, this->wake_word.size()); if (!this->trained_languages.empty()) { for (const auto &it : this->trained_languages) { size.add_length_force(1, it.size()); @@ -2478,24 +2617,29 @@ bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarIn } bool VoiceAssistantExternalWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 1: - this->id = value.as_string(); + case 1: { + this->id = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 2: - this->wake_word = value.as_string(); + } + case 2: { + this->wake_word = StringRef(reinterpret_cast(value.data()), value.size()); break; + } case 3: this->trained_languages.push_back(value.as_string()); break; - case 4: - this->model_type = value.as_string(); + case 4: { + this->model_type = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 6: - this->model_hash = value.as_string(); + } + case 6: { + this->model_hash = StringRef(reinterpret_cast(value.data()), value.size()); break; - case 7: - this->url = value.as_string(); + } + case 7: { + this->url = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2514,7 +2658,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { for (auto &it : this->available_wake_words) { - buffer.encode_message(1, it, true); + buffer.encode_message(1, it); } for (const auto &it : *this->active_wake_words) { buffer.encode_string(2, it, true); @@ -2543,11 +2687,11 @@ bool VoiceAssistantSetConfiguration::decode_length(uint32_t field_id, ProtoLengt #endif #ifdef USE_ALARM_CONTROL_PANEL void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); @@ -2559,11 +2703,11 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) cons #endif } void ListEntitiesAlarmControlPanelResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); @@ -2605,9 +2749,10 @@ bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarI } bool AlarmControlPanelCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 3: - this->code = value.as_string(); + case 3: { + this->code = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2626,34 +2771,34 @@ bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit #endif #ifdef USE_TEXT void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_uint32(8, this->min_length); buffer.encode_uint32(9, this->max_length); - buffer.encode_string(10, this->pattern_ref_); + buffer.encode_string(10, this->pattern); buffer.encode_uint32(11, static_cast(this->mode)); #ifdef USE_DEVICES buffer.encode_uint32(12, this->device_id); #endif } void ListEntitiesTextResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); size.add_uint32(1, this->min_length); size.add_uint32(1, this->max_length); - size.add_length(1, this->pattern_ref_.size()); + size.add_length(1, this->pattern.size()); size.add_uint32(1, static_cast(this->mode)); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); @@ -2661,7 +2806,7 @@ void ListEntitiesTextResponse::calculate_size(ProtoSize &size) const { } void TextStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_string(2, this->state_ref_); + buffer.encode_string(2, this->state); buffer.encode_bool(3, this->missing_state); #ifdef USE_DEVICES buffer.encode_uint32(4, this->device_id); @@ -2669,7 +2814,7 @@ void TextStateResponse::encode(ProtoWriteBuffer buffer) const { } void TextStateResponse::calculate_size(ProtoSize &size) const { size.add_fixed32(1, this->key); - size.add_length(1, this->state_ref_.size()); + size.add_length(1, this->state.size()); size.add_bool(1, this->missing_state); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); @@ -2689,9 +2834,10 @@ bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { } bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { - case 2: - this->state = value.as_string(); + case 2: { + this->state = StringRef(reinterpret_cast(value.data()), value.size()); break; + } default: return false; } @@ -2710,11 +2856,11 @@ bool TextCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_DATE void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); @@ -2723,11 +2869,11 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesDateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); @@ -2789,11 +2935,11 @@ bool DateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_TIME void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); @@ -2802,11 +2948,11 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesTimeResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); @@ -2868,15 +3014,15 @@ bool TimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_EVENT void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class_ref_); + buffer.encode_string(8, this->device_class); for (const char *it : *this->event_types) { buffer.encode_string(9, it, strlen(it), true); } @@ -2885,15 +3031,15 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class_ref_.size()); + size.add_length(1, this->device_class.size()); if (!this->event_types->empty()) { for (const char *it : *this->event_types) { size.add_length_force(1, strlen(it)); @@ -2905,14 +3051,14 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { } void EventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_string(2, this->event_type_ref_); + buffer.encode_string(2, this->event_type); #ifdef USE_DEVICES buffer.encode_uint32(3, this->device_id); #endif } void EventResponse::calculate_size(ProtoSize &size) const { size.add_fixed32(1, this->key); - size.add_length(1, this->event_type_ref_.size()); + size.add_length(1, this->event_type.size()); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); #endif @@ -2920,15 +3066,15 @@ void EventResponse::calculate_size(ProtoSize &size) const { #endif #ifdef USE_VALVE void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class_ref_); + buffer.encode_string(8, this->device_class); buffer.encode_bool(9, this->assumed_state); buffer.encode_bool(10, this->supports_position); buffer.encode_bool(11, this->supports_stop); @@ -2937,15 +3083,15 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesValveResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class_ref_.size()); + size.add_length(1, this->device_class.size()); size.add_bool(1, this->assumed_state); size.add_bool(1, this->supports_position); size.add_bool(1, this->supports_stop); @@ -3003,11 +3149,11 @@ bool ValveCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_DATETIME_DATETIME void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); @@ -3016,11 +3162,11 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const { #endif } void ListEntitiesDateTimeResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); @@ -3072,29 +3218,29 @@ bool DateTimeCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { #endif #ifdef USE_UPDATE void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const { - buffer.encode_string(1, this->object_id_ref_); + buffer.encode_string(1, this->object_id); buffer.encode_fixed32(2, this->key); - buffer.encode_string(3, this->name_ref_); + buffer.encode_string(3, this->name); #ifdef USE_ENTITY_ICON - buffer.encode_string(5, this->icon_ref_); + buffer.encode_string(5, this->icon); #endif buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); - buffer.encode_string(8, this->device_class_ref_); + buffer.encode_string(8, this->device_class); #ifdef USE_DEVICES buffer.encode_uint32(9, this->device_id); #endif } void ListEntitiesUpdateResponse::calculate_size(ProtoSize &size) const { - size.add_length(1, this->object_id_ref_.size()); + size.add_length(1, this->object_id.size()); size.add_fixed32(1, this->key); - size.add_length(1, this->name_ref_.size()); + size.add_length(1, this->name.size()); #ifdef USE_ENTITY_ICON - size.add_length(1, this->icon_ref_.size()); + size.add_length(1, this->icon.size()); #endif size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); - size.add_length(1, this->device_class_ref_.size()); + size.add_length(1, this->device_class.size()); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); #endif @@ -3105,11 +3251,11 @@ void UpdateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->in_progress); buffer.encode_bool(4, this->has_progress); buffer.encode_float(5, this->progress); - buffer.encode_string(6, this->current_version_ref_); - buffer.encode_string(7, this->latest_version_ref_); - buffer.encode_string(8, this->title_ref_); - buffer.encode_string(9, this->release_summary_ref_); - buffer.encode_string(10, this->release_url_ref_); + buffer.encode_string(6, this->current_version); + buffer.encode_string(7, this->latest_version); + buffer.encode_string(8, this->title); + buffer.encode_string(9, this->release_summary); + buffer.encode_string(10, this->release_url); #ifdef USE_DEVICES buffer.encode_uint32(11, this->device_id); #endif @@ -3120,11 +3266,11 @@ void UpdateStateResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->in_progress); size.add_bool(1, this->has_progress); size.add_float(1, this->progress); - size.add_length(1, this->current_version_ref_.size()); - size.add_length(1, this->latest_version_ref_.size()); - size.add_length(1, this->title_ref_.size()); - size.add_length(1, this->release_summary_ref_.size()); - size.add_length(1, this->release_url_ref_.size()); + size.add_length(1, this->current_version.size()); + size.add_length(1, this->latest_version.size()); + size.add_length(1, this->title.size()); + size.add_length(1, this->release_summary.size()); + size.add_length(1, this->release_url.size()); #ifdef USE_DEVICES size.add_uint32(1, this->device_id); #endif @@ -3159,7 +3305,6 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { - // Use raw data directly to avoid allocation this->data = value.data(); this->data_len = value.size(); break; @@ -3184,7 +3329,6 @@ bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 2: { - // Use raw data directly to avoid allocation this->data = value.data(); this->data_len = value.size(); break; @@ -3200,7 +3344,7 @@ void ZWaveProxyRequest::encode(ProtoWriteBuffer buffer) const { } void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { size.add_uint32(1, static_cast(this->type)); - size.add_length(2, this->data_len); + size.add_length(1, this->data_len); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 358049026e..01fe44d7c7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -51,6 +51,7 @@ enum SensorStateClass : uint32_t { STATE_CLASS_MEASUREMENT = 1, STATE_CLASS_TOTAL_INCREASING = 2, STATE_CLASS_TOTAL = 3, + STATE_CLASS_MEASUREMENT_ANGLE = 4, }; #endif enum LogLevel : uint32_t { @@ -63,7 +64,7 @@ enum LogLevel : uint32_t { LOG_LEVEL_VERBOSE = 6, LOG_LEVEL_VERY_VERBOSE = 7, }; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_BOOL = 0, SERVICE_ARG_TYPE_INT = 1, @@ -74,6 +75,12 @@ enum ServiceArgType : uint32_t { SERVICE_ARG_TYPE_FLOAT_ARRAY = 6, SERVICE_ARG_TYPE_STRING_ARRAY = 7, }; +enum SupportsResponseType : uint32_t { + SUPPORTS_RESPONSE_NONE = 0, + SUPPORTS_RESPONSE_OPTIONAL = 1, + SUPPORTS_RESPONSE_ONLY = 2, + SUPPORTS_RESPONSE_STATUS = 100, +}; #endif #ifdef USE_CLIMATE enum ClimateMode : uint32_t { @@ -122,6 +129,25 @@ enum ClimatePreset : uint32_t { CLIMATE_PRESET_ACTIVITY = 7, }; #endif +#ifdef USE_WATER_HEATER +enum WaterHeaterMode : uint32_t { + WATER_HEATER_MODE_OFF = 0, + WATER_HEATER_MODE_ECO = 1, + WATER_HEATER_MODE_ELECTRIC = 2, + WATER_HEATER_MODE_PERFORMANCE = 3, + WATER_HEATER_MODE_HIGH_DEMAND = 4, + WATER_HEATER_MODE_HEAT_PUMP = 5, + WATER_HEATER_MODE_GAS = 6, +}; +#endif +enum WaterHeaterCommandHasField : uint32_t { + WATER_HEATER_COMMAND_HAS_NONE = 0, + WATER_HEATER_COMMAND_HAS_MODE = 1, + WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2, + WATER_HEATER_COMMAND_HAS_STATE = 4, + WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8, + WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16, +}; #ifdef USE_NUMBER enum NumberMode : uint32_t { NUMBER_MODE_AUTO = 0, @@ -289,15 +315,12 @@ enum ZWaveProxyRequestType : uint32_t { class InfoResponseProtoMessage : public ProtoMessage { public: ~InfoResponseProtoMessage() override = default; - StringRef object_id_ref_{}; - void set_object_id(const StringRef &ref) { this->object_id_ref_ = ref; } + StringRef object_id{}; uint32_t key{0}; - StringRef name_ref_{}; - void set_name(const StringRef &ref) { this->name_ref_ = ref; } + StringRef name{}; bool disabled_by_default{false}; #ifdef USE_ENTITY_ICON - StringRef icon_ref_{}; - void set_icon(const StringRef &ref) { this->icon_ref_ = ref; } + StringRef icon{}; #endif enums::EntityCategory entity_category{}; #ifdef USE_DEVICES @@ -331,12 +354,11 @@ class CommandProtoMessage : public ProtoDecodableMessage { class HelloRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 1; - static constexpr uint8_t ESTIMATED_SIZE = 27; + static constexpr uint8_t ESTIMATED_SIZE = 17; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "hello_request"; } #endif - const uint8_t *client_info{nullptr}; - uint16_t client_info_len{0}; + StringRef client_info{}; uint32_t api_version_major{0}; uint32_t api_version_minor{0}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -356,10 +378,8 @@ class HelloResponse final : public ProtoMessage { #endif uint32_t api_version_major{0}; uint32_t api_version_minor{0}; - StringRef server_info_ref_{}; - void set_server_info(const StringRef &ref) { this->server_info_ref_ = ref; } - StringRef name_ref_{}; - void set_name(const StringRef &ref) { this->name_ref_ = ref; } + StringRef server_info{}; + StringRef name{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -368,40 +388,6 @@ class HelloResponse final : public ProtoMessage { protected: }; -#ifdef USE_API_PASSWORD -class AuthenticationRequest final : public ProtoDecodableMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 3; - static constexpr uint8_t ESTIMATED_SIZE = 19; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "authentication_request"; } -#endif - const uint8_t *password{nullptr}; - uint16_t password_len{0}; -#ifdef HAS_PROTO_MESSAGE_DUMP - void dump_to(std::string &out) const override; -#endif - - protected: - bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; -}; -class AuthenticationResponse final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 4; - static constexpr uint8_t ESTIMATED_SIZE = 2; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "authentication_response"; } -#endif - bool invalid_password{false}; - void encode(ProtoWriteBuffer buffer) const override; - void calculate_size(ProtoSize &size) const override; -#ifdef HAS_PROTO_MESSAGE_DUMP - void dump_to(std::string &out) const override; -#endif - - protected: -}; -#endif class DisconnectRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 5; @@ -471,8 +457,7 @@ class DeviceInfoRequest final : public ProtoMessage { class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; - StringRef name_ref_{}; - void set_name(const StringRef &ref) { this->name_ref_ = ref; } + StringRef name{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -486,8 +471,7 @@ class AreaInfo final : public ProtoMessage { class DeviceInfo final : public ProtoMessage { public: uint32_t device_id{0}; - StringRef name_ref_{}; - void set_name(const StringRef &ref) { this->name_ref_ = ref; } + StringRef name{}; uint32_t area_id{0}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -501,33 +485,23 @@ class DeviceInfo final : public ProtoMessage { class DeviceInfoResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint16_t ESTIMATED_SIZE = 257; + static constexpr uint8_t ESTIMATED_SIZE = 255; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif -#ifdef USE_API_PASSWORD - bool uses_password{false}; -#endif - StringRef name_ref_{}; - void set_name(const StringRef &ref) { this->name_ref_ = ref; } - StringRef mac_address_ref_{}; - void set_mac_address(const StringRef &ref) { this->mac_address_ref_ = ref; } - StringRef esphome_version_ref_{}; - void set_esphome_version(const StringRef &ref) { this->esphome_version_ref_ = ref; } - StringRef compilation_time_ref_{}; - void set_compilation_time(const StringRef &ref) { this->compilation_time_ref_ = ref; } - StringRef model_ref_{}; - void set_model(const StringRef &ref) { this->model_ref_ = ref; } + StringRef name{}; + StringRef mac_address{}; + StringRef esphome_version{}; + StringRef compilation_time{}; + StringRef model{}; #ifdef USE_DEEP_SLEEP bool has_deep_sleep{false}; #endif #ifdef ESPHOME_PROJECT_NAME - StringRef project_name_ref_{}; - void set_project_name(const StringRef &ref) { this->project_name_ref_ = ref; } + StringRef project_name{}; #endif #ifdef ESPHOME_PROJECT_NAME - StringRef project_version_ref_{}; - void set_project_version(const StringRef &ref) { this->project_version_ref_ = ref; } + StringRef project_version{}; #endif #ifdef USE_WEBSERVER uint32_t webserver_port{0}; @@ -535,20 +509,16 @@ class DeviceInfoResponse final : public ProtoMessage { #ifdef USE_BLUETOOTH_PROXY uint32_t bluetooth_proxy_feature_flags{0}; #endif - StringRef manufacturer_ref_{}; - void set_manufacturer(const StringRef &ref) { this->manufacturer_ref_ = ref; } - StringRef friendly_name_ref_{}; - void set_friendly_name(const StringRef &ref) { this->friendly_name_ref_ = ref; } + StringRef manufacturer{}; + StringRef friendly_name{}; #ifdef USE_VOICE_ASSISTANT uint32_t voice_assistant_feature_flags{0}; #endif #ifdef USE_AREAS - StringRef suggested_area_ref_{}; - void set_suggested_area(const StringRef &ref) { this->suggested_area_ref_ = ref; } + StringRef suggested_area{}; #endif #ifdef USE_BLUETOOTH_PROXY - StringRef bluetooth_mac_address_ref_{}; - void set_bluetooth_mac_address(const StringRef &ref) { this->bluetooth_mac_address_ref_ = ref; } + StringRef bluetooth_mac_address{}; #endif #ifdef USE_API_NOISE bool api_encryption_supported{false}; @@ -623,8 +593,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_binary_sensor_response"; } #endif - StringRef device_class_ref_{}; - void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } + StringRef device_class{}; bool is_status_binary_sensor{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -663,8 +632,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_position{false}; bool supports_tilt{false}; - StringRef device_class_ref_{}; - void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } + StringRef device_class{}; bool supports_stop{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -745,8 +713,7 @@ class FanStateResponse final : public StateResponseProtoMessage { bool oscillating{false}; enums::FanDirection direction{}; int32_t speed_level{0}; - StringRef preset_mode_ref_{}; - void set_preset_mode(const StringRef &ref) { this->preset_mode_ref_ = ref; } + StringRef preset_mode{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -771,7 +738,7 @@ class FanCommandRequest final : public CommandProtoMessage { bool has_speed_level{false}; int32_t speed_level{0}; bool has_preset_mode{false}; - std::string preset_mode{}; + StringRef preset_mode{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -793,7 +760,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage { const light::ColorModeMask *supported_color_modes{}; float min_mireds{0.0f}; float max_mireds{0.0f}; - std::vector effects{}; + const FixedVector *effects{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -820,8 +787,7 @@ class LightStateResponse final : public StateResponseProtoMessage { float color_temperature{0.0f}; float cold_white{0.0f}; float warm_white{0.0f}; - StringRef effect_ref_{}; - void set_effect(const StringRef &ref) { this->effect_ref_ = ref; } + StringRef effect{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -862,7 +828,7 @@ class LightCommandRequest final : public CommandProtoMessage { bool has_flash_length{false}; uint32_t flash_length{0}; bool has_effect{false}; - std::string effect{}; + StringRef effect{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -881,12 +847,10 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_sensor_response"; } #endif - StringRef unit_of_measurement_ref_{}; - void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; } + StringRef unit_of_measurement{}; int32_t accuracy_decimals{0}; bool force_update{false}; - StringRef device_class_ref_{}; - void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } + StringRef device_class{}; enums::SensorStateClass state_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -923,8 +887,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { const char *message_name() const override { return "list_entities_switch_response"; } #endif bool assumed_state{false}; - StringRef device_class_ref_{}; - void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } + StringRef device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -974,8 +937,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_text_sensor_response"; } #endif - StringRef device_class_ref_{}; - void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } + StringRef device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -991,8 +953,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "text_sensor_state_response"; } #endif - StringRef state_ref_{}; - void set_state(const StringRef &ref) { this->state_ref_ = ref; } + StringRef state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -1022,7 +983,7 @@ class SubscribeLogsRequest final : public ProtoDecodableMessage { class SubscribeLogsResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 29; - static constexpr uint8_t ESTIMATED_SIZE = 11; + static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_logs_response"; } #endif @@ -1045,11 +1006,12 @@ class SubscribeLogsResponse final : public ProtoMessage { class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 124; - static constexpr uint8_t ESTIMATED_SIZE = 9; + static constexpr uint8_t ESTIMATED_SIZE = 19; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "noise_encryption_set_key_request"; } #endif - std::string key{}; + const uint8_t *key{nullptr}; + uint16_t key_len{0}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1090,8 +1052,7 @@ class SubscribeHomeassistantServicesRequest final : public ProtoMessage { }; class HomeassistantServiceMap final : public ProtoMessage { public: - StringRef key_ref_{}; - void set_key(const StringRef &ref) { this->key_ref_ = ref; } + StringRef key{}; std::string value{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -1108,8 +1069,7 @@ class HomeassistantActionRequest final : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "homeassistant_action_request"; } #endif - StringRef service_ref_{}; - void set_service(const StringRef &ref) { this->service_ref_ = ref; } + StringRef service{}; FixedVector data{}; FixedVector data_template{}; FixedVector variables{}; @@ -1142,7 +1102,7 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage { #endif uint32_t call_id{0}; bool success{false}; - std::string error_message{}; + StringRef error_message{}; #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON const uint8_t *response_data{nullptr}; uint16_t response_data_len{0}; @@ -1177,10 +1137,8 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "subscribe_home_assistant_state_response"; } #endif - StringRef entity_id_ref_{}; - void set_entity_id(const StringRef &ref) { this->entity_id_ref_ = ref; } - StringRef attribute_ref_{}; - void set_attribute(const StringRef &ref) { this->attribute_ref_ = ref; } + StringRef entity_id{}; + StringRef attribute{}; bool once{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -1197,9 +1155,9 @@ class HomeAssistantStateResponse final : public ProtoDecodableMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "home_assistant_state_response"; } #endif - std::string entity_id{}; - std::string state{}; - std::string attribute{}; + StringRef entity_id{}; + StringRef state{}; + StringRef attribute{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1224,13 +1182,12 @@ class GetTimeRequest final : public ProtoMessage { class GetTimeResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; - static constexpr uint8_t ESTIMATED_SIZE = 24; + static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "get_time_response"; } #endif uint32_t epoch_seconds{0}; - const uint8_t *timezone{nullptr}; - uint16_t timezone_len{0}; + StringRef timezone{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1239,11 +1196,10 @@ class GetTimeResponse final : public ProtoDecodableMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS class ListEntitiesServicesArgument final : public ProtoMessage { public: - StringRef name_ref_{}; - void set_name(const StringRef &ref) { this->name_ref_ = ref; } + StringRef name{}; enums::ServiceArgType type{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -1256,14 +1212,14 @@ class ListEntitiesServicesArgument final : public ProtoMessage { class ListEntitiesServicesResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 41; - static constexpr uint8_t ESTIMATED_SIZE = 48; + static constexpr uint8_t ESTIMATED_SIZE = 50; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_services_response"; } #endif - StringRef name_ref_{}; - void set_name(const StringRef &ref) { this->name_ref_ = ref; } + StringRef name{}; uint32_t key{0}; FixedVector args{}; + enums::SupportsResponseType supports_response{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1277,7 +1233,7 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage { bool bool_{false}; int32_t legacy_int{0}; float float_{0.0f}; - std::string string_{}; + StringRef string_{}; int32_t int_{0}; FixedVector bool_array{}; FixedVector int_array{}; @@ -1296,12 +1252,18 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage { class ExecuteServiceRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 42; - static constexpr uint8_t ESTIMATED_SIZE = 39; + static constexpr uint8_t ESTIMATED_SIZE = 45; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "execute_service_request"; } #endif uint32_t key{0}; FixedVector args{}; +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + uint32_t call_id{0}; +#endif +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + bool return_response{false}; +#endif void decode(const uint8_t *buffer, size_t length) override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1310,6 +1272,31 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +#endif +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES +class ExecuteServiceResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 131; + static constexpr uint8_t ESTIMATED_SIZE = 34; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "execute_service_response"; } +#endif + uint32_t call_id{0}; + bool success{false}; + StringRef error_message{}; +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON + const uint8_t *response_data{nullptr}; + uint16_t response_data_len{0}; +#endif + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(ProtoSize &size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: }; #endif #ifdef USE_CAMERA @@ -1331,7 +1318,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { class CameraImageResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 44; - static constexpr uint8_t ESTIMATED_SIZE = 20; + static constexpr uint8_t ESTIMATED_SIZE = 30; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "camera_image_response"; } #endif @@ -1416,11 +1403,9 @@ class ClimateStateResponse final : public StateResponseProtoMessage { enums::ClimateAction action{}; enums::ClimateFanMode fan_mode{}; enums::ClimateSwingMode swing_mode{}; - StringRef custom_fan_mode_ref_{}; - void set_custom_fan_mode(const StringRef &ref) { this->custom_fan_mode_ref_ = ref; } + StringRef custom_fan_mode{}; enums::ClimatePreset preset{}; - StringRef custom_preset_ref_{}; - void set_custom_preset(const StringRef &ref) { this->custom_preset_ref_ = ref; } + StringRef custom_preset{}; float current_humidity{0.0f}; float target_humidity{0.0f}; void encode(ProtoWriteBuffer buffer) const override; @@ -1451,11 +1436,11 @@ class ClimateCommandRequest final : public CommandProtoMessage { bool has_swing_mode{false}; enums::ClimateSwingMode swing_mode{}; bool has_custom_fan_mode{false}; - std::string custom_fan_mode{}; + StringRef custom_fan_mode{}; bool has_preset{false}; enums::ClimatePreset preset{}; bool has_custom_preset{false}; - std::string custom_preset{}; + StringRef custom_preset{}; bool has_target_humidity{false}; float target_humidity{0.0f}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1468,6 +1453,70 @@ class ClimateCommandRequest final : public CommandProtoMessage { bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif +#ifdef USE_WATER_HEATER +class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 132; + static constexpr uint8_t ESTIMATED_SIZE = 63; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "list_entities_water_heater_response"; } +#endif + float min_temperature{0.0f}; + float max_temperature{0.0f}; + float target_temperature_step{0.0f}; + const water_heater::WaterHeaterModeMask *supported_modes{}; + uint32_t supported_features{0}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(ProtoSize &size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: +}; +class WaterHeaterStateResponse final : public StateResponseProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 133; + static constexpr uint8_t ESTIMATED_SIZE = 35; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "water_heater_state_response"; } +#endif + float current_temperature{0.0f}; + float target_temperature{0.0f}; + enums::WaterHeaterMode mode{}; + uint32_t state{0}; + float target_temperature_low{0.0f}; + float target_temperature_high{0.0f}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(ProtoSize &size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: +}; +class WaterHeaterCommandRequest final : public CommandProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 134; + static constexpr uint8_t ESTIMATED_SIZE = 34; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "water_heater_command_request"; } +#endif + uint32_t has_fields{0}; + enums::WaterHeaterMode mode{}; + float target_temperature{0.0f}; + uint32_t state{0}; + float target_temperature_low{0.0f}; + float target_temperature_high{0.0f}; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +#endif #ifdef USE_NUMBER class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { public: @@ -1479,11 +1528,9 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { float min_value{0.0f}; float max_value{0.0f}; float step{0.0f}; - StringRef unit_of_measurement_ref_{}; - void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; } + StringRef unit_of_measurement{}; enums::NumberMode mode{}; - StringRef device_class_ref_{}; - void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } + StringRef device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1550,8 +1597,7 @@ class SelectStateResponse final : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "select_state_response"; } #endif - StringRef state_ref_{}; - void set_state(const StringRef &ref) { this->state_ref_ = ref; } + StringRef state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -1568,7 +1614,7 @@ class SelectCommandRequest final : public CommandProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "select_command_request"; } #endif - std::string state{}; + StringRef state{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1587,7 +1633,7 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_siren_response"; } #endif - std::vector tones{}; + const FixedVector *tones{}; bool supports_duration{false}; bool supports_volume{false}; void encode(ProtoWriteBuffer buffer) const override; @@ -1624,7 +1670,7 @@ class SirenCommandRequest final : public CommandProtoMessage { bool has_state{false}; bool state{false}; bool has_tone{false}; - std::string tone{}; + StringRef tone{}; bool has_duration{false}; uint32_t duration{0}; bool has_volume{false}; @@ -1650,8 +1696,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage { bool assumed_state{false}; bool supports_open{false}; bool requires_code{false}; - StringRef code_format_ref_{}; - void set_code_format(const StringRef &ref) { this->code_format_ref_ = ref; } + StringRef code_format{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1685,7 +1730,7 @@ class LockCommandRequest final : public CommandProtoMessage { #endif enums::LockCommand command{}; bool has_code{false}; - std::string code{}; + StringRef code{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -1704,8 +1749,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_button_response"; } #endif - StringRef device_class_ref_{}; - void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } + StringRef device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1733,8 +1777,7 @@ class ButtonCommandRequest final : public CommandProtoMessage { #ifdef USE_MEDIA_PLAYER class MediaPlayerSupportedFormat final : public ProtoMessage { public: - StringRef format_ref_{}; - void set_format(const StringRef &ref) { this->format_ref_ = ref; } + StringRef format{}; uint32_t sample_rate{0}; uint32_t num_channels{0}; enums::MediaPlayerFormatPurpose purpose{}; @@ -1795,7 +1838,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage { bool has_volume{false}; float volume{0.0f}; bool has_media_url{false}; - std::string media_url{}; + StringRef media_url{}; bool has_announcement{false}; bool announcement{false}; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2002,7 +2045,7 @@ class BluetoothGATTReadRequest final : public ProtoDecodableMessage { class BluetoothGATTReadResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 74; - static constexpr uint8_t ESTIMATED_SIZE = 17; + static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_read_response"; } #endif @@ -2097,7 +2140,7 @@ class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage { class BluetoothGATTNotifyDataResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 79; - static constexpr uint8_t ESTIMATED_SIZE = 17; + static constexpr uint8_t ESTIMATED_SIZE = 27; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_gatt_notify_data_response"; } #endif @@ -2339,12 +2382,10 @@ class VoiceAssistantRequest final : public ProtoMessage { const char *message_name() const override { return "voice_assistant_request"; } #endif bool start{false}; - StringRef conversation_id_ref_{}; - void set_conversation_id(const StringRef &ref) { this->conversation_id_ref_ = ref; } + StringRef conversation_id{}; uint32_t flags{0}; VoiceAssistantAudioSettings audio_settings{}; - StringRef wake_word_phrase_ref_{}; - void set_wake_word_phrase(const StringRef &ref) { this->wake_word_phrase_ref_ = ref; } + StringRef wake_word_phrase{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2371,8 +2412,8 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage { }; class VoiceAssistantEventData final : public ProtoDecodableMessage { public: - std::string name{}; - std::string value{}; + StringRef name{}; + StringRef value{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2400,17 +2441,12 @@ class VoiceAssistantEventResponse final : public ProtoDecodableMessage { class VoiceAssistantAudio final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 106; - static constexpr uint8_t ESTIMATED_SIZE = 11; + static constexpr uint8_t ESTIMATED_SIZE = 21; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_audio"; } #endif - std::string data{}; - const uint8_t *data_ptr_{nullptr}; - size_t data_len_{0}; - void set_data(const uint8_t *data, size_t len) { - this->data_ptr_ = data; - this->data_len_ = len; - } + const uint8_t *data{nullptr}; + uint16_t data_len{0}; bool end{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -2430,8 +2466,8 @@ class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { const char *message_name() const override { return "voice_assistant_timer_event_response"; } #endif enums::VoiceAssistantTimerEvent event_type{}; - std::string timer_id{}; - std::string name{}; + StringRef timer_id{}; + StringRef name{}; uint32_t total_seconds{0}; uint32_t seconds_left{0}; bool is_active{false}; @@ -2450,9 +2486,9 @@ class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "voice_assistant_announce_request"; } #endif - std::string media_id{}; - std::string text{}; - std::string preannounce_media_id{}; + StringRef media_id{}; + StringRef text{}; + StringRef preannounce_media_id{}; bool start_conversation{false}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -2480,10 +2516,8 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage { }; class VoiceAssistantWakeWord final : public ProtoMessage { public: - StringRef id_ref_{}; - void set_id(const StringRef &ref) { this->id_ref_ = ref; } - StringRef wake_word_ref_{}; - void set_wake_word(const StringRef &ref) { this->wake_word_ref_ = ref; } + StringRef id{}; + StringRef wake_word{}; std::vector trained_languages{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -2495,13 +2529,13 @@ class VoiceAssistantWakeWord final : public ProtoMessage { }; class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage { public: - std::string id{}; - std::string wake_word{}; + StringRef id{}; + StringRef wake_word{}; std::vector trained_languages{}; - std::string model_type{}; + StringRef model_type{}; uint32_t model_size{0}; - std::string model_hash{}; - std::string url{}; + StringRef model_hash{}; + StringRef url{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2602,7 +2636,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage { const char *message_name() const override { return "alarm_control_panel_command_request"; } #endif enums::AlarmControlPanelStateCommand command{}; - std::string code{}; + StringRef code{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2623,8 +2657,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage { #endif uint32_t min_length{0}; uint32_t max_length{0}; - StringRef pattern_ref_{}; - void set_pattern(const StringRef &ref) { this->pattern_ref_ = ref; } + StringRef pattern{}; enums::TextMode mode{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -2641,8 +2674,7 @@ class TextStateResponse final : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "text_state_response"; } #endif - StringRef state_ref_{}; - void set_state(const StringRef &ref) { this->state_ref_ = ref; } + StringRef state{}; bool missing_state{false}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -2659,7 +2691,7 @@ class TextCommandRequest final : public CommandProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "text_command_request"; } #endif - std::string state{}; + StringRef state{}; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; #endif @@ -2786,8 +2818,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_event_response"; } #endif - StringRef device_class_ref_{}; - void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } + StringRef device_class{}; const FixedVector *event_types{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; @@ -2804,8 +2835,7 @@ class EventResponse final : public StateResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "event_response"; } #endif - StringRef event_type_ref_{}; - void set_event_type(const StringRef &ref) { this->event_type_ref_ = ref; } + StringRef event_type{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2823,8 +2853,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_valve_response"; } #endif - StringRef device_class_ref_{}; - void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } + StringRef device_class{}; bool assumed_state{false}; bool supports_position{false}; bool supports_stop{false}; @@ -2930,8 +2959,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "list_entities_update_response"; } #endif - StringRef device_class_ref_{}; - void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } + StringRef device_class{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -2951,16 +2979,11 @@ class UpdateStateResponse final : public StateResponseProtoMessage { bool in_progress{false}; bool has_progress{false}; float progress{0.0f}; - StringRef current_version_ref_{}; - void set_current_version(const StringRef &ref) { this->current_version_ref_ = ref; } - StringRef latest_version_ref_{}; - void set_latest_version(const StringRef &ref) { this->latest_version_ref_ = ref; } - StringRef title_ref_{}; - void set_title(const StringRef &ref) { this->title_ref_ = ref; } - StringRef release_summary_ref_{}; - void set_release_summary(const StringRef &ref) { this->release_summary_ref_ = ref; } - StringRef release_url_ref_{}; - void set_release_url(const StringRef &ref) { this->release_url_ref_ = ref; } + StringRef current_version{}; + StringRef latest_version{}; + StringRef title{}; + StringRef release_summary{}; + StringRef release_url{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index d9662483bf..160a9a93c9 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -66,7 +66,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%llu", value); + snprintf(buffer, 64, "%" PRIu64, value); append_with_newline(out, buffer); } @@ -179,6 +179,8 @@ template<> const char *proto_enum_to_string(enums::Sens return "STATE_CLASS_TOTAL_INCREASING"; case enums::STATE_CLASS_TOTAL: return "STATE_CLASS_TOTAL"; + case enums::STATE_CLASS_MEASUREMENT_ANGLE: + return "STATE_CLASS_MEASUREMENT_ANGLE"; default: return "UNKNOWN"; } @@ -206,7 +208,7 @@ template<> const char *proto_enum_to_string(enums::LogLevel val return "UNKNOWN"; } } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template<> const char *proto_enum_to_string(enums::ServiceArgType value) { switch (value) { case enums::SERVICE_ARG_TYPE_BOOL: @@ -229,6 +231,20 @@ template<> const char *proto_enum_to_string(enums::Servic return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::SupportsResponseType value) { + switch (value) { + case enums::SUPPORTS_RESPONSE_NONE: + return "SUPPORTS_RESPONSE_NONE"; + case enums::SUPPORTS_RESPONSE_OPTIONAL: + return "SUPPORTS_RESPONSE_OPTIONAL"; + case enums::SUPPORTS_RESPONSE_ONLY: + return "SUPPORTS_RESPONSE_ONLY"; + case enums::SUPPORTS_RESPONSE_STATUS: + return "SUPPORTS_RESPONSE_STATUS"; + default: + return "UNKNOWN"; + } +} #endif #ifdef USE_CLIMATE template<> const char *proto_enum_to_string(enums::ClimateMode value) { @@ -332,6 +348,47 @@ template<> const char *proto_enum_to_string(enums::Climate } } #endif +#ifdef USE_WATER_HEATER +template<> const char *proto_enum_to_string(enums::WaterHeaterMode value) { + switch (value) { + case enums::WATER_HEATER_MODE_OFF: + return "WATER_HEATER_MODE_OFF"; + case enums::WATER_HEATER_MODE_ECO: + return "WATER_HEATER_MODE_ECO"; + case enums::WATER_HEATER_MODE_ELECTRIC: + return "WATER_HEATER_MODE_ELECTRIC"; + case enums::WATER_HEATER_MODE_PERFORMANCE: + return "WATER_HEATER_MODE_PERFORMANCE"; + case enums::WATER_HEATER_MODE_HIGH_DEMAND: + return "WATER_HEATER_MODE_HIGH_DEMAND"; + case enums::WATER_HEATER_MODE_HEAT_PUMP: + return "WATER_HEATER_MODE_HEAT_PUMP"; + case enums::WATER_HEATER_MODE_GAS: + return "WATER_HEATER_MODE_GAS"; + default: + return "UNKNOWN"; + } +} +#endif +template<> +const char *proto_enum_to_string(enums::WaterHeaterCommandHasField value) { + switch (value) { + case enums::WATER_HEATER_COMMAND_HAS_NONE: + return "WATER_HEATER_COMMAND_HAS_NONE"; + case enums::WATER_HEATER_COMMAND_HAS_MODE: + return "WATER_HEATER_COMMAND_HAS_MODE"; + case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE: + return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE"; + case enums::WATER_HEATER_COMMAND_HAS_STATE: + return "WATER_HEATER_COMMAND_HAS_STATE"; + case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW: + return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"; + case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH: + return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"; + default: + return "UNKNOWN"; + } +} #ifdef USE_NUMBER template<> const char *proto_enum_to_string(enums::NumberMode value) { switch (value) { @@ -678,9 +735,7 @@ template<> const char *proto_enum_to_string(enums: void HelloRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HelloRequest"); - out.append(" client_info: "); - out.append(format_hex_pretty(this->client_info, this->client_info_len)); - out.append("\n"); + dump_field(out, "client_info", this->client_info); dump_field(out, "api_version_major", this->api_version_major); dump_field(out, "api_version_minor", this->api_version_minor); } @@ -688,21 +743,9 @@ void HelloResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HelloResponse"); dump_field(out, "api_version_major", this->api_version_major); dump_field(out, "api_version_minor", this->api_version_minor); - dump_field(out, "server_info", this->server_info_ref_); - dump_field(out, "name", this->name_ref_); + dump_field(out, "server_info", this->server_info); + dump_field(out, "name", this->name); } -#ifdef USE_API_PASSWORD -void AuthenticationRequest::dump_to(std::string &out) const { - MessageDumpHelper helper(out, "AuthenticationRequest"); - out.append(" password: "); - out.append(format_hex_pretty(this->password, this->password_len)); - out.append("\n"); -} -void AuthenticationResponse::dump_to(std::string &out) const { - MessageDumpHelper helper(out, "AuthenticationResponse"); - dump_field(out, "invalid_password", this->invalid_password); -} -#endif void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } @@ -712,35 +755,32 @@ void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfo void AreaInfo::dump_to(std::string &out) const { MessageDumpHelper helper(out, "AreaInfo"); dump_field(out, "area_id", this->area_id); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); } #endif #ifdef USE_DEVICES void DeviceInfo::dump_to(std::string &out) const { MessageDumpHelper helper(out, "DeviceInfo"); dump_field(out, "device_id", this->device_id); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); dump_field(out, "area_id", this->area_id); } #endif void DeviceInfoResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "DeviceInfoResponse"); -#ifdef USE_API_PASSWORD - dump_field(out, "uses_password", this->uses_password); -#endif - dump_field(out, "name", this->name_ref_); - dump_field(out, "mac_address", this->mac_address_ref_); - dump_field(out, "esphome_version", this->esphome_version_ref_); - dump_field(out, "compilation_time", this->compilation_time_ref_); - dump_field(out, "model", this->model_ref_); + dump_field(out, "name", this->name); + dump_field(out, "mac_address", this->mac_address); + dump_field(out, "esphome_version", this->esphome_version); + dump_field(out, "compilation_time", this->compilation_time); + dump_field(out, "model", this->model); #ifdef USE_DEEP_SLEEP dump_field(out, "has_deep_sleep", this->has_deep_sleep); #endif #ifdef ESPHOME_PROJECT_NAME - dump_field(out, "project_name", this->project_name_ref_); + dump_field(out, "project_name", this->project_name); #endif #ifdef ESPHOME_PROJECT_NAME - dump_field(out, "project_version", this->project_version_ref_); + dump_field(out, "project_version", this->project_version); #endif #ifdef USE_WEBSERVER dump_field(out, "webserver_port", this->webserver_port); @@ -748,16 +788,16 @@ void DeviceInfoResponse::dump_to(std::string &out) const { #ifdef USE_BLUETOOTH_PROXY dump_field(out, "bluetooth_proxy_feature_flags", this->bluetooth_proxy_feature_flags); #endif - dump_field(out, "manufacturer", this->manufacturer_ref_); - dump_field(out, "friendly_name", this->friendly_name_ref_); + dump_field(out, "manufacturer", this->manufacturer); + dump_field(out, "friendly_name", this->friendly_name); #ifdef USE_VOICE_ASSISTANT dump_field(out, "voice_assistant_feature_flags", this->voice_assistant_feature_flags); #endif #ifdef USE_AREAS - dump_field(out, "suggested_area", this->suggested_area_ref_); + dump_field(out, "suggested_area", this->suggested_area); #endif #ifdef USE_BLUETOOTH_PROXY - dump_field(out, "bluetooth_mac_address", this->bluetooth_mac_address_ref_); + dump_field(out, "bluetooth_mac_address", this->bluetooth_mac_address); #endif #ifdef USE_API_NOISE dump_field(out, "api_encryption_supported", this->api_encryption_supported); @@ -794,14 +834,14 @@ void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("Subsc #ifdef USE_BINARY_SENSOR void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); - dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "name", this->name); + dump_field(out, "device_class", this->device_class); dump_field(out, "is_status_binary_sensor", this->is_status_binary_sensor); dump_field(out, "disabled_by_default", this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "entity_category", static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -821,16 +861,16 @@ void BinarySensorStateResponse::dump_to(std::string &out) const { #ifdef USE_COVER void ListEntitiesCoverResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesCoverResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); dump_field(out, "assumed_state", this->assumed_state); dump_field(out, "supports_position", this->supports_position); dump_field(out, "supports_tilt", this->supports_tilt); - dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "device_class", this->device_class); dump_field(out, "disabled_by_default", this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "entity_category", static_cast(this->entity_category)); dump_field(out, "supports_stop", this->supports_stop); @@ -864,16 +904,16 @@ void CoverCommandRequest::dump_to(std::string &out) const { #ifdef USE_FAN void ListEntitiesFanResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesFanResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); dump_field(out, "supports_oscillation", this->supports_oscillation); dump_field(out, "supports_speed", this->supports_speed); dump_field(out, "supports_direction", this->supports_direction); dump_field(out, "supported_speed_count", this->supported_speed_count); dump_field(out, "disabled_by_default", this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "entity_category", static_cast(this->entity_category)); for (const auto &it : *this->supported_preset_modes) { @@ -890,7 +930,7 @@ void FanStateResponse::dump_to(std::string &out) const { dump_field(out, "oscillating", this->oscillating); dump_field(out, "direction", static_cast(this->direction)); dump_field(out, "speed_level", this->speed_level); - dump_field(out, "preset_mode", this->preset_mode_ref_); + dump_field(out, "preset_mode", this->preset_mode); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif @@ -916,20 +956,20 @@ void FanCommandRequest::dump_to(std::string &out) const { #ifdef USE_LIGHT void ListEntitiesLightResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesLightResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); for (const auto &it : *this->supported_color_modes) { dump_field(out, "supported_color_modes", static_cast(it), 4); } dump_field(out, "min_mireds", this->min_mireds); dump_field(out, "max_mireds", this->max_mireds); - for (const auto &it : this->effects) { + for (const auto &it : *this->effects) { dump_field(out, "effects", it, 4); } dump_field(out, "disabled_by_default", this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "entity_category", static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -950,7 +990,7 @@ void LightStateResponse::dump_to(std::string &out) const { dump_field(out, "color_temperature", this->color_temperature); dump_field(out, "cold_white", this->cold_white); dump_field(out, "warm_white", this->warm_white); - dump_field(out, "effect", this->effect_ref_); + dump_field(out, "effect", this->effect); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif @@ -992,16 +1032,16 @@ void LightCommandRequest::dump_to(std::string &out) const { #ifdef USE_SENSOR void ListEntitiesSensorResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesSensorResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif - dump_field(out, "unit_of_measurement", this->unit_of_measurement_ref_); + dump_field(out, "unit_of_measurement", this->unit_of_measurement); dump_field(out, "accuracy_decimals", this->accuracy_decimals); dump_field(out, "force_update", this->force_update); - dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "device_class", this->device_class); dump_field(out, "state_class", static_cast(this->state_class)); dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); @@ -1022,16 +1062,16 @@ void SensorStateResponse::dump_to(std::string &out) const { #ifdef USE_SWITCH void ListEntitiesSwitchResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesSwitchResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "assumed_state", this->assumed_state); dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "device_class", this->device_class); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif @@ -1056,15 +1096,15 @@ void SwitchCommandRequest::dump_to(std::string &out) const { #ifdef USE_TEXT_SENSOR void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesTextSensorResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "device_class", this->device_class); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif @@ -1072,7 +1112,7 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const { void TextSensorStateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "TextSensorStateResponse"); dump_field(out, "key", this->key); - dump_field(out, "state", this->state_ref_); + dump_field(out, "state", this->state); dump_field(out, "missing_state", this->missing_state); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); @@ -1095,10 +1135,13 @@ void SubscribeLogsResponse::dump_to(std::string &out) const { void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest"); out.append(" key: "); - out.append(format_hex_pretty(reinterpret_cast(this->key.data()), this->key.size())); + out.append(format_hex_pretty(this->key, this->key_len)); out.append("\n"); } -void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); } +void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "NoiseEncryptionSetKeyResponse"); + dump_field(out, "success", this->success); +} #endif #ifdef USE_API_HOMEASSISTANT_SERVICES void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { @@ -1106,12 +1149,12 @@ void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { } void HomeassistantServiceMap::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HomeassistantServiceMap"); - dump_field(out, "key", this->key_ref_); + dump_field(out, "key", this->key); dump_field(out, "value", this->value); } void HomeassistantActionRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "HomeassistantActionRequest"); - dump_field(out, "service", this->service_ref_); + dump_field(out, "service", this->service); for (const auto &it : this->data) { out.append(" data: "); it.dump_to(out); @@ -1158,8 +1201,8 @@ void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { } void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse"); - dump_field(out, "entity_id", this->entity_id_ref_); - dump_field(out, "attribute", this->attribute_ref_); + dump_field(out, "entity_id", this->entity_id); + dump_field(out, "attribute", this->attribute); dump_field(out, "once", this->once); } void HomeAssistantStateResponse::dump_to(std::string &out) const { @@ -1173,25 +1216,24 @@ void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeReques void GetTimeResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "GetTimeResponse"); dump_field(out, "epoch_seconds", this->epoch_seconds); - out.append(" timezone: "); - out.append(format_hex_pretty(this->timezone, this->timezone_len)); - out.append("\n"); + dump_field(out, "timezone", this->timezone); } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void ListEntitiesServicesArgument::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); dump_field(out, "type", static_cast(this->type)); } void ListEntitiesServicesResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesServicesResponse"); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); dump_field(out, "key", this->key); for (const auto &it : this->args) { out.append(" args: "); it.dump_to(out); out.append("\n"); } + dump_field(out, "supports_response", static_cast(this->supports_response)); } void ExecuteServiceArgument::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ExecuteServiceArgument"); @@ -1221,17 +1263,36 @@ void ExecuteServiceRequest::dump_to(std::string &out) const { it.dump_to(out); out.append("\n"); } +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + dump_field(out, "call_id", this->call_id); +#endif +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + dump_field(out, "return_response", this->return_response); +#endif +} +#endif +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES +void ExecuteServiceResponse::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "ExecuteServiceResponse"); + dump_field(out, "call_id", this->call_id); + dump_field(out, "success", this->success); + dump_field(out, "error_message", this->error_message); +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON + out.append(" response_data: "); + out.append(format_hex_pretty(this->response_data, this->response_data_len)); + out.append("\n"); +#endif } #endif #ifdef USE_CAMERA void ListEntitiesCameraResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesCameraResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); dump_field(out, "disabled_by_default", this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "entity_category", static_cast(this->entity_category)); #ifdef USE_DEVICES @@ -1258,9 +1319,9 @@ void CameraImageRequest::dump_to(std::string &out) const { #ifdef USE_CLIMATE void ListEntitiesClimateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesClimateResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); dump_field(out, "supports_current_temperature", this->supports_current_temperature); dump_field(out, "supports_two_point_target_temperature", this->supports_two_point_target_temperature); for (const auto &it : *this->supported_modes) { @@ -1287,7 +1348,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { } dump_field(out, "disabled_by_default", this->disabled_by_default); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "entity_category", static_cast(this->entity_category)); dump_field(out, "visual_current_temperature_step", this->visual_current_temperature_step); @@ -1311,9 +1372,9 @@ void ClimateStateResponse::dump_to(std::string &out) const { dump_field(out, "action", static_cast(this->action)); dump_field(out, "fan_mode", static_cast(this->fan_mode)); dump_field(out, "swing_mode", static_cast(this->swing_mode)); - dump_field(out, "custom_fan_mode", this->custom_fan_mode_ref_); + dump_field(out, "custom_fan_mode", this->custom_fan_mode); dump_field(out, "preset", static_cast(this->preset)); - dump_field(out, "custom_preset", this->custom_preset_ref_); + dump_field(out, "custom_preset", this->custom_preset); dump_field(out, "current_humidity", this->current_humidity); dump_field(out, "target_humidity", this->target_humidity); #ifdef USE_DEVICES @@ -1348,23 +1409,72 @@ void ClimateCommandRequest::dump_to(std::string &out) const { #endif } #endif +#ifdef USE_WATER_HEATER +void ListEntitiesWaterHeaterResponse::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "ListEntitiesWaterHeaterResponse"); + dump_field(out, "object_id", this->object_id); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif + dump_field(out, "min_temperature", this->min_temperature); + dump_field(out, "max_temperature", this->max_temperature); + dump_field(out, "target_temperature_step", this->target_temperature_step); + for (const auto &it : *this->supported_modes) { + dump_field(out, "supported_modes", static_cast(it), 4); + } + dump_field(out, "supported_features", this->supported_features); +} +void WaterHeaterStateResponse::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "WaterHeaterStateResponse"); + dump_field(out, "key", this->key); + dump_field(out, "current_temperature", this->current_temperature); + dump_field(out, "target_temperature", this->target_temperature); + dump_field(out, "mode", static_cast(this->mode)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif + dump_field(out, "state", this->state); + dump_field(out, "target_temperature_low", this->target_temperature_low); + dump_field(out, "target_temperature_high", this->target_temperature_high); +} +void WaterHeaterCommandRequest::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "WaterHeaterCommandRequest"); + dump_field(out, "key", this->key); + dump_field(out, "has_fields", this->has_fields); + dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, "target_temperature", this->target_temperature); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif + dump_field(out, "state", this->state); + dump_field(out, "target_temperature_low", this->target_temperature_low); + dump_field(out, "target_temperature_high", this->target_temperature_high); +} +#endif #ifdef USE_NUMBER void ListEntitiesNumberResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesNumberResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "min_value", this->min_value); dump_field(out, "max_value", this->max_value); dump_field(out, "step", this->step); dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "unit_of_measurement", this->unit_of_measurement_ref_); + dump_field(out, "unit_of_measurement", this->unit_of_measurement); dump_field(out, "mode", static_cast(this->mode)); - dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "device_class", this->device_class); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif @@ -1390,11 +1500,11 @@ void NumberCommandRequest::dump_to(std::string &out) const { #ifdef USE_SELECT void ListEntitiesSelectResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesSelectResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif for (const auto &it : *this->options) { dump_field(out, "options", it, 4); @@ -1408,7 +1518,7 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const { void SelectStateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "SelectStateResponse"); dump_field(out, "key", this->key); - dump_field(out, "state", this->state_ref_); + dump_field(out, "state", this->state); dump_field(out, "missing_state", this->missing_state); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); @@ -1426,14 +1536,14 @@ void SelectCommandRequest::dump_to(std::string &out) const { #ifdef USE_SIREN void ListEntitiesSirenResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesSirenResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); - for (const auto &it : this->tones) { + for (const auto &it : *this->tones) { dump_field(out, "tones", it, 4); } dump_field(out, "supports_duration", this->supports_duration); @@ -1470,18 +1580,18 @@ void SirenCommandRequest::dump_to(std::string &out) const { #ifdef USE_LOCK void ListEntitiesLockResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesLockResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); dump_field(out, "assumed_state", this->assumed_state); dump_field(out, "supports_open", this->supports_open); dump_field(out, "requires_code", this->requires_code); - dump_field(out, "code_format", this->code_format_ref_); + dump_field(out, "code_format", this->code_format); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif @@ -1508,15 +1618,15 @@ void LockCommandRequest::dump_to(std::string &out) const { #ifdef USE_BUTTON void ListEntitiesButtonResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesButtonResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "device_class", this->device_class); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif @@ -1532,7 +1642,7 @@ void ButtonCommandRequest::dump_to(std::string &out) const { #ifdef USE_MEDIA_PLAYER void MediaPlayerSupportedFormat::dump_to(std::string &out) const { MessageDumpHelper helper(out, "MediaPlayerSupportedFormat"); - dump_field(out, "format", this->format_ref_); + dump_field(out, "format", this->format); dump_field(out, "sample_rate", this->sample_rate); dump_field(out, "num_channels", this->num_channels); dump_field(out, "purpose", static_cast(this->purpose)); @@ -1540,11 +1650,11 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const { } void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesMediaPlayerResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); @@ -1621,7 +1731,10 @@ void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const { dump_field(out, "mtu", this->mtu); dump_field(out, "error", this->error); } -void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { dump_field(out, "address", this->address); } +void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "BluetoothGATTGetServicesRequest"); + dump_field(out, "address", this->address); +} void BluetoothGATTDescriptor::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothGATTDescriptor"); for (const auto &it : this->uuid) { @@ -1793,12 +1906,12 @@ void VoiceAssistantAudioSettings::dump_to(std::string &out) const { void VoiceAssistantRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantRequest"); dump_field(out, "start", this->start); - dump_field(out, "conversation_id", this->conversation_id_ref_); + dump_field(out, "conversation_id", this->conversation_id); dump_field(out, "flags", this->flags); out.append(" audio_settings: "); this->audio_settings.dump_to(out); out.append("\n"); - dump_field(out, "wake_word_phrase", this->wake_word_phrase_ref_); + dump_field(out, "wake_word_phrase", this->wake_word_phrase); } void VoiceAssistantResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantResponse"); @@ -1822,11 +1935,7 @@ void VoiceAssistantEventResponse::dump_to(std::string &out) const { void VoiceAssistantAudio::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantAudio"); out.append(" data: "); - if (this->data_ptr_ != nullptr) { - out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); - } else { - out.append(format_hex_pretty(reinterpret_cast(this->data.data()), this->data.size())); - } + out.append(format_hex_pretty(this->data, this->data_len)); out.append("\n"); dump_field(out, "end", this->end); } @@ -1846,11 +1955,14 @@ void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const { dump_field(out, "preannounce_media_id", this->preannounce_media_id); dump_field(out, "start_conversation", this->start_conversation); } -void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { dump_field(out, "success", this->success); } +void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "VoiceAssistantAnnounceFinished"); + dump_field(out, "success", this->success); +} void VoiceAssistantWakeWord::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantWakeWord"); - dump_field(out, "id", this->id_ref_); - dump_field(out, "wake_word", this->wake_word_ref_); + dump_field(out, "id", this->id); + dump_field(out, "wake_word", this->wake_word); for (const auto &it : this->trained_languages) { dump_field(out, "trained_languages", it, 4); } @@ -1897,11 +2009,11 @@ void VoiceAssistantSetConfiguration::dump_to(std::string &out) const { #ifdef USE_ALARM_CONTROL_PANEL void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesAlarmControlPanelResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); @@ -1933,17 +2045,17 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { #ifdef USE_TEXT void ListEntitiesTextResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesTextResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); dump_field(out, "min_length", this->min_length); dump_field(out, "max_length", this->max_length); - dump_field(out, "pattern", this->pattern_ref_); + dump_field(out, "pattern", this->pattern); dump_field(out, "mode", static_cast(this->mode)); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); @@ -1952,7 +2064,7 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const { void TextStateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "TextStateResponse"); dump_field(out, "key", this->key); - dump_field(out, "state", this->state_ref_); + dump_field(out, "state", this->state); dump_field(out, "missing_state", this->missing_state); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); @@ -1970,11 +2082,11 @@ void TextCommandRequest::dump_to(std::string &out) const { #ifdef USE_DATETIME_DATE void ListEntitiesDateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesDateResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); @@ -2007,11 +2119,11 @@ void DateCommandRequest::dump_to(std::string &out) const { #ifdef USE_DATETIME_TIME void ListEntitiesTimeResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesTimeResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); @@ -2044,15 +2156,15 @@ void TimeCommandRequest::dump_to(std::string &out) const { #ifdef USE_EVENT void ListEntitiesEventResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesEventResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "device_class", this->device_class); for (const auto &it : *this->event_types) { dump_field(out, "event_types", it, 4); } @@ -2063,7 +2175,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { void EventResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "EventResponse"); dump_field(out, "key", this->key); - dump_field(out, "event_type", this->event_type_ref_); + dump_field(out, "event_type", this->event_type); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif @@ -2072,15 +2184,15 @@ void EventResponse::dump_to(std::string &out) const { #ifdef USE_VALVE void ListEntitiesValveResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesValveResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "device_class", this->device_class); dump_field(out, "assumed_state", this->assumed_state); dump_field(out, "supports_position", this->supports_position); dump_field(out, "supports_stop", this->supports_stop); @@ -2111,11 +2223,11 @@ void ValveCommandRequest::dump_to(std::string &out) const { #ifdef USE_DATETIME_DATETIME void ListEntitiesDateTimeResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesDateTimeResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); @@ -2144,15 +2256,15 @@ void DateTimeCommandRequest::dump_to(std::string &out) const { #ifdef USE_UPDATE void ListEntitiesUpdateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesUpdateResponse"); - dump_field(out, "object_id", this->object_id_ref_); + dump_field(out, "object_id", this->object_id); dump_field(out, "key", this->key); - dump_field(out, "name", this->name_ref_); + dump_field(out, "name", this->name); #ifdef USE_ENTITY_ICON - dump_field(out, "icon", this->icon_ref_); + dump_field(out, "icon", this->icon); #endif dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); - dump_field(out, "device_class", this->device_class_ref_); + dump_field(out, "device_class", this->device_class); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif @@ -2164,11 +2276,11 @@ void UpdateStateResponse::dump_to(std::string &out) const { dump_field(out, "in_progress", this->in_progress); dump_field(out, "has_progress", this->has_progress); dump_field(out, "progress", this->progress); - dump_field(out, "current_version", this->current_version_ref_); - dump_field(out, "latest_version", this->latest_version_ref_); - dump_field(out, "title", this->title_ref_); - dump_field(out, "release_summary", this->release_summary_ref_); - dump_field(out, "release_url", this->release_url_ref_); + dump_field(out, "current_version", this->current_version); + dump_field(out, "latest_version", this->latest_version); + dump_field(out, "title", this->title); + dump_field(out, "release_summary", this->release_summary); + dump_field(out, "release_url", this->release_url); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); #endif diff --git a/esphome/components/api/api_pb2_includes.h b/esphome/components/api/api_pb2_includes.h index 55d95304b1..f45e091c6f 100644 --- a/esphome/components/api/api_pb2_includes.h +++ b/esphome/components/api/api_pb2_includes.h @@ -10,6 +10,10 @@ #include "esphome/components/climate/climate_traits.h" #endif +#ifdef USE_WATER_HEATER +#include "esphome/components/water_heater/water_heater.h" +#endif + #ifdef USE_LIGHT #include "esphome/components/light/light_traits.h" #endif diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 9d227af0a3..c9bf638ad7 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -13,7 +13,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str } #endif -void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { +void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { switch (msg_type) { case HelloRequest::MESSAGE_TYPE: { HelloRequest msg; @@ -24,17 +24,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_hello_request(msg); break; } -#ifdef USE_API_PASSWORD - case AuthenticationRequest::MESSAGE_TYPE: { - AuthenticationRequest msg; - msg.decode(msg_data, msg_size); -#ifdef HAS_PROTO_MESSAGE_DUMP - ESP_LOGVV(TAG, "on_authentication_request: %s", msg.dump().c_str()); -#endif - this->on_authentication_request(msg); - break; - } -#endif case DisconnectRequest::MESSAGE_TYPE: { DisconnectRequest msg; // Empty message: no decode needed @@ -193,7 +182,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS case ExecuteServiceRequest::MESSAGE_TYPE: { ExecuteServiceRequest msg; msg.decode(msg_data, msg_size); @@ -621,6 +610,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_homeassistant_action_response(msg); break; } +#endif +#ifdef USE_WATER_HEATER + case WaterHeaterCommandRequest::MESSAGE_TYPE: { + WaterHeaterCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_water_heater_command_request: %s", msg.dump().c_str()); +#endif + this->on_water_heater_command_request(msg); + break; + } #endif default: break; @@ -632,13 +632,6 @@ void APIServerConnection::on_hello_request(const HelloRequest &msg) { this->on_fatal_error(); } } -#ifdef USE_API_PASSWORD -void APIServerConnection::on_authentication_request(const AuthenticationRequest &msg) { - if (!this->send_authenticate_response(msg)) { - this->on_fatal_error(); - } -} -#endif void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) { if (!this->send_disconnect_response(msg)) { this->on_fatal_error(); @@ -670,7 +663,7 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc this->subscribe_home_assistant_states(msg); } #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); } #endif #ifdef USE_API_NOISE @@ -827,13 +820,10 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); } #endif -void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { +void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { // Check authentication/connection requirements for messages switch (msg_type) { - case HelloRequest::MESSAGE_TYPE: // No setup required -#ifdef USE_API_PASSWORD - case AuthenticationRequest::MESSAGE_TYPE: // No setup required -#endif + case HelloRequest::MESSAGE_TYPE: // No setup required case DisconnectRequest::MESSAGE_TYPE: // No setup required case PingRequest::MESSAGE_TYPE: // No setup required break; // Skip all checks for these messages diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 549b00ee6a..e2a23827dc 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -26,10 +26,6 @@ class APIServerConnectionBase : public ProtoService { virtual void on_hello_request(const HelloRequest &value){}; -#ifdef USE_API_PASSWORD - virtual void on_authentication_request(const AuthenticationRequest &value){}; -#endif - virtual void on_disconnect_request(const DisconnectRequest &value){}; virtual void on_disconnect_response(const DisconnectResponse &value){}; virtual void on_ping_request(const PingRequest &value){}; @@ -79,7 +75,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_get_time_response(const GetTimeResponse &value){}; -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; #endif @@ -91,6 +87,10 @@ class APIServerConnectionBase : public ProtoService { virtual void on_climate_command_request(const ClimateCommandRequest &value){}; #endif +#ifdef USE_WATER_HEATER + virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){}; +#endif + #ifdef USE_NUMBER virtual void on_number_command_request(const NumberCommandRequest &value){}; #endif @@ -218,15 +218,12 @@ class APIServerConnectionBase : public ProtoService { virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; #endif protected: - void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; + void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; class APIServerConnection : public APIServerConnectionBase { public: virtual bool send_hello_response(const HelloRequest &msg) = 0; -#ifdef USE_API_PASSWORD - virtual bool send_authenticate_response(const AuthenticationRequest &msg) = 0; -#endif virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0; virtual bool send_ping_response(const PingRequest &msg) = 0; virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0; @@ -239,7 +236,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_API_HOMEASSISTANT_STATES virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS virtual void execute_service(const ExecuteServiceRequest &msg) = 0; #endif #ifdef USE_API_NOISE @@ -353,9 +350,6 @@ class APIServerConnection : public APIServerConnectionBase { #endif protected: void on_hello_request(const HelloRequest &msg) override; -#ifdef USE_API_PASSWORD - void on_authentication_request(const AuthenticationRequest &msg) override; -#endif void on_disconnect_request(const DisconnectRequest &msg) override; void on_ping_request(const PingRequest &msg) override; void on_device_info_request(const DeviceInfoRequest &msg) override; @@ -368,7 +362,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_API_HOMEASSISTANT_STATES void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void on_execute_service_request(const ExecuteServiceRequest &msg) override; #endif #ifdef USE_API_NOISE @@ -480,7 +474,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_ZWAVE_PROXY void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; #endif - void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; + void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; } // namespace esphome::api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 18601d74ff..4ececfec94 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -4,8 +4,8 @@ #include "api_connection.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" -#include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -52,11 +52,6 @@ void APIServer::setup() { #endif #endif - // Schedule reboot if no clients connect within timeout - if (this->reboot_timeout_ != 0) { - this->schedule_reboot_timeout_(); - } - this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections if (this->socket_ == nullptr) { ESP_LOGW(TAG, "Could not create socket"); @@ -101,42 +96,22 @@ void APIServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr) { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - if (this->shutting_down_) { - // Don't try to send logs during shutdown - // as it could result in a recursion and - // we would be filling a buffer we are trying to clear - return; - } - for (auto &c : this->clients_) { - if (!c->flags_.remove && c->get_log_subscription_level() >= level) - c->try_send_log_message(level, tag, message, message_len); - } - }); + logger::global_logger->add_log_listener(this); } #endif #ifdef USE_CAMERA if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) { - camera::Camera::instance()->add_image_callback([this](const std::shared_ptr &image) { - for (auto &c : this->clients_) { - if (!c->flags_.remove) - c->set_camera_state(image); - } - }); + camera::Camera::instance()->add_listener(this); } #endif -} -void APIServer::schedule_reboot_timeout_() { - this->status_set_warning(); - this->set_timeout("api_reboot", this->reboot_timeout_, []() { - if (!global_api_server->is_connected()) { - ESP_LOGE(TAG, "No clients; rebooting"); - App.reboot(); - } - }); + // Initialize last_connected_ for reboot timeout tracking + this->last_connected_ = App.get_loop_component_start_time(); + // Set warning status if reboot timeout is enabled + if (this->reboot_timeout_ != 0) { + this->status_set_warning(); + } } void APIServer::loop() { @@ -150,29 +125,41 @@ void APIServer::loop() { if (!sock) break; + char peername[socket::SOCKADDR_STR_LEN]; + sock->getpeername_to(peername); + // Check if we're at the connection limit if (this->clients_.size() >= this->max_connections_) { - ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, sock->getpeername().c_str()); + ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); // Immediately close - socket destructor will handle cleanup sock.reset(); continue; } - ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str()); + ESP_LOGD(TAG, "Accept %s", peername); auto *conn = new APIConnection(std::move(sock), this); this->clients_.emplace_back(conn); conn->start(); - // Clear warning status and cancel reboot when first client connects + // First client connected - clear warning and update timestamp if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { this->status_clear_warning(); - this->cancel_timeout("api_reboot"); + this->last_connected_ = App.get_loop_component_start_time(); } } } if (this->clients_.empty()) { + // Check reboot timeout - done in loop to avoid scheduler heap churn + // (cancelled scheduler items sit in heap memory until their scheduled time) + if (this->reboot_timeout_ != 0) { + const uint32_t now = App.get_loop_component_start_time(); + if (now - this->last_connected_ > this->reboot_timeout_) { + ESP_LOGE(TAG, "No clients; rebooting"); + App.reboot(); + } + } return; } @@ -182,8 +169,7 @@ void APIServer::loop() { // Network is down - disconnect all clients for (auto &client : this->clients_) { client->on_fatal_error(); - ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(), - client->client_info_.peername.c_str()); + client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect")); } // Continue to process and clean up the clients below } @@ -201,9 +187,12 @@ void APIServer::loop() { // Rare case: handle disconnection #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername); + this->client_disconnected_trigger_->trigger(std::string(client->get_name()), std::string(client->get_peername())); #endif - ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str()); +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + this->unregister_active_action_calls_for_connection(client.get()); +#endif + ESP_LOGV(TAG, "Remove connection %s", client->get_name()); // Swap with the last element and pop (avoids expensive vector shifts) if (client_index < this->clients_.size() - 1) { @@ -211,9 +200,10 @@ void APIServer::loop() { } this->clients_.pop_back(); - // Schedule reboot when last client disconnects + // Last client disconnected - set warning and start tracking for reboot timeout if (this->clients_.empty() && this->reboot_timeout_ != 0) { - this->schedule_reboot_timeout_(); + this->status_set_warning(); + this->last_connected_ = App.get_loop_component_start_time(); } // Don't increment client_index since we need to process the swapped element } @@ -227,8 +217,8 @@ void APIServer::dump_config() { " Max connections: %u", network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_); #ifdef USE_API_NOISE - ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); - if (!this->noise_ctx_->has_psk()) { + ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk())); + if (!this->noise_ctx_.has_psk()) { ESP_LOGCONFIG(TAG, " Supports encryption: YES"); } #else @@ -236,38 +226,6 @@ void APIServer::dump_config() { #endif } -#ifdef USE_API_PASSWORD -bool APIServer::check_password(const uint8_t *password_data, size_t password_len) const { - // depend only on input password length - const char *a = this->password_.c_str(); - uint32_t len_a = this->password_.length(); - const char *b = reinterpret_cast(password_data); - uint32_t len_b = password_len; - - // disable optimization with volatile - volatile uint32_t length = len_b; - volatile const char *left = nullptr; - volatile const char *right = b; - uint8_t result = 0; - - if (len_a == length) { - left = *((volatile const char **) &a); - result = 0; - } - if (len_a != length) { - left = b; - result = 1; - } - - for (size_t i = 0; i < length; i++) { - result |= *left++ ^ *right++; // NOLINT - } - - return result == 0; -} - -#endif - void APIServer::handle_disconnect(APIConnection *conn) {} // Macro for controller update dispatch @@ -347,6 +305,10 @@ API_DISPATCH_UPDATE(valve::Valve, valve) API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) #endif +#ifdef USE_WATER_HEATER +API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater) +#endif + #ifdef USE_EVENT // Event is a special case - unlike other entities with simple state fields, // events store their state in a member accessed via obj->get_last_event_type() @@ -385,10 +347,6 @@ float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; void APIServer::set_port(uint16_t port) { this->port_ = port; } -#ifdef USE_API_PASSWORD -void APIServer::set_password(const std::string &password) { this->password_ = password; } -#endif - void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } #ifdef USE_API_HOMEASSISTANT_SERVICES @@ -402,7 +360,7 @@ void APIServer::register_action_response_callback(uint32_t call_id, ActionRespon this->action_response_callbacks_.push_back({call_id, std::move(callback)}); } -void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) { +void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message) { for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) { if (it->call_id == call_id) { auto callback = std::move(it->callback); @@ -414,7 +372,7 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std } } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON -void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message, +void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message, const uint8_t *response_data, size_t response_data_len) { for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) { if (it->call_id == call_id) { @@ -431,25 +389,76 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std #endif // USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_STATES -void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { +// Helper to add subscription (reduces duplication) +void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function f, + bool once) { this->state_subs_.push_back(HomeAssistantStateSubscription{ - .entity_id = std::move(entity_id), - .attribute = std::move(attribute), - .callback = std::move(f), - .once = false, + .entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once, + // entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation) }); } +// Helper to add subscription with heap-allocated strings (reduces duplication) +void APIServer::add_state_subscription_(std::string entity_id, optional attribute, + std::function f, bool once) { + HomeAssistantStateSubscription sub; + // Allocate heap storage for the strings + sub.entity_id_dynamic_storage = std::make_unique(std::move(entity_id)); + sub.entity_id = sub.entity_id_dynamic_storage->c_str(); + + if (attribute.has_value()) { + sub.attribute_dynamic_storage = std::make_unique(std::move(attribute.value())); + sub.attribute = sub.attribute_dynamic_storage->c_str(); + } else { + sub.attribute = nullptr; + } + + sub.callback = std::move(f); + sub.once = once; + this->state_subs_.push_back(std::move(sub)); +} + +// New const char* overload (for internal components - zero allocation) +void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute, + std::function f) { + this->add_state_subscription_(entity_id, attribute, std::move(f), false); +} + +void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute, + std::function f) { + this->add_state_subscription_(entity_id, attribute, std::move(f), true); +} + +// std::string overload with StringRef callback (zero-allocation callback) +void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, + std::function f) { + this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false); +} + void APIServer::get_home_assistant_state(std::string entity_id, optional attribute, - std::function f) { - this->state_subs_.push_back(HomeAssistantStateSubscription{ - .entity_id = std::move(entity_id), - .attribute = std::move(attribute), - .callback = std::move(f), - .once = true, - }); -}; + std::function f) { + this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true); +} + +// Legacy helper: wraps std::string callback and delegates to StringRef version +void APIServer::add_state_subscription_(std::string entity_id, optional attribute, + std::function f, bool once) { + // Wrap callback to convert StringRef -> std::string, then delegate + this->add_state_subscription_(std::move(entity_id), std::move(attribute), + std::function([f = std::move(f)](StringRef state) { f(state.str()); }), + once); +} + +// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string) +void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, + std::function f) { + this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false); +} + +void APIServer::get_home_assistant_state(std::string entity_id, optional attribute, + std::function f) { + this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true); +} const std::vector &APIServer::get_state_subs() const { return this->state_subs_; @@ -493,7 +502,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { ESP_LOGW(TAG, "Key set in YAML"); return false; #else - auto &old_psk = this->noise_ctx_->get_psk(); + auto &old_psk = this->noise_ctx_.get_psk(); if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) { ESP_LOGW(TAG, "New PSK matches old"); return true; @@ -528,7 +537,42 @@ void APIServer::request_time() { } #endif -bool APIServer::is_connected() const { return !this->clients_.empty(); } +bool APIServer::is_connected(bool state_subscription_only) const { + if (!state_subscription_only) { + return !this->clients_.empty(); + } + + for (const auto &client : this->clients_) { + if (client->flags_.state_subscription) { + return true; + } + } + return false; +} + +#ifdef USE_LOGGER +void APIServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + if (this->shutting_down_) { + // Don't try to send logs during shutdown + // as it could result in a recursion and + // we would be filling a buffer we are trying to clear + return; + } + for (auto &c : this->clients_) { + if (!c->flags_.remove && c->get_log_subscription_level() >= level) + c->try_send_log_message(level, tag, message, message_len); + } +} +#endif + +#ifdef USE_CAMERA +void APIServer::on_camera_image(const std::shared_ptr &image) { + for (auto &c : this->clients_) { + if (!c->flags_.remove) + c->set_camera_state(image); + } +} +#endif void APIServer::on_shutdown() { this->shutting_down_ = true; @@ -565,5 +609,84 @@ bool APIServer::teardown() { return this->clients_.empty(); } +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES +// Timeout for action calls - matches aioesphomeapi client timeout (default 30s) +// Can be overridden via USE_API_ACTION_CALL_TIMEOUT_MS define for testing +#ifndef USE_API_ACTION_CALL_TIMEOUT_MS +#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT +#endif + +uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConnection *conn) { + uint32_t action_call_id = this->next_action_call_id_++; + // Handle wraparound (skip 0 as it means "no call") + if (this->next_action_call_id_ == 0) { + this->next_action_call_id_ = 1; + } + this->active_action_calls_.push_back({action_call_id, client_call_id, conn}); + + // Schedule automatic cleanup after timeout (client will have given up by then) + this->set_timeout(str_sprintf("action_call_%u", action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS, + [this, action_call_id]() { + ESP_LOGD(TAG, "Action call %u timed out", action_call_id); + this->unregister_active_action_call(action_call_id); + }); + + return action_call_id; +} + +void APIServer::unregister_active_action_call(uint32_t action_call_id) { + // Cancel the timeout for this action call + this->cancel_timeout(str_sprintf("action_call_%u", action_call_id)); + + // Swap-and-pop is more efficient than remove_if for unordered vectors + for (size_t i = 0; i < this->active_action_calls_.size(); i++) { + if (this->active_action_calls_[i].action_call_id == action_call_id) { + std::swap(this->active_action_calls_[i], this->active_action_calls_.back()); + this->active_action_calls_.pop_back(); + return; + } + } +} + +void APIServer::unregister_active_action_calls_for_connection(APIConnection *conn) { + // Remove all active action calls for disconnected connection using swap-and-pop + for (size_t i = 0; i < this->active_action_calls_.size();) { + if (this->active_action_calls_[i].connection == conn) { + // Cancel the timeout for this action call + this->cancel_timeout(str_sprintf("action_call_%u", this->active_action_calls_[i].action_call_id)); + + std::swap(this->active_action_calls_[i], this->active_action_calls_.back()); + this->active_action_calls_.pop_back(); + // Don't increment i - need to check the swapped element + } else { + i++; + } + } +} + +void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message) { + for (auto &call : this->active_action_calls_) { + if (call.action_call_id == action_call_id) { + call.connection->send_execute_service_response(call.client_call_id, success, error_message); + return; + } + } + ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id); +} +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON +void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message, + const uint8_t *response_data, size_t response_data_len) { + for (auto &call : this->active_action_calls_) { + if (call.action_call_id == action_call_id) { + call.connection->send_execute_service_response(call.client_call_id, success, error_message, response_data, + response_data_len); + return; + } + } + ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id); +} +#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON +#endif // USE_API_USER_DEFINED_ACTION_RESPONSES + } // namespace esphome::api #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 2d58063d6c..f5b57f994a 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -10,24 +10,42 @@ #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" #include "list_entities.h" #include "subscribe_state.h" -#ifdef USE_API_SERVICES -#include "user_services.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif +#ifdef USE_CAMERA +#include "esphome/components/camera/camera.h" #endif -#include #include namespace esphome::api { +#ifdef USE_API_USER_DEFINED_ACTIONS +// Forward declaration - full definition in user_services.h +class UserServiceDescriptor; +#endif + #ifdef USE_API_NOISE struct SavedNoisePsk { psk_t psk; } PACKED; // NOLINT #endif -class APIServer : public Component, public Controller { +class APIServer : public Component, + public Controller +#ifdef USE_LOGGER + , + public logger::LogListener +#endif +#ifdef USE_CAMERA + , + public camera::CameraListener +#endif +{ public: APIServer(); void setup() override; @@ -37,9 +55,11 @@ class APIServer : public Component, public Controller { void dump_config() override; void on_shutdown() override; bool teardown() override; -#ifdef USE_API_PASSWORD - bool check_password(const uint8_t *password_data, size_t password_len) const; - void set_password(const std::string &password); +#ifdef USE_LOGGER + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; +#endif +#ifdef USE_CAMERA + void on_camera_image(const std::shared_ptr &image) override; #endif void set_port(uint16_t port); void set_reboot_timeout(uint32_t reboot_timeout); @@ -54,8 +74,8 @@ class APIServer : public Component, public Controller { #ifdef USE_API_NOISE bool save_noise_psk(psk_t psk, bool make_active = true); bool clear_noise_psk(bool make_active = true); - void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } - std::shared_ptr get_noise_ctx() { return noise_ctx_; } + void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); } + APINoiseContext &get_noise_ctx() { return this->noise_ctx_; } #endif // USE_API_NOISE void handle_disconnect(APIConnection *conn); @@ -110,6 +130,9 @@ class APIServer : public Component, public Controller { #ifdef USE_MEDIA_PLAYER void on_media_player_update(media_player::MediaPlayer *obj) override; #endif +#ifdef USE_WATER_HEATER + void on_water_heater_update(water_heater::WaterHeater *obj) override; +#endif #ifdef USE_API_HOMEASSISTANT_SERVICES void send_homeassistant_action(const HomeassistantActionRequest &call); @@ -117,14 +140,14 @@ class APIServer : public Component, public Controller { // Action response handling using ActionResponseCallback = std::function; void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback); - void handle_action_response(uint32_t call_id, bool success, const std::string &error_message); + void handle_action_response(uint32_t call_id, bool success, StringRef error_message); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - void handle_action_response(uint32_t call_id, bool success, const std::string &error_message, - const uint8_t *response_data, size_t response_data_len); + void handle_action_response(uint32_t call_id, bool success, StringRef error_message, const uint8_t *response_data, + size_t response_data_len); #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_SERVICES -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS void initialize_user_services(std::initializer_list services) { this->user_services_.assign(services); } @@ -132,6 +155,19 @@ class APIServer : public Component, public Controller { // Only compile push_back method when custom_services: true (external components) void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #endif +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + // Action call context management - supports concurrent calls from multiple clients + // Returns server-generated action_call_id to avoid collisions when clients use same call_id + uint32_t register_active_action_call(uint32_t client_call_id, APIConnection *conn); + void unregister_active_action_call(uint32_t action_call_id); + void unregister_active_action_calls_for_connection(APIConnection *conn); + // Send response for a specific action call (uses action_call_id, sends client_call_id in response) + void send_action_response(uint32_t action_call_id, bool success, StringRef error_message); +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON + void send_action_response(uint32_t action_call_id, bool success, StringRef error_message, + const uint8_t *response_data, size_t response_data_len); +#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON +#endif // USE_API_USER_DEFINED_ACTION_RESPONSES #endif #ifdef USE_HOMEASSISTANT_TIME void request_time(); @@ -150,23 +186,40 @@ class APIServer : public Component, public Controller { void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); #endif - bool is_connected() const; + bool is_connected(bool state_subscription_only = false) const; #ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { - std::string entity_id; - optional attribute; - std::function callback; + const char *entity_id; // Pointer to flash (internal) or heap (external) + const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute) + std::function callback; bool once; + + // Dynamic storage for external components using std::string API (custom_api_device.h) + // These are only allocated when using the std::string overload (nullptr for const char* overload) + std::unique_ptr entity_id_dynamic_storage; + std::unique_ptr attribute_dynamic_storage; }; + // New const char* overload (for internal components - zero allocation) + void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function f); + void get_home_assistant_state(const char *entity_id, const char *attribute, std::function f); + + // std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback) void subscribe_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function f); void get_home_assistant_state(std::string entity_id, optional attribute, - std::function f); + std::function f); + + // Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback) + void subscribe_home_assistant_state(std::string entity_id, optional attribute, + std::function f); + void get_home_assistant_state(std::string entity_id, optional attribute, + std::function f); + const std::vector &get_state_subs() const; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS const std::vector &get_user_services() const { return this->user_services_; } #endif @@ -180,11 +233,20 @@ class APIServer : public Component, public Controller { #endif protected: - void schedule_reboot_timeout_(); #ifdef USE_API_NOISE bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, const psk_t &active_psk, bool make_active); #endif // USE_API_NOISE +#ifdef USE_API_HOMEASSISTANT_STATES + // Helper methods to reduce code duplication + void add_state_subscription_(const char *entity_id, const char *attribute, std::function f, + bool once); + void add_state_subscription_(std::string entity_id, optional attribute, std::function f, + bool once); + // Legacy helper: wraps std::string callback and delegates to StringRef version + void add_state_subscription_(std::string entity_id, optional attribute, + std::function f, bool once); +#endif // USE_API_HOMEASSISTANT_STATES // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER @@ -196,18 +258,27 @@ class APIServer : public Component, public Controller { // 4-byte aligned types uint32_t reboot_timeout_{300000}; + uint32_t last_connected_{0}; // Vectors and strings (12 bytes each on 32-bit) std::vector> clients_; -#ifdef USE_API_PASSWORD - std::string password_; -#endif std::vector shared_write_buffer_; // Shared proto write buffer for all connections #ifdef USE_API_HOMEASSISTANT_STATES std::vector state_subs_; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS std::vector user_services_; +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + // Active action calls - supports concurrent calls from multiple clients + // Uses server-generated action_call_id to avoid collisions when multiple clients use same call_id + struct ActiveActionCall { + uint32_t action_call_id; // Server-generated unique ID (passed to actions) + uint32_t client_call_id; // Client's original call_id (used in response) + APIConnection *connection; + }; + std::vector active_action_calls_; + uint32_t next_action_call_id_{1}; // Counter for generating unique action_call_ids +#endif // USE_API_USER_DEFINED_ACTION_RESPONSES #endif #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES struct PendingActionResponse { @@ -228,7 +299,7 @@ class APIServer : public Component, public Controller { // 7 bytes used, 1 byte padding #ifdef USE_API_NOISE - std::shared_ptr noise_ctx_ = std::make_shared(); + APINoiseContext noise_ctx_; ESPPreferenceObject noise_pref_; #endif // USE_API_NOISE }; @@ -236,8 +307,11 @@ class APIServer : public Component, public Controller { extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) template class APIConnectedCondition : public Condition { + TEMPLATABLE_VALUE(bool, state_subscription_only) public: - bool check(const Ts &...x) override { return global_api_server->is_connected(); } + bool check(const Ts &...x) override { + return global_api_server->is_connected(this->state_subscription_only_.value(x...)); + } }; } // namespace esphome::api diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index ca1fc089fa..200d0938bd 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -16,7 +16,7 @@ with warnings.catch_warnings(): import contextlib -from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ +from esphome.const import CONF_KEY, CONF_PORT, __version__ from esphome.core import CORE from . import CONF_ENCRYPTION @@ -35,7 +35,6 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: conf = config["api"] name = config["esphome"]["name"] port: int = int(conf[CONF_PORT]) - password: str = conf[CONF_PASSWORD] noise_psk: str | None = None if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)): noise_psk = key @@ -50,7 +49,7 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: cli = APIClient( addresses[0], # Primary address for compatibility port, - password, + "", # Password auth removed in 2026.1.0 client_info=f"ESPHome Logs {__version__}", noise_psk=noise_psk, addresses=addresses, # Pass all addresses for automatic retry diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index 43ea644f0c..b16164270b 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -3,12 +3,12 @@ #include #include "api_server.h" #ifdef USE_API -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS #include "user_services.h" #endif namespace esphome::api { -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template class CustomAPIDeviceService : public UserServiceDynamic { public: CustomAPIDeviceService(const std::string &name, const std::array &arg_names, T *obj, @@ -16,12 +16,15 @@ template class CustomAPIDeviceService : public UserS : UserServiceDynamic(name, arg_names), obj_(obj), callback_(callback) {} protected: - void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT + // CustomAPIDevice services don't support action responses - ignore call_id and return_response + void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { + (this->obj_->*this->callback_)(x...); // NOLINT + } T *obj_; void (T::*callback_)(Ts...); }; -#endif // USE_API_SERVICES +#endif // USE_API_USER_DEFINED_ACTIONS class CustomAPIDevice { public: @@ -49,7 +52,7 @@ class CustomAPIDevice { * @param name The name of the service to register. * @param arg_names The name of the arguments for the service, must match the arguments of the function. */ -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template void register_service(void (T::*callback)(Ts...), const std::string &name, const std::array &arg_names) { @@ -90,7 +93,7 @@ class CustomAPIDevice { * @param callback The member function to call when the service is triggered. * @param name The name of the arguments for the service, must match the arguments of the function. */ -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS template void register_service(void (T::*callback)(), const std::string &name) { #ifdef USE_API_CUSTOM_SERVICES auto *service = new CustomAPIDeviceService(name, {}, (T *) this, callback); // NOLINT @@ -119,21 +122,36 @@ class CustomAPIDevice { * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature"); * } * - * void on_state_changed(std::string state) { - * // State of sensor.weather_forecast is `state` + * void on_state_changed(StringRef state) { + * // State of climate.kitchen current_temperature is `state` + * // Use state.c_str() for C string, state.str() for std::string * } * ``` * * @tparam T The class type creating the service, automatically deduced from the function pointer. - * @param callback The member function to call when the entity state changes. + * @param callback The member function to call when the entity state changes (zero-allocation). * @param entity_id The entity_id to track. * @param attribute The entity state attribute to track. */ template + void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id, + const std::string &attribute = "") { + auto f = std::bind(callback, (T *) this, std::placeholders::_1); + global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), std::move(f)); + } + + /** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version). + * + * @deprecated Use the StringRef overload for zero-allocation callbacks. Will be removed in 2027.1.0. + */ + template + ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0") void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, const std::string &attribute = "") { auto f = std::bind(callback, (T *) this, std::placeholders::_1); - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), f); + // Explicit type to disambiguate overload resolution + global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), + std::function(f)); } /** Subscribe to the state (or attribute state) of an entity from Home Assistant. @@ -145,23 +163,45 @@ class CustomAPIDevice { * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast"); * } * - * void on_state_changed(std::string entity_id, std::string state) { + * void on_state_changed(const std::string &entity_id, StringRef state) { * // State of `entity_id` is `state` * } * ``` * * @tparam T The class type creating the service, automatically deduced from the function pointer. - * @param callback The member function to call when the entity state changes. + * @param callback The member function to call when the entity state changes (zero-allocation for state). * @param entity_id The entity_id to track. * @param attribute The entity state attribute to track. */ template + void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id, + const std::string &attribute = "") { + auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); + global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), std::move(f)); + } + + /** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version). + * + * @deprecated Use the StringRef overload for zero-allocation callbacks. Will be removed in 2027.1.0. + */ + template + ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0") void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, const std::string &attribute = "") { auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); - global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), f); + // Explicit type to disambiguate overload resolution + global_api_server->subscribe_home_assistant_state(entity_id, optional(attribute), + std::function(f)); } #else + template + void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id, + const std::string &attribute = "") { + static_assert(sizeof(T) == 0, + "subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section " + "of your YAML configuration"); + } + template void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, const std::string &attribute = "") { @@ -170,6 +210,14 @@ class CustomAPIDevice { "of your YAML configuration"); } + template + void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id, + const std::string &attribute = "") { + static_assert(sizeof(T) == 0, + "subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section " + "of your YAML configuration"); + } + template void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, const std::string &attribute = "") { @@ -192,7 +240,7 @@ class CustomAPIDevice { */ void call_homeassistant_service(const std::string &service_name) { HomeassistantActionRequest resp; - resp.set_service(StringRef(service_name)); + resp.service = StringRef(service_name); global_api_server->send_homeassistant_action(resp); } @@ -212,12 +260,12 @@ class CustomAPIDevice { */ void call_homeassistant_service(const std::string &service_name, const std::map &data) { HomeassistantActionRequest resp; - resp.set_service(StringRef(service_name)); + resp.service = StringRef(service_name); resp.data.init(data.size()); for (auto &it : data) { auto &kv = resp.data.emplace_back(); - kv.set_key(StringRef(it.first)); - kv.value = it.second; + kv.key = StringRef(it.first); + kv.value = it.second; // value is std::string (no_zero_copy), assign directly } global_api_server->send_homeassistant_action(resp); } @@ -234,7 +282,7 @@ class CustomAPIDevice { */ void fire_homeassistant_event(const std::string &event_name) { HomeassistantActionRequest resp; - resp.set_service(StringRef(event_name)); + resp.service = StringRef(event_name); resp.is_event = true; global_api_server->send_homeassistant_action(resp); } @@ -254,13 +302,13 @@ class CustomAPIDevice { */ void fire_homeassistant_event(const std::string &service_name, const std::map &data) { HomeassistantActionRequest resp; - resp.set_service(StringRef(service_name)); + resp.service = StringRef(service_name); resp.is_event = true; resp.data.init(data.size()); for (auto &it : data) { auto &kv = resp.data.emplace_back(); - kv.set_key(StringRef(it.first)); - kv.value = it.second; + kv.key = StringRef(it.first); + kv.value = it.second; // value is std::string (no_zero_copy), assign directly } global_api_server->send_homeassistant_action(resp); } diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index d00e9e6257..a17c99b8ba 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -12,10 +12,17 @@ #endif #include "esphome/core/automation.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" namespace esphome::api { template class TemplatableStringValue : public TemplatableValue { + // Verify that const char* uses the base class STATIC_STRING optimization (no heap allocation) + // rather than being wrapped in a lambda. The base class constructor for const char* is more + // specialized than the templated constructor here, so it should be selected. + static_assert(std::is_constructible_v, const char *>, + "Base class must have const char* constructor for STATIC_STRING optimization"); + private: // Helper to convert value to string - handles the case where value is already a string template static std::string value_to_string(T &&val) { return to_string(std::forward(val)); } @@ -46,23 +53,25 @@ template class TemplatableKeyValuePair { // Keys are always string literals from YAML dictionary keys (e.g., "code", "event") // and never templatable values or lambdas. Only the value parameter can be a lambda/template. - // Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues. - template TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} + // Using const char* avoids std::string heap allocation - keys remain in flash. + template TemplatableKeyValuePair(const char *key, T value) : key(key), value(value) {} - std::string key; + const char *key{nullptr}; TemplatableStringValue value; }; #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES // Represents the response data from a Home Assistant action +// Note: This class holds a StringRef to the error_message from the protobuf message. +// The protobuf message must outlive the ActionResponse (which is guaranteed since +// the callback is invoked synchronously while the message is on the stack). class ActionResponse { public: - ActionResponse(bool success, std::string error_message = "") - : success_(success), error_message_(std::move(error_message)) {} + ActionResponse(bool success, StringRef error_message) : success_(success), error_message_(error_message) {} #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len) - : success_(success), error_message_(std::move(error_message)) { + ActionResponse(bool success, StringRef error_message, const uint8_t *data, size_t data_len) + : success_(success), error_message_(error_message) { if (data == nullptr || data_len == 0) return; this->json_document_ = json::parse_json(data, data_len); @@ -70,7 +79,8 @@ class ActionResponse { #endif bool is_success() const { return this->success_; } - const std::string &get_error_message() const { return this->error_message_; } + // Returns reference to error message - can be implicitly converted to std::string if needed + const StringRef &get_error_message() const { return this->error_message_; } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON // Get data as parsed JSON object (const version returns read-only view) @@ -79,7 +89,7 @@ class ActionResponse { protected: bool success_; - std::string error_message_; + StringRef error_message_; #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON JsonDocument json_document_; #endif @@ -105,14 +115,15 @@ template class HomeAssistantServiceCallAction : public Action void add_data(K &&key, V &&value) { - this->add_kv_(this->data_, std::forward(key), std::forward(value)); + // Using const char* for keys avoids std::string heap allocation - keys remain in flash. + template void add_data(const char *key, V &&value) { + this->add_kv_(this->data_, key, std::forward(value)); } - template void add_data_template(K &&key, V &&value) { - this->add_kv_(this->data_template_, std::forward(key), std::forward(value)); + template void add_data_template(const char *key, V &&value) { + this->add_kv_(this->data_template_, key, std::forward(value)); } - template void add_variable(K &&key, V &&value) { - this->add_kv_(this->variables_, std::forward(key), std::forward(value)); + template void add_variable(const char *key, V &&value) { + this->add_kv_(this->variables_, key, std::forward(value)); } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -136,7 +147,7 @@ template class HomeAssistantServiceCallAction : public Actionservice_.value(x...); - resp.set_service(StringRef(service_value)); + resp.service = StringRef(service_value); resp.is_event = this->flags_.is_event; this->populate_service_map(resp.data, this->data_, x...); this->populate_service_map(resp.data_template, this->data_template_, x...); @@ -185,10 +196,11 @@ template class HomeAssistantServiceCallAction : public Action void add_kv_(FixedVector> &vec, K &&key, V &&value) { + // Helper to add key-value pairs to FixedVectors + // Keys are always string literals (const char*), values can be lambdas/templates + template void add_kv_(FixedVector> &vec, const char *key, V &&value) { auto &kv = vec.emplace_back(); - kv.key = std::forward(key); + kv.key = key; kv.value = std::forward(value); } @@ -197,7 +209,7 @@ template class HomeAssistantServiceCallAction : public Actionclient_->send_list_info_done( ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 769d7b9b6e..04e6525eb0 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -43,7 +43,7 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_TEXT_SENSOR bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS bool on_service(UserServiceDescriptor *service) override; #endif #ifdef USE_CAMERA @@ -82,6 +82,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_ALARM_CONTROL_PANEL bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; #endif +#ifdef USE_WATER_HEATER + bool on_water_heater(water_heater::WaterHeater *entity) override; +#endif #ifdef USE_EVENT bool on_event(event::Event *entity) override; #endif diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index e7585924a5..2d93adbb47 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -54,16 +54,16 @@ inline constexpr int64_t decode_zigzag64(uint64_t value) { * 3. Global/static strings: StringRef(GLOBAL_CONSTANT) - Always safe * 4. Local variables: Safe ONLY if encoding happens before function returns: * std::string temp = compute_value(); - * msg.set_field(StringRef(temp)); + * msg.field = StringRef(temp); * return this->send_message(msg); // temp is valid during encoding * * Unsafe Patterns (WILL cause crashes/corruption): - * 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value - * 2. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary + * 1. Temporaries: msg.field = StringRef(obj.get_string()) // get_string() returns by value + * 2. Concatenation: msg.field = StringRef(str1 + str2) // Result is temporary * * For unsafe patterns, store in a local variable first: * std::string temp = get_string(); // or str1 + str2 - * msg.set_field(StringRef(temp)); + * msg.field = StringRef(temp); * * The send_*_response pattern ensures proper lifetime management by encoding * within the same function scope where temporaries are created. @@ -334,7 +334,7 @@ class ProtoWriteBuffer { void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { this->encode_uint64(field_id, encode_zigzag64(value), force); } - void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false); + void encode_message(uint32_t field_id, const ProtoMessage &value); std::vector *get_buffer() const { return buffer_; } protected: @@ -795,7 +795,7 @@ class ProtoSize { }; // Implementation of encode_message - must be after ProtoMessage is defined -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { +inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) { this->encode_field_raw(field_id, 2); // type 2: Length-delimited message // Calculate the message size first @@ -833,9 +833,6 @@ class ProtoService { virtual bool is_authenticated() = 0; virtual bool is_connection_setup() = 0; virtual void on_fatal_error() = 0; -#ifdef USE_API_PASSWORD - virtual void on_unauthenticated_access() = 0; -#endif virtual void on_no_setup_connection() = 0; /** * Create a buffer with a reserved size. @@ -846,7 +843,7 @@ class ProtoService { */ virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; - virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; + virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; // Optimized method that pre-allocates buffer based on message size bool send_message_(const ProtoMessage &msg, uint8_t message_type) { @@ -873,20 +870,7 @@ class ProtoService { return true; } - inline bool check_authenticated_() { -#ifdef USE_API_PASSWORD - if (!this->check_connection_setup_()) { - return false; - } - if (!this->is_authenticated()) { - this->on_unauthenticated_access(); - return false; - } - return true; -#else - return this->check_connection_setup_(); -#endif - } + inline bool check_authenticated_() { return this->check_connection_setup_(); } }; } // namespace esphome::api diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 3a563f2221..4bbc17018e 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -60,6 +60,9 @@ INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer) #ifdef USE_ALARM_CONTROL_PANEL INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel) #endif +#ifdef USE_WATER_HEATER +INITIAL_STATE_HANDLER(water_heater, water_heater::WaterHeater) +#endif #ifdef USE_UPDATE INITIAL_STATE_HANDLER(update, update::UpdateEntity) #endif diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 2c22c322ec..9230000ace 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -76,6 +76,9 @@ class InitialStateIterator : public ComponentIterator { #ifdef USE_ALARM_CONTROL_PANEL bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; #endif +#ifdef USE_WATER_HEATER + bool on_water_heater(water_heater::WaterHeater *entity) override; +#endif #ifdef USE_EVENT bool on_event(event::Event *event) override { return true; }; #endif diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 2a887fc52d..85fba2a435 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -1,20 +1,31 @@ #pragma once +#include #include #include -#include "esphome/core/component.h" -#include "esphome/core/automation.h" #include "api_pb2.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON +#include "esphome/components/json/json_util.h" +#endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS namespace esphome::api { +// Forward declaration - full definition in api_server.h +class APIServer; + class UserServiceDescriptor { public: virtual ListEntitiesServicesResponse encode_list_service_response() = 0; virtual bool execute_service(const ExecuteServiceRequest &req) = 0; +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + // Overload that accepts server-generated action_call_id (avoids client call_id collisions) + virtual bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) = 0; +#endif bool is_internal() { return false; } }; @@ -27,21 +38,23 @@ template enums::ServiceArgType to_service_arg_type(); // Stores only pointers to string literals in flash - no heap allocation template class UserServiceBase : public UserServiceDescriptor { public: - UserServiceBase(const char *name, const std::array &arg_names) - : name_(name), arg_names_(arg_names) { + UserServiceBase(const char *name, const std::array &arg_names, + enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE) + : name_(name), arg_names_(arg_names), supports_response_(supports_response) { this->key_ = fnv1_hash(name); } ListEntitiesServicesResponse encode_list_service_response() override { ListEntitiesServicesResponse msg; - msg.set_name(StringRef(this->name_)); + msg.name = StringRef(this->name_); msg.key = this->key_; + msg.supports_response = this->supports_response_; std::array arg_types = {to_service_arg_type()...}; msg.args.init(sizeof...(Ts)); for (size_t i = 0; i < sizeof...(Ts); i++) { auto &arg = msg.args.emplace_back(); arg.type = arg_types[i]; - arg.set_name(StringRef(this->arg_names_[i])); + arg.name = StringRef(this->arg_names_[i]); } return msg; } @@ -51,20 +64,37 @@ template class UserServiceBase : public UserServiceDescriptor { return false; if (req.args.size() != sizeof...(Ts)) return false; - this->execute_(req.args, typename gens::type()); +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence{}); +#else + this->execute_(req.args, 0, false, std::make_index_sequence{}); +#endif return true; } +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override { + if (req.key != this->key_) + return false; + if (req.args.size() != sizeof...(Ts)) + return false; + this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence{}); + return true; + } +#endif + protected: - virtual void execute(Ts... x) = 0; - template void execute_(const ArgsContainer &args, seq type) { - this->execute((get_execute_arg_value(args[S]))...); + virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0; + template + void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence /*type*/) { + this->execute(call_id, return_response, (get_execute_arg_value(args[S]))...); } // Pointers to string literals in flash - no heap allocation const char *name_; std::array arg_names_; uint32_t key_{0}; + enums::SupportsResponseType supports_response_{enums::SUPPORTS_RESPONSE_NONE}; }; // Separate class for custom_api_device services (rare case) @@ -78,14 +108,15 @@ template class UserServiceDynamic : public UserServiceDescriptor ListEntitiesServicesResponse encode_list_service_response() override { ListEntitiesServicesResponse msg; - msg.set_name(StringRef(this->name_)); + msg.name = StringRef(this->name_); msg.key = this->key_; + msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet std::array arg_types = {to_service_arg_type()...}; msg.args.init(sizeof...(Ts)); for (size_t i = 0; i < sizeof...(Ts); i++) { auto &arg = msg.args.emplace_back(); arg.type = arg_types[i]; - arg.set_name(StringRef(this->arg_names_[i])); + arg.name = StringRef(this->arg_names_[i]); } return msg; } @@ -95,14 +126,31 @@ template class UserServiceDynamic : public UserServiceDescriptor return false; if (req.args.size() != sizeof...(Ts)) return false; - this->execute_(req.args, typename gens::type()); +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence{}); +#else + this->execute_(req.args, 0, false, std::make_index_sequence{}); +#endif return true; } +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES + // Dynamic services don't support responses yet, but need to implement the interface + bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override { + if (req.key != this->key_) + return false; + if (req.args.size() != sizeof...(Ts)) + return false; + this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence{}); + return true; + } +#endif + protected: - virtual void execute(Ts... x) = 0; - template void execute_(const ArgsContainer &args, seq type) { - this->execute((get_execute_arg_value(args[S]))...); + virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0; + template + void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence /*type*/) { + this->execute(call_id, return_response, (get_execute_arg_value(args[S]))...); } // Heap-allocated strings for runtime-generated names @@ -111,15 +159,149 @@ template class UserServiceDynamic : public UserServiceDescriptor uint32_t key_{0}; }; -template class UserServiceTrigger : public UserServiceBase, public Trigger { +// Primary template declaration +template class UserServiceTrigger; + +// Specialization for NONE - no extra trigger arguments +template +class UserServiceTrigger : public UserServiceBase, public Trigger { public: - // Constructor for static names (YAML-defined services - used by code generator) UserServiceTrigger(const char *name, const std::array &arg_names) - : UserServiceBase(name, arg_names) {} + : UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {} protected: - void execute(Ts... x) override { this->trigger(x...); } // NOLINT + void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); } +}; + +// Specialization for OPTIONAL - call_id and return_response trigger arguments +template +class UserServiceTrigger : public UserServiceBase, + public Trigger { + public: + UserServiceTrigger(const char *name, const std::array &arg_names) + : UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {} + + protected: + void execute(uint32_t call_id, bool return_response, Ts... x) override { + this->trigger(call_id, return_response, x...); + } +}; + +// Specialization for ONLY - just call_id trigger argument +template +class UserServiceTrigger : public UserServiceBase, + public Trigger { + public: + UserServiceTrigger(const char *name, const std::array &arg_names) + : UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {} + + protected: + void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); } +}; + +// Specialization for STATUS - just call_id trigger argument (reports success/error without data) +template +class UserServiceTrigger : public UserServiceBase, + public Trigger { + public: + UserServiceTrigger(const char *name, const std::array &arg_names) + : UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {} + + protected: + void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); } }; } // namespace esphome::api -#endif // USE_API_SERVICES +#endif // USE_API_USER_DEFINED_ACTIONS + +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES +// Include full definition of APIServer for template implementation +// Must be outside namespace to avoid including STL headers inside namespace +#include "api_server.h" + +namespace esphome::api { + +template class APIRespondAction : public Action { + public: + explicit APIRespondAction(APIServer *parent) : parent_(parent) {} + + template void set_success(V success) { this->success_ = success; } + template void set_error_message(V error) { this->error_message_ = error; } + void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; } + +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON + void set_data(std::function func) { + this->json_builder_ = std::move(func); + this->has_data_ = true; + } +#endif + + void play(const Ts &...x) override { + // Extract call_id from first argument - it's always first for optional/only/status modes + auto args = std::make_tuple(x...); + uint32_t call_id = std::get<0>(args); + + bool success = this->success_.value(x...); + std::string error_message = this->error_message_.value(x...); + +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON + if (this->has_data_) { + // For optional mode, check return_response (second arg) to decide if client wants data + // Use nested if constexpr to avoid compile error when tuple doesn't have enough elements + // (std::tuple_element_t is evaluated before the && short-circuit, so we must nest) + if constexpr (sizeof...(Ts) >= 2) { + if constexpr (std::is_same_v>, bool>) { + if (this->is_optional_mode_) { + bool return_response = std::get<1>(args); + if (!return_response) { + // Client doesn't want response data, just send success/error + this->parent_->send_action_response(call_id, success, StringRef(error_message)); + return; + } + } + } + } + // Build and send JSON response + json::JsonBuilder builder; + this->json_builder_(x..., builder.root()); + std::string json_str = builder.serialize(); + this->parent_->send_action_response(call_id, success, StringRef(error_message), + reinterpret_cast(json_str.data()), json_str.size()); + return; + } +#endif + this->parent_->send_action_response(call_id, success, StringRef(error_message)); + } + + protected: + APIServer *parent_; + TemplatableValue success_{true}; + TemplatableValue error_message_{""}; +#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON + std::function json_builder_; + bool has_data_{false}; +#endif + bool is_optional_mode_{false}; +}; + +// Action to unregister a service call after execution completes +// Automatically appended to the end of action lists for non-none response modes +template class APIUnregisterServiceCallAction : public Action { + public: + explicit APIUnregisterServiceCallAction(APIServer *parent) : parent_(parent) {} + + void play(const Ts &...x) override { + // Extract call_id from first argument - same convention as APIRespondAction + auto args = std::make_tuple(x...); + uint32_t call_id = std::get<0>(args); + if (call_id != 0) { + this->parent_->unregister_active_action_call(call_id); + } + } + + protected: + APIServer *parent_; +}; + +} // namespace esphome::api +#endif // USE_API_USER_DEFINED_ACTION_RESPONSES diff --git a/esphome/components/aqi/__init__.py b/esphome/components/aqi/__init__.py new file mode 100644 index 0000000000..4b979ab406 --- /dev/null +++ b/esphome/components/aqi/__init__.py @@ -0,0 +1,14 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@jasstrong", "@ximex", "@freekode"] + +aqi_ns = cg.esphome_ns.namespace("aqi") +AQICalculatorType = aqi_ns.enum("AQICalculatorType") + +CONF_AQI = "aqi" +CONF_CALCULATION_TYPE = "calculation_type" + +AQI_CALCULATION_TYPE = { + "CAQI": AQICalculatorType.CAQI_TYPE, + "AQI": AQICalculatorType.AQI_TYPE, +} diff --git a/esphome/components/hm3301/abstract_aqi_calculator.h b/esphome/components/aqi/abstract_aqi_calculator.h similarity index 64% rename from esphome/components/hm3301/abstract_aqi_calculator.h rename to esphome/components/aqi/abstract_aqi_calculator.h index 038828e9de..7836c76cdc 100644 --- a/esphome/components/hm3301/abstract_aqi_calculator.h +++ b/esphome/components/aqi/abstract_aqi_calculator.h @@ -2,13 +2,11 @@ #include -namespace esphome { -namespace hm3301 { +namespace esphome::aqi { class AbstractAQICalculator { public: virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0; }; -} // namespace hm3301 -} // namespace esphome +} // namespace esphome::aqi diff --git a/esphome/components/aqi/aqi_calculator.h b/esphome/components/aqi/aqi_calculator.h new file mode 100644 index 0000000000..35dc35a44a --- /dev/null +++ b/esphome/components/aqi/aqi_calculator.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include "abstract_aqi_calculator.h" + +// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf + +namespace esphome::aqi { + +class AQICalculator : public AbstractAQICalculator { + public: + uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + int pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); + int pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); + + return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; + } + + protected: + static constexpr int NUM_LEVELS = 6; + + static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; + + static constexpr int PM2_5_GRID[NUM_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55}, {56, 125}, {126, 225}, {226, INT_MAX}}; + + static constexpr int PM10_0_GRID[NUM_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, + {255, 354}, {355, 424}, {425, INT_MAX}}; + + static int calculate_index(uint16_t value, const int array[NUM_LEVELS][2]) { + int grid_index = get_grid_index(value, array); + if (grid_index == -1) { + return -1; + } + int aqi_lo = INDEX_GRID[grid_index][0]; + int aqi_hi = INDEX_GRID[grid_index][1]; + int conc_lo = array[grid_index][0]; + int conc_hi = array[grid_index][1]; + + return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; + } + + static int get_grid_index(uint16_t value, const int array[NUM_LEVELS][2]) { + for (int i = 0; i < NUM_LEVELS; i++) { + if (value >= array[i][0] && value <= array[i][1]) { + return i; + } + } + return -1; + } +}; + +} // namespace esphome::aqi diff --git a/esphome/components/hm3301/aqi_calculator_factory.h b/esphome/components/aqi/aqi_calculator_factory.h similarity index 55% rename from esphome/components/hm3301/aqi_calculator_factory.h rename to esphome/components/aqi/aqi_calculator_factory.h index 55608b6e51..db7eaab1bb 100644 --- a/esphome/components/hm3301/aqi_calculator_factory.h +++ b/esphome/components/aqi/aqi_calculator_factory.h @@ -3,8 +3,7 @@ #include "caqi_calculator.h" #include "aqi_calculator.h" -namespace esphome { -namespace hm3301 { +namespace esphome::aqi { enum AQICalculatorType { CAQI_TYPE = 0, AQI_TYPE = 1 }; @@ -12,18 +11,17 @@ class AQICalculatorFactory { public: AbstractAQICalculator *get_calculator(AQICalculatorType type) { if (type == 0) { - return caqi_calculator_; + return &this->caqi_calculator_; } else if (type == 1) { - return aqi_calculator_; + return &this->aqi_calculator_; } return nullptr; } protected: - CAQICalculator *caqi_calculator_ = new CAQICalculator(); - AQICalculator *aqi_calculator_ = new AQICalculator(); + CAQICalculator caqi_calculator_; + AQICalculator aqi_calculator_; }; -} // namespace hm3301 -} // namespace esphome +} // namespace esphome::aqi diff --git a/esphome/components/aqi/aqi_sensor.cpp b/esphome/components/aqi/aqi_sensor.cpp new file mode 100644 index 0000000000..cdc9f35ba6 --- /dev/null +++ b/esphome/components/aqi/aqi_sensor.cpp @@ -0,0 +1,52 @@ +#include "aqi_sensor.h" +#include "esphome/core/log.h" + +namespace esphome::aqi { + +static const char *const TAG = "aqi"; + +void AQISensor::setup() { + if (this->pm_2_5_sensor_ != nullptr) { + this->pm_2_5_sensor_->add_on_state_callback([this](float value) { + this->pm_2_5_value_ = value; + // Defer calculation to avoid double-publishing if both sensors update in the same loop + this->defer("update", [this]() { this->calculate_aqi_(); }); + }); + } + if (this->pm_10_0_sensor_ != nullptr) { + this->pm_10_0_sensor_->add_on_state_callback([this](float value) { + this->pm_10_0_value_ = value; + this->defer("update", [this]() { this->calculate_aqi_(); }); + }); + } +} + +void AQISensor::dump_config() { + ESP_LOGCONFIG(TAG, "AQI Sensor:"); + ESP_LOGCONFIG(TAG, " Calculation Type: %s", this->aqi_calc_type_ == AQI_TYPE ? "AQI" : "CAQI"); + if (this->pm_2_5_sensor_ != nullptr) { + ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str()); + } + if (this->pm_10_0_sensor_ != nullptr) { + ESP_LOGCONFIG(TAG, " PM10 Sensor: '%s'", this->pm_10_0_sensor_->get_name().c_str()); + } + LOG_SENSOR(" ", "AQI", this); +} + +void AQISensor::calculate_aqi_() { + if (std::isnan(this->pm_2_5_value_) || std::isnan(this->pm_10_0_value_)) { + return; + } + + AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); + if (calculator == nullptr) { + ESP_LOGW(TAG, "Unknown AQI calculator type"); + return; + } + + uint16_t aqi = + calculator->get_aqi(static_cast(this->pm_2_5_value_), static_cast(this->pm_10_0_value_)); + this->publish_state(aqi); +} + +} // namespace esphome::aqi diff --git a/esphome/components/aqi/aqi_sensor.h b/esphome/components/aqi/aqi_sensor.h new file mode 100644 index 0000000000..a990f815fe --- /dev/null +++ b/esphome/components/aqi/aqi_sensor.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "aqi_calculator_factory.h" + +namespace esphome::aqi { + +class AQISensor : public sensor::Sensor, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_pm_2_5_sensor(sensor::Sensor *sensor) { this->pm_2_5_sensor_ = sensor; } + void set_pm_10_0_sensor(sensor::Sensor *sensor) { this->pm_10_0_sensor_ = sensor; } + void set_aqi_calculation_type(AQICalculatorType type) { this->aqi_calc_type_ = type; } + + protected: + void calculate_aqi_(); + + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + AQICalculatorType aqi_calc_type_{AQI_TYPE}; + AQICalculatorFactory aqi_calculator_factory_; + + float pm_2_5_value_{NAN}; + float pm_10_0_value_{NAN}; +}; + +} // namespace esphome::aqi diff --git a/esphome/components/aqi/caqi_calculator.h b/esphome/components/aqi/caqi_calculator.h new file mode 100644 index 0000000000..9906c179f6 --- /dev/null +++ b/esphome/components/aqi/caqi_calculator.h @@ -0,0 +1,49 @@ +#pragma once + +#include "abstract_aqi_calculator.h" + +namespace esphome::aqi { + +class CAQICalculator : public AbstractAQICalculator { + public: + uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + int pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); + int pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); + + return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; + } + + protected: + static constexpr int NUM_LEVELS = 5; + + static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; + + static constexpr int PM2_5_GRID[NUM_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}}; + + static constexpr int PM10_0_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}}; + + static int calculate_index(uint16_t value, const int array[NUM_LEVELS][2]) { + int grid_index = get_grid_index(value, array); + if (grid_index == -1) { + return -1; + } + + int aqi_lo = INDEX_GRID[grid_index][0]; + int aqi_hi = INDEX_GRID[grid_index][1]; + int conc_lo = array[grid_index][0]; + int conc_hi = array[grid_index][1]; + + return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; + } + + static int get_grid_index(uint16_t value, const int array[NUM_LEVELS][2]) { + for (int i = 0; i < NUM_LEVELS; i++) { + if (value >= array[i][0] && value <= array[i][1]) { + return i; + } + } + return -1; + } +}; + +} // namespace esphome::aqi diff --git a/esphome/components/aqi/sensor.py b/esphome/components/aqi/sensor.py new file mode 100644 index 0000000000..0b5ee8d75a --- /dev/null +++ b/esphome/components/aqi/sensor.py @@ -0,0 +1,51 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_PM_2_5, + CONF_PM_10_0, + DEVICE_CLASS_AQI, + STATE_CLASS_MEASUREMENT, +) + +from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns + +CODEOWNERS = ["@jasstrong"] +DEPENDENCIES = ["sensor"] + +UNIT_INDEX = "index" + +AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + AQISensor, + unit_of_measurement=UNIT_INDEX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.Required(CONF_PM_2_5): cv.use_id(sensor.Sensor), + cv.Required(CONF_PM_10_0): cv.use_id(sensor.Sensor), + cv.Required(CONF_CALCULATION_TYPE): cv.enum( + AQI_CALCULATION_TYPE, upper=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + + pm_2_5_sensor = await cg.get_variable(config[CONF_PM_2_5]) + cg.add(var.set_pm_2_5_sensor(pm_2_5_sensor)) + + pm_10_0_sensor = await cg.get_variable(config[CONF_PM_10_0]) + cg.add(var.set_pm_10_0_sensor(pm_10_0_sensor)) + + cg.add(var.set_aqi_calculation_type(config[CONF_CALCULATION_TYPE])) diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index 2609af07d3..93a0bff5b3 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -305,12 +305,14 @@ bool AS3935Component::calibrate_oscillator() { } void AS3935Component::tune_antenna() { - ESP_LOGI(TAG, "Starting antenna tuning"); uint8_t div_ratio = this->read_div_ratio(); uint8_t tune_val = this->read_capacitance(); - ESP_LOGI(TAG, "Division Ratio is set to: %d", div_ratio); - ESP_LOGI(TAG, "Internal Capacitor is set to: %d", tune_val); - ESP_LOGI(TAG, "Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio"); + ESP_LOGI(TAG, + "Starting antenna tuning\n" + "Division Ratio is set to: %d\n" + "Internal Capacitor is set to: %d\n" + "Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio", + div_ratio, tune_val); this->display_oscillator(true, ANTFREQ); } diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index f2d8895b39..1ff4805f03 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -1,37 +1,50 @@ -# Dummy integration to allow relying on AsyncTCP +# Async TCP client support for all platforms import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import ( - PLATFORM_BK72XX, - PLATFORM_ESP32, - PLATFORM_ESP8266, - PLATFORM_LN882X, - PLATFORM_RTL87XX, -) from esphome.core import CORE, CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] +DEPENDENCIES = ["network"] -CONFIG_SCHEMA = cv.All( - cv.Schema({}), - cv.only_with_arduino, - cv.only_on( - [ - PLATFORM_ESP32, - PLATFORM_ESP8266, - PLATFORM_BK72XX, - PLATFORM_LN882X, - PLATFORM_RTL87XX, - ] - ), -) + +def AUTO_LOAD() -> list[str]: + # Socket component needed for platforms using socket-based implementation + # ESP32, ESP8266, RP2040, and LibreTiny use AsyncTCP libraries, others use sockets + if ( + not CORE.is_esp32 + and not CORE.is_esp8266 + and not CORE.is_rp2040 + and not CORE.is_libretiny + ): + return ["socket"] + return [] + + +# Support all platforms - Arduino/ESP-IDF get libraries, other platforms use socket implementation +CONFIG_SCHEMA = cv.Schema({}) @coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT) async def to_code(config): - if CORE.is_esp32 or CORE.is_libretiny: + if CORE.is_esp32: + # https://github.com/ESP32Async/AsyncTCP + from esphome.components.esp32 import add_idf_component + + add_idf_component(name="esp32async/asynctcp", ref="3.4.91") + elif CORE.is_libretiny: # https://github.com/ESP32Async/AsyncTCP cg.add_library("ESP32Async/AsyncTCP", "3.4.5") elif CORE.is_esp8266: # https://github.com/ESP32Async/ESPAsyncTCP cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") + elif CORE.is_rp2040: + # https://github.com/khoih-prog/AsyncTCP_RP2040W + cg.add_library("khoih-prog/AsyncTCP_RP2040W", "1.2.0") + # Other platforms (host, etc) use socket-based implementation + + +def FILTER_SOURCE_FILES() -> list[str]: + # Exclude socket implementation for platforms that use AsyncTCP libraries + if CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2040 or CORE.is_libretiny: + return ["async_tcp_socket.cpp"] + return [] diff --git a/esphome/components/async_tcp/async_tcp.h b/esphome/components/async_tcp/async_tcp.h new file mode 100644 index 0000000000..6d9211f023 --- /dev/null +++ b/esphome/components/async_tcp/async_tcp.h @@ -0,0 +1,16 @@ +#pragma once +#include "esphome/core/defines.h" + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) +// Use AsyncTCP library for ESP32 (Arduino or ESP-IDF) and LibreTiny +#include +#elif defined(USE_ESP8266) +// Use ESPAsyncTCP library for ESP8266 (always Arduino) +#include +#elif defined(USE_RP2040) +// Use AsyncTCP_RP2040W library for RP2040 +#include +#else +// Use socket-based implementation for other platforms +#include "async_tcp_socket.h" +#endif diff --git a/esphome/components/async_tcp/async_tcp_socket.cpp b/esphome/components/async_tcp/async_tcp_socket.cpp new file mode 100644 index 0000000000..f64e494f5f --- /dev/null +++ b/esphome/components/async_tcp/async_tcp_socket.cpp @@ -0,0 +1,162 @@ +#include "async_tcp_socket.h" + +#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \ + (defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)) + +#include "esphome/components/network/util.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome::async_tcp { + +static const char *const TAG = "async_tcp"; + +// Read buffer size matches TCP MSS (1500 MTU - 40 bytes IP/TCP headers). +// This implementation only runs on ESP-IDF and host which have ample stack. +static constexpr size_t READ_BUFFER_SIZE = 1460; + +bool AsyncClient::connect(const char *host, uint16_t port) { + if (connected_ || connecting_) { + ESP_LOGW(TAG, "Already connected/connecting"); + return false; + } + + // Resolve address + struct sockaddr_storage addr; + socklen_t addrlen = esphome::socket::set_sockaddr((struct sockaddr *) &addr, sizeof(addr), host, port); + if (addrlen == 0) { + ESP_LOGE(TAG, "Invalid address: %s", host); + if (error_cb_) + error_cb_(error_arg_, this, -1); + return false; + } + + // Create socket with loop monitoring + int family = ((struct sockaddr *) &addr)->sa_family; + socket_ = esphome::socket::socket_loop_monitored(family, SOCK_STREAM, IPPROTO_TCP); + if (!socket_) { + ESP_LOGE(TAG, "Failed to create socket"); + if (error_cb_) + error_cb_(error_arg_, this, -1); + return false; + } + + socket_->setblocking(false); + + int err = socket_->connect((struct sockaddr *) &addr, addrlen); + if (err == 0) { + // Connection succeeded immediately (rare, but possible for localhost) + connected_ = true; + if (connect_cb_) + connect_cb_(connect_arg_, this); + return true; + } + if (errno != EINPROGRESS) { + ESP_LOGE(TAG, "Connect failed: %d", errno); + close(); + if (error_cb_) + error_cb_(error_arg_, this, errno); + return false; + } + + connecting_ = true; + return true; +} + +void AsyncClient::close() { + socket_.reset(); + bool was_connected = connected_; + connected_ = false; + connecting_ = false; + if (was_connected && disconnect_cb_) + disconnect_cb_(disconnect_arg_, this); +} + +size_t AsyncClient::write(const char *data, size_t len) { + if (!socket_ || !connected_) + return 0; + + ssize_t sent = socket_->write(data, len); + if (sent < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + ESP_LOGE(TAG, "Write error: %d", errno); + close(); + if (error_cb_) + error_cb_(error_arg_, this, errno); + } + return 0; + } + return sent; +} + +void AsyncClient::loop() { + if (!socket_) + return; + + if (connecting_) { + // For connecting, we need to check writability, not readability + // The Application's select() only monitors read FDs, so we do our own check here + // For ESP platforms lwip_select() might be faster, but this code isn't used + // on those platforms anyway. If it was, we'd fix the Application select() + // to report writability instead of doing it this way. + int fd = socket_->get_fd(); + if (fd < 0) { + ESP_LOGW(TAG, "Invalid socket fd"); + close(); + return; + } + + fd_set writefds; + FD_ZERO(&writefds); + FD_SET(fd, &writefds); + + struct timeval tv = {0, 0}; + int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv); + + if (ret > 0 && FD_ISSET(fd, &writefds)) { + int error = 0; + socklen_t len = sizeof(error); + if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) { + connecting_ = false; + connected_ = true; + if (connect_cb_) + connect_cb_(connect_arg_, this); + } else { + ESP_LOGW(TAG, "Connection failed: %d", error); + close(); + if (error_cb_) + error_cb_(error_arg_, this, error); + } + } else if (ret < 0) { + ESP_LOGE(TAG, "Select error: %d", errno); + close(); + if (error_cb_) + error_cb_(error_arg_, this, errno); + } + } else if (connected_) { + // For connected sockets, use the Application's select() results + if (!socket_->ready()) + return; + + uint8_t buf[READ_BUFFER_SIZE]; + ssize_t len = socket_->read(buf, READ_BUFFER_SIZE); + + if (len == 0) { + ESP_LOGI(TAG, "Connection closed by peer"); + close(); + } else if (len > 0) { + if (data_cb_) + data_cb_(data_arg_, this, buf, len); + } else if (errno != EAGAIN && errno != EWOULDBLOCK) { + ESP_LOGW(TAG, "Read error: %d", errno); + close(); + if (error_cb_) + error_cb_(error_arg_, this, errno); + } + } +} + +} // namespace esphome::async_tcp + +#endif diff --git a/esphome/components/async_tcp/async_tcp_socket.h b/esphome/components/async_tcp/async_tcp_socket.h new file mode 100644 index 0000000000..28714a7752 --- /dev/null +++ b/esphome/components/async_tcp/async_tcp_socket.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \ + (defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)) + +#include "esphome/components/socket/socket.h" +#include +#include +#include +#include + +namespace esphome::async_tcp { + +/// AsyncClient API for platforms using sockets (ESP-IDF, host, etc.) +/// NOTE: This class is NOT thread-safe. All methods must be called from the main loop. +class AsyncClient { + public: + using AcConnectHandler = std::function; + using AcDataHandler = std::function; + using AcErrorHandler = std::function; + + AsyncClient() = default; + ~AsyncClient() = default; + + [[nodiscard]] bool connect(const char *host, uint16_t port); + void close(); + [[nodiscard]] bool connected() const { return connected_; } + size_t write(const char *data, size_t len); + + void onConnect(AcConnectHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming) + connect_cb_ = std::move(cb); + connect_arg_ = arg; + } + void onDisconnect(AcConnectHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming) + disconnect_cb_ = std::move(cb); + disconnect_arg_ = arg; + } + /// Set data callback. NOTE: data pointer is only valid during callback execution. + void onData(AcDataHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming) + data_cb_ = std::move(cb); + data_arg_ = arg; + } + void onError(AcErrorHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming) + error_cb_ = std::move(cb); + error_arg_ = arg; + } + + // Must be called from loop() + void loop(); + + private: + std::unique_ptr socket_; + + AcConnectHandler connect_cb_{nullptr}; + void *connect_arg_{nullptr}; + AcConnectHandler disconnect_cb_{nullptr}; + void *disconnect_arg_{nullptr}; + AcDataHandler data_cb_{nullptr}; + void *data_arg_{nullptr}; + AcErrorHandler error_cb_{nullptr}; + void *error_arg_{nullptr}; + + bool connected_{false}; + bool connecting_{false}; +}; + +} // namespace esphome::async_tcp + +// Expose AsyncClient in global namespace to match library behavior +using esphome::async_tcp::AsyncClient; // NOLINT(google-global-names-in-headers) +#endif diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.cpp b/esphome/components/atc_mithermometer/atc_mithermometer.cpp index 9d550fcf8c..b4d2929742 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.cpp +++ b/esphome/components/atc_mithermometer/atc_mithermometer.cpp @@ -21,7 +21,9 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -32,7 +34,7 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device if (!(parse_message_(service_data.data, *res))) { continue; } - if (!(report_results_(res, device.address_str()))) { + if (!(report_results_(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) @@ -103,13 +105,13 @@ bool ATCMiThermometer::parse_message_(const std::vector &message, Parse return true; } -bool ATCMiThermometer::report_results_(const optional &result, const std::string &address) { +bool ATCMiThermometer::report_results_(const optional &result, const char *address) { if (!result.has_value()) { ESP_LOGVV(TAG, "report_results(): no results available."); return false; } - ESP_LOGD(TAG, "Got ATC MiThermometer (%s):", address.c_str()); + ESP_LOGD(TAG, "Got ATC MiThermometer (%s):", address); if (result->temperature.has_value()) { ESP_LOGD(TAG, " Temperature: %.1f °C", *result->temperature); diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h index d22e3f069b..e37b5f4350 100644 --- a/esphome/components/atc_mithermometer/atc_mithermometer.h +++ b/esphome/components/atc_mithermometer/atc_mithermometer.h @@ -41,7 +41,7 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice optional parse_header_(const esp32_ble_tracker::ServiceData &service_data); bool parse_message_(const std::vector &message, ParseResult &result); - bool report_results_(const optional &result, const std::string &address); + bool report_results_(const optional &result, const char *address); }; } // namespace atc_mithermometer diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 6966c95db7..7794187a69 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -1,6 +1,6 @@ #include "audio_reader.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/defines.h" #include "esphome/core/hal.h" diff --git a/esphome/components/audio/audio_reader.h b/esphome/components/audio/audio_reader.h index 3fdc3c3ff2..0b73923e84 100644 --- a/esphome/components/audio/audio_reader.h +++ b/esphome/components/audio/audio_reader.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "audio.h" #include "audio_transfer_buffer.h" diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp index 2e548a8072..356f396476 100644 --- a/esphome/components/b_parasite/b_parasite.cpp +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -22,7 +22,8 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str_to(addr_buf)); const auto &service_datas = device.get_service_datas(); if (service_datas.size() != 1) { ESP_LOGE(TAG, "Unexpected service_datas size (%d)", service_datas.size()); diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index 38fcf29b3b..fec34c5b2a 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -193,8 +193,9 @@ bool BedJetHub::discover_characteristics_() { result = false; } else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { + char uuid_buf[espbt::UUID_STR_LEN]; ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_, - descr->uuid.to_string().c_str()); + descr->uuid.to_str(uuid_buf)); result = false; } else { this->config_descr_status_ = descr->handle; @@ -216,11 +217,14 @@ bool BedJetHub::discover_characteristics_() { } } - ESP_LOGI(TAG, "[%s] Discovered service characteristics: ", this->get_name().c_str()); - ESP_LOGI(TAG, " - Command char: 0x%x", this->char_handle_cmd_); - ESP_LOGI(TAG, " - Status char: 0x%x", this->char_handle_status_); - ESP_LOGI(TAG, " - config descriptor: 0x%x", this->config_descr_status_); - ESP_LOGI(TAG, " - Name char: 0x%x", this->char_handle_name_); + ESP_LOGI(TAG, + "[%s] Discovered service characteristics:\n" + " - Command char: 0x%x\n" + " - Status char: 0x%x\n" + " - config descriptor: 0x%x\n" + " - Name char: 0x%x", + this->get_name().c_str(), this->char_handle_cmd_, this->char_handle_status_, this->config_descr_status_, + this->char_handle_name_); return result; } diff --git a/esphome/components/bedjet/climate/__init__.py b/esphome/components/bedjet/climate/__init__.py index e9c5510256..4de9dcca0b 100644 --- a/esphome/components/bedjet/climate/__init__.py +++ b/esphome/components/bedjet/climate/__init__.py @@ -1,12 +1,7 @@ import esphome.codegen as cg -from esphome.components import ble_client, climate +from esphome.components import climate import esphome.config_validation as cv -from esphome.const import ( - CONF_HEAT_MODE, - CONF_RECEIVE_TIMEOUT, - CONF_TEMPERATURE_SOURCE, - CONF_TIME_ID, -) +from esphome.const import CONF_HEAT_MODE, CONF_TEMPERATURE_SOURCE from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child @@ -38,22 +33,6 @@ CONFIG_SCHEMA = ( } ) .extend(cv.polling_component_schema("60s")) - .extend( - # TODO: remove compat layer. - { - cv.Optional(ble_client.CONF_BLE_CLIENT_ID): cv.invalid( - "The 'ble_client_id' option has been removed. Please migrate " - "to the new `bedjet_id` option in the `bedjet` component.\n" - "See https://esphome.io/components/climate/bedjet.html" - ), - cv.Optional(CONF_TIME_ID): cv.invalid( - "The 'time_id' option has been moved to the `bedjet` component." - ), - cv.Optional(CONF_RECEIVE_TIMEOUT): cv.invalid( - "The 'receive_timeout' option has been moved to the `bedjet` component." - ), - } - ) .extend(BEDJET_CLIENT_SCHEMA) ) diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py index 7c7eecb88c..36af5aeef9 100644 --- a/esphome/components/bh1750/sensor.py +++ b/esphome/components/bh1750/sensor.py @@ -20,16 +20,6 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ) - .extend( - { - cv.Optional("resolution"): cv.invalid( - "The 'resolution' option has been removed. The optimal value is now dynamically calculated." - ), - cv.Optional("measurement_duration"): cv.invalid( - "The 'measurement_duration' option has been removed. The optimal value is now dynamically calculated." - ), - } - ) .extend(cv.polling_component_schema("60s")) .extend(i2c.i2c_device_schema(0x23)) ) diff --git a/esphome/components/bh1900nux/bh1900nux.cpp b/esphome/components/bh1900nux/bh1900nux.cpp index 96a06adaa0..0e71bd6532 100644 --- a/esphome/components/bh1900nux/bh1900nux.cpp +++ b/esphome/components/bh1900nux/bh1900nux.cpp @@ -23,7 +23,7 @@ void BH1900NUXSensor::setup() { i2c::ErrorCode result_code = this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication if (result_code != i2c::ERROR_OK) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } } diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index cbf935a501..c38d6b78d3 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -3,7 +3,7 @@ from logging import getLogger from esphome import automation, core from esphome.automation import Condition, maybe_simple_id import esphome.codegen as cg -from esphome.components import mqtt, web_server +from esphome.components import mqtt, web_server, zigbee from esphome.components.const import CONF_ON_STATE_CHANGE import esphome.config_validation as cv from esphome.const import ( @@ -439,6 +439,7 @@ def validate_publish_initial_state(value): _BINARY_SENSOR_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMPONENT_SCHEMA) + .extend(zigbee.BINARY_SENSOR_SCHEMA) .extend( { cv.GenerateID(): cv.declare_id(BinarySensor), @@ -520,6 +521,7 @@ _BINARY_SENSOR_SCHEMA = ( _BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor")) +_BINARY_SENSOR_SCHEMA.add_extra(zigbee.validate_binary_sensor) def binary_sensor_schema( @@ -621,6 +623,8 @@ async def setup_binary_sensor_core_(var, config): if web_server_config := config.get(CONF_WEB_SERVER): await web_server.add_entity_config(var, web_server_config) + await zigbee.setup_binary_sensor(var, config) + async def register_binary_sensor(var, config): if not CORE.has_id(config[CONF_ID]): diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index 66d8d6e90f..dfe911a2f8 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -21,8 +21,10 @@ void MultiClickTrigger::on_state_(bool state) { // Start matching MultiClickTriggerEvent evt = this->timing_[0]; if (evt.state == state) { - ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length); - ESP_LOGV(TAG, "Multi Click: Starting multi click action!"); + ESP_LOGV(TAG, + "START min=%" PRIu32 " max=%" PRIu32 "\n" + "Multi Click: Starting multi click action!", + evt.min_length, evt.max_length); this->at_index_ = 1; if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) { this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); }); diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 92b8db5c51..86b7350aa8 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -34,13 +34,20 @@ void BinarySensor::publish_initial_state(bool new_state) { void BinarySensor::send_state_internal(bool new_state) { // copy the new state to the visible property for backwards compatibility, before any callbacks this->state = new_state; - // Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed - if (this->set_state_(new_state)) { - ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state)); + // Note that set_new_state_ de-dups and will only trigger callbacks if the state has actually changed + this->set_new_state(new_state); +} + +bool BinarySensor::set_new_state(const optional &new_state) { + if (StatefulEntityBase::set_new_state(new_state)) { + // weirdly, this file could be compiled even without USE_BINARY_SENSOR defined #if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_binary_sensor_update(this); #endif + ESP_LOGD(TAG, "'%s': %s", this->get_name().c_str(), ONOFFMAYBE(new_state)); + return true; } + return false; } void BinarySensor::add_filter(Filter *filter) { diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 0dca3e1520..83c992bfed 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -61,6 +61,8 @@ class BinarySensor : public StatefulEntityBase, public EntityBase_DeviceCl protected: Filter *filter_list_{nullptr}; + + bool set_new_state(const optional &new_state) override; }; class BinarySensorInitiallyOff : public BinarySensor { diff --git a/esphome/components/ble_client/automation.cpp b/esphome/components/ble_client/automation.cpp index 9a0233eb70..cd2802f617 100644 --- a/esphome/components/ble_client/automation.cpp +++ b/esphome/components/ble_client/automation.cpp @@ -2,12 +2,10 @@ #include "automation.h" -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { const char *const Automation::TAG = "ble_client.automation"; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 9c5646b3d1..01590d1d53 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -7,10 +7,13 @@ #include "esphome/core/automation.h" #include "esphome/components/ble_client/ble_client.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ble_client { +// Maximum bytes to log in hex format for BLE writes (many logging buffers are 256 chars) +static constexpr size_t BLE_WRITE_MAX_LOG_BYTES = 64; + +namespace esphome::ble_client { // placeholder class for static TAG . class Automation { @@ -122,16 +125,19 @@ template class BLEClientWriteAction : public Action, publ void play_complex(const Ts &...x) override { this->num_running_++; this->var_ = std::make_tuple(x...); - std::vector value; + + bool result; if (this->len_ >= 0) { - // Static mode: copy from flash to vector - value.assign(this->value_.data, this->value_.data + this->len_); + // Static mode: write directly from flash pointer + result = this->write(this->value_.data, this->len_); } else { - // Template mode: call function - value = this->value_.func(x...); + // Template mode: call function and write the vector + std::vector value = this->value_.func(x...); + result = this->write(value); } + // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. - if (!write(value)) + if (!result) this->play_next_(x...); } @@ -144,15 +150,18 @@ template class BLEClientWriteAction : public Action, publ * errors. */ // initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event. - bool write(const std::vector &value) { + bool write(const uint8_t *data, size_t len) { if (this->node_state != espbt::ClientState::ESTABLISHED) { esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected"); return false; } - esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str()); - esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), - this->char_handle_, value.size(), const_cast(value.data()), - this->write_type_, ESP_GATT_AUTH_REQ_NONE); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(BLE_WRITE_MAX_LOG_BYTES)]; + esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len)); +#endif + esp_err_t err = + esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len, + const_cast(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE); if (err != ESP_OK) { esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); return false; @@ -160,6 +169,8 @@ template class BLEClientWriteAction : public Action, publ return true; } + bool write(const std::vector &value) { return this->write(value.data(), value.size()); } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { switch (event) { @@ -175,8 +186,10 @@ template class BLEClientWriteAction : public Action, publ case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); if (chr == nullptr) { + char char_buf[esp32_ble::UUID_STR_LEN]; + char service_buf[esp32_ble::UUID_STR_LEN]; esph_log_w("ble_write_action", "Characteristic %s was not found in service %s", - this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str()); + this->char_uuid_.to_str(char_buf), this->service_uuid_.to_str(service_buf)); break; } this->char_handle_ = chr->handle; @@ -188,12 +201,14 @@ template class BLEClientWriteAction : public Action, publ this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP; esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP"); } else { - esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str()); + char char_buf[esp32_ble::UUID_STR_LEN]; + esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_str(char_buf)); break; } this->node_state = espbt::ClientState::ESTABLISHED; - esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), - ble_client_->address_str().c_str()); + char char_buf[esp32_ble::UUID_STR_LEN]; + esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf), + ble_client_->address_str()); break; } default: @@ -386,7 +401,6 @@ template class BLEClientDisconnectAction : public Action, BLEClient *ble_client_; std::tuple var_{}; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 5cf096c9d4..d41fb17961 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -7,8 +7,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_client"; @@ -39,7 +38,7 @@ void BLEClient::set_enabled(bool enabled) { return; this->enabled = enabled; if (!enabled) { - ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); + ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str()); this->disconnect(); } } @@ -82,7 +81,6 @@ bool BLEClient::all_nodes_established_() { return true; } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index e04f4a8042..ca523251ef 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -15,8 +15,7 @@ #include #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -75,7 +74,6 @@ class BLEClient : public BLEClientBase { std::vector nodes_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/output/ble_binary_output.cpp b/esphome/components/ble_client/output/ble_binary_output.cpp index ce67193be7..1cb83b9d8b 100644 --- a/esphome/components/ble_client/output/ble_binary_output.cpp +++ b/esphome/components/ble_client/output/ble_binary_output.cpp @@ -3,19 +3,21 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_binary_output"; void BLEBinaryOutput::dump_config() { ESP_LOGCONFIG(TAG, "BLE Binary Output:"); + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; + this->service_uuid_.to_str(service_buf); + this->char_uuid_.to_str(char_buf); ESP_LOGCONFIG(TAG, " MAC address : %s\n" " Service UUID : %s\n" " Characteristic UUID: %s", - this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(), - this->char_uuid_.to_string().c_str()); + this->parent_->address_str(), service_buf, char_buf); LOG_BINARY_OUTPUT(this); } @@ -25,8 +27,10 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); if (chr == nullptr) { - ESP_LOGW(TAG, "Characteristic %s was not found in service %s", this->char_uuid_.to_string().c_str(), - this->service_uuid_.to_string().c_str()); + char char_buf[esp32_ble::UUID_STR_LEN]; + char service_buf[esp32_ble::UUID_STR_LEN]; + ESP_LOGW(TAG, "Characteristic %s was not found in service %s", this->char_uuid_.to_str(char_buf), + this->service_uuid_.to_str(service_buf)); break; } this->char_handle_ = chr->handle; @@ -38,20 +42,24 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP; ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP"); } else { - ESP_LOGE(TAG, "Characteristic %s does not allow writing with%s response", this->char_uuid_.to_string().c_str(), + char char_buf[esp32_ble::UUID_STR_LEN]; + ESP_LOGE(TAG, "Characteristic %s does not allow writing with%s response", this->char_uuid_.to_str(char_buf), this->require_response_ ? "" : "out"); break; } this->node_state = espbt::ClientState::ESTABLISHED; - ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), - this->parent()->address_str().c_str()); + char char_buf[esp32_ble::UUID_STR_LEN]; + ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf), + this->parent()->address_str()); this->node_state = espbt::ClientState::ESTABLISHED; break; } case ESP_GATTC_WRITE_CHAR_EVT: { if (param->write.handle == this->char_handle_) { - if (param->write.status != 0) - ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status); + if (param->write.status != 0) { + char char_buf[esp32_ble::UUID_STR_LEN]; + ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_str(char_buf), param->write.status); + } } break; } @@ -61,20 +69,20 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i } void BLEBinaryOutput::write_state(bool state) { + char char_buf[esp32_ble::UUID_STR_LEN]; if (this->node_state != espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", - this->char_uuid_.to_string().c_str()); + this->char_uuid_.to_str(char_buf)); return; } uint8_t state_as_uint = (uint8_t) state; - ESP_LOGV(TAG, "[%s] Write State: %d", this->char_uuid_.to_string().c_str(), state_as_uint); + ESP_LOGV(TAG, "[%s] Write State: %d", this->char_uuid_.to_str(char_buf), state_as_uint); esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE); if (err != ESP_GATT_OK) - ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_string().c_str(), err); + ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_str(char_buf), err); } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/output/ble_binary_output.h b/esphome/components/ble_client/output/ble_binary_output.h index 5e8bd6da62..299de9b860 100644 --- a/esphome/components/ble_client/output/ble_binary_output.h +++ b/esphome/components/ble_client/output/ble_binary_output.h @@ -7,8 +7,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -36,7 +35,6 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi esp_gatt_write_type_t write_type_{}; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/automation.h b/esphome/components/ble_client/sensor/automation.h index 56ab7ba4c9..84430cb7d9 100644 --- a/esphome/components/ble_client/sensor/automation.h +++ b/esphome/components/ble_client/sensor/automation.h @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { class BLESensorNotifyTrigger : public Trigger, public BLESensor { public: @@ -35,7 +34,6 @@ class BLESensorNotifyTrigger : public Trigger, public BLESensor { BLESensor *sensor_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp index 663c52ac10..dc032a7a98 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.cpp @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_rssi_sensor"; @@ -19,7 +18,7 @@ void BLEClientRSSISensor::loop() { void BLEClientRSSISensor::dump_config() { LOG_SENSOR("", "BLE Client RSSI Sensor", this); - ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str()); LOG_UPDATE_INTERVAL(this); } @@ -69,15 +68,14 @@ void BLEClientRSSISensor::update() { this->get_rssi_(); } void BLEClientRSSISensor::get_rssi_() { - ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str().c_str()); + ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str()); auto status = esp_ble_gap_read_rssi(this->parent()->get_remote_bda()); if (status != ESP_OK) { - ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str().c_str(), status); + ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str(), status); this->status_set_warning(); this->publish_state(NAN); } } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_rssi_sensor.h b/esphome/components/ble_client/sensor/ble_rssi_sensor.h index 76cd8345a6..570a5b423c 100644 --- a/esphome/components/ble_client/sensor/ble_rssi_sensor.h +++ b/esphome/components/ble_client/sensor/ble_rssi_sensor.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -29,6 +28,5 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ bool should_update_{false}; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index 61685c0566..fe5f11bbc2 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_sensor"; @@ -19,14 +18,17 @@ void BLESensor::loop() { void BLESensor::dump_config() { LOG_SENSOR("", "BLE Sensor", this); + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; + char descr_buf[esp32_ble::UUID_STR_LEN]; ESP_LOGCONFIG(TAG, " MAC address : %s\n" " Service UUID : %s\n" " Characteristic UUID: %s\n" " Descriptor UUID : %s\n" " Notifications : %s", - this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(), - this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_)); + this->parent()->address_str(), this->service_uuid_.to_str(service_buf), + this->char_uuid_.to_str(char_buf), this->descr_uuid_.to_str(descr_buf), YESNO(this->notify_)); LOG_UPDATE_INTERVAL(this); } @@ -52,8 +54,10 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga if (chr == nullptr) { this->status_set_warning(); this->publish_state(NAN); - ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), - this->char_uuid_.to_string().c_str()); + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_str(service_buf), + this->char_uuid_.to_str(char_buf)); break; } this->handle = chr->handle; @@ -62,9 +66,12 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga if (descr == nullptr) { this->status_set_warning(); this->publish_state(NAN); + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; + char descr_buf[esp32_ble::UUID_STR_LEN]; ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s", - this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), - this->descr_uuid_.to_string().c_str()); + this->service_uuid_.to_str(service_buf), this->char_uuid_.to_str(char_buf), + this->descr_uuid_.to_str(descr_buf)); break; } this->handle = descr->handle; @@ -110,7 +117,8 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga break; } this->node_state = espbt::ClientState::ESTABLISHED; - ESP_LOGD(TAG, "Register for notify on %s complete", this->char_uuid_.to_string().c_str()); + char char_buf[esp32_ble::UUID_STR_LEN]; + ESP_LOGD(TAG, "Register for notify on %s complete", this->char_uuid_.to_str(char_buf)); } break; } @@ -147,6 +155,5 @@ void BLESensor::update() { } } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h index c6335d5836..fe5b5ecd53 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.h +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -10,8 +10,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -48,6 +47,5 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie espbt::ESPBTUUID descr_uuid_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/switch/ble_switch.cpp b/esphome/components/ble_client/switch/ble_switch.cpp index 9d92b1b2b5..5baca2adcf 100644 --- a/esphome/components/ble_client/switch/ble_switch.cpp +++ b/esphome/components/ble_client/switch/ble_switch.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_switch"; @@ -31,6 +30,5 @@ void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h index 9809f904e7..9be6d06b1c 100644 --- a/esphome/components/ble_client/switch/ble_switch.h +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -24,6 +23,5 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie void write_state(bool state) override; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/text_sensor/automation.h b/esphome/components/ble_client/text_sensor/automation.h index c504c35a58..d4114cd1ba 100644 --- a/esphome/components/ble_client/text_sensor/automation.h +++ b/esphome/components/ble_client/text_sensor/automation.h @@ -5,8 +5,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { class BLETextSensorNotifyTrigger : public Trigger, public BLETextSensor { public: @@ -22,7 +21,7 @@ class BLETextSensorNotifyTrigger : public Trigger, public BLETextSe if (param->notify.conn_id != this->sensor_->parent()->get_conn_id() || param->notify.handle != this->sensor_->handle) break; - this->trigger(this->sensor_->parse_data(param->notify.value, param->notify.value_len)); + this->trigger(std::string(reinterpret_cast(param->notify.value), param->notify.value_len)); } default: break; @@ -33,7 +32,6 @@ class BLETextSensorNotifyTrigger : public Trigger, public BLETextSe BLETextSensor *sensor_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index b7a6d154db..cacf1b4835 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -7,13 +7,10 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { static const char *const TAG = "ble_text_sensor"; -static const std::string EMPTY = ""; - void BLETextSensor::loop() { // Parent BLEClientNode has a loop() method, but this component uses // polling via update() and BLE callbacks so loop isn't needed @@ -22,14 +19,17 @@ void BLETextSensor::loop() { void BLETextSensor::dump_config() { LOG_TEXT_SENSOR("", "BLE Text Sensor", this); + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; + char descr_buf[esp32_ble::UUID_STR_LEN]; ESP_LOGCONFIG(TAG, " MAC address : %s\n" " Service UUID : %s\n" " Characteristic UUID: %s\n" " Descriptor UUID : %s\n" " Notifications : %s", - this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(), - this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_)); + this->parent()->address_str(), this->service_uuid_.to_str(service_buf), + this->char_uuid_.to_str(char_buf), this->descr_uuid_.to_str(descr_buf), YESNO(this->notify_)); LOG_UPDATE_INTERVAL(this); } @@ -45,7 +45,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } case ESP_GATTC_CLOSE_EVT: { this->status_set_warning(); - this->publish_state(EMPTY); + this->publish_state(""); break; } case ESP_GATTC_SEARCH_CMPL_EVT: { @@ -53,9 +53,11 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); if (chr == nullptr) { this->status_set_warning(); - this->publish_state(EMPTY); - ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), - this->char_uuid_.to_string().c_str()); + this->publish_state(""); + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_str(service_buf), + this->char_uuid_.to_str(char_buf)); break; } this->handle = chr->handle; @@ -63,10 +65,13 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ auto *descr = chr->get_descriptor(this->descr_uuid_); if (descr == nullptr) { this->status_set_warning(); - this->publish_state(EMPTY); + this->publish_state(""); + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; + char descr_buf[esp32_ble::UUID_STR_LEN]; ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s", - this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), - this->descr_uuid_.to_string().c_str()); + this->service_uuid_.to_str(service_buf), this->char_uuid_.to_str(char_buf), + this->descr_uuid_.to_str(descr_buf)); break; } this->handle = descr->handle; @@ -92,7 +97,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } this->status_clear_warning(); - this->publish_state(this->parse_data(param->read.value, param->read.value_len)); + this->publish_state(reinterpret_cast(param->read.value), param->read.value_len); } break; } @@ -101,7 +106,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); - this->publish_state(this->parse_data(param->notify.value, param->notify.value_len)); + this->publish_state(reinterpret_cast(param->notify.value), param->notify.value_len); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { @@ -114,11 +119,6 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } } -std::string BLETextSensor::parse_data(uint8_t *value, uint16_t value_len) { - std::string text(value, value + value_len); - return text; -} - void BLETextSensor::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str()); @@ -133,11 +133,10 @@ void BLETextSensor::update() { ESP_GATT_AUTH_REQ_NONE); if (status) { this->status_set_warning(); - this->publish_state(EMPTY); + this->publish_state(""); ESP_LOGW(TAG, "[%s] Error sending read request for sensor, status=%d", this->get_name().c_str(), status); } } -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.h b/esphome/components/ble_client/text_sensor/ble_text_sensor.h index c75a4df952..b4374e4016 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.h +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.h @@ -8,8 +8,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace ble_client { +namespace esphome::ble_client { namespace espbt = esphome::esp32_ble_tracker; @@ -30,7 +29,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_enable_notify(bool notify) { this->notify_ = notify; } - std::string parse_data(uint8_t *value, uint16_t value_len); uint16_t handle; protected: @@ -40,6 +38,5 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p espbt::ESPBTUUID descr_uuid_; }; -} // namespace ble_client -} // namespace esphome +} // namespace esphome::ble_client #endif diff --git a/esphome/components/ble_nus/ble_nus.cpp b/esphome/components/ble_nus/ble_nus.cpp index 9c4d0a3938..0de65b623f 100644 --- a/esphome/components/ble_nus/ble_nus.cpp +++ b/esphome/components/ble_nus/ble_nus.cpp @@ -87,20 +87,26 @@ void BLENUS::setup() { global_ble_nus = this; #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - this->write_array(reinterpret_cast(message), message_len); - const char c = '\n'; - this->write_array(reinterpret_cast(&c), 1); - }); + logger::global_logger->add_log_listener(this); } - #endif } +#ifdef USE_LOGGER +void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + (void) level; + (void) tag; + this->write_array(reinterpret_cast(message), message_len); + const char c = '\n'; + this->write_array(reinterpret_cast(&c), 1); +} +#endif + void BLENUS::dump_config() { - ESP_LOGCONFIG(TAG, "ble nus:"); - ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_)); + ESP_LOGCONFIG(TAG, + "ble nus:\n" + " log: %s", + YESNO(this->expose_log_)); uint32_t mtu = 0; bt_conn *conn = this->conn_.load(); if (conn) { diff --git a/esphome/components/ble_nus/ble_nus.h b/esphome/components/ble_nus/ble_nus.h index e8cba32b4c..ef20fc5e5b 100644 --- a/esphome/components/ble_nus/ble_nus.h +++ b/esphome/components/ble_nus/ble_nus.h @@ -2,12 +2,20 @@ #ifdef USE_ZEPHYR #include "esphome/core/defines.h" #include "esphome/core/component.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #include #include namespace esphome::ble_nus { -class BLENUS : public Component { +class BLENUS : public Component +#ifdef USE_LOGGER + , + public logger::LogListener +#endif +{ enum TxStatus { TX_DISABLED, TX_ENABLED, @@ -20,6 +28,9 @@ class BLENUS : public Component { void loop() override; size_t write_array(const uint8_t *data, size_t len); void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; } +#ifdef USE_LOGGER + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; +#endif protected: static void send_enabled_callback(bt_nus_send_status status); diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h index 8bb51fcff2..7061b6d336 100644 --- a/esphome/components/ble_scanner/ble_scanner.h +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -1,7 +1,8 @@ #pragma once +#include +#include #include -#include #include "esphome/core/component.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" @@ -15,17 +16,13 @@ namespace ble_scanner { class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - this->publish_state("{\"timestamp\":" + to_string(::time(nullptr)) + - "," - "\"address\":\"" + - device.address_str() + - "\"," - "\"rssi\":" + - to_string(device.get_rssi()) + - "," - "\"name\":\"" + - device.get_name() + "\"}"); - + // Format JSON using stack buffer to avoid heap allocations from string concatenation + char buf[128]; + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + snprintf(buf, sizeof(buf), "{\"timestamp\":%" PRId64 ",\"address\":\"%s\",\"rssi\":%d,\"name\":\"%s\"}", + static_cast(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(), + device.get_name().c_str()); + this->publish_state(buf); return true; } void dump_config() override; diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index fcc344dda9..1d6f7e23b3 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -196,8 +196,8 @@ void BluetoothConnection::send_service_for_discovery_() { if (service_status != ESP_GATT_OK || service_count == 0) { ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d", - this->connection_index_, this->address_str().c_str(), - service_status != ESP_GATT_OK ? "error" : "missing", service_status, service_count, this->send_service_); + this->connection_index_, this->address_str(), service_status != ESP_GATT_OK ? "error" : "missing", + service_status, service_count, this->send_service_); this->send_service_ = DONE_SENDING_SERVICES; return; } @@ -312,13 +312,13 @@ void BluetoothConnection::send_service_for_discovery_() { if (resp.services.size() > 1) { resp.services.pop_back(); ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch", - this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size, + this->connection_index_, this->address_str(), this->send_service_, current_size, service_size, MAX_PACKET_SIZE); // Don't increment send_service_ - we'll retry this service in next batch } else { // This single service is too large, but we have to send it anyway ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_, - this->address_str().c_str(), this->send_service_, service_size); + this->address_str(), this->send_service_, service_size); // Increment so we don't get stuck this->send_service_++; } @@ -337,21 +337,20 @@ void BluetoothConnection::send_service_for_discovery_() { } void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) { - ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str().c_str(), operation, - status); + ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str(), operation, status); } void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) { - ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str().c_str(), operation, err); + ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str(), operation, err); } void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) { - ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str().c_str(), - action, type); + ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str(), action, + type); } void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) { - ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str().c_str(), + ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str(), operation, handle, status); } @@ -372,14 +371,14 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga case ESP_GATTC_DISCONNECT_EVT: { // Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources // This prevents race condition where we mark slot as free before controller cleanup is complete - ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_, param->disconnect.reason); // Send disconnection notification but don't free the slot yet this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); break; } case ESP_GATTC_CLOSE_EVT: { - ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_, param->close.reason); // Now the GATT connection is fully closed and controller resources are freed // Safe to mark the connection slot as available @@ -463,7 +462,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga break; } case ESP_GATTC_NOTIFY_EVT: { - ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_.c_str(), + ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_, param->notify.handle); api::BluetoothGATTNotifyDataResponse resp; resp.address = this->address_; @@ -502,8 +501,7 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) { return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle); esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); return this->check_and_log_error_("esp_ble_gattc_read_char", err); @@ -515,8 +513,7 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8 this->log_gatt_not_connected_("write", "characteristic"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle); // ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data // The BTC layer immediately copies the data to its own buffer (see btc_gattc.c) @@ -532,8 +529,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) { this->log_gatt_not_connected_("read", "descriptor"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle); esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err); @@ -544,8 +540,7 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t * this->log_gatt_not_connected_("write", "descriptor"); return ESP_GATT_NOT_CONNECTED; } - ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), - handle); + ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle); // ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data // The BTC layer immediately copies the data to its own buffer (see btc_gattc.c) @@ -564,13 +559,13 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl if (enable) { ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_, - this->address_str_.c_str(), handle); + this->address_str_, handle); esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle); return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err); } ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_, - this->address_str_.c_str(), handle); + this->address_str_, handle); esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle); return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err); } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 34e0aa93a3..d45377b3f6 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -27,11 +27,13 @@ void BluetoothProxy::setup() { // Capture the configured scan mode from YAML before any API changes this->configured_scan_active_ = this->parent_->get_scan_active(); - this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { - if (this->api_connection_ != nullptr) { - this->send_bluetooth_scanner_state_(state); - } - }); + this->parent_->add_scanner_state_listener(this); +} + +void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) { + if (this->api_connection_ != nullptr) { + this->send_bluetooth_scanner_state_(state); + } } void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state) { @@ -47,12 +49,11 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) { ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(), - connection->address_str().c_str(), espbt::client_state_to_string(state)); + connection->address_str(), espbt::client_state_to_string(state)); } void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) { - ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str().c_str(), - message); + ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str(), message); } void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) { @@ -186,7 +187,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest } if (!msg.has_address_type) { ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(), - connection->address_str().c_str()); + connection->address_str()); this->send_device_connection(msg.address, false); return; } @@ -199,7 +200,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest } else if (connection->state() == espbt::ClientState::CONNECTING) { if (connection->disconnect_pending()) { ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect", - connection->get_connection_index(), connection->address_str().c_str()); + connection->get_connection_index(), connection->address_str()); connection->cancel_pending_disconnect(); return; } @@ -339,7 +340,7 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer return; } if (!connection->service_count_) { - ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str()); + ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str()); this->send_gatt_services_done(msg.address); return; } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index a5f0fbe32f..ab9aee2d81 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -52,7 +52,9 @@ enum BluetoothProxySubscriptionFlag : uint32_t { SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, }; -class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component { +class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, + public esp32_ble_tracker::BLEScannerStateListener, + public Component { friend class BluetoothConnection; // Allow connection to update connections_free_response_ public: BluetoothProxy(); @@ -108,6 +110,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ void set_active(bool active) { this->active_ = active; } bool has_active() { return this->active_; } + /// BLEScannerStateListener interface + void on_scanner_state(esp32_ble_tracker::ScannerState state) override; + uint32_t get_legacy_version() const { if (this->active_) { return LEGACY_ACTIVE_CONNECTIONS_VERSION; @@ -130,11 +135,13 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ return flags; } - std::string get_bluetooth_mac_address_pretty() { + void get_bluetooth_mac_address_pretty(std::span output) { const uint8_t *mac = esp_bt_dev_get_address(); - char buf[18]; - format_mac_addr_upper(mac, buf); - return std::string(buf); + if (mac != nullptr) { + format_mac_addr_upper(mac, output.data()); + } else { + output[0] = '\0'; + } } protected: diff --git a/esphome/components/bm8563/__init__.py b/esphome/components/bm8563/__init__.py new file mode 100644 index 0000000000..20254a8b00 --- /dev/null +++ b/esphome/components/bm8563/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@abmantis"] diff --git a/esphome/components/bm8563/bm8563.cpp b/esphome/components/bm8563/bm8563.cpp new file mode 100644 index 0000000000..07831485c1 --- /dev/null +++ b/esphome/components/bm8563/bm8563.cpp @@ -0,0 +1,198 @@ +#include "bm8563.h" +#include "esphome/core/log.h" + +namespace esphome::bm8563 { + +static const char *const TAG = "bm8563"; + +static constexpr uint8_t CONTROL_STATUS_1_REG = 0x00; +static constexpr uint8_t CONTROL_STATUS_2_REG = 0x01; +static constexpr uint8_t TIME_FIRST_REG = 0x02; // Time uses reg 2, 3, 4 +static constexpr uint8_t DATE_FIRST_REG = 0x05; // Date uses reg 5, 6, 7, 8 +static constexpr uint8_t TIMER_CONTROL_REG = 0x0E; +static constexpr uint8_t TIMER_VALUE_REG = 0x0F; +static constexpr uint8_t CLOCK_1_HZ = 0x82; +static constexpr uint8_t CLOCK_1_60_HZ = 0x83; +// Maximum duration: 255 minutes (at 1/60 Hz) = 15300 seconds +static constexpr uint32_t MAX_TIMER_DURATION_S = 255 * 60; + +void BM8563::setup() { + if (!this->write_byte_16(CONTROL_STATUS_1_REG, 0)) { + this->mark_failed(); + return; + } +} + +void BM8563::update() { this->read_time(); } + +void BM8563::dump_config() { + ESP_LOGCONFIG(TAG, "BM8563:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +void BM8563::start_timer(uint32_t duration_s) { + this->clear_irq_(); + this->set_timer_irq_(duration_s); +} + +void BM8563::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + + ESP_LOGD(TAG, "Writing time: %i-%i-%i %i, %i:%i:%i", now.year, now.month, now.day_of_month, now.day_of_week, now.hour, + now.minute, now.second); + + this->set_time_(now); + this->set_date_(now); +} + +void BM8563::read_time() { + ESPTime rtc_time; + this->get_time_(rtc_time); + this->get_date_(rtc_time); + rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid + ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month, + rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second); + + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +uint8_t BM8563::bcd2_to_byte_(uint8_t value) { + const uint8_t tmp = ((value & 0xF0) >> 0x4) * 10; + return tmp + (value & 0x0F); +} + +uint8_t BM8563::byte_to_bcd2_(uint8_t value) { + const uint8_t bcdhigh = value / 10; + value -= bcdhigh * 10; + return (bcdhigh << 4) | value; +} + +void BM8563::get_time_(ESPTime &time) { + uint8_t buf[3] = {0}; + this->read_register(TIME_FIRST_REG, buf, 3); + + time.second = this->bcd2_to_byte_(buf[0] & 0x7f); + time.minute = this->bcd2_to_byte_(buf[1] & 0x7f); + time.hour = this->bcd2_to_byte_(buf[2] & 0x3f); +} + +void BM8563::set_time_(const ESPTime &time) { + uint8_t buf[3] = {this->byte_to_bcd2_(time.second), this->byte_to_bcd2_(time.minute), this->byte_to_bcd2_(time.hour)}; + this->write_register_(TIME_FIRST_REG, buf, 3); +} + +void BM8563::get_date_(ESPTime &time) { + uint8_t buf[4] = {0}; + this->read_register(DATE_FIRST_REG, buf, sizeof(buf)); + + time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f); + time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07); + time.month = this->bcd2_to_byte_(buf[2] & 0x1f); + + uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff); + + if (buf[2] & 0x80) { + time.year = 1900 + year_byte; + } else { + time.year = 2000 + year_byte; + } +} + +void BM8563::set_date_(const ESPTime &time) { + uint8_t buf[4] = { + this->byte_to_bcd2_(time.day_of_month), + this->byte_to_bcd2_(time.day_of_week), + this->byte_to_bcd2_(time.month), + this->byte_to_bcd2_(time.year % 100), + }; + + if (time.year < 2000) { + buf[2] = buf[2] | 0x80; + } + + this->write_register_(DATE_FIRST_REG, buf, 4); +} + +void BM8563::write_byte_(uint8_t reg, uint8_t value) { + if (!this->write_byte(reg, value)) { + ESP_LOGE(TAG, "Failed to write byte 0x%02X with value 0x%02X", reg, value); + } +} + +void BM8563::write_register_(uint8_t reg, const uint8_t *data, size_t len) { + if (auto error = this->write_register(reg, data, len); error != i2c::ErrorCode::NO_ERROR) { + ESP_LOGE(TAG, "Failed to write register 0x%02X with %zu bytes", reg, len); + } +} + +optional BM8563::read_register_(uint8_t reg) { + uint8_t data; + if (auto error = this->read_register(reg, &data, 1); error != i2c::ErrorCode::NO_ERROR) { + ESP_LOGE(TAG, "Failed to read register 0x%02X", reg); + return {}; + } + return data; +} + +void BM8563::set_timer_irq_(uint32_t duration_s) { + ESP_LOGI(TAG, "Timer Duration: %u s", duration_s); + + if (duration_s > MAX_TIMER_DURATION_S) { + ESP_LOGW(TAG, "Timer duration %u s exceeds maximum %u s", duration_s, MAX_TIMER_DURATION_S); + return; + } + + if (duration_s > 255) { + uint8_t duration_minutes = duration_s / 60; + this->write_byte_(TIMER_VALUE_REG, duration_minutes); + this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_60_HZ); + } else { + this->write_byte_(TIMER_VALUE_REG, duration_s); + this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_HZ); + } + + auto maybe_ctrl_status_2 = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_ctrl_status_2.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t ctrl_status_2_reg_value = maybe_ctrl_status_2.value(); + ctrl_status_2_reg_value |= (1 << 0); + ctrl_status_2_reg_value &= ~(1 << 7); + this->write_byte_(CONTROL_STATUS_2_REG, ctrl_status_2_reg_value); +} + +void BM8563::clear_irq_() { + auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_data.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t data = maybe_data.value(); + this->write_byte_(CONTROL_STATUS_2_REG, data & 0xf3); +} + +void BM8563::disable_irq_() { + this->clear_irq_(); + auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG); + if (!maybe_data.has_value()) { + ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG"); + return; + } + uint8_t data = maybe_data.value(); + this->write_byte_(CONTROL_STATUS_2_REG, data & 0xfc); +} + +} // namespace esphome::bm8563 diff --git a/esphome/components/bm8563/bm8563.h b/esphome/components/bm8563/bm8563.h new file mode 100644 index 0000000000..eda2d1b3c0 --- /dev/null +++ b/esphome/components/bm8563/bm8563.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome::bm8563 { + +class BM8563 : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + void write_time(); + void read_time(); + void start_timer(uint32_t duration_s); + + private: + void get_time_(ESPTime &time); + void get_date_(ESPTime &time); + + void set_time_(const ESPTime &time); + void set_date_(const ESPTime &time); + + void set_timer_irq_(uint32_t duration_s); + void clear_irq_(); + void disable_irq_(); + + void write_byte_(uint8_t reg, uint8_t value); + void write_register_(uint8_t reg, const uint8_t *data, size_t len); + optional read_register_(uint8_t reg); + + uint8_t bcd2_to_byte_(uint8_t value); + uint8_t byte_to_bcd2_(uint8_t value); +}; + +template class WriteAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->read_time(); } +}; + +template class TimerAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint32_t, duration) + + void play(const Ts &...x) override { + auto duration = this->duration_.value(x...); + this->parent_->start_timer(duration); + } +}; + +} // namespace esphome::bm8563 diff --git a/esphome/components/bm8563/time.py b/esphome/components/bm8563/time.py new file mode 100644 index 0000000000..2785315af2 --- /dev/null +++ b/esphome/components/bm8563/time.py @@ -0,0 +1,80 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import i2c, time +import esphome.config_validation as cv +from esphome.const import CONF_DURATION, CONF_ID + +DEPENDENCIES = ["i2c"] + +I2C_ADDR = 0x51 + +bm8563_ns = cg.esphome_ns.namespace("bm8563") +BM8563 = bm8563_ns.class_("BM8563", time.RealTimeClock, i2c.I2CDevice) +WriteAction = bm8563_ns.class_("WriteAction", automation.Action) +ReadAction = bm8563_ns.class_("ReadAction", automation.Action) +TimerAction = bm8563_ns.class_("TimerAction", automation.Action) + +CONFIG_SCHEMA = ( + time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BM8563), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(I2C_ADDR)) +) + + +@automation.register_action( + "bm8563.write_time", + WriteAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(BM8563), + } + ), +) +async def bm8563_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "bm8563.start_timer", + TimerAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(BM8563), + cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds), + } + ), +) +async def bm8563_start_timer_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32) + cg.add(var.set_duration(template_)) + return var + + +@automation.register_action( + "bm8563.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(BM8563), + } + ), +) +async def bm8563_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await time.register_time(var, config) diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index 86b65d361d..c5d4c9c0a5 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -100,18 +100,18 @@ void BME280Component::setup() { if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (chip_id != 0x60) { this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(BME280_ERROR_WRONG_CHIP_ID); + this->mark_failed(LOG_STR(BME280_ERROR_WRONG_CHIP_ID)); return; } // Send a soft reset. if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) { - this->mark_failed("Reset failed"); + this->mark_failed(LOG_STR("Reset failed")); return; } // Wait until the NVM data has finished loading. @@ -120,12 +120,12 @@ void BME280Component::setup() { do { // NOLINT delay(2); if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { - this->mark_failed("Error reading status register"); + this->mark_failed(LOG_STR("Error reading status register")); return; } } while ((status & BME280_STATUS_IM_UPDATE) && (--retry)); if (status & BME280_STATUS_IM_UPDATE) { - this->mark_failed("Timeout loading NVM"); + this->mark_failed(LOG_STR("Timeout loading NVM")); return; } @@ -153,26 +153,26 @@ void BME280Component::setup() { uint8_t humid_control_val = 0; if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) { - this->mark_failed("Read humidity control"); + this->mark_failed(LOG_STR("Read humidity control")); return; } humid_control_val &= ~0b00000111; humid_control_val |= this->humidity_oversampling_ & 0b111; if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) { - this->mark_failed("Write humidity control"); + this->mark_failed(LOG_STR("Write humidity control")); return; } uint8_t config_register = 0; if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) { - this->mark_failed("Read config"); + this->mark_failed(LOG_STR("Read config")); return; } config_register &= ~0b11111100; config_register |= 0b101 << 5; // 1000 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) { - this->mark_failed("Write config"); + this->mark_failed(LOG_STR("Write config")); return; } } diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 8a8d74b5f3..06e641d34d 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -69,7 +69,7 @@ CONFIG_SCHEMA = cv.All( cv.only_on_esp8266, cv.All( cv.only_on_esp32, - esp32.only_on_variant(supported=[esp32.const.VARIANT_ESP32]), + esp32.only_on_variant(supported=[esp32.VARIANT_ESP32]), ), ), ) diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index f5dcfd65a1..91383c8d45 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -70,6 +70,9 @@ void BME68xBSEC2Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_, this->bme68x_status_); + if (this->bsec_status_ == BSEC_I_SU_SUBSCRIBEDOUTPUTGATES) { + ESP_LOGE(TAG, "No sensors, add at least one sensor to the config"); + } } if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) { diff --git a/esphome/components/bme68x_bsec2/sensor.py b/esphome/components/bme68x_bsec2/sensor.py index c7dca437d7..45a9e54c1e 100644 --- a/esphome/components/bme68x_bsec2/sensor.py +++ b/esphome/components/bme68x_bsec2/sensor.py @@ -50,6 +50,7 @@ TYPES = [ CONFIG_SCHEMA = cv.Schema( { + cv.GenerateID(): cv.declare_id(cg.Component), cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, diff --git a/esphome/components/bme68x_bsec2_i2c/__init__.py b/esphome/components/bme68x_bsec2_i2c/__init__.py index d6fb7fa9be..c8ca0ba022 100644 --- a/esphome/components/bme68x_bsec2_i2c/__init__.py +++ b/esphome/components/bme68x_bsec2_i2c/__init__.py @@ -11,6 +11,7 @@ CODEOWNERS = ["@neffs", "@kbx81"] AUTO_LOAD = ["bme68x_bsec2"] DEPENDENCIES = ["i2c"] +MULTI_CONF = True bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c") BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_( diff --git a/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp index 50eaf33add..2d74ba6b12 100644 --- a/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp +++ b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp @@ -31,7 +31,11 @@ void BME68xBSEC2I2CComponent::dump_config() { BME68xBSEC2Component::dump_config(); } -uint32_t BME68xBSEC2I2CComponent::get_hash() { return fnv1_hash("bme68x_bsec_state_" + to_string(this->address_)); } +uint32_t BME68xBSEC2I2CComponent::get_hash() { + char buf[22]; // "bme68x_bsec_state_" (18) + uint8_t max (3) + null + snprintf(buf, sizeof(buf), "bme68x_bsec_state_%u", this->address_); + return fnv1_hash(buf); +} int8_t BME68xBSEC2I2CComponent::read_bytes_wrapper(uint8_t a_register, uint8_t *data, uint32_t len, void *intfPtr) { ESP_LOGVV(TAG, "read_bytes_wrapper: reg = %u", a_register); diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 39654f5875..728eead521 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -65,23 +65,23 @@ void BMP280Component::setup() { // https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855 if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } if (chip_id != 0x58) { this->error_code_ = WRONG_CHIP_ID; - this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID); + this->mark_failed(LOG_STR(BMP280_ERROR_WRONG_CHIP_ID)); return; } // Send a soft reset. if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { - this->mark_failed("Reset failed"); + this->mark_failed(LOG_STR("Reset failed")); return; } // Wait until the NVM data has finished loading. @@ -90,12 +90,12 @@ void BMP280Component::setup() { do { delay(2); if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) { - this->mark_failed("Error reading status register"); + this->mark_failed(LOG_STR("Error reading status register")); return; } } while ((status & BMP280_STATUS_IM_UPDATE) && (--retry)); if (status & BMP280_STATUS_IM_UPDATE) { - this->mark_failed("Timeout loading NVM"); + this->mark_failed(LOG_STR("Timeout loading NVM")); return; } @@ -116,14 +116,14 @@ void BMP280Component::setup() { uint8_t config_register = 0; if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) { - this->mark_failed("Read config"); + this->mark_failed(LOG_STR("Read config")); return; } config_register &= ~0b11111100; config_register |= 0b000 << 5; // 0.5 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) { - this->mark_failed("Write config"); + this->mark_failed(LOG_STR("Write config")); return; } } diff --git a/esphome/components/bp1658cj/bp1658cj.cpp b/esphome/components/bp1658cj/bp1658cj.cpp index b8ad5dc3d2..d5516384ff 100644 --- a/esphome/components/bp1658cj/bp1658cj.cpp +++ b/esphome/components/bp1658cj/bp1658cj.cpp @@ -22,13 +22,13 @@ void BP1658CJ::setup() { this->pwm_amounts_.resize(5, 0); } void BP1658CJ::dump_config() { - ESP_LOGCONFIG(TAG, "BP1658CJ:"); - LOG_PIN(" Data Pin: ", this->data_pin_); - LOG_PIN(" Clock Pin: ", this->clock_pin_); ESP_LOGCONFIG(TAG, + "BP1658CJ:\n" " Color Channels Max Power: %u\n" " White Channels Max Power: %u", this->max_power_color_channels_, this->max_power_white_channels_); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); } void BP1658CJ::loop() { diff --git a/esphome/components/bthome_mithermometer/__init__.py b/esphome/components/bthome_mithermometer/__init__.py new file mode 100644 index 0000000000..0e84278afa --- /dev/null +++ b/esphome/components/bthome_mithermometer/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +from esphome.components import esp32_ble_tracker +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_MAC_ADDRESS + +CODEOWNERS = ["@nagyrobi"] +DEPENDENCIES = ["esp32_ble_tracker"] + +BLE_DEVICE_SCHEMA = esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA + +bthome_mithermometer_ns = cg.esphome_ns.namespace("bthome_mithermometer") +BTHomeMiThermometer = bthome_mithermometer_ns.class_( + "BTHomeMiThermometer", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + + +def bthome_mithermometer_base_schema(extra_schema=None): + if extra_schema is None: + extra_schema = {} + return ( + cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + } + ) + .extend(BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) + .extend(extra_schema) + ) + + +async def setup_bthome_mithermometer(var, config): + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) diff --git a/esphome/components/bthome_mithermometer/bthome_ble.cpp b/esphome/components/bthome_mithermometer/bthome_ble.cpp new file mode 100644 index 0000000000..d1c5165896 --- /dev/null +++ b/esphome/components/bthome_mithermometer/bthome_ble.cpp @@ -0,0 +1,300 @@ +#include "bthome_ble.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include +#include + +#ifdef USE_ESP32 + +namespace esphome { +namespace bthome_mithermometer { + +static const char *const TAG = "bthome_mithermometer"; + +static const char *format_mac_address(std::span buffer, uint64_t address) { + std::array mac{}; + for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) { + mac[i] = (address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF; + } + + format_mac_addr_upper(mac.data(), buffer.data()); + return buffer.data(); +} + +static bool get_bthome_value_length(uint8_t obj_type, size_t &value_length) { + switch (obj_type) { + case 0x00: // packet id + case 0x01: // battery + case 0x09: // count (uint8) + case 0x0F: // generic boolean + case 0x10: // power (bool) + case 0x11: // opening + case 0x15: // battery low + case 0x16: // battery charging + case 0x17: // carbon monoxide + case 0x18: // cold + case 0x19: // connectivity + case 0x1A: // door + case 0x1B: // garage door + case 0x1C: // gas + case 0x1D: // heat + case 0x1E: // light + case 0x1F: // lock + case 0x20: // moisture + case 0x21: // motion + case 0x22: // moving + case 0x23: // occupancy + case 0x24: // plug + case 0x25: // presence + case 0x26: // problem + case 0x27: // running + case 0x28: // safety + case 0x29: // smoke + case 0x2A: // sound + case 0x2B: // tamper + case 0x2C: // vibration + case 0x2D: // water leak + case 0x2E: // humidity (uint8) + case 0x2F: // moisture (uint8) + case 0x46: // UV index + case 0x57: // temperature (sint8) + case 0x58: // temperature (0.35C step) + case 0x59: // count (sint8) + case 0x60: // channel + value_length = 1; + return true; + case 0x02: // temperature (0.01C) + case 0x03: // humidity + case 0x06: // mass (kg) + case 0x07: // mass (lb) + case 0x08: // dewpoint + case 0x0C: // voltage (mV) + case 0x0D: // pm2.5 + case 0x0E: // pm10 + case 0x12: // CO2 + case 0x13: // TVOC + case 0x14: // moisture + case 0x3D: // count (uint16) + case 0x3F: // rotation + case 0x40: // distance (mm) + case 0x41: // distance (m) + case 0x43: // current (A) + case 0x44: // speed + case 0x45: // temperature (0.1C) + case 0x47: // volume (L) + case 0x48: // volume (mL) + case 0x49: // volume flow rate + case 0x4A: // voltage (0.1V) + case 0x51: // acceleration + case 0x52: // gyroscope + case 0x56: // conductivity + case 0x5A: // count (sint16) + case 0x5D: // current (sint16) + case 0x5E: // direction + case 0x5F: // precipitation + case 0x61: // rotational speed + case 0xF0: // button event + value_length = 2; + return true; + case 0x04: // pressure + case 0x05: // illuminance + case 0x0A: // energy + case 0x0B: // power + case 0x42: // duration + case 0x4B: // gas (uint24) + case 0xF2: // firmware version (uint24) + value_length = 3; + return true; + case 0x3E: // count (uint32) + case 0x4C: // gas (uint32) + case 0x4D: // energy (uint32) + case 0x4E: // volume (uint32) + case 0x4F: // water (uint32) + case 0x50: // timestamp + case 0x55: // volume storage + case 0x5B: // count (sint32) + case 0x5C: // power (sint32) + case 0x62: // speed (sint32) + case 0x63: // acceleration (sint32) + case 0xF1: // firmware version (uint32) + value_length = 4; + return true; + default: + return false; + } +} + +void BTHomeMiThermometer::dump_config() { + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGCONFIG(TAG, "BTHome MiThermometer"); + ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_)); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); + LOG_SENSOR(" ", "Signal Strength", this->signal_strength_); +} + +bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + bool matched = false; + for (auto &service_data : device.get_service_datas()) { + if (this->handle_service_data_(service_data, device)) { + matched = true; + } + } + if (matched && this->signal_strength_ != nullptr) { + this->signal_strength_->publish_state(device.get_rssi()); + } + return matched; +} + +bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, + const esp32_ble_tracker::ESPBTDevice &device) { + if (!service_data.uuid.contains(0xD2, 0xFC)) { + return false; + } + + const auto &data = service_data.data; + if (data.size() < 2) { + ESP_LOGVV(TAG, "BTHome data too short: %zu", data.size()); + return false; + } + + const uint8_t adv_info = data[0]; + const bool is_encrypted = adv_info & 0x01; + const bool mac_included = adv_info & 0x02; + const bool is_trigger_based = adv_info & 0x04; + const uint8_t version = (adv_info >> 5) & 0x07; + + if (version != 0x02) { + ESP_LOGVV(TAG, "Unsupported BTHome version %u", version); + return false; + } + + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + if (is_encrypted) { + ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf)); + return false; + } + + size_t payload_index = 1; + uint64_t source_address = device.address_uint64(); + + if (mac_included) { + if (data.size() < 7) { + ESP_LOGVV(TAG, "BTHome payload missing MAC address"); + return false; + } + source_address = 0; + for (int i = 5; i >= 0; i--) { + source_address = (source_address << 8) | data[1 + i]; + } + payload_index = 7; + } + + if (source_address != this->address_) { + ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address)); + return false; + } + + if (payload_index >= data.size()) { + ESP_LOGVV(TAG, "BTHome payload empty after header"); + return false; + } + + bool reported = false; + size_t offset = payload_index; + uint8_t last_type = 0; + + while (offset < data.size()) { + const uint8_t obj_type = data[offset++]; + size_t value_length = 0; + bool has_length_byte = obj_type == 0x53; // text objects include explicit length + + if (has_length_byte) { + if (offset >= data.size()) { + break; + } + value_length = data[offset++]; + } else { + if (!get_bthome_value_length(obj_type, value_length)) { + ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type); + break; + } + } + + if (value_length == 0) { + break; + } + + if (offset + value_length > data.size()) { + ESP_LOGVV(TAG, "BTHome object length exceeds payload"); + break; + } + + const uint8_t *value = &data[offset]; + offset += value_length; + + if (obj_type < last_type) { + ESP_LOGVV(TAG, "BTHome objects not in ascending order"); + } + last_type = obj_type; + + switch (obj_type) { + case 0x00: { // packet id + const uint8_t packet_id = value[0]; + if (this->last_packet_id_.has_value() && *this->last_packet_id_ == packet_id) { + return reported; + } + this->last_packet_id_ = packet_id; + break; + } + case 0x01: { // battery percentage + if (this->battery_level_ != nullptr) { + this->battery_level_->publish_state(value[0]); + reported = true; + } + break; + } + case 0x0C: { // battery voltage (mV) + if (this->battery_voltage_ != nullptr) { + const uint16_t raw = encode_uint16(value[1], value[0]); + this->battery_voltage_->publish_state(raw * 0.001f); + reported = true; + } + break; + } + case 0x02: { // temperature + if (this->temperature_ != nullptr) { + const int16_t raw = encode_uint16(value[1], value[0]); + this->temperature_->publish_state(raw * 0.01f); + reported = true; + } + break; + } + case 0x03: { // humidity + if (this->humidity_ != nullptr) { + const uint16_t raw = encode_uint16(value[1], value[0]); + this->humidity_->publish_state(raw * 0.01f); + reported = true; + } + break; + } + default: + break; + } + } + + if (reported) { + ESP_LOGD(TAG, "BTHome data%sfrom %s", is_trigger_based ? " (triggered) " : " ", device.address_str_to(addr_buf)); + } + + return reported; +} + +} // namespace bthome_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/bthome_mithermometer/bthome_ble.h b/esphome/components/bthome_mithermometer/bthome_ble.h new file mode 100644 index 0000000000..3d2380b48d --- /dev/null +++ b/esphome/components/bthome_mithermometer/bthome_ble.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +#include + +#ifdef USE_ESP32 + +namespace esphome { +namespace bthome_mithermometer { + +class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component { + public: + void set_address(uint64_t address) { this->address_ = address; } + + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } + void set_battery_voltage(sensor::Sensor *battery_voltage) { this->battery_voltage_ = battery_voltage; } + void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; } + + void dump_config() override; + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + protected: + bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, + const esp32_ble_tracker::ESPBTDevice &device); + + uint64_t address_{0}; + optional last_packet_id_{}; + + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + sensor::Sensor *battery_voltage_{nullptr}; + sensor::Sensor *signal_strength_{nullptr}; +}; + +} // namespace bthome_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/bthome_mithermometer/sensor.py b/esphome/components/bthome_mithermometer/sensor.py new file mode 100644 index 0000000000..9b50866db0 --- /dev/null +++ b/esphome/components/bthome_mithermometer/sensor.py @@ -0,0 +1,88 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_BATTERY_VOLTAGE, + CONF_HUMIDITY, + CONF_ID, + CONF_SIGNAL_STRENGTH, + CONF_TEMPERATURE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_DECIBEL_MILLIWATT, + UNIT_PERCENT, + UNIT_VOLT, +) + +from . import bthome_mithermometer_base_schema, setup_bthome_mithermometer + +CODEOWNERS = ["@nagyrobi"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +CONFIG_SCHEMA = bthome_mithermometer_base_schema( + { + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:battery-plus", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await setup_bthome_mithermometer(var, config) + + if temp_sens := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temp_sens) + cg.add(var.set_temperature(sens)) + if humi_sens := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humi_sens) + cg.add(var.set_humidity(sens)) + if batl_sens := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(batl_sens) + cg.add(var.set_battery_level(sens)) + if batv_sens := config.get(CONF_BATTERY_VOLTAGE): + sens = await sensor.new_sensor(batv_sens) + cg.add(var.set_battery_voltage(sens)) + if sgnl_sens := config.get(CONF_SIGNAL_STRENGTH): + sens = await sensor.new_sensor(sgnl_sens) + cg.add(var.set_signal_strength(sens)) diff --git a/esphome/components/button/automation.h b/esphome/components/button/automation.h index 3b792eb5d7..6a54b141a3 100644 --- a/esphome/components/button/automation.h +++ b/esphome/components/button/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace button { +namespace esphome::button { template class PressAction : public Action { public: @@ -24,5 +23,4 @@ class ButtonPressTrigger : public Trigger<> { } }; -} // namespace button -} // namespace esphome +} // namespace esphome::button diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index c968d31088..87a222776e 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -1,8 +1,7 @@ #include "button.h" #include "esphome/core/log.h" -namespace esphome { -namespace button { +namespace esphome::button { static const char *const TAG = "button"; @@ -26,5 +25,4 @@ void Button::press() { } void Button::add_on_press_callback(std::function &&callback) { this->press_callback_.add(std::move(callback)); } -} // namespace button -} // namespace esphome +} // namespace esphome::button diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h index 75b76f9dcf..be6e080917 100644 --- a/esphome/components/button/button.h +++ b/esphome/components/button/button.h @@ -4,8 +4,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace button { +namespace esphome::button { class Button; void log_button(const char *tag, const char *prefix, const char *type, Button *obj); @@ -42,8 +41,7 @@ class Button : public EntityBase, public EntityBase_DeviceClass { */ virtual void press_action() = 0; - CallbackManager press_callback_{}; + LazyCallbackManager press_callback_{}; }; -} // namespace button -} // namespace esphome +} // namespace esphome::button diff --git a/esphome/components/camera/camera.cpp b/esphome/components/camera/camera.cpp index 3bd632af5c..66b8138f38 100644 --- a/esphome/components/camera/camera.cpp +++ b/esphome/components/camera/camera.cpp @@ -8,7 +8,7 @@ Camera *Camera::global_camera = nullptr; Camera::Camera() { if (global_camera != nullptr) { - this->status_set_error("Multiple cameras are configured, but only one is supported."); + this->status_set_error(LOG_STR("Multiple cameras are configured, but only one is supported.")); this->mark_failed(); return; } diff --git a/esphome/components/camera/camera.h b/esphome/components/camera/camera.h index c28a756a06..6e1fc8cc06 100644 --- a/esphome/components/camera/camera.h +++ b/esphome/components/camera/camera.h @@ -35,6 +35,21 @@ inline const char *to_string(PixelFormat format) { return "PIXEL_FORMAT_UNKNOWN"; } +// Forward declaration +class CameraImage; + +/** Listener interface for camera events. + * + * Components can implement this interface to receive camera notifications + * (new images, stream start/stop) without the overhead of std::function callbacks. + */ +class CameraListener { + public: + virtual void on_camera_image(const std::shared_ptr &image) {} + virtual void on_stream_start() {} + virtual void on_stream_stop() {} +}; + /** Abstract camera image base class. * Encapsulates the JPEG encoded data and it is shared among * all connected clients. @@ -87,12 +102,12 @@ struct CameraImageSpec { }; /** Abstract camera base class. Collaborates with API. - * 1) API server starts and installs callback (add_image_callback) - * which is called by the camera when a new image is available. + * 1) API server starts and registers as a listener (add_listener) + * to receive new images from the camera. * 2) New API client connects and creates a new image reader (create_image_reader). * 3) API connection receives protobuf CameraImageRequest and calls request_image. * 3.a) API connection receives protobuf CameraImageRequest and calls start_stream. - * 4) Camera implementation provides JPEG data in the CameraImage and calls callback. + * 4) Camera implementation provides JPEG data in the CameraImage and notifies listeners. * 5) API connection sets the image in the image reader. * 6) API connection consumes data from the image reader and returns the image when finished. * 7.a) Camera captures a new image and continues with 4) until start_stream is called. @@ -100,8 +115,8 @@ struct CameraImageSpec { class Camera : public EntityBase, public Component { public: Camera(); - // Camera implementation invokes callback to publish a new image. - virtual void add_image_callback(std::function)> &&callback) = 0; + /// Add a listener to receive camera events + virtual void add_listener(CameraListener *listener) = 0; /// Returns a new camera image reader that keeps track of the JPEG data in the camera image. virtual CameraImageReader *create_image_reader() = 0; // Connection, camera or web server requests one new JPEG image. diff --git a/esphome/components/cap1188/cap1188.cpp b/esphome/components/cap1188/cap1188.cpp index 683e5cf487..9e8c87d147 100644 --- a/esphome/components/cap1188/cap1188.cpp +++ b/esphome/components/cap1188/cap1188.cpp @@ -63,14 +63,14 @@ void CAP1188Component::finish_setup_() { } void CAP1188Component::dump_config() { - ESP_LOGCONFIG(TAG, "CAP1188:"); - LOG_I2C_DEVICE(this); - LOG_PIN(" Reset Pin: ", this->reset_pin_); ESP_LOGCONFIG(TAG, + "CAP1188:\n" " Product ID: 0x%x\n" " Manufacture ID: 0x%x\n" " Revision ID: 0x%x", this->cap1188_product_id_, this->cap1188_manufacture_id_, this->cap1188_revision_); + LOG_I2C_DEVICE(this); + LOG_PIN(" Reset Pin: ", this->reset_pin_); switch (this->error_code_) { case COMMUNICATION_FAILED: diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 9bd3ef8a05..232b868e82 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) def AUTO_LOAD() -> list[str]: auto_load = ["web_server_base", "ota.web_server"] - if CORE.using_esp_idf: + if CORE.is_esp32: auto_load.append("socket") return auto_load @@ -72,6 +72,16 @@ def _final_validate(config: ConfigType) -> ConfigType: "Add 'ap:' to your WiFi configuration to enable the captive portal." ) + # Register socket needs for DNS server and additional HTTP connections + # - 1 UDP socket for DNS server + # - 3 additional TCP sockets for captive portal detection probes + configuration requests + # OS captive portal detection makes multiple probe requests that stay in TIME_WAIT. + # Need headroom for actual user configuration requests. + # LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts. + from esphome.components import socket + + socket.consume_sockets(4, "captive_portal")(config) + return config @@ -87,10 +97,6 @@ async def to_code(config): cg.add_define("USE_CAPTIVE_PORTAL") if CORE.using_arduino: - if CORE.is_esp32: - cg.add_library("ESP32 Async UDP", None) - cg.add_library("DNSServer", None) - cg.add_library("WiFi", None) if CORE.is_esp8266: cg.add_library("DNSServer", None) if CORE.is_libretiny: @@ -100,6 +106,9 @@ async def to_code(config): # Only compile the ESP-IDF DNS server when using ESP-IDF framework FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "dns_server_esp32_idf.cpp": {PlatformFramework.ESP32_IDF}, + "dns_server_esp32_idf.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, } ) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 30438747f2..5ba70bcc50 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -13,14 +13,16 @@ static const char *const TAG = "captive_portal"; void CaptivePortal::handle_config(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json")); stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate")); + char mac_s[18]; + const char *mac_str = get_mac_address_pretty_into_buffer(mac_s); #ifdef USE_ESP8266 stream->print(ESPHOME_F("{\"mac\":\"")); - stream->print(get_mac_address_pretty().c_str()); + stream->print(mac_str); stream->print(ESPHOME_F("\",\"name\":\"")); stream->print(App.get_name().c_str()); stream->print(ESPHOME_F("\",\"aps\":[{}")); #else - stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); + stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", mac_str, App.get_name().c_str()); #endif for (auto &scan : wifi::global_wifi_component->get_scan_result()) { @@ -47,11 +49,18 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) { void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr) std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr) - ESP_LOGI(TAG, "Requested WiFi Settings Change:"); - ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); - ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); + ESP_LOGI(TAG, + "Requested WiFi Settings Change:\n" + " SSID='%s'\n" + " Password=" LOG_SECRET("'%s'"), + ssid.c_str(), psk.c_str()); +#ifdef USE_ESP8266 + // ESP8266 is single-threaded, call directly wifi::global_wifi_component->save_wifi_sta(ssid, psk); - wifi::global_wifi_component->start_scanning(); +#else + // Defer save to main loop thread to avoid NVS operations from HTTP thread + this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); }); +#endif request->redirect(ESPHOME_F("/?save")); } @@ -67,12 +76,11 @@ void CaptivePortal::start() { network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); -#ifdef USE_ESP_IDF +#if defined(USE_ESP32) // Create DNS server instance for ESP-IDF this->dns_server_ = make_unique(); this->dns_server_->start(ip); -#endif -#ifdef USE_ARDUINO +#elif defined(USE_ARDUINO) this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); this->dns_server_->start(53, ESPHOME_F("*"), ip); diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index f48c286f0c..0c63a3670a 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -2,11 +2,10 @@ #include "esphome/core/defines.h" #ifdef USE_CAPTIVE_PORTAL #include -#ifdef USE_ARDUINO -#include -#endif -#ifdef USE_ESP_IDF +#if defined(USE_ESP32) #include "dns_server_esp32_idf.h" +#elif defined(USE_ARDUINO) +#include #endif #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -23,15 +22,14 @@ class CaptivePortal : public AsyncWebHandler, public Component { void setup() override; void dump_config() override; void loop() override { -#ifdef USE_ARDUINO - if (this->dns_server_ != nullptr) { - this->dns_server_->processNextRequest(); - } -#endif -#ifdef USE_ESP_IDF +#if defined(USE_ESP32) if (this->dns_server_ != nullptr) { this->dns_server_->process_next_request(); } +#elif defined(USE_ARDUINO) + if (this->dns_server_ != nullptr) { + this->dns_server_->processNextRequest(); + } #endif } float get_setup_priority() const override; @@ -64,7 +62,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { web_server_base::WebServerBase *base_; bool initialized_{false}; bool active_{false}; -#if defined(USE_ARDUINO) || defined(USE_ESP_IDF) +#if defined(USE_ARDUINO) || defined(USE_ESP32) std::unique_ptr dns_server_{nullptr}; #endif }; diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.cpp b/esphome/components/captive_portal/dns_server_esp32_idf.cpp index 740107400a..5743cbd671 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.cpp +++ b/esphome/components/captive_portal/dns_server_esp32_idf.cpp @@ -1,5 +1,5 @@ #include "dns_server_esp32_idf.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/log.h" #include "esphome/core/hal.h" @@ -47,7 +47,10 @@ struct DNSAnswer { void DNSServer::start(const network::IPAddress &ip) { this->server_ip_ = ip; - ESP_LOGV(TAG, "Starting DNS server on %s", ip.str().c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGV(TAG, "Starting DNS server on %s", ip.str_to(ip_buf)); +#endif // Create loop-monitored UDP socket this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP); @@ -202,4 +205,4 @@ void DNSServer::process_next_request() { } // namespace esphome::captive_portal -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/captive_portal/dns_server_esp32_idf.h b/esphome/components/captive_portal/dns_server_esp32_idf.h index 13d9def8e3..3e0ac07373 100644 --- a/esphome/components/captive_portal/dns_server_esp32_idf.h +++ b/esphome/components/captive_portal/dns_server_esp32_idf.h @@ -1,5 +1,5 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include "esphome/core/helpers.h" @@ -24,4 +24,4 @@ class DNSServer { } // namespace esphome::captive_portal -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/cc1101/__init__.py b/esphome/components/cc1101/__init__.py new file mode 100644 index 0000000000..fbdd7010b4 --- /dev/null +++ b/esphome/components/cc1101/__init__.py @@ -0,0 +1,335 @@ +from esphome import automation, pins +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components import spi +from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET +import esphome.config_validation as cv +from esphome.const import ( + CONF_CHANNEL, + CONF_DATA, + CONF_FREQUENCY, + CONF_ID, + CONF_WAIT_TIME, +) +from esphome.core import ID + +CODEOWNERS = ["@lygris", "@gabest11"] +DEPENDENCIES = ["spi"] +MULTI_CONF = True + +ns = cg.esphome_ns.namespace("cc1101") +CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice) + +# Config keys +CONF_OUTPUT_POWER = "output_power" +CONF_RX_ATTENUATION = "rx_attenuation" +CONF_DC_BLOCKING_FILTER = "dc_blocking_filter" +CONF_IF_FREQUENCY = "if_frequency" +CONF_FILTER_BANDWIDTH = "filter_bandwidth" +CONF_CHANNEL_SPACING = "channel_spacing" +CONF_FSK_DEVIATION = "fsk_deviation" +CONF_MSK_DEVIATION = "msk_deviation" +CONF_SYMBOL_RATE = "symbol_rate" +CONF_SYNC_MODE = "sync_mode" +CONF_CARRIER_SENSE_ABOVE_THRESHOLD = "carrier_sense_above_threshold" +CONF_MODULATION_TYPE = "modulation_type" +CONF_MANCHESTER = "manchester" +CONF_NUM_PREAMBLE = "num_preamble" +CONF_SYNC1 = "sync1" +CONF_SYNC0 = "sync0" +CONF_MAGN_TARGET = "magn_target" +CONF_MAX_LNA_GAIN = "max_lna_gain" +CONF_MAX_DVGA_GAIN = "max_dvga_gain" +CONF_CARRIER_SENSE_ABS_THR = "carrier_sense_abs_thr" +CONF_CARRIER_SENSE_REL_THR = "carrier_sense_rel_thr" +CONF_LNA_PRIORITY = "lna_priority" +CONF_FILTER_LENGTH_FSK_MSK = "filter_length_fsk_msk" +CONF_FILTER_LENGTH_ASK_OOK = "filter_length_ask_ook" +CONF_FREEZE = "freeze" +CONF_HYST_LEVEL = "hyst_level" + +# Packet mode config keys +CONF_PACKET_MODE = "packet_mode" +CONF_PACKET_LENGTH = "packet_length" +CONF_WHITENING = "whitening" +CONF_GDO0_PIN = "gdo0_pin" + +# Enums +SyncMode = ns.enum("SyncMode", True) +SYNC_MODE = { + "None": SyncMode.SYNC_MODE_NONE, + "15/16": SyncMode.SYNC_MODE_15_16, + "16/16": SyncMode.SYNC_MODE_16_16, + "30/32": SyncMode.SYNC_MODE_30_32, +} + +Modulation = ns.enum("Modulation", True) +MODULATION = { + "2-FSK": Modulation.MODULATION_2_FSK, + "GFSK": Modulation.MODULATION_GFSK, + "ASK/OOK": Modulation.MODULATION_ASK_OOK, + "4-FSK": Modulation.MODULATION_4_FSK, + "MSK": Modulation.MODULATION_MSK, +} + +RxAttenuation = ns.enum("RxAttenuation", True) +RX_ATTENUATION = { + "0dB": RxAttenuation.RX_ATTENUATION_0DB, + "6dB": RxAttenuation.RX_ATTENUATION_6DB, + "12dB": RxAttenuation.RX_ATTENUATION_12DB, + "18dB": RxAttenuation.RX_ATTENUATION_18DB, +} + +MagnTarget = ns.enum("MagnTarget", True) +MAGN_TARGET = { + "24dB": MagnTarget.MAGN_TARGET_24DB, + "27dB": MagnTarget.MAGN_TARGET_27DB, + "30dB": MagnTarget.MAGN_TARGET_30DB, + "33dB": MagnTarget.MAGN_TARGET_33DB, + "36dB": MagnTarget.MAGN_TARGET_36DB, + "38dB": MagnTarget.MAGN_TARGET_38DB, + "40dB": MagnTarget.MAGN_TARGET_40DB, + "42dB": MagnTarget.MAGN_TARGET_42DB, +} + +MaxLnaGain = ns.enum("MaxLnaGain", True) +MAX_LNA_GAIN = { + "Default": MaxLnaGain.MAX_LNA_GAIN_DEFAULT, + "2.6dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_2P6DB, + "6.1dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_6P1DB, + "7.4dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_7P4DB, + "9.2dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_9P2DB, + "11.5dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_11P5DB, + "14.6dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_14P6DB, + "17.1dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_17P1DB, +} + +MaxDvgaGain = ns.enum("MaxDvgaGain", True) +MAX_DVGA_GAIN = { + "Default": MaxDvgaGain.MAX_DVGA_GAIN_DEFAULT, + "-1": MaxDvgaGain.MAX_DVGA_GAIN_MINUS_1, + "-2": MaxDvgaGain.MAX_DVGA_GAIN_MINUS_2, + "-3": MaxDvgaGain.MAX_DVGA_GAIN_MINUS_3, +} + +CarrierSenseRelThr = ns.enum("CarrierSenseRelThr", True) +CARRIER_SENSE_REL_THR = { + "Default": CarrierSenseRelThr.CARRIER_SENSE_REL_THR_DEFAULT, + "+6dB": CarrierSenseRelThr.CARRIER_SENSE_REL_THR_PLUS_6DB, + "+10dB": CarrierSenseRelThr.CARRIER_SENSE_REL_THR_PLUS_10DB, + "+14dB": CarrierSenseRelThr.CARRIER_SENSE_REL_THR_PLUS_14DB, +} + +FilterLengthFskMsk = ns.enum("FilterLengthFskMsk", True) +FILTER_LENGTH_FSK_MSK = { + "8": FilterLengthFskMsk.FILTER_LENGTH_8DB, + "16": FilterLengthFskMsk.FILTER_LENGTH_16DB, + "32": FilterLengthFskMsk.FILTER_LENGTH_32DB, + "64": FilterLengthFskMsk.FILTER_LENGTH_64DB, +} + +FilterLengthAskOok = ns.enum("FilterLengthAskOok", True) +FILTER_LENGTH_ASK_OOK = { + "4dB": FilterLengthAskOok.FILTER_LENGTH_4DB, + "8dB": FilterLengthAskOok.FILTER_LENGTH_8DB, + "12dB": FilterLengthAskOok.FILTER_LENGTH_12DB, + "16dB": FilterLengthAskOok.FILTER_LENGTH_16DB, +} + +Freeze = ns.enum("Freeze", True) +FREEZE = { + "Default": Freeze.FREEZE_DEFAULT, + "On Sync": Freeze.FREEZE_ON_SYNC, + "Analog Only": Freeze.FREEZE_ANALOG_ONLY, + "Analog And Digital": Freeze.FREEZE_ANALOG_AND_DIGITAL, +} + +WaitTime = ns.enum("WaitTime", True) +WAIT_TIME = { + "8": WaitTime.WAIT_TIME_8_SAMPLES, + "16": WaitTime.WAIT_TIME_16_SAMPLES, + "24": WaitTime.WAIT_TIME_24_SAMPLES, + "32": WaitTime.WAIT_TIME_32_SAMPLES, +} + +HystLevel = ns.enum("HystLevel", True) +HYST_LEVEL = { + "None": HystLevel.HYST_LEVEL_NONE, + "Low": HystLevel.HYST_LEVEL_LOW, + "Medium": HystLevel.HYST_LEVEL_MEDIUM, + "High": HystLevel.HYST_LEVEL_HIGH, +} + +# Optional settings to generate setter calls for +CONFIG_MAP = { + cv.Optional(CONF_OUTPUT_POWER, default=10): cv.float_range(min=-30.0, max=11.0), + cv.Optional(CONF_RX_ATTENUATION, default="0dB"): cv.enum( + RX_ATTENUATION, upper=False + ), + cv.Optional(CONF_DC_BLOCKING_FILTER, default=True): cv.boolean, + cv.Optional(CONF_FREQUENCY, default="433.92MHz"): cv.All( + cv.frequency, cv.float_range(min=300.0e6, max=928.0e6) + ), + cv.Optional(CONF_IF_FREQUENCY, default="153kHz"): cv.All( + cv.frequency, cv.float_range(min=25000, max=788000) + ), + cv.Optional(CONF_FILTER_BANDWIDTH, default="203kHz"): cv.All( + cv.frequency, cv.float_range(min=58000, max=812000) + ), + cv.Optional(CONF_CHANNEL, default=0): cv.uint8_t, + cv.Optional(CONF_CHANNEL_SPACING, default="200kHz"): cv.All( + cv.frequency, cv.float_range(min=25000, max=405000) + ), + cv.Optional(CONF_FSK_DEVIATION): cv.All( + cv.frequency, cv.float_range(min=1500, max=381000) + ), + cv.Optional(CONF_MSK_DEVIATION): cv.int_range(min=1, max=8), + cv.Optional(CONF_SYMBOL_RATE, default=5000): cv.float_range(min=600, max=500000), + cv.Optional(CONF_SYNC_MODE, default="16/16"): cv.enum(SYNC_MODE, upper=False), + cv.Optional(CONF_CARRIER_SENSE_ABOVE_THRESHOLD, default=False): cv.boolean, + cv.Optional(CONF_MODULATION_TYPE, default="ASK/OOK"): cv.enum( + MODULATION, upper=False + ), + cv.Optional(CONF_MANCHESTER, default=False): cv.boolean, + cv.Optional(CONF_NUM_PREAMBLE, default=2): cv.int_range(min=0, max=7), + cv.Optional(CONF_SYNC1, default=0xD3): cv.hex_uint8_t, + cv.Optional(CONF_SYNC0, default=0x91): cv.hex_uint8_t, + cv.Optional(CONF_MAGN_TARGET, default="42dB"): cv.enum(MAGN_TARGET, upper=False), + cv.Optional(CONF_MAX_LNA_GAIN, default="Default"): cv.enum( + MAX_LNA_GAIN, upper=False + ), + cv.Optional(CONF_MAX_DVGA_GAIN, default="-3"): cv.enum(MAX_DVGA_GAIN, upper=False), + cv.Optional(CONF_CARRIER_SENSE_ABS_THR): cv.int_range(min=-8, max=7), + cv.Optional(CONF_CARRIER_SENSE_REL_THR): cv.enum( + CARRIER_SENSE_REL_THR, upper=False + ), + cv.Optional(CONF_LNA_PRIORITY, default=False): cv.boolean, + cv.Optional(CONF_FILTER_LENGTH_FSK_MSK): cv.enum( + FILTER_LENGTH_FSK_MSK, upper=False + ), + cv.Optional(CONF_FILTER_LENGTH_ASK_OOK): cv.enum( + FILTER_LENGTH_ASK_OOK, upper=False + ), + cv.Optional(CONF_FREEZE): cv.enum(FREEZE, upper=False), + cv.Optional(CONF_WAIT_TIME, default="32"): cv.enum(WAIT_TIME, upper=False), + cv.Optional(CONF_HYST_LEVEL): cv.enum(HYST_LEVEL, upper=False), + cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean, + cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t, + cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, + cv.Optional(CONF_WHITENING, default=False): cv.boolean, +} + + +def _validate_packet_mode(config): + if config.get(CONF_PACKET_MODE, False): + if CONF_GDO0_PIN not in config: + raise cv.Invalid("gdo0_pin is required when packet_mode is enabled") + if CONF_PACKET_LENGTH not in config: + raise cv.Invalid("packet_length is required when packet_mode is enabled") + if config[CONF_PACKET_LENGTH] > 64: + raise cv.Invalid("packet_length must be <= 64 (FIFO size)") + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CC1101Component), + cv.Optional(CONF_GDO0_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), + } + ) + .extend(CONFIG_MAP) + .extend(cv.COMPONENT_SCHEMA) + .extend(spi.spi_device_schema(cs_pin_required=True)), + _validate_packet_mode, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + + for opt in CONFIG_MAP: + key = opt.schema + if key in config: + cg.add(getattr(var, f"set_{key}")(config[key])) + + if CONF_GDO0_PIN in config: + gdo0_pin = await cg.gpio_pin_expression(config[CONF_GDO0_PIN]) + cg.add(var.set_gdo0_pin(gdo0_pin)) + if CONF_ON_PACKET in config: + await automation.build_automation( + var.get_packet_trigger(), + [ + (cg.std_vector.template(cg.uint8), "x"), + (cg.float_, "freq_offset"), + (cg.float_, "rssi"), + (cg.uint8, "lqi"), + ], + config[CONF_ON_PACKET], + ) + + +# Actions +BeginTxAction = ns.class_("BeginTxAction", automation.Action) +BeginRxAction = ns.class_("BeginRxAction", automation.Action) +ResetAction = ns.class_("ResetAction", automation.Action) +SetIdleAction = ns.class_("SetIdleAction", automation.Action) +SendPacketAction = ns.class_( + "SendPacketAction", automation.Action, cg.Parented.template(CC1101Component) +) + +CC1101_ACTION_SCHEMA = cv.Schema( + maybe_simple_id({cv.GenerateID(CONF_ID): cv.use_id(CC1101Component)}) +) + + +@automation.register_action("cc1101.begin_tx", BeginTxAction, CC1101_ACTION_SCHEMA) +@automation.register_action("cc1101.begin_rx", BeginRxAction, CC1101_ACTION_SCHEMA) +@automation.register_action("cc1101.reset", ResetAction, CC1101_ACTION_SCHEMA) +@automation.register_action("cc1101.set_idle", SetIdleAction, CC1101_ACTION_SCHEMA) +async def cc1101_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +def validate_raw_data(value): + if isinstance(value, str): + return value.encode("utf-8") + if isinstance(value, list): + return cv.Schema([cv.hex_uint8_t])(value) + raise cv.Invalid( + "data must either be a string wrapped in quotes or a list of bytes" + ) + + +SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(CC1101Component), + cv.Required(CONF_DATA): cv.templatable(validate_raw_data), + }, + key=CONF_DATA, +) + + +@automation.register_action( + "cc1101.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA +) +async def send_packet_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + data = config[CONF_DATA] + if isinstance(data, bytes): + data = list(data) + if cg.is_template(data): + templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_data_template(templ)) + else: + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) + return var diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp new file mode 100644 index 0000000000..c4507a54e5 --- /dev/null +++ b/esphome/components/cc1101/cc1101.cpp @@ -0,0 +1,694 @@ +#include "cc1101.h" +#include "cc1101pa.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include + +namespace esphome::cc1101 { + +static const char *const TAG = "cc1101"; + +static void split_float(float value, int mbits, uint8_t &e, uint32_t &m) { + int e_tmp; + float m_tmp = std::frexp(value, &e_tmp); + if (e_tmp <= mbits) { + e = 0; + m = 0; + return; + } + e = static_cast(e_tmp - mbits - 1); + m = static_cast(((m_tmp * 2 - 1) * (1 << (mbits + 1))) + 1) >> 1; + if (m == (1UL << mbits)) { + e = e + 1; + m = 0; + } +} + +CC1101Component::CC1101Component() { + // Datasheet defaults + memset(&this->state_, 0, sizeof(this->state_)); + this->state_.GDO2_CFG = 0x0D; // Serial Data (for RX on GDO2) + this->state_.GDO1_CFG = 0x2E; + this->state_.GDO0_CFG = 0x0D; // Serial Data (for RX on GDO0 / TX Input) + this->state_.FIFO_THR = 7; + this->state_.SYNC1 = 0xD3; + this->state_.SYNC0 = 0x91; + this->state_.PKTLEN = 0xFF; + this->state_.APPEND_STATUS = 1; + this->state_.LENGTH_CONFIG = 1; + this->state_.CRC_EN = 1; + this->state_.WHITE_DATA = 1; + this->state_.FREQ_IF = 0x0F; + this->state_.FREQ2 = 0x1E; + this->state_.FREQ1 = 0xC4; + this->state_.FREQ0 = 0xEC; + this->state_.DRATE_E = 0x0C; + this->state_.CHANBW_E = 0x02; + this->state_.DRATE_M = 0x22; + this->state_.SYNC_MODE = 2; + this->state_.CHANSPC_E = 2; + this->state_.NUM_PREAMBLE = 2; + this->state_.CHANSPC_M = 0xF8; + this->state_.DEVIATION_M = 7; + this->state_.DEVIATION_E = 4; + this->state_.RX_TIME = 7; + this->state_.CCA_MODE = 3; + this->state_.PO_TIMEOUT = 1; + this->state_.FOC_LIMIT = 2; + this->state_.FOC_POST_K = 1; + this->state_.FOC_PRE_K = 2; + this->state_.FOC_BS_CS_GATE = 1; + this->state_.BS_POST_KP = 1; + this->state_.BS_POST_KI = 1; + this->state_.BS_PRE_KP = 2; + this->state_.BS_PRE_KI = 1; + this->state_.MAGN_TARGET = 3; + this->state_.AGC_LNA_PRIORITY = 1; + this->state_.FILTER_LENGTH = 1; + this->state_.WAIT_TIME = 1; + this->state_.HYST_LEVEL = 2; + this->state_.WOREVT1 = 0x87; + this->state_.WOREVT0 = 0x6B; + this->state_.RC_CAL = 1; + this->state_.EVENT1 = 7; + this->state_.RC_PD = 1; + this->state_.MIX_CURRENT = 2; + this->state_.LODIV_BUF_CURRENT_RX = 1; + this->state_.LNA2MIX_CURRENT = 1; + this->state_.LNA_CURRENT = 1; + this->state_.LODIV_BUF_CURRENT_TX = 1; + this->state_.FSCAL3_LO = 9; + this->state_.CHP_CURR_CAL_EN = 2; + this->state_.FSCAL3_HI = 2; + this->state_.FSCAL2 = 0x0A; + this->state_.FSCAL1 = 0x20; + this->state_.FSCAL0 = 0x0D; + this->state_.RCCTRL1 = 0x41; + this->state_.FSTEST = 0x59; + this->state_.PTEST = 0x7F; + this->state_.AGCTEST = 0x3F; + this->state_.TEST2 = 0x88; + this->state_.TEST1 = 0x31; + this->state_.TEST0_LO = 1; + this->state_.VCO_SEL_CAL_EN = 1; + this->state_.TEST0_HI = 2; + + // PKTCTRL0 + this->state_.PKT_FORMAT = 3; + this->state_.LENGTH_CONFIG = 2; + this->state_.FS_AUTOCAL = 1; + + // CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence) + memset(this->pa_table_, 0, sizeof(this->pa_table_)); +} + +void CC1101Component::setup() { + this->spi_setup(); + this->cs_->digital_write(true); + delayMicroseconds(1); + this->cs_->digital_write(false); + delayMicroseconds(1); + this->cs_->digital_write(true); + delayMicroseconds(41); + this->cs_->digital_write(false); + delay(5); + + this->strobe_(Command::RES); + delay(5); + + this->read_(Register::PARTNUM); + this->read_(Register::VERSION); + this->chip_id_ = encode_uint16(this->state_.PARTNUM, this->state_.VERSION); + ESP_LOGD(TAG, "CC1101 found! Chip ID: 0x%04X", this->chip_id_); + if (this->state_.VERSION == 0 || this->state_.PARTNUM == 0xFF) { + ESP_LOGE(TAG, "Failed to verify CC1101."); + this->mark_failed(); + return; + } + + // Setup GDO0 pin if configured + if (this->gdo0_pin_ != nullptr) { + this->gdo0_pin_->setup(); + } + + this->initialized_ = true; + + for (uint8_t i = 0; i <= static_cast(Register::TEST0); i++) { + if (i == static_cast(Register::FSTEST) || i == static_cast(Register::AGCTEST)) { + continue; + } + this->write_(static_cast(i)); + } + this->set_output_power(this->output_power_requested_); + if (!this->enter_rx_()) { + this->mark_failed(); + return; + } + + // Defer pin mode setup until after all components have completed setup() + // This handles the case where remote_transmitter runs after CC1101 and changes pin mode + if (this->gdo0_pin_ != nullptr) { + this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); }); + } +} + +void CC1101Component::loop() { + if (this->state_.PKT_FORMAT != static_cast(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr || + !this->gdo0_pin_->digital_read()) { + return; + } + + // Read state + this->read_(Register::RXBYTES); + uint8_t rx_bytes = this->state_.NUM_RXBYTES; + bool overflow = this->state_.RXFIFO_OVERFLOW; + if (overflow || rx_bytes == 0) { + ESP_LOGW(TAG, "RX FIFO overflow, flushing"); + this->enter_idle_(); + this->strobe_(Command::FRX); + this->enter_rx_(); + return; + } + + // Read packet + uint8_t payload_length, expected_rx; + if (this->state_.LENGTH_CONFIG == static_cast(LengthConfig::LENGTH_CONFIG_VARIABLE)) { + this->read_(Register::FIFO, &payload_length, 1); + expected_rx = payload_length + 1; + } else { + payload_length = this->state_.PKTLEN; + expected_rx = payload_length; + } + if (payload_length == 0 || payload_length > 64 || rx_bytes != expected_rx) { + ESP_LOGW(TAG, "Invalid packet: rx_bytes %u, payload_length %u", rx_bytes, payload_length); + this->enter_idle_(); + this->strobe_(Command::FRX); + this->enter_rx_(); + return; + } + this->packet_.resize(payload_length); + this->read_(Register::FIFO, this->packet_.data(), payload_length); + + // Read status from registers (more reliable than FIFO status bytes due to timing issues) + this->read_(Register::FREQEST); + this->read_(Register::RSSI); + this->read_(Register::LQI); + float freq_offset = static_cast(this->state_.FREQEST) * (XTAL_FREQUENCY / (1 << 14)); + float rssi = (this->state_.RSSI * RSSI_STEP) - RSSI_OFFSET; + bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0; + uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK; + if (this->state_.CRC_EN == 0 || crc_ok) { + this->packet_trigger_->trigger(this->packet_, freq_offset, rssi, lqi); + } + + // Return to rx + this->enter_idle_(); + this->strobe_(Command::FRX); + this->enter_rx_(); +} + +void CC1101Component::dump_config() { + static const char *const MODULATION_NAMES[] = {"2-FSK", "GFSK", "UNUSED", "ASK/OOK", + "4-FSK", "UNUSED", "UNUSED", "MSK"}; + int32_t freq = static_cast(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) * + XTAL_FREQUENCY / (1 << 16); + float symbol_rate = (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY; + float bw = XTAL_FREQUENCY / (8.0f * (4 + this->state_.CHANBW_M) * (1 << this->state_.CHANBW_E)); + ESP_LOGCONFIG(TAG, + "CC1101:\n" + " Chip ID: 0x%04X\n" + " Frequency: %" PRId32 " Hz\n" + " Channel: %u\n" + " Modulation: %s\n" + " Symbol Rate: %.0f baud\n" + " Filter Bandwidth: %.1f Hz\n" + " Output Power: %.1f dBm", + this->chip_id_, freq, this->state_.CHANNR, MODULATION_NAMES[this->state_.MOD_FORMAT & 0x07], + symbol_rate, bw, this->output_power_effective_); + LOG_PIN(" CS Pin: ", this->cs_); +} + +void CC1101Component::begin_tx() { + // Ensure Packet Format is 3 (Async Serial) + this->write_(Register::PKTCTRL0, 0x32); + ESP_LOGV(TAG, "Beginning TX sequence"); + if (this->gdo0_pin_ != nullptr) { + this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT); + } + if (!this->enter_tx_()) { + ESP_LOGW(TAG, "Failed to enter TX state!"); + } +} + +void CC1101Component::begin_rx() { + ESP_LOGV(TAG, "Beginning RX sequence"); + if (this->gdo0_pin_ != nullptr) { + this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); + } + if (!this->enter_rx_()) { + ESP_LOGW(TAG, "Failed to enter RX state!"); + } +} + +void CC1101Component::reset() { + this->strobe_(Command::RES); + this->setup(); +} + +void CC1101Component::set_idle() { + ESP_LOGV(TAG, "Setting IDLE state"); + this->enter_idle_(); +} + +bool CC1101Component::wait_for_state_(State target_state, uint32_t timeout_ms) { + uint32_t start = millis(); + while (millis() - start < timeout_ms) { + this->read_(Register::MARCSTATE); + State s = static_cast(this->state_.MARC_STATE); + if (s == target_state) { + return true; + } + delayMicroseconds(100); + } + return false; +} + +bool CC1101Component::enter_calibrated_(State target_state, Command cmd) { + // The PLL must be recalibrated until PLL lock is achieved + for (uint8_t retries = PLL_LOCK_RETRIES; retries > 0; retries--) { + this->strobe_(cmd); + if (!this->wait_for_state_(target_state)) { + return false; + } + this->read_(Register::FSCAL1); + if (this->state_.FSCAL1 != FSCAL1_PLL_NOT_LOCKED) { + return true; + } + ESP_LOGW(TAG, "PLL lock failed, retrying calibration"); + this->enter_idle_(); + } + ESP_LOGE(TAG, "PLL lock failed after retries"); + return false; +} + +void CC1101Component::enter_idle_() { + this->strobe_(Command::IDLE); + this->wait_for_state_(State::IDLE); +} + +bool CC1101Component::enter_rx_() { return this->enter_calibrated_(State::RX, Command::RX); } + +bool CC1101Component::enter_tx_() { return this->enter_calibrated_(State::TX, Command::TX); } + +uint8_t CC1101Component::strobe_(Command cmd) { + uint8_t index = static_cast(cmd); + if (cmd < Command::RES || cmd > Command::NOP) { + return 0xFF; + } + this->enable(); + uint8_t status_byte = this->transfer_byte(index); + this->disable(); + return status_byte; +} + +void CC1101Component::write_(Register reg) { + uint8_t index = static_cast(reg); + this->enable(); + this->write_byte(index); + this->write_array(&this->state_.regs()[index], 1); + this->disable(); +} + +void CC1101Component::write_(Register reg, uint8_t value) { + uint8_t index = static_cast(reg); + this->state_.regs()[index] = value; + this->write_(reg); +} + +void CC1101Component::write_(Register reg, const uint8_t *buffer, size_t length) { + uint8_t index = static_cast(reg); + this->enable(); + this->write_byte(index | BUS_WRITE | BUS_BURST); + this->write_array(buffer, length); + this->disable(); +} + +void CC1101Component::read_(Register reg) { + uint8_t index = static_cast(reg); + this->enable(); + this->write_byte(index | BUS_READ | BUS_BURST); + this->state_.regs()[index] = this->transfer_byte(0); + this->disable(); +} + +void CC1101Component::read_(Register reg, uint8_t *buffer, size_t length) { + uint8_t index = static_cast(reg); + this->enable(); + this->write_byte(index | BUS_READ | BUS_BURST); + this->read_array(buffer, length); + this->disable(); +} + +CC1101Error CC1101Component::transmit_packet(const std::vector &packet) { + if (this->state_.PKT_FORMAT != static_cast(PacketFormat::PACKET_FORMAT_FIFO)) { + return CC1101Error::PARAMS; + } + + // Write packet + this->enter_idle_(); + this->strobe_(Command::FTX); + if (this->state_.LENGTH_CONFIG == static_cast(LengthConfig::LENGTH_CONFIG_VARIABLE)) { + this->write_(Register::FIFO, static_cast(packet.size())); + } + this->write_(Register::FIFO, packet.data(), packet.size()); + + // Calibrate PLL + if (!this->enter_calibrated_(State::FSTXON, Command::FSTXON)) { + ESP_LOGW(TAG, "PLL lock failed during TX"); + this->enter_idle_(); + this->enter_rx_(); + return CC1101Error::PLL_LOCK; + } + + // Transmit packet + this->strobe_(Command::TX); + if (!this->wait_for_state_(State::IDLE, 1000)) { + ESP_LOGW(TAG, "TX timeout"); + this->enter_idle_(); + this->enter_rx_(); + return CC1101Error::TIMEOUT; + } + + // Return to rx + this->enter_rx_(); + return CC1101Error::NONE; +} + +// Setters +void CC1101Component::set_output_power(float value) { + this->output_power_requested_ = value; + int32_t freq = static_cast(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) * + XTAL_FREQUENCY / (1 << 16); + uint8_t a = 0xC0; + if (freq >= 300000000 && freq <= 348000000) { + a = PowerTableItem::find(PA_TABLE_315, sizeof(PA_TABLE_315) / sizeof(PA_TABLE_315[0]), value); + } else if (freq >= 378000000 && freq <= 464000000) { + a = PowerTableItem::find(PA_TABLE_433, sizeof(PA_TABLE_433) / sizeof(PA_TABLE_433[0]), value); + } else if (freq >= 779000000 && freq < 900000000) { + a = PowerTableItem::find(PA_TABLE_868, sizeof(PA_TABLE_868) / sizeof(PA_TABLE_868[0]), value); + } else if (freq >= 900000000 && freq <= 928000000) { + a = PowerTableItem::find(PA_TABLE_915, sizeof(PA_TABLE_915) / sizeof(PA_TABLE_915[0]), value); + } + + if (static_cast(this->state_.MOD_FORMAT) == Modulation::MODULATION_ASK_OOK) { + this->pa_table_[0] = 0; + this->pa_table_[1] = a; + } else { + this->pa_table_[0] = a; + this->pa_table_[1] = 0; + } + this->output_power_effective_ = value; + if (this->initialized_) { + this->write_(Register::PATABLE, this->pa_table_, sizeof(this->pa_table_)); + } +} + +void CC1101Component::set_rx_attenuation(RxAttenuation value) { + this->state_.CLOSE_IN_RX = static_cast(value); + if (this->initialized_) { + this->write_(Register::FIFOTHR); + } +} + +void CC1101Component::set_dc_blocking_filter(bool value) { + this->state_.DEM_DCFILT_OFF = value ? 0 : 1; + if (this->initialized_) { + this->write_(Register::MDMCFG2); + } +} + +void CC1101Component::set_frequency(float value) { + int32_t freq = static_cast(value * (1 << 16) / XTAL_FREQUENCY); + this->state_.FREQ2 = static_cast(freq >> 16); + this->state_.FREQ1 = static_cast(freq >> 8); + this->state_.FREQ0 = static_cast(freq); + if (this->initialized_) { + this->enter_idle_(); + this->write_(Register::FREQ2); + this->write_(Register::FREQ1); + this->write_(Register::FREQ0); + this->enter_rx_(); + } +} + +void CC1101Component::set_if_frequency(float value) { + this->state_.FREQ_IF = value * (1 << 10) / XTAL_FREQUENCY; + if (this->initialized_) { + this->write_(Register::FSCTRL1); + } +} + +void CC1101Component::set_filter_bandwidth(float value) { + uint8_t e; + uint32_t m; + split_float(XTAL_FREQUENCY / (value * 8), 2, e, m); + this->state_.CHANBW_E = e; + this->state_.CHANBW_M = static_cast(m); + if (this->initialized_) { + this->write_(Register::MDMCFG4); + } +} + +void CC1101Component::set_channel(uint8_t value) { + this->state_.CHANNR = value; + if (this->initialized_) { + this->enter_idle_(); + this->write_(Register::CHANNR); + this->enter_rx_(); + } +} + +void CC1101Component::set_channel_spacing(float value) { + uint8_t e; + uint32_t m; + split_float(value * (1 << 18) / XTAL_FREQUENCY, 8, e, m); + this->state_.CHANSPC_E = e; + this->state_.CHANSPC_M = static_cast(m); + if (this->initialized_) { + this->write_(Register::MDMCFG1); + this->write_(Register::MDMCFG0); + } +} + +void CC1101Component::set_fsk_deviation(float value) { + uint8_t e; + uint32_t m; + split_float(value * (1 << 17) / XTAL_FREQUENCY, 3, e, m); + this->state_.DEVIATION_E = e; + this->state_.DEVIATION_M = static_cast(m); + if (this->initialized_) { + this->write_(Register::DEVIATN); + } +} + +void CC1101Component::set_msk_deviation(uint8_t value) { + this->state_.DEVIATION_E = 0; + this->state_.DEVIATION_M = value - 1; + if (this->initialized_) { + this->write_(Register::DEVIATN); + } +} + +void CC1101Component::set_symbol_rate(float value) { + uint8_t e; + uint32_t m; + split_float(value * (1 << 28) / XTAL_FREQUENCY, 8, e, m); + this->state_.DRATE_E = e; + this->state_.DRATE_M = static_cast(m); + if (this->initialized_) { + this->write_(Register::MDMCFG4); + this->write_(Register::MDMCFG3); + } +} + +void CC1101Component::set_sync_mode(SyncMode value) { + this->state_.SYNC_MODE = static_cast(value); + if (this->initialized_) { + this->write_(Register::MDMCFG2); + } +} + +void CC1101Component::set_carrier_sense_above_threshold(bool value) { + this->state_.CARRIER_SENSE_ABOVE_THRESHOLD = value ? 1 : 0; + if (this->initialized_) { + this->write_(Register::MDMCFG2); + } +} + +void CC1101Component::set_modulation_type(Modulation value) { + this->state_.MOD_FORMAT = static_cast(value); + this->state_.PA_POWER = value == Modulation::MODULATION_ASK_OOK ? 1 : 0; + if (this->initialized_) { + this->enter_idle_(); + this->set_output_power(this->output_power_requested_); + this->write_(Register::MDMCFG2); + this->write_(Register::FREND0); + this->enter_rx_(); + } +} + +void CC1101Component::set_manchester(bool value) { + this->state_.MANCHESTER_EN = value ? 1 : 0; + if (this->initialized_) { + this->write_(Register::MDMCFG2); + } +} + +void CC1101Component::set_num_preamble(uint8_t value) { + this->state_.NUM_PREAMBLE = value; + if (this->initialized_) { + this->write_(Register::MDMCFG1); + } +} + +void CC1101Component::set_sync1(uint8_t value) { + this->state_.SYNC1 = value; + if (this->initialized_) { + this->write_(Register::SYNC1); + } +} + +void CC1101Component::set_sync0(uint8_t value) { + this->state_.SYNC0 = value; + if (this->initialized_) { + this->write_(Register::SYNC0); + } +} + +void CC1101Component::set_magn_target(MagnTarget value) { + this->state_.MAGN_TARGET = static_cast(value); + if (this->initialized_) { + this->write_(Register::AGCCTRL2); + } +} + +void CC1101Component::set_max_lna_gain(MaxLnaGain value) { + this->state_.MAX_LNA_GAIN = static_cast(value); + if (this->initialized_) { + this->write_(Register::AGCCTRL2); + } +} + +void CC1101Component::set_max_dvga_gain(MaxDvgaGain value) { + this->state_.MAX_DVGA_GAIN = static_cast(value); + if (this->initialized_) { + this->write_(Register::AGCCTRL2); + } +} + +void CC1101Component::set_carrier_sense_abs_thr(int8_t value) { + this->state_.CARRIER_SENSE_ABS_THR = static_cast(value & 0b1111); + if (this->initialized_) { + this->write_(Register::AGCCTRL1); + } +} + +void CC1101Component::set_carrier_sense_rel_thr(CarrierSenseRelThr value) { + this->state_.CARRIER_SENSE_REL_THR = static_cast(value); + if (this->initialized_) { + this->write_(Register::AGCCTRL1); + } +} + +void CC1101Component::set_lna_priority(bool value) { + this->state_.AGC_LNA_PRIORITY = value ? 1 : 0; + if (this->initialized_) { + this->write_(Register::AGCCTRL1); + } +} + +void CC1101Component::set_filter_length_fsk_msk(FilterLengthFskMsk value) { + this->state_.FILTER_LENGTH = static_cast(value); + if (this->initialized_) { + this->write_(Register::AGCCTRL0); + } +} + +void CC1101Component::set_filter_length_ask_ook(FilterLengthAskOok value) { + this->state_.FILTER_LENGTH = static_cast(value); + if (this->initialized_) { + this->write_(Register::AGCCTRL0); + } +} + +void CC1101Component::set_freeze(Freeze value) { + this->state_.AGC_FREEZE = static_cast(value); + if (this->initialized_) { + this->write_(Register::AGCCTRL0); + } +} + +void CC1101Component::set_wait_time(WaitTime value) { + this->state_.WAIT_TIME = static_cast(value); + if (this->initialized_) { + this->write_(Register::AGCCTRL0); + } +} + +void CC1101Component::set_hyst_level(HystLevel value) { + this->state_.HYST_LEVEL = static_cast(value); + if (this->initialized_) { + this->write_(Register::AGCCTRL0); + } +} + +void CC1101Component::set_packet_mode(bool value) { + this->state_.PKT_FORMAT = + static_cast(value ? PacketFormat::PACKET_FORMAT_FIFO : PacketFormat::PACKET_FORMAT_ASYNC_SERIAL); + if (value) { + // Configure GDO0 for FIFO status (asserts on RX FIFO threshold or end of packet) + this->state_.GDO0_CFG = 0x01; + // Set max RX FIFO threshold to ensure we only trigger on end-of-packet + this->state_.FIFO_THR = 15; + // Don't append status bytes to FIFO - we read from registers instead + this->state_.APPEND_STATUS = 0; + } else { + // Configure GDO0 for serial data (async serial mode) + this->state_.GDO0_CFG = 0x0D; + } + if (this->initialized_) { + this->write_(Register::PKTCTRL0); + this->write_(Register::PKTCTRL1); + this->write_(Register::IOCFG0); + this->write_(Register::FIFOTHR); + } +} + +void CC1101Component::set_packet_length(uint8_t value) { + if (value == 0) { + this->state_.LENGTH_CONFIG = static_cast(LengthConfig::LENGTH_CONFIG_VARIABLE); + } else { + this->state_.LENGTH_CONFIG = static_cast(LengthConfig::LENGTH_CONFIG_FIXED); + this->state_.PKTLEN = value; + } + if (this->initialized_) { + this->write_(Register::PKTCTRL0); + this->write_(Register::PKTLEN); + } +} + +void CC1101Component::set_crc_enable(bool value) { + this->state_.CRC_EN = value ? 1 : 0; + if (this->initialized_) { + this->write_(Register::PKTCTRL0); + } +} + +void CC1101Component::set_whitening(bool value) { + this->state_.WHITE_DATA = value ? 1 : 0; + if (this->initialized_) { + this->write_(Register::PKTCTRL0); + } +} + +} // namespace esphome::cc1101 diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h new file mode 100644 index 0000000000..43ae5b3612 --- /dev/null +++ b/esphome/components/cc1101/cc1101.h @@ -0,0 +1,157 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/spi/spi.h" +#include "esphome/core/automation.h" +#include "cc1101defs.h" +#include + +namespace esphome::cc1101 { + +enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK }; + +class CC1101Component : public Component, + public spi::SPIDevice { + public: + CC1101Component(); + + void setup() override; + void loop() override; + void dump_config() override; + + // Actions + void begin_tx(); + void begin_rx(); + void reset(); + void set_idle(); + + // GDO Pin Configuration + void set_gdo0_pin(InternalGPIOPin *pin) { this->gdo0_pin_ = pin; } + + // Configuration Setters + void set_output_power(float value); + void set_rx_attenuation(RxAttenuation value); + void set_dc_blocking_filter(bool value); + + // Tuner settings + void set_frequency(float value); + void set_if_frequency(float value); + void set_filter_bandwidth(float value); + void set_channel(uint8_t value); + void set_channel_spacing(float value); + void set_fsk_deviation(float value); + void set_msk_deviation(uint8_t value); + void set_symbol_rate(float value); + void set_sync_mode(SyncMode value); + void set_carrier_sense_above_threshold(bool value); + void set_modulation_type(Modulation value); + void set_manchester(bool value); + void set_num_preamble(uint8_t value); + void set_sync1(uint8_t value); + void set_sync0(uint8_t value); + + // AGC settings + void set_magn_target(MagnTarget value); + void set_max_lna_gain(MaxLnaGain value); + void set_max_dvga_gain(MaxDvgaGain value); + void set_carrier_sense_abs_thr(int8_t value); + void set_carrier_sense_rel_thr(CarrierSenseRelThr value); + void set_lna_priority(bool value); + void set_filter_length_fsk_msk(FilterLengthFskMsk value); + void set_filter_length_ask_ook(FilterLengthAskOok value); + void set_freeze(Freeze value); + void set_wait_time(WaitTime value); + void set_hyst_level(HystLevel value); + + // Packet mode settings + void set_packet_mode(bool value); + void set_packet_length(uint8_t value); + void set_crc_enable(bool value); + void set_whitening(bool value); + + // Packet mode operations + CC1101Error transmit_packet(const std::vector &packet); + Trigger, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; } + + protected: + uint16_t chip_id_{0}; + bool initialized_{false}; + + float output_power_requested_{10.0f}; + float output_power_effective_{10.0f}; + uint8_t pa_table_[PA_TABLE_SIZE]{}; + + CC1101State state_; + + // GDO pin for packet reception + InternalGPIOPin *gdo0_pin_{nullptr}; + + // Packet handling + Trigger, float, float, uint8_t> *packet_trigger_{ + new Trigger, float, float, uint8_t>()}; + std::vector packet_; + + // Low-level Helpers + uint8_t strobe_(Command cmd); + void write_(Register reg); + void write_(Register reg, uint8_t value); + void write_(Register reg, const uint8_t *buffer, size_t length); + void read_(Register reg); + void read_(Register reg, uint8_t *buffer, size_t length); + + // State Management + bool wait_for_state_(State target_state, uint32_t timeout_ms = 100); + bool enter_calibrated_(State target_state, Command cmd); + void enter_idle_(); + bool enter_rx_(); + bool enter_tx_(); +}; + +// Action Wrappers +template class BeginTxAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->begin_tx(); } +}; + +template class BeginRxAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->begin_rx(); } +}; + +template class ResetAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->reset(); } +}; + +template class SetIdleAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->set_idle(); } +}; + +template class SendPacketAction : public Action, public Parented { + public: + void set_data_template(std::function(Ts...)> func) { this->data_func_ = func; } + void set_data_static(const uint8_t *data, size_t len) { + this->data_static_ = data; + this->data_static_len_ = len; + } + + void play(const Ts &...x) override { + if (this->data_func_) { + auto data = this->data_func_(x...); + this->parent_->transmit_packet(data); + } else if (this->data_static_ != nullptr) { + std::vector data(this->data_static_, this->data_static_ + this->data_static_len_); + this->parent_->transmit_packet(data); + } + } + + protected: + std::function(Ts...)> data_func_{}; + const uint8_t *data_static_{nullptr}; + size_t data_static_len_{0}; +}; + +} // namespace esphome::cc1101 diff --git a/esphome/components/cc1101/cc1101defs.h b/esphome/components/cc1101/cc1101defs.h new file mode 100644 index 0000000000..59b29f7478 --- /dev/null +++ b/esphome/components/cc1101/cc1101defs.h @@ -0,0 +1,670 @@ +#pragma once + +#include + +namespace esphome::cc1101 { + +static constexpr float XTAL_FREQUENCY = 26000000; + +static constexpr float RSSI_OFFSET = 74.0f; +static constexpr float RSSI_STEP = 0.5f; + +static constexpr uint8_t FSCAL1_PLL_NOT_LOCKED = 0x3F; +static constexpr uint8_t PLL_LOCK_RETRIES = 3; + +static constexpr uint8_t STATUS_CRC_OK_MASK = 0x80; +static constexpr uint8_t STATUS_LQI_MASK = 0x7F; + +static constexpr uint8_t BUS_BURST = 0x40; +static constexpr uint8_t BUS_READ = 0x80; +static constexpr uint8_t BUS_WRITE = 0x00; +static constexpr uint8_t BYTES_IN_RXFIFO = 0x7F; // byte number in RXfifo +static constexpr size_t PA_TABLE_SIZE = 8; + +enum class Register : uint8_t { + IOCFG2 = 0x00, // GDO2 output pin configuration + IOCFG1 = 0x01, // GDO1 output pin configuration + IOCFG0 = 0x02, // GDO0 output pin configuration + FIFOTHR = 0x03, // RX FIFO and TX FIFO thresholds + SYNC1 = 0x04, // Sync word, high INT8U + SYNC0 = 0x05, // Sync word, low INT8U + PKTLEN = 0x06, // Packet length + PKTCTRL1 = 0x07, // Packet automation control + PKTCTRL0 = 0x08, // Packet automation control + ADDR = 0x09, // Device address + CHANNR = 0x0A, // Channel number + FSCTRL1 = 0x0B, // Frequency synthesizer control + FSCTRL0 = 0x0C, // Frequency synthesizer control + FREQ2 = 0x0D, // Frequency control word, high INT8U + FREQ1 = 0x0E, // Frequency control word, middle INT8U + FREQ0 = 0x0F, // Frequency control word, low INT8U + MDMCFG4 = 0x10, // Modem configuration + MDMCFG3 = 0x11, // Modem configuration + MDMCFG2 = 0x12, // Modem configuration + MDMCFG1 = 0x13, // Modem configuration + MDMCFG0 = 0x14, // Modem configuration + DEVIATN = 0x15, // Modem deviation setting + MCSM2 = 0x16, // Main Radio Control State Machine configuration + MCSM1 = 0x17, // Main Radio Control State Machine configuration + MCSM0 = 0x18, // Main Radio Control State Machine configuration + FOCCFG = 0x19, // Frequency Offset Compensation configuration + BSCFG = 0x1A, // Bit Synchronization configuration + AGCCTRL2 = 0x1B, // AGC control + AGCCTRL1 = 0x1C, // AGC control + AGCCTRL0 = 0x1D, // AGC control + WOREVT1 = 0x1E, // High INT8U Event 0 timeout + WOREVT0 = 0x1F, // Low INT8U Event 0 timeout + WORCTRL = 0x20, // Wake On Radio control + FREND1 = 0x21, // Front end RX configuration + FREND0 = 0x22, // Front end TX configuration + FSCAL3 = 0x23, // Frequency synthesizer calibration + FSCAL2 = 0x24, // Frequency synthesizer calibration + FSCAL1 = 0x25, // Frequency synthesizer calibration + FSCAL0 = 0x26, // Frequency synthesizer calibration + RCCTRL1 = 0x27, // RC oscillator configuration + RCCTRL0 = 0x28, // RC oscillator configuration + FSTEST = 0x29, // Frequency synthesizer calibration control + PTEST = 0x2A, // Production test + AGCTEST = 0x2B, // AGC test + TEST2 = 0x2C, // Various test settings + TEST1 = 0x2D, // Various test settings + TEST0 = 0x2E, // Various test settings + UNUSED = 0x2F, + PARTNUM = 0x30, + VERSION = 0x31, + FREQEST = 0x32, + LQI = 0x33, + RSSI = 0x34, + MARCSTATE = 0x35, + WORTIME1 = 0x36, + WORTIME0 = 0x37, + PKTSTATUS = 0x38, + VCO_VC_DAC = 0x39, + TXBYTES = 0x3A, + RXBYTES = 0x3B, + RCCTRL1_STATUS = 0x3C, + RCCTRL0_STATUS = 0x3D, + PATABLE = 0x3E, + FIFO = 0x3F, +}; + +enum class Command : uint8_t { + RES = 0x30, // Reset chip. + FSTXON = 0x31, // Enable and calibrate frequency synthesizer + XOFF = 0x32, // Turn off crystal oscillator. + CAL = 0x33, // Calibrate frequency synthesizer and turn it off + RX = 0x34, // Enable RX. + TX = 0x35, // Enable TX. + IDLE = 0x36, // Exit RX / TX + // 0x37 is RESERVED / UNDEFINED in CC1101 Datasheet + WOR = 0x38, // Start automatic RX polling sequence (Wake-on-Radio) + PWD = 0x39, // Enter power down mode when CSn goes high. + FRX = 0x3A, // Flush the RX FIFO buffer. + FTX = 0x3B, // Flush the TX FIFO buffer. + WORRST = 0x3C, // Reset real time clock. + NOP = 0x3D, // No operation. +}; + +enum class State : uint8_t { + SLEEP, + IDLE, + XOFF, + VCOON_MC, + REGON_MC, + MANCAL, + VCOON, + REGON, + STARTCAL, + BWBOOST, + FS_LOCK, + IFADCON, + ENDCAL, + RX, + RX_END, + RX_RST, + TXRX_SWITCH, + RXFIFO_OVERFLOW, + FSTXON, + TX, + TX_END, + RXTX_SWITCH, + TXFIFO_UNDERFLOW, +}; + +enum class RxAttenuation : uint8_t { + RX_ATTENUATION_0DB, + RX_ATTENUATION_6DB, + RX_ATTENUATION_12DB, + RX_ATTENUATION_18DB, +}; + +enum class SyncMode : uint8_t { + SYNC_MODE_NONE, + SYNC_MODE_15_16, + SYNC_MODE_16_16, + SYNC_MODE_30_32, + SYNC_MODE_NONE_CS, + SYNC_MODE_15_16_CS, + SYNC_MODE_16_16_CS, + SYNC_MODE_30_32_CS, +}; + +enum class Modulation : uint8_t { + MODULATION_2_FSK, + MODULATION_GFSK, + MODULATION_UNUSED_2, + MODULATION_ASK_OOK, + MODULATION_4_FSK, + MODULATION_UNUSED_5, + MODULATION_UNUSED_6, + MODULATION_MSK, +}; + +enum class MagnTarget : uint8_t { + MAGN_TARGET_24DB, + MAGN_TARGET_27DB, + MAGN_TARGET_30DB, + MAGN_TARGET_33DB, + MAGN_TARGET_36DB, + MAGN_TARGET_38DB, + MAGN_TARGET_40DB, + MAGN_TARGET_42DB, +}; + +enum class MaxLnaGain : uint8_t { + MAX_LNA_GAIN_DEFAULT, + MAX_LNA_GAIN_MINUS_2P6DB, + MAX_LNA_GAIN_MINUS_6P1DB, + MAX_LNA_GAIN_MINUS_7P4DB, + MAX_LNA_GAIN_MINUS_9P2DB, + MAX_LNA_GAIN_MINUS_11P5DB, + MAX_LNA_GAIN_MINUS_14P6DB, + MAX_LNA_GAIN_MINUS_17P1DB, +}; + +enum class MaxDvgaGain : uint8_t { + MAX_DVGA_GAIN_DEFAULT, + MAX_DVGA_GAIN_MINUS_1, + MAX_DVGA_GAIN_MINUS_2, + MAX_DVGA_GAIN_MINUS_3, +}; + +enum class CarrierSenseRelThr : uint8_t { + CARRIER_SENSE_REL_THR_DEFAULT, + CARRIER_SENSE_REL_THR_PLUS_6DB, + CARRIER_SENSE_REL_THR_PLUS_10DB, + CARRIER_SENSE_REL_THR_PLUS_14DB, +}; + +enum class FilterLengthFskMsk : uint8_t { + FILTER_LENGTH_8DB, + FILTER_LENGTH_16DB, + FILTER_LENGTH_32DB, + FILTER_LENGTH_64DB, +}; + +enum class FilterLengthAskOok : uint8_t { + FILTER_LENGTH_4DB, + FILTER_LENGTH_8DB, + FILTER_LENGTH_12DB, + FILTER_LENGTH_16DB, +}; + +enum class Freeze : uint8_t { + FREEZE_DEFAULT, + FREEZE_ON_SYNC, + FREEZE_ANALOG_ONLY, + FREEZE_ANALOG_AND_DIGITAL, +}; + +enum class WaitTime : uint8_t { + WAIT_TIME_8_SAMPLES, + WAIT_TIME_16_SAMPLES, + WAIT_TIME_24_SAMPLES, + WAIT_TIME_32_SAMPLES, +}; + +enum class HystLevel : uint8_t { + HYST_LEVEL_NONE, + HYST_LEVEL_LOW, + HYST_LEVEL_MEDIUM, + HYST_LEVEL_HIGH, +}; + +enum class PacketFormat : uint8_t { + PACKET_FORMAT_FIFO, + PACKET_FORMAT_SYNC_SERIAL, + PACKET_FORMAT_RANDOM_TX, + PACKET_FORMAT_ASYNC_SERIAL, +}; + +enum class LengthConfig : uint8_t { + LENGTH_CONFIG_FIXED, + LENGTH_CONFIG_VARIABLE, + LENGTH_CONFIG_INFINITE, +}; + +struct __attribute__((packed)) CC1101State { + // Byte array accessors for bulk SPI transfers + uint8_t *regs() { return reinterpret_cast(this); } + const uint8_t *regs() const { return reinterpret_cast(this); } + + // 0x00 + union { + uint8_t IOCFG2; + struct { + uint8_t GDO2_CFG : 6; + uint8_t GDO2_INV : 1; + uint8_t : 1; + }; + }; + // 0x01 + union { + uint8_t IOCFG1; + struct { + uint8_t GDO1_CFG : 6; + uint8_t GDO1_INV : 1; + uint8_t GDO_DS : 1; // GDO, not GD0 + }; + }; + // 0x02 + union { + uint8_t IOCFG0; + struct { + uint8_t GDO0_CFG : 6; + uint8_t GDO0_INV : 1; + uint8_t TEMP_SENSOR_ENABLE : 1; + }; + }; + // 0x03 + union { + uint8_t FIFOTHR; + struct { + uint8_t FIFO_THR : 4; + uint8_t CLOSE_IN_RX : 2; // RxAttenuation + uint8_t ADC_RETENTION : 1; + uint8_t : 1; + }; + }; + // 0x04 + uint8_t SYNC1; + // 0x05 + uint8_t SYNC0; + // 0x06 + uint8_t PKTLEN; + // 0x07 + union { + uint8_t PKTCTRL1; + struct { + uint8_t ADR_CHK : 2; + uint8_t APPEND_STATUS : 1; + uint8_t CRC_AUTOFLUSH : 1; + uint8_t : 1; + uint8_t PQT : 3; + }; + }; + // 0x08 + union { + uint8_t PKTCTRL0; + struct { + uint8_t LENGTH_CONFIG : 2; + uint8_t CRC_EN : 1; + uint8_t : 1; + uint8_t PKT_FORMAT : 2; + uint8_t WHITE_DATA : 1; + uint8_t : 1; + }; + }; + // 0x09 + uint8_t ADDR; + // 0x0A + uint8_t CHANNR; + // 0x0B + union { + uint8_t FSCTRL1; + struct { + uint8_t FREQ_IF : 5; + uint8_t RESERVED : 1; // hm? + uint8_t : 2; + }; + }; + // 0x0C + uint8_t FSCTRL0; + // 0x0D + uint8_t FREQ2; // [7:6] always zero + // 0x0E + uint8_t FREQ1; + // 0x0F + uint8_t FREQ0; + // 0x10 + union { + uint8_t MDMCFG4; + struct { + uint8_t DRATE_E : 4; + uint8_t CHANBW_M : 2; + uint8_t CHANBW_E : 2; + }; + }; + // 0x11 + union { + uint8_t MDMCFG3; + struct { + uint8_t DRATE_M : 8; + }; + }; + // 0x12 + union { + uint8_t MDMCFG2; + struct { + uint8_t SYNC_MODE : 2; + uint8_t CARRIER_SENSE_ABOVE_THRESHOLD : 1; + uint8_t MANCHESTER_EN : 1; + uint8_t MOD_FORMAT : 3; // Modulation + uint8_t DEM_DCFILT_OFF : 1; + }; + }; + // 0x13 + union { + uint8_t MDMCFG1; + struct { + uint8_t CHANSPC_E : 2; + uint8_t : 2; + uint8_t NUM_PREAMBLE : 3; + uint8_t FEC_EN : 1; + }; + }; + // 0x14 + union { + uint8_t MDMCFG0; + struct { + uint8_t CHANSPC_M : 8; + }; + }; + // 0x15 + union { + uint8_t DEVIATN; + struct { + uint8_t DEVIATION_M : 3; + uint8_t : 1; + uint8_t DEVIATION_E : 3; + uint8_t : 1; + }; + }; + // 0x16 + union { + uint8_t MCSM2; + struct { + uint8_t RX_TIME : 3; + uint8_t RX_TIME_QUAL : 1; + uint8_t RX_TIME_RSSI : 1; + uint8_t : 3; + }; + }; + // 0x17 + union { + uint8_t MCSM1; + struct { + uint8_t TXOFF_MODE : 2; + uint8_t RXOFF_MODE : 2; + uint8_t CCA_MODE : 2; + uint8_t : 2; + }; + }; + // 0x18 + union { + uint8_t MCSM0; + struct { + uint8_t XOSC_FORCE_ON : 1; + uint8_t PIN_CTRL_EN : 1; + uint8_t PO_TIMEOUT : 2; + uint8_t FS_AUTOCAL : 2; + uint8_t : 2; + }; + }; + // 0x19 + union { + uint8_t FOCCFG; + struct { + uint8_t FOC_LIMIT : 2; + uint8_t FOC_POST_K : 1; + uint8_t FOC_PRE_K : 2; + uint8_t FOC_BS_CS_GATE : 1; + uint8_t : 2; + }; + }; + // 0x1A + union { + uint8_t BSCFG; + struct { + uint8_t BS_LIMIT : 2; + uint8_t BS_POST_KP : 1; + uint8_t BS_POST_KI : 1; + uint8_t BS_PRE_KP : 2; + uint8_t BS_PRE_KI : 2; + }; + }; + // 0x1B + union { + uint8_t AGCCTRL2; + struct { + uint8_t MAGN_TARGET : 3; // MagnTarget + uint8_t MAX_LNA_GAIN : 3; // MaxLnaGain + uint8_t MAX_DVGA_GAIN : 2; // MaxDvgaGain + }; + }; + // 0x1C + union { + uint8_t AGCCTRL1; + struct { + uint8_t CARRIER_SENSE_ABS_THR : 4; + uint8_t CARRIER_SENSE_REL_THR : 2; // CarrierSenseRelThr + uint8_t AGC_LNA_PRIORITY : 1; + uint8_t : 1; + }; + }; + // 0x1D + union { + uint8_t AGCCTRL0; + struct { + uint8_t FILTER_LENGTH : 2; // FilterLengthFskMsk or FilterLengthAskOok + uint8_t AGC_FREEZE : 2; // Freeze + uint8_t WAIT_TIME : 2; // WaitTime + uint8_t HYST_LEVEL : 2; // HystLevel + }; + }; + // 0x1E + uint8_t WOREVT1; + // 0x1F + uint8_t WOREVT0; + // 0x20 + union { + uint8_t WORCTRL; + struct { + uint8_t WOR_RES : 2; + uint8_t : 1; + uint8_t RC_CAL : 1; + uint8_t EVENT1 : 3; + uint8_t RC_PD : 1; + }; + }; + // 0x21 + union { + uint8_t FREND1; + struct { + uint8_t MIX_CURRENT : 2; + uint8_t LODIV_BUF_CURRENT_RX : 2; + uint8_t LNA2MIX_CURRENT : 2; + uint8_t LNA_CURRENT : 2; + }; + }; + // 0x22 + union { + uint8_t FREND0; + struct { + uint8_t PA_POWER : 3; + uint8_t : 1; + uint8_t LODIV_BUF_CURRENT_TX : 2; + uint8_t : 2; + }; + }; + // 0x23 + union { + uint8_t FSCAL3; + struct { + uint8_t FSCAL3_LO : 4; + uint8_t CHP_CURR_CAL_EN : 2; // Disable charge pump calibration stage when 0. + uint8_t FSCAL3_HI : 2; + }; + }; + // 0x24 + union { + // uint8_t FSCAL2; + struct { + uint8_t FSCAL2 : 5; + uint8_t VCO_CORE_H_EN : 1; + uint8_t : 2; + }; + }; + // 0x25 + union { + // uint8_t FSCAL1; + struct { + uint8_t FSCAL1 : 6; + uint8_t : 2; + }; + }; + // 0x26 + union { + // uint8_t FSCAL0; + struct { + uint8_t FSCAL0 : 7; + uint8_t : 1; + }; + }; + // 0x27 + union { + // uint8_t RCCTRL1; + struct { + uint8_t RCCTRL1 : 7; + uint8_t : 1; + }; + }; + // 0x28 + union { + // uint8_t RCCTRL0; + struct { + uint8_t RCCTRL0 : 7; + uint8_t : 1; + }; + }; + // 0x29 + uint8_t FSTEST; + // 0x2A + uint8_t PTEST; + // 0x2B + uint8_t AGCTEST; + // 0x2C + uint8_t TEST2; + // 0x2D + uint8_t TEST1; + // 0x2E + union { + uint8_t TEST0; + struct { + uint8_t TEST0_LO : 1; + uint8_t VCO_SEL_CAL_EN : 1; // Enable VCO selection calibration stage when 1 + uint8_t TEST0_HI : 6; + }; + }; + // 0x2F + uint8_t REG_2F; + // 0x30 + uint8_t PARTNUM; + // 0x31 + uint8_t VERSION; + // 0x32 + union { + uint8_t FREQEST; + struct { + int8_t FREQOFF_EST : 8; + }; + }; + // 0x33 + union { + uint8_t LQI; + struct { + uint8_t LQI_EST : 7; + uint8_t LQI_CRC_OK : 1; + }; + }; + // 0x34 + int8_t RSSI; + // 0x35 + union { + // uint8_t MARCSTATE; + struct { + uint8_t MARC_STATE : 5; // State + uint8_t : 3; + }; + }; + // 0x36 + uint8_t WORTIME1; + // 0x37 + uint8_t WORTIME0; + // 0x38 + union { + uint8_t PKTSTATUS; + struct { + uint8_t GDO0 : 1; + uint8_t : 1; + uint8_t GDO2 : 1; + uint8_t SFD : 1; + uint8_t CCA : 1; + uint8_t PQT_REACHED : 1; + uint8_t CS : 1; + uint8_t CRC_OK : 1; // same as LQI_CRC_OK? + }; + }; + // 0x39 + uint8_t VCO_VC_DAC; + // 0x3A + union { + uint8_t TXBYTES; + struct { + uint8_t NUM_TXBYTES : 7; + uint8_t TXFIFO_UNDERFLOW : 1; + }; + }; + // 0x3B + union { + uint8_t RXBYTES; + struct { + uint8_t NUM_RXBYTES : 7; + uint8_t RXFIFO_OVERFLOW : 1; + }; + }; + // 0x3C + union { + // uint8_t RCCTRL1_STATUS; + struct { + uint8_t RCCTRL1_STATUS : 7; + uint8_t : 1; + }; + }; + // 0x3D + union { + // uint8_t RCCTRL0_STATUS; + struct { + uint8_t RCCTRL0_STATUS : 7; + uint8_t : 1; + }; + }; + // 0x3E + uint8_t REG_3E; + // 0x3F + uint8_t REG_3F; +}; + +static_assert(sizeof(CC1101State) == 0x40, "CC1101State size mismatch"); + +} // namespace esphome::cc1101 diff --git a/esphome/components/cc1101/cc1101pa.h b/esphome/components/cc1101/cc1101pa.h new file mode 100644 index 0000000000..e5e7a47c51 --- /dev/null +++ b/esphome/components/cc1101/cc1101pa.h @@ -0,0 +1,174 @@ +#pragma once + +#include +#include +#include + +namespace esphome::cc1101 { + +// CC1101 Design Note DN013 + +struct PowerTableItem { + uint8_t value; + uint8_t dbm_diff; // starts from 12.0, diff to previous entry, scaled by 10 + + static uint8_t find(const PowerTableItem *items, size_t count, float &dbm_target) { + int32_t dbmi = 120; + int32_t dbmi_target = static_cast(std::lround(dbm_target * 10)); + for (size_t i = 0; i < count; i++) { + dbmi -= items[i].dbm_diff; + if (dbmi_target >= dbmi) { + // Skip invalid PA settings (magic numbers derived from TI DN013/SmartRC logic) + if (items[i].value >= 0x61 && items[i].value <= 0x6F) { + continue; + } + dbm_target = static_cast(dbmi) / 10.0f; + return items[i].value; + } + } + dbm_target = -30.0f; + return 0x03; + } +}; + +static const PowerTableItem PA_TABLE_315[] = { + {0xC0, 14}, // C0 10.6 -35.3 -44.4 -57.8 -53.8 -58.3 -57.2 -57.8 -56.7 28.5 + {0xC3, 10}, // C3 9.6 -39.2 -45.3 -59.0 -54.2 -59.0 -57.5 -58.3 -57.2 26.2 + {0xC6, 11}, // C6 8.5 -43.2 -46.3 -59.2 -54.7 -59.1 -57.7 -58.3 -57.4 24.4 + {0xC9, 10}, // C9 7.5 -47.0 -47.3 -58.9 -55.0 -59.0 -57.9 -58.4 -57.5 23.0 + {0x81, 12}, // 81 6.3 -49.2 -45.7 -57.3 -53.6 -59.0 -56.0 -56.5 -57.5 19.5 + {0x85, 13}, // 85 5.0 -51.0 -47.2 -59.8 -54.2 -59.0 -56.9 -57.9 -58.0 18.3 + {0x88, 11}, // 88 3.9 -46.6 -48.1 -60.0 -55.0 -58.9 -57.5 -58.2 -58.2 17.4 + {0xCF, 11}, // CF 2.8 -49.8 -51.3 -57.6 -56.8 -59.1 -58.4 -58.1 -58.3 18.0 + {0x8D, 11}, // 8D 1.7 -43.8 -49.5 -58.9 -56.3 -58.8 -58.2 -58.4 -58.5 15.8 + {0x50, 10}, // 50 0.7 -59.2 -51.2 -59.0 -56.5 -59.0 -58.3 -58.3 -58.2 15.3 + {0x40, 10}, // 40 -0.3 -58.2 -52.1 -59.4 -56.9 -59.0 -58.4 -58.4 -58.3 14.7 + {0x3D, 10}, // 3D -1.3 -54.4 -48.4 -59.8 -57.5 -58.9 -58.3 -58.5 -58.5 19.3 + {0x55, 10}, // 55 -2.3 -56.7 -53.6 -59.7 -57.5 -59.1 -58.7 -58.4 -58.4 13.7 + {0x39, 11}, // 39 -3.4 -50.9 -49.5 -59.8 -58.0 -59.0 -58.5 -58.4 -58.4 16.8 + {0x2B, 15}, // 2B -4.9 -51.2 -50.4 -59.9 -58.0 -58.9 -58.7 -58.3 -58.4 15.6 + {0x29, 16}, // 29 -6.5 -51.8 -51.6 -59.9 -58.4 -59.0 -58.8 -58.3 -58.3 14.7 + {0x28, 10}, // 28 -7.5 -52.2 -52.5 -60.0 -58.6 -59.0 -58.8 -58.2 -58.4 14.3 + {0x27, 11}, // 27 -8.6 -52.9 -53.1 -60.0 -58.8 -59.1 -58.8 -58.3 -58.5 13.9 + {0x26, 12}, // 26 -9.8 -53.6 -54.3 -60.1 -58.7 -59.0 -58.7 -58.4 -58.4 13.4 + {0x25, 13}, // 25 -11.1 -54.3 -55.5 -60.1 -58.8 -59.1 -58.8 -58.4 -58.4 13.0 + {0x33, 11}, // 33 -12.2 -55.0 -56.3 -60.0 -58.7 -59.0 -58.9 -58.4 -58.4 12.8 + {0x1F, 11}, // 1F -13.3 -55.6 -57.2 -60.0 -58.8 -58.9 -58.9 -58.3 -58.4 12.4 + {0x1D, 12}, // 1D -14.5 -56.0 -58.0 -60.0 -58.8 -59.1 -58.7 -58.4 -58.5 12.1 + {0x32, 11}, // 32 -15.6 -56.9 -58.8 -59.9 -58.8 -59.0 -58.8 -58.3 -58.5 12.2 + {0x1A, 10}, // 1A -16.6 -57.3 -59.5 -59.9 -58.8 -59.1 -58.8 -58.4 -58.4 11.8 + {0x18, 19}, // 18 -18.5 -57.8 -60.3 -60.0 -58.8 -59.0 -58.9 -58.2 -58.5 11.6 + {0x17, 11}, // 17 -19.6 -58.7 -60.9 -60.0 -58.7 -58.9 -58.9 -58.5 -58.4 11.4 + {0x0C, 11}, // C -20.7 -59.4 -61.1 -60.0 -58.8 -59.1 -58.9 -58.4 -58.3 11.3 + {0x0A, 15}, // A -22.2 -59.9 -61.9 -60.0 -58.9 -59.0 -58.9 -58.4 -58.5 11.2 + {0x08, 18}, // 8 -24.0 -60.5 -62.5 -60.0 -58.7 -59.1 -58.8 -58.3 -58.5 11.1 + {0x07, 11}, // 7 -25.1 -61.3 -62.9 -60.1 -58.8 -59.1 -58.8 -58.4 -58.4 11.0 + {0x06, 13}, // 6 -26.4 -61.6 -63.2 -60.1 -58.7 -59.0 -58.9 -58.5 -58.5 11.0 + {0x05, 13}, // 5 -27.7 -62.3 -63.4 -60.1 -58.7 -59.2 -58.8 -58.4 -58.5 10.9 + {0x04, 19}, // 4 -29.6 -62.7 -63.6 -59.9 -58.7 -59.0 -58.9 -58.4 -58.4 10.8 +}; + +static const PowerTableItem PA_TABLE_433[] = { + {0xC0, 21}, // C0 9.9 -43.4 -45.0 -53.9 -55.2 -55.8 -52.3 -55.6 29.1 + {0xC3, 11}, // C3 8.8 -49.3 -45.9 -55.9 -55.4 -57.2 -52.6 -57.5 26.9 + {0xC6, 10}, // C6 7.8 -56.2 -46.9 -56.9 -55.6 -58.2 -53.2 -57.9 25.2 + {0xC9, 10}, // C9 6.8 -56.1 -47.9 -57.3 -55.9 -58.5 -54.0 -56.9 23.8 + {0xCC, 10}, // CC 5.8 -52.8 -48.9 -57.0 -56.1 -58.4 -54.6 -56.2 22.6 + {0x85, 10}, // 85 4.8 -54.2 -53.0 -58.3 -55.0 -57.8 -56.8 -58.0 19.1 + {0x88, 12}, // 88 3.6 -56.2 -53.8 -58.3 -55.7 -58.1 -57.2 -58.2 18.2 + {0x8B, 13}, // 8B 2.3 -57.7 -54.5 -58.0 -56.3 -58.1 -57.5 -58.2 17.3 + {0x8E, 19}, // 8E 0.4 -58.0 -55.5 -57.8 -57.4 -58.2 -58.1 -58.4 16.2 + {0x40, 12}, // 40 -0.8 -59.7 -56.1 -58.2 -57.7 -58.4 -58.3 -58.2 15.4 + {0x3C, 13}, // 3C -2.1 -60.6 -57.3 -58.2 -58.0 -58.5 -58.4 -58.5 19.3 + {0x3A, 10}, // 3A -3.1 -59.5 -57.5 -58.3 -58.3 -58.6 -58.1 -58.6 18.1 + {0x8F, 15}, // 8F -4.6 -52.2 -57.7 -58.1 -58.8 -58.4 -58.7 -58.3 14.4 + {0x37, 10}, // 37 -5.6 -56.8 -58.3 -58.3 -58.8 -58.4 -58.5 -58.4 16.2 + {0x36, 12}, // 36 -6.8 -56.8 -58.9 -58.3 -58.8 -58.3 -58.5 -58.5 15.6 + {0x28, 10}, // 28 -7.8 -56.6 -59.0 -58.2 -59.0 -58.4 -58.5 -58.4 15.1 + {0x26, 21}, // 26 -9.9 -57.0 -59.4 -58.3 -59.0 -58.4 -58.7 -58.4 14.3 + {0x25, 15}, // 25 -11.4 -57.3 -59.7 -58.4 -59.0 -58.3 -58.7 -58.5 13.9 + {0x24, 19}, // 24 -13.3 -57.9 -59.9 -58.2 -59.0 -58.6 -58.7 -58.5 13.5 + {0x1E, 10}, // 1E -14.3 -58.4 -59.8 -58.2 -59.0 -58.4 -58.6 -58.6 13.2 + {0x1C, 12}, // 1C -15.5 -58.6 -59.9 -58.4 -58.8 -58.6 -58.8 -58.5 12.9 + {0x1A, 15}, // 1A -17.0 -59.4 -59.9 -58.3 -59.1 -58.5 -58.7 -58.4 12.7 + {0x18, 18}, // 18 -18.8 -60.2 -59.9 -58.2 -59.0 -58.5 -58.7 -58.6 12.5 + {0x17, 10}, // 17 -19.8 -60.6 -59.9 -58.2 -58.9 -58.4 -58.7 -58.4 12.4 + {0x0C, 12}, // C -21.0 -61.1 -59.9 -58.4 -59.0 -58.5 -58.7 -58.6 12.3 + {0x15, 15}, // 15 -22.5 -61.7 -60.0 -58.2 -59.1 -58.3 -58.6 -58.7 12.2 + {0x08, 18}, // 8 -24.3 -62.3 -59.9 -58.3 -59.0 -58.4 -58.8 -58.5 12.1 + {0x07, 10}, // 7 -25.3 -62.6 -59.9 -58.2 -59.0 -58.6 -58.7 -58.5 12.0 + {0x06, 12}, // 6 -26.5 -63.2 -59.9 -58.3 -58.9 -58.5 -58.6 -58.6 12.0 + {0x05, 14}, // 5 -27.9 -63.5 -59.8 -58.3 -59.1 -58.5 -58.7 -58.4 11.9 + {0x04, 16}, // 4 -29.5 -63.7 -59.9 -58.3 -58.9 -58.5 -58.5 -58.5 11.9 +}; + +static const PowerTableItem PA_TABLE_868[] = { + {0xC0, 13}, // C0 10.7 -35.1 -58.6 -58.6 -57.5 -50.0 34.2 + {0xC3, 11}, // C3 9.6 -41.5 -58.5 -58.3 -57.4 -54.4 31.6 + {0xC6, 11}, // C6 8.5 -47.7 -58.5 -58.3 -57.6 -55.0 29.5 + {0xC9, 10}, // C9 7.5 -44.4 -58.5 -58.5 -57.7 -53.6 27.8 + {0xCC, 10}, // CC 6.5 -40.6 -58.6 -58.4 -57.6 -52.5 26.3 + {0xCE, 10}, // CE 5.5 -38.5 -58.5 -58.4 -57.8 -52.2 25.0 + {0x84, 11}, // 84 4.4 -35.3 -58.7 -58.5 -57.8 -55.8 20.3 + {0x87, 10}, // 87 3.4 -39.4 -58.6 -58.6 -57.8 -55.7 19.5 + {0xCF, 10}, // CF 2.4 -36.6 -58.6 -58.4 -57.7 -53.6 22.0 + {0x8C, 13}, // 8C 1.1 -50.2 -58.6 -58.5 -57.7 -55.9 17.9 + {0x50, 14}, // 50 -0.3 -42.1 -58.5 -58.5 -57.6 -57.1 16.9 + {0x40, 12}, // 40 -1.5 -43.2 -58.5 -58.7 -57.7 -57.2 16.1 + {0x3F, 11}, // 3F -2.6 -53.7 -58.6 -58.5 -57.8 -57.5 21.4 + {0x55, 10}, // 55 -3.6 -44.9 -58.6 -58.4 -57.8 -57.5 15.0 + {0x57, 12}, // 57 -4.8 -46.0 -58.6 -58.5 -57.6 -57.4 14.5 + {0x8F, 12}, // 8F -6.0 -51.6 -58.5 -58.6 -57.7 -57.1 15.0 + {0x2A, 14}, // 2A -7.4 -49.3 -58.5 -58.6 -57.7 -57.4 16.2 + {0x28, 16}, // 28 -9.0 -49.0 -58.5 -58.6 -57.7 -57.4 15.4 + {0x26, 20}, // 26 -11.0 -49.2 -58.5 -58.5 -57.7 -57.4 14.6 + {0x25, 15}, // 25 -12.5 -49.5 -58.6 -58.6 -57.8 -57.3 14.1 + {0x24, 18}, // 24 -14.3 -50.2 -58.5 -58.4 -57.8 -57.4 13.7 + {0x1D, 14}, // 1D -15.7 -50.7 -58.6 -58.6 -57.8 -57.5 13.3 + {0x1B, 13}, // 1B -17.0 -51.3 -58.5 -58.4 -57.7 -57.5 13.1 + {0x19, 16}, // 19 -18.6 -52.0 -58.6 -58.5 -57.8 -57.5 12.9 + {0x22, 10}, // 22 -19.6 -52.5 -58.5 -58.6 -57.7 -57.4 12.9 + {0x0D, 15}, // D -21.1 -53.3 -58.6 -58.6 -57.8 -57.4 12.6 + {0x0B, 12}, // B -22.3 -53.9 -58.6 -58.5 -57.8 -57.4 12.5 + {0x09, 15}, // 9 -23.8 -54.7 -58.5 -58.5 -57.8 -57.5 12.4 + {0x21, 10}, // 21 -24.8 -55.1 -58.5 -58.5 -57.7 -57.5 12.5 + {0x13, 17}, // 13 -26.5 -55.9 -58.6 -58.5 -57.6 -57.6 12.3 + {0x05, 12}, // 5 -27.7 -56.4 -58.4 -58.4 -57.7 -57.5 12.2 + {0x12, 12}, // 12 -28.9 -57.1 -58.4 -58.5 -57.7 -57.3 12.2 +}; + +static const PowerTableItem PA_TABLE_915[] = { + {0xC0, 26}, // C0 9.4 -33.5 -58.5 -58.4 -55.8 -32.6 31.8 + {0xC3, 11}, // C3 8.3 -41.5 -58.6 -58.4 -56.3 -38.0 29.3 + {0xC6, 11}, // C6 7.2 -42.5 -58.5 -58.4 -56.7 -40.5 27.4 + {0xC9, 10}, // C9 6.2 -37.6 -58.6 -58.4 -57.2 -38.8 25.9 + {0xCD, 12}, // CD 5.0 -34.2 -58.6 -58.5 -57.5 -37.3 24.3 + {0x84, 11}, // 84 3.9 -32.0 -58.6 -58.4 -57.7 -40.1 19.7 + {0x87, 10}, // 87 2.9 -36.5 -58.4 -58.5 -57.7 -39.6 18.9 + {0x8A, 11}, // 8A 1.8 -42.2 -58.5 -58.4 -57.7 -39.6 18.1 + {0x8D, 13}, // 8D 0.5 -46.8 -58.5 -58.5 -57.7 -40.4 17.3 + {0x8E, 11}, // 8E -0.6 -46.6 -58.5 -58.5 -57.8 -41.1 16.7 + {0x51, 10}, // 51 -1.6 -38.7 -58.4 -58.5 -57.7 -46.9 16.0 + {0x3E, 11}, // 3E -2.7 -50.0 -58.5 -58.4 -57.6 -55.3 20.7 + {0x3B, 11}, // 3B -3.8 -50.7 -58.6 -58.4 -57.6 -55.2 18.9 + {0x39, 13}, // 39 -5.1 -50.0 -58.5 -58.5 -57.6 -54.0 17.7 + {0x2B, 13}, // 2B -6.4 -47.6 -58.4 -58.4 -57.8 -52.1 16.5 + {0x36, 15}, // 36 -7.9 -46.9 -58.5 -58.4 -57.7 -51.2 15.8 + {0x35, 14}, // 35 -9.3 -46.7 -58.6 -58.4 -57.7 -50.7 15.2 + {0x26, 16}, // 26 -10.9 -47.0 -58.6 -58.4 -57.8 -50.9 14.5 + {0x25, 14}, // 25 -12.3 -47.2 -58.6 -58.3 -57.7 -51.0 14.1 + {0x24, 18}, // 24 -14.1 -48.1 -58.4 -58.4 -57.8 -51.4 13.7 + {0x1D, 14}, // 1D -15.5 -48.7 -58.4 -58.5 -57.7 -51.9 13.2 + {0x1B, 13}, // 1B -16.8 -49.3 -58.6 -58.4 -57.8 -52.3 13.0 + {0x19, 15}, // 19 -18.3 -50.2 -58.5 -58.5 -57.6 -52.8 12.8 + {0x18, 10}, // 18 -19.3 -50.6 -58.5 -58.5 -57.7 -53.1 12.7 + {0x17, 10}, // 17 -20.3 -51.2 -58.6 -58.5 -57.8 -53.1 12.6 + {0x0C, 11}, // C -21.4 -51.8 -58.4 -58.5 -57.7 -53.4 12.5 + {0x0A, 13}, // A -22.7 -52.6 -58.5 -58.4 -57.7 -53.6 12.4 + {0x08, 16}, // 8 -24.3 -53.6 -58.4 -58.4 -57.6 -54.1 12.3 + {0x13, 19}, // 13 -26.2 -54.6 -58.4 -58.5 -57.7 -54.3 12.2 + {0x05, 11}, // 5 -27.3 -55.3 -58.4 -58.4 -57.8 -54.5 12.1 + {0x12, 13}, // 12 -28.6 -55.9 -58.6 -58.5 -57.7 -54.7 12.1 + {0x03, 12}, // 3 -29.8 -56.9 -58.5 -58.4 -57.7 -54.7 12.0 +}; +} // namespace esphome::cc1101 diff --git a/esphome/components/cd74hc4067/cd74hc4067.cpp b/esphome/components/cd74hc4067/cd74hc4067.cpp index 174dc676f9..4293d7af07 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.cpp +++ b/esphome/components/cd74hc4067/cd74hc4067.cpp @@ -21,12 +21,14 @@ void CD74HC4067Component::setup() { } void CD74HC4067Component::dump_config() { - ESP_LOGCONFIG(TAG, "CD74HC4067 Multiplexer:"); + ESP_LOGCONFIG(TAG, + "CD74HC4067 Multiplexer:\n" + " switch delay: %" PRIu32, + this->switch_delay_); LOG_PIN(" S0 Pin: ", this->pin_s0_); LOG_PIN(" S1 Pin: ", this->pin_s1_); LOG_PIN(" S2 Pin: ", this->pin_s2_); LOG_PIN(" S3 Pin: ", this->pin_s3_); - ESP_LOGCONFIG(TAG, "switch delay: %" PRIu32, this->switch_delay_); } void CD74HC4067Component::activate_pin(uint8_t pin) { diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index 9a4e342525..f47b67da6f 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -128,7 +128,9 @@ void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this-> bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); } -std::string CH422GGPIOPin::dump_summary() const { return str_sprintf("EXIO%u via CH422G", pin_); } +size_t CH422GGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "EXIO%u via CH422G", this->pin_); +} void CH422GGPIOPin::set_flags(gpio::Flags flags) { flags_ = flags; this->parent_->pin_mode(this->pin_, flags); diff --git a/esphome/components/ch422g/ch422g.h b/esphome/components/ch422g/ch422g.h index 1193a3db27..8ed63db90a 100644 --- a/esphome/components/ch422g/ch422g.h +++ b/esphome/components/ch422g/ch422g.h @@ -50,7 +50,7 @@ class CH422GGPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(CH422GComponent *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/chsc6x/chsc6x_touchscreen.cpp b/esphome/components/chsc6x/chsc6x_touchscreen.cpp index 31c9466691..941144e451 100644 --- a/esphome/components/chsc6x/chsc6x_touchscreen.cpp +++ b/esphome/components/chsc6x/chsc6x_touchscreen.cpp @@ -32,14 +32,14 @@ void CHSC6XTouchscreen::update_touches() { } void CHSC6XTouchscreen::dump_config() { - ESP_LOGCONFIG(TAG, "CHSC6X Touchscreen:"); - LOG_I2C_DEVICE(this); - LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); ESP_LOGCONFIG(TAG, + "CHSC6X Touchscreen:\n" " Touch timeout: %d\n" " x_raw_max_: %d\n" " y_raw_max_: %d", this->touch_timeout_, this->x_raw_max_, this->y_raw_max_); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); } } // namespace chsc6x diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 5824e68141..2150a30c3e 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -117,9 +117,7 @@ CONF_MIN_HUMIDITY = "min_humidity" CONF_MAX_HUMIDITY = "max_humidity" CONF_TARGET_HUMIDITY = "target_humidity" -visual_temperature = cv.float_with_unit( - "visual_temperature", "(°C|° C|°|C|°K|° K|K|°F|° F|F)?" -) +visual_temperature = cv.float_with_unit("visual_temperature", "(°|(° ?)?[CKF])?") VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema( @@ -275,10 +273,13 @@ async def setup_climate_core_(var, config): visual = config[CONF_VISUAL] if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: + cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") cg.add(var.set_visual_min_temperature_override(min_temp)) if (max_temp := visual.get(CONF_MAX_TEMPERATURE)) is not None: + cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") cg.add(var.set_visual_max_temperature_override(max_temp)) if (temp_step := visual.get(CONF_TEMPERATURE_STEP)) is not None: + cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") cg.add( var.set_visual_temperature_step_override( temp_step[CONF_TARGET_TEMPERATURE], @@ -286,8 +287,10 @@ async def setup_climate_core_(var, config): ) ) if (min_humidity := visual.get(CONF_MIN_HUMIDITY)) is not None: + cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") cg.add(var.set_visual_min_humidity_override(min_humidity)) if (max_humidity := visual.get(CONF_MAX_HUMIDITY)) is not None: + cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES") cg.add(var.set_visual_max_humidity_override(max_humidity)) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 36cc8f4f21..fac56d9d9e 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -3,8 +3,7 @@ #include "esphome/core/automation.h" #include "climate.h" -namespace esphome { -namespace climate { +namespace esphome::climate { template class ControlAction : public Action { public: @@ -58,5 +57,4 @@ class StateTrigger : public Trigger { } }; -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 82b75660ba..2d35509493 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -2,9 +2,9 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/macros.h" +#include -namespace esphome { -namespace climate { +namespace esphome::climate { static const char *const TAG = "climate"; @@ -191,24 +191,30 @@ ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { } ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { + return this->set_fan_mode(custom_fan_mode, strlen(custom_fan_mode)); +} + +ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { + return this->set_fan_mode(fan_mode.data(), fan_mode.size()); +} + +ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode, size_t len) { // Check if it's a standard enum mode first for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { - if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) { + if (strncasecmp(custom_fan_mode, mode_entry.str, len) == 0 && mode_entry.str[len] == '\0') { return this->set_fan_mode(static_cast(mode_entry.value)); } } // Find the matching pointer from parent climate device - if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) { + if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode, len)) { this->custom_fan_mode_ = mode_ptr; this->fan_mode_.reset(); return *this; } - ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode); + ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %.*s", this->parent_->get_name().c_str(), (int) len, custom_fan_mode); return *this; } -ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); } - ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { if (fan_mode.has_value()) { this->set_fan_mode(fan_mode.value()); @@ -223,24 +229,30 @@ ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { } ClimateCall &ClimateCall::set_preset(const char *custom_preset) { + return this->set_preset(custom_preset, strlen(custom_preset)); +} + +ClimateCall &ClimateCall::set_preset(const std::string &preset) { + return this->set_preset(preset.data(), preset.size()); +} + +ClimateCall &ClimateCall::set_preset(const char *custom_preset, size_t len) { // Check if it's a standard enum preset first for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { - if (str_equals_case_insensitive(custom_preset, preset_entry.str)) { + if (strncasecmp(custom_preset, preset_entry.str, len) == 0 && preset_entry.str[len] == '\0') { return this->set_preset(static_cast(preset_entry.value)); } } // Find the matching pointer from parent climate device - if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) { + if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset, len)) { this->custom_preset_ = preset_ptr; this->preset_.reset(); return *this; } - ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset); + ESP_LOGW(TAG, "'%s' - Unrecognized preset %.*s", this->parent_->get_name().c_str(), (int) len, custom_preset); return *this; } -ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); } - ClimateCall &ClimateCall::set_preset(optional preset) { if (preset.has_value()) { this->set_preset(preset.value()); @@ -357,7 +369,7 @@ optional Climate::restore_state_() { } void Climate::save_state_() { -#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ +#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ !defined(CLANG_TIDY) #pragma GCC diagnostic ignored "-Wclass-memaccess" #define TEMP_IGNORE_MEMACCESS @@ -474,26 +486,28 @@ void Climate::publish_state() { ClimateTraits Climate::get_traits() { auto traits = this->traits(); - if (this->visual_min_temperature_override_.has_value()) { - traits.set_visual_min_temperature(*this->visual_min_temperature_override_); +#ifdef USE_CLIMATE_VISUAL_OVERRIDES + if (!std::isnan(this->visual_min_temperature_override_)) { + traits.set_visual_min_temperature(this->visual_min_temperature_override_); } - if (this->visual_max_temperature_override_.has_value()) { - traits.set_visual_max_temperature(*this->visual_max_temperature_override_); + if (!std::isnan(this->visual_max_temperature_override_)) { + traits.set_visual_max_temperature(this->visual_max_temperature_override_); } - if (this->visual_target_temperature_step_override_.has_value()) { - traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_); - traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_); + if (!std::isnan(this->visual_target_temperature_step_override_)) { + traits.set_visual_target_temperature_step(this->visual_target_temperature_step_override_); + traits.set_visual_current_temperature_step(this->visual_current_temperature_step_override_); } - if (this->visual_min_humidity_override_.has_value()) { - traits.set_visual_min_humidity(*this->visual_min_humidity_override_); + if (!std::isnan(this->visual_min_humidity_override_)) { + traits.set_visual_min_humidity(this->visual_min_humidity_override_); } - if (this->visual_max_humidity_override_.has_value()) { - traits.set_visual_max_humidity(*this->visual_max_humidity_override_); + if (!std::isnan(this->visual_max_humidity_override_)) { + traits.set_visual_max_humidity(this->visual_max_humidity_override_); } - +#endif return traits; } +#ifdef USE_CLIMATE_VISUAL_OVERRIDES void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) { this->visual_min_temperature_override_ = visual_min_temperature_override; } @@ -514,6 +528,7 @@ void Climate::set_visual_min_humidity_override(float visual_min_humidity_overrid void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) { this->visual_max_humidity_override_ = visual_max_humidity_override; } +#endif ClimateCall Climate::make_call() { return ClimateCall(this); } @@ -686,11 +701,19 @@ bool Climate::set_custom_preset_(const char *preset) { void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; } const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { - return this->get_traits().find_custom_fan_mode_(custom_fan_mode); + return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode)); +} + +const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode, size_t len) { + return this->get_traits().find_custom_fan_mode_(custom_fan_mode, len); } const char *Climate::find_custom_preset_(const char *custom_preset) { - return this->get_traits().find_custom_preset_(custom_preset); + return this->find_custom_preset_(custom_preset, strlen(custom_preset)); +} + +const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) { + return this->get_traits().find_custom_preset_(custom_preset, len); } void Climate::dump_traits_(const char *tag) { @@ -762,5 +785,4 @@ void Climate::dump_traits_(const char *tag) { } } -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index b277877c3e..06adb580cf 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -8,8 +8,7 @@ #include "climate_mode.h" #include "climate_traits.h" -namespace esphome { -namespace climate { +namespace esphome::climate { #define LOG_CLIMATE(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -79,6 +78,8 @@ class ClimateCall { ClimateCall &set_fan_mode(optional fan_mode); /// Set the custom fan mode of the climate device. ClimateCall &set_fan_mode(const char *custom_fan_mode); + /// Set the custom fan mode of the climate device (zero-copy API path). + ClimateCall &set_fan_mode(const char *custom_fan_mode, size_t len); /// Set the swing mode of the climate device. ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); /// Set the swing mode of the climate device. @@ -95,6 +96,8 @@ class ClimateCall { ClimateCall &set_preset(optional preset); /// Set the custom preset of the climate device. ClimateCall &set_preset(const char *custom_preset); + /// Set the custom preset of the climate device (zero-copy API path). + ClimateCall &set_preset(const char *custom_preset, size_t len); void perform(); @@ -214,11 +217,13 @@ class Climate : public EntityBase { */ ClimateTraits get_traits(); +#ifdef USE_CLIMATE_VISUAL_OVERRIDES void set_visual_min_temperature_override(float visual_min_temperature_override); void set_visual_max_temperature_override(float visual_max_temperature_override); void set_visual_temperature_step_override(float target, float current); void set_visual_min_humidity_override(float visual_min_humidity_override); void set_visual_max_humidity_override(float visual_max_humidity_override); +#endif /// Check if a custom fan mode is currently active. bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } @@ -289,9 +294,11 @@ class Climate : public EntityBase { /// Find and return the matching custom fan mode pointer from traits, or nullptr if not found. const char *find_custom_fan_mode_(const char *custom_fan_mode); + const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len); /// Find and return the matching custom preset pointer from traits, or nullptr if not found. const char *find_custom_preset_(const char *custom_preset); + const char *find_custom_preset_(const char *custom_preset, size_t len); /** Get the default traits of this climate device. * @@ -319,15 +326,17 @@ class Climate : public EntityBase { void dump_traits_(const char *tag); - CallbackManager state_callback_{}; - CallbackManager control_callback_{}; + LazyCallbackManager state_callback_{}; + LazyCallbackManager control_callback_{}; ESPPreferenceObject rtc_; - optional visual_min_temperature_override_{}; - optional visual_max_temperature_override_{}; - optional visual_target_temperature_step_override_{}; - optional visual_current_temperature_step_override_{}; - optional visual_min_humidity_override_{}; - optional visual_max_humidity_override_{}; +#ifdef USE_CLIMATE_VISUAL_OVERRIDES + float visual_min_temperature_override_{NAN}; + float visual_max_temperature_override_{NAN}; + float visual_target_temperature_step_override_{NAN}; + float visual_current_temperature_step_override_{NAN}; + float visual_min_humidity_override_{NAN}; + float visual_max_humidity_override_{NAN}; +#endif private: /** The active custom fan mode (private - enforces use of safe setters). @@ -345,5 +354,4 @@ class Climate : public EntityBase { const char *custom_preset_{nullptr}; }; -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index 794f45ccd6..b153ee0424 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -1,7 +1,6 @@ #include "climate_mode.h" -namespace esphome { -namespace climate { +namespace esphome::climate { const LogString *climate_mode_to_string(ClimateMode mode) { switch (mode) { @@ -107,5 +106,4 @@ const LogString *climate_preset_to_string(ClimatePreset preset) { } } -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 44423d2f22..c961c44248 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -3,8 +3,7 @@ #include #include "esphome/core/log.h" -namespace esphome { -namespace climate { +namespace esphome::climate { /// Enum for all modes a climate device can be in. /// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value @@ -132,5 +131,4 @@ const LogString *climate_swing_mode_to_string(ClimateSwingMode mode); /// Convert the given PresetMode to a human-readable string. const LogString *climate_preset_to_string(ClimatePreset preset); -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 342dffaad6..9bf2d9acd3 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -1,7 +1,6 @@ #include "climate_traits.h" -namespace esphome { -namespace climate { +namespace esphome::climate { int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const { return step_to_accuracy_decimals(this->visual_target_temperature_step_); @@ -11,5 +10,4 @@ int8_t ClimateTraits::get_current_temperature_accuracy_decimals() const { return step_to_accuracy_decimals(this->visual_current_temperature_step_); } -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 0eecf9789f..80ef0854d5 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -6,8 +6,7 @@ #include "esphome/core/finite_set_mask.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace climate { +namespace esphome::climate { // Type aliases for climate enum bitmasks // These replace std::set to eliminate red-black tree overhead @@ -21,18 +20,22 @@ using ClimatePresetMask = FiniteSetMask &vec, const char *value) { +inline bool vector_contains(const std::vector &vec, const char *value, size_t len) { for (const char *item : vec) { - if (strcmp(item, value) == 0) + if (strncmp(item, value, len) == 0 && item[len] == '\0') return true; } return false; } +inline bool vector_contains(const std::vector &vec, const char *value) { + return vector_contains(vec, value, strlen(value)); +} + // Find and return matching pointer from vector, or nullptr if not found -inline const char *vector_find(const std::vector &vec, const char *value) { +inline const char *vector_find(const std::vector &vec, const char *value, size_t len) { for (const char *item : vec) { - if (strcmp(item, value) == 0) + if (strncmp(item, value, len) == 0 && item[len] == '\0') return item; } return nullptr; @@ -258,13 +261,19 @@ class ClimateTraits { /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found /// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead const char *find_custom_fan_mode_(const char *custom_fan_mode) const { - return vector_find(this->supported_custom_fan_modes_, custom_fan_mode); + return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode)); + } + const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len) const { + return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len); } /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found /// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead const char *find_custom_preset_(const char *custom_preset) const { - return vector_find(this->supported_custom_presets_, custom_preset); + return this->find_custom_preset_(custom_preset, strlen(custom_preset)); + } + const char *find_custom_preset_(const char *custom_preset, size_t len) const { + return vector_find(this->supported_custom_presets_, custom_preset, len); } uint32_t feature_flags_{0}; @@ -292,5 +301,4 @@ class ClimateTraits { std::vector supported_custom_presets_; }; -} // namespace climate -} // namespace esphome +} // namespace esphome::climate diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index 6d66abf4cd..5315be3db6 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -3,7 +3,12 @@ import logging import esphome.codegen as cg from esphome.components import climate, remote_base, sensor import esphome.config_validation as cv -from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT +from esphome.const import ( + CONF_HUMIDITY_SENSOR, + CONF_SENSOR, + CONF_SUPPORTS_COOL, + CONF_SUPPORTS_HEAT, +) from esphome.cpp_generator import MockObjClass _LOGGER = logging.getLogger(__name__) @@ -32,6 +37,7 @@ def climate_ir_schema( cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), } ) .extend(cv.COMPONENT_SCHEMA) @@ -61,6 +67,9 @@ async def register_climate_ir(var, config): if sensor_id := config.get(CONF_SENSOR): sens = await cg.get_variable(sensor_id) cg.add(var.set_sensor(sens)) + if sensor_id := config.get(CONF_HUMIDITY_SENSOR): + sens = await cg.get_variable(sensor_id) + cg.add(var.set_humidity_sensor(sens)) async def new_climate_ir(config, *args): diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 2b95792a6c..50c8d459b0 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -11,7 +11,9 @@ climate::ClimateTraits ClimateIR::traits() { if (this->sensor_ != nullptr) { traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); } - + if (this->humidity_sensor_ != nullptr) { + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY); + } traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); if (this->supports_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); @@ -39,9 +41,16 @@ void ClimateIR::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - } else { - this->current_temperature = NAN; } + if (this->humidity_sensor_ != nullptr) { + this->humidity_sensor_->add_on_state_callback([this](float state) { + this->current_humidity = state; + // current humidity changed, publish state + this->publish_state(); + }); + this->current_humidity = this->humidity_sensor_->state; + } + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 62a43f0b2d..ac76d33853 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -43,6 +43,7 @@ class ClimateIR : public Component, void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } protected: float minimum_temperature_, maximum_temperature_, temperature_step_; @@ -67,6 +68,7 @@ class ClimateIR : public Component, climate::ClimatePresetMask presets_{}; sensor::Sensor *sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; }; } // namespace climate_ir diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 12a69551f5..fcfafa0c1a 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -7,9 +7,11 @@ BYTE_ORDER_LITTLE = "little_endian" BYTE_ORDER_BIG = "big_endian" CONF_COLOR_DEPTH = "color_depth" +CONF_CRC_ENABLE = "crc_enable" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ENABLED = "enabled" CONF_IGNORE_NOT_FOUND = "ignore_not_found" +CONF_ON_PACKET = "on_packet" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index e45338e785..e85e08e353 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -7,7 +7,7 @@ namespace copy { static const char *const TAG = "copy.select"; void CopySelect::setup() { - source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); }); + source_->add_on_state_callback([this](size_t index) { this->publish_state(index); }); traits.set_options(source_->traits.get_options()); diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 8f735982f1..feac9823b9 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -13,25 +13,25 @@ static const char *const TAG = "cover"; const float COVER_OPEN = 1.0f; const float COVER_CLOSED = 0.0f; -const char *cover_command_to_str(float pos) { +const LogString *cover_command_to_str(float pos) { if (pos == COVER_OPEN) { - return "OPEN"; + return LOG_STR("OPEN"); } else if (pos == COVER_CLOSED) { - return "CLOSE"; + return LOG_STR("CLOSE"); } else { - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } -const char *cover_operation_to_str(CoverOperation op) { +const LogString *cover_operation_to_str(CoverOperation op) { switch (op) { case COVER_OPERATION_IDLE: - return "IDLE"; + return LOG_STR("IDLE"); case COVER_OPERATION_OPENING: - return "OPENING"; + return LOG_STR("OPENING"); case COVER_OPERATION_CLOSING: - return "CLOSING"; + return LOG_STR("CLOSING"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } @@ -87,7 +87,7 @@ void CoverCall::perform() { if (traits.get_supports_position()) { ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f); } else { - ESP_LOGD(TAG, " Command: %s", cover_command_to_str(*this->position_)); + ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(cover_command_to_str(*this->position_))); } } if (this->tilt_.has_value()) { @@ -169,7 +169,7 @@ void Cover::publish_state(bool save) { if (traits.get_supports_tilt()) { ESP_LOGD(TAG, " Tilt: %.0f%%", this->tilt * 100.0f); } - ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation)); + ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(cover_operation_to_str(this->current_operation))); this->state_callback_.call(); #if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 6c69c05e71..e710915a0e 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include "esphome/core/preferences.h" #include "cover_traits.h" @@ -86,7 +87,7 @@ enum CoverOperation : uint8_t { COVER_OPERATION_CLOSING, }; -const char *cover_operation_to_str(CoverOperation op); +const LogString *cover_operation_to_str(CoverOperation op); /** Base class for all cover devices. * @@ -151,7 +152,7 @@ class Cover : public EntityBase, public EntityBase_DeviceClass { optional restore_state_(); - CallbackManager state_callback_{}; + LazyCallbackManager state_callback_{}; ESPPreferenceObject rtc_; }; diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index fe81ae91fe..71fe15f0ae 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -1,11 +1,13 @@ #include "cse7766.h" -#include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" namespace esphome { namespace cse7766 { static const char *const TAG = "cse7766"; +static constexpr size_t CSE7766_RAW_DATA_SIZE = 24; void CSE7766Component::loop() { const uint32_t now = App.get_loop_component_start_time(); @@ -70,8 +72,8 @@ bool CSE7766Component::check_byte_() { void CSE7766Component::parse_data_() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE { - std::string s = format_hex_pretty(this->raw_data_, sizeof(this->raw_data_)); - ESP_LOGVV(TAG, "Raw data: %s", s.c_str()); + char hex_buf[format_hex_pretty_size(CSE7766_RAW_DATA_SIZE)]; + ESP_LOGVV(TAG, "Raw data: %s", format_hex_pretty_to(hex_buf, this->raw_data_, sizeof(this->raw_data_))); } #endif diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp index 0ba2d9df94..d18d4e7c94 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.cpp @@ -9,26 +9,40 @@ void CST816Touchscreen::continue_setup_() { this->interrupt_pin_->setup(); this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); } - if (this->read_byte(REG_CHIP_ID, &this->chip_id_)) { - switch (this->chip_id_) { - case CST820_CHIP_ID: - case CST826_CHIP_ID: - case CST716_CHIP_ID: - case CST816S_CHIP_ID: - case CST816D_CHIP_ID: - case CST816T_CHIP_ID: - break; - default: - this->mark_failed(); - this->status_set_error(str_sprintf("Unknown chip ID 0x%02X", this->chip_id_).c_str()); - return; - } - this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION); - } else if (!this->skip_probe_) { - this->status_set_error("Failed to read chip id"); + + if (!this->read_byte(REG_CHIP_ID, &this->chip_id_) && !this->skip_probe_) { + this->status_set_error(LOG_STR("Failed to read chip ID")); this->mark_failed(); return; } + + // CST826/CST836 return 0 for chip ID, need to read from factory ID register + if (this->chip_id_ == 0) { + if (!this->read_byte(REG_FACTORY_ID, &this->chip_id_) && !this->skip_probe_) { + this->status_set_error(LOG_STR("Failed to read chip ID")); + this->mark_failed(); + return; + } + } + + switch (this->chip_id_) { + case CST716_CHIP_ID: + case CST816S_CHIP_ID: + case CST816D_CHIP_ID: + case CST816T_CHIP_ID: + case CST820_CHIP_ID: + case CST826_CHIP_ID: + case CST836_CHIP_ID: + break; + default: + if (!this->skip_probe_) { + ESP_LOGE(TAG, "Unknown chip ID: 0x%02X", this->chip_id_); + this->status_set_error(LOG_STR("Unknown chip ID")); + this->mark_failed(); + return; + } + } + this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION); if (this->x_raw_max_ == this->x_raw_min_) { this->x_raw_max_ = this->display_->get_native_width(); } @@ -69,21 +83,18 @@ void CST816Touchscreen::update_touches() { } void CST816Touchscreen::dump_config() { - ESP_LOGCONFIG(TAG, "CST816 Touchscreen:"); - LOG_I2C_DEVICE(this); - LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); - LOG_PIN(" Reset Pin: ", this->reset_pin_); ESP_LOGCONFIG(TAG, + "CST816 Touchscreen:\n" " X Raw Min: %d, X Raw Max: %d\n" " Y Raw Min: %d, Y Raw Max: %d", this->x_raw_min_, this->x_raw_max_, this->y_raw_min_, this->y_raw_max_); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); const char *name; switch (this->chip_id_) { - case CST820_CHIP_ID: - name = "CST820"; - break; - case CST826_CHIP_ID: - name = "CST826"; + case CST716_CHIP_ID: + name = "CST716"; break; case CST816S_CHIP_ID: name = "CST816S"; @@ -91,12 +102,18 @@ void CST816Touchscreen::dump_config() { case CST816D_CHIP_ID: name = "CST816D"; break; - case CST716_CHIP_ID: - name = "CST716"; - break; case CST816T_CHIP_ID: name = "CST816T"; break; + case CST820_CHIP_ID: + name = "CST820"; + break; + case CST826_CHIP_ID: + name = "CST826"; + break; + case CST836_CHIP_ID: + name = "CST836"; + break; default: name = "Unknown"; break; diff --git a/esphome/components/cst816/touchscreen/cst816_touchscreen.h b/esphome/components/cst816/touchscreen/cst816_touchscreen.h index 99ea085e37..99b93d8342 100644 --- a/esphome/components/cst816/touchscreen/cst816_touchscreen.h +++ b/esphome/components/cst816/touchscreen/cst816_touchscreen.h @@ -19,12 +19,14 @@ static const uint8_t REG_YPOS_HIGH = 0x05; static const uint8_t REG_YPOS_LOW = 0x06; static const uint8_t REG_DIS_AUTOSLEEP = 0xFE; static const uint8_t REG_CHIP_ID = 0xA7; +static const uint8_t REG_FACTORY_ID = 0xAA; static const uint8_t REG_FW_VERSION = 0xA9; static const uint8_t REG_SLEEP = 0xE5; static const uint8_t REG_IRQ_CTL = 0xFA; static const uint8_t IRQ_EN_MOTION = 0x70; static const uint8_t CST826_CHIP_ID = 0x11; +static const uint8_t CST836_CHIP_ID = 0x13; static const uint8_t CST820_CHIP_ID = 0xB7; static const uint8_t CST816S_CHIP_ID = 0xB4; static const uint8_t CST816D_CHIP_ID = 0xB6; diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 895b5515cb..cb3f65c9cd 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -146,8 +146,10 @@ void CurrentBasedCover::dump_config() { if (this->close_obstacle_current_threshold_ != FLT_MAX) { ESP_LOGCONFIG(TAG, " Close obstacle current threshold: %.11fA", this->close_obstacle_current_threshold_); } - ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f); - ESP_LOGCONFIG(TAG, "Obstacle Rollback: %.1f%%", this->obstacle_rollback_ * 100); + ESP_LOGCONFIG(TAG, + " Close Duration: %.1fs\n" + "Obstacle Rollback: %.1f%%", + this->close_duration_ / 1e3f, this->obstacle_rollback_ * 100); if (this->max_duration_ != UINT32_MAX) { ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f); } diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index a3969e081e..a1b684abbf 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -51,7 +51,7 @@ void DallasTemperatureSensor::update() { } float tempc = this->get_temp_c_(); - ESP_LOGD(TAG, "'%s': Got Temperature=%.1f°C", this->get_name().c_str(), tempc); + ESP_LOGD(TAG, "'%s': Got Temperature=%f°C", this->get_name().c_str(), tempc); this->publish_state(tempc); }); } diff --git a/esphome/components/dashboard_import/dashboard_import.cpp b/esphome/components/dashboard_import/dashboard_import.cpp index c04696fd53..d4a95b81f6 100644 --- a/esphome/components/dashboard_import/dashboard_import.cpp +++ b/esphome/components/dashboard_import/dashboard_import.cpp @@ -3,10 +3,10 @@ namespace esphome { namespace dashboard_import { -static std::string g_package_import_url; // NOLINT +static const char *g_package_import_url = ""; // NOLINT -const std::string &get_package_import_url() { return g_package_import_url; } -void set_package_import_url(std::string url) { g_package_import_url = std::move(url); } +const char *get_package_import_url() { return g_package_import_url; } +void set_package_import_url(const char *url) { g_package_import_url = url; } } // namespace dashboard_import } // namespace esphome diff --git a/esphome/components/dashboard_import/dashboard_import.h b/esphome/components/dashboard_import/dashboard_import.h index edcda6b803..488bf80a2e 100644 --- a/esphome/components/dashboard_import/dashboard_import.h +++ b/esphome/components/dashboard_import/dashboard_import.h @@ -1,12 +1,10 @@ #pragma once -#include - namespace esphome { namespace dashboard_import { -const std::string &get_package_import_url(); -void set_package_import_url(std::string url); +const char *get_package_import_url(); +void set_package_import_url(const char *url); } // namespace dashboard_import } // namespace esphome diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index 2c2775ecf4..c061bc81f7 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace datetime { +namespace esphome::datetime { static const char *const TAG = "datetime.date_entity"; @@ -129,7 +128,6 @@ void DateEntityRestoreState::apply(DateEntity *date) { date->publish_state(); } -} // namespace datetime -} // namespace esphome +} // namespace esphome::datetime #endif // USE_DATETIME_DATE diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index ba2edb127a..069116d162 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -10,8 +10,7 @@ #include "datetime_base.h" -namespace esphome { -namespace datetime { +namespace esphome::datetime { #define LOG_DATETIME_DATE(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -111,7 +110,6 @@ template class DateSetAction : public Action, public Pare } }; -} // namespace datetime -} // namespace esphome +} // namespace esphome::datetime #endif // USE_DATETIME_DATE diff --git a/esphome/components/datetime/datetime_base.h b/esphome/components/datetime/datetime_base.h index b5f54ac96f..1b0b3d5463 100644 --- a/esphome/components/datetime/datetime_base.h +++ b/esphome/components/datetime/datetime_base.h @@ -8,8 +8,7 @@ #include "esphome/components/time/real_time_clock.h" #endif -namespace esphome { -namespace datetime { +namespace esphome::datetime { class DateTimeBase : public EntityBase { public: @@ -23,7 +22,7 @@ class DateTimeBase : public EntityBase { #endif protected: - CallbackManager state_callback_; + LazyCallbackManager state_callback_; #ifdef USE_TIME time::RealTimeClock *rtc_; @@ -37,5 +36,4 @@ class DateTimeStateTrigger : public Trigger { } }; -} // namespace datetime -} // namespace esphome +} // namespace esphome::datetime diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 8606a47fa7..694f9c5721 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace datetime { +namespace esphome::datetime { static const char *const TAG = "datetime.datetime_entity"; @@ -250,7 +249,6 @@ bool OnDateTimeTrigger::matches_(const ESPTime &time) const { } #endif -} // namespace datetime -} // namespace esphome +} // namespace esphome::datetime #endif // USE_DATETIME_TIME diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index 43bff5a181..018346b34b 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -10,8 +10,7 @@ #include "datetime_base.h" -namespace esphome { -namespace datetime { +namespace esphome::datetime { #define LOG_DATETIME_DATETIME(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -146,7 +145,6 @@ class OnDateTimeTrigger : public Trigger<>, public Component, public Parented, public Component, public ParentedCODEPAGESIZE, NRF_FICR->CODESIZE, - NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0]); - ESP_LOGD(TAG, "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x", NRF_FICR->ER[0], + ESP_LOGD(TAG, + "Code page size: %u, code size: %u, device id: 0x%08x%08x\n" + "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" + "Device address type: %s, address: %s\n" + "Part code: nRF%x, version: %c%c%c%c, package: %s\n" + "RAM: %ukB, Flash: %ukB, production test: %sdone", + NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0], NRF_FICR->ER[0], NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2], - NRF_FICR->IR[3]); - ESP_LOGD(TAG, "Device address type: %s, address: %s", (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), - get_mac_address_pretty().c_str()); - ESP_LOGD(TAG, "Part code: nRF%x, version: %c%c%c%c, package: %s", NRF_FICR->INFO.PART, - NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF, NRF_FICR->INFO.VARIANT >> 8 & 0xFF, - NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE)); - ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, - (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); + NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), get_mac_address_pretty().c_str(), + NRF_FICR->INFO.PART, NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF, + NRF_FICR->INFO.VARIANT >> 8 & 0xFF, NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE), + NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] && (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos; @@ -329,9 +329,10 @@ void DebugComponent::get_device_info_(std::string &device_info) { #else ESP_LOGD(TAG, "bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF, (BOOTLOADER_VERSION_REGISTER >> 8) & 0xFF, BOOTLOADER_VERSION_REGISTER & 0xFF); - ESP_LOGD(TAG, "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x", read_mem_u32(MBR_BOOTLOADER_ADDR), - NRF_UICR->NRFFW[0]); - ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR), + ESP_LOGD(TAG, + "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x\n" + "MBR param page addr 0x%08x, UICR param page addr 0x%08x", + read_mem_u32(MBR_BOOTLOADER_ADDR), NRF_UICR->NRFFW[0], read_mem_u32(MBR_PARAM_PAGE_ADDR), NRF_UICR->NRFFW[1]); if (is_sd_present()) { uint32_t const sd_id = sd_id_get(); @@ -368,8 +369,10 @@ void DebugComponent::get_device_info_(std::string &device_info) { } return res; }; - ESP_LOGD(TAG, "NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); - ESP_LOGD(TAG, "NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); + ESP_LOGD(TAG, + "NRFFW %s\n" + "NRFHW %s", + uicr(NRF_UICR->NRFFW, 13).c_str(), uicr(NRF_UICR->NRFHW, 12).c_str()); } void DebugComponent::update_platform_() {} diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 19fb726016..3cfe7aa641 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -1,15 +1,18 @@ -from esphome import automation, pins +from esphome import automation, core, pins import esphome.codegen as cg from esphome.components import esp32, time -from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import ( +from esphome.components.esp32 import ( VARIANT_ESP32, VARIANT_ESP32C2, VARIANT_ESP32C3, + VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + get_esp32_variant, ) from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv @@ -20,16 +23,20 @@ from esphome.const import ( CONF_MINUTE, CONF_MODE, CONF_NUMBER, + CONF_PIN, CONF_PINS, CONF_RUN_DURATION, CONF_SECOND, CONF_SLEEP_DURATION, CONF_TIME_ID, CONF_WAKEUP_PIN, + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, PlatformFramework, ) +from esphome.core import CORE +from esphome.types import ConfigType WAKEUP_PINS = { VARIANT_ESP32: [ @@ -52,7 +59,13 @@ WAKEUP_PINS = { 38, 39, ], + VARIANT_ESP32C2: [0, 1, 2, 3, 4, 5], VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5], + VARIANT_ESP32C5: [0, 1, 2, 3, 4, 5, 6, 7], + VARIANT_ESP32C6: [0, 1, 2, 3, 4, 5, 6, 7], + VARIANT_ESP32C61: [0, 1, 2, 3, 4, 5, 6], + VARIANT_ESP32H2: [7, 8, 9, 10, 11, 12, 13, 14], + VARIANT_ESP32P4: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], VARIANT_ESP32S2: [ 0, 1, @@ -101,13 +114,10 @@ WAKEUP_PINS = { 20, 21, ], - VARIANT_ESP32C2: [0, 1, 2, 3, 4, 5], - VARIANT_ESP32C6: [0, 1, 2, 3, 4, 5, 6, 7], - VARIANT_ESP32H2: [7, 8, 9, 10, 11, 12, 13, 14], } -def validate_pin_number(value): +def validate_pin_number_esp32(value: ConfigType) -> ConfigType: valid_pins = WAKEUP_PINS.get(get_esp32_variant(), WAKEUP_PINS[VARIANT_ESP32]) if value[CONF_NUMBER] not in valid_pins: raise cv.Invalid( @@ -116,22 +126,79 @@ def validate_pin_number(value): return value +def validate_pin_number(value: ConfigType) -> ConfigType: + if not CORE.is_esp32: + return value + return validate_pin_number_esp32(value) + + +def validate_wakeup_pin( + value: ConfigType | list[ConfigType], +) -> list[ConfigType]: + if not isinstance(value, list): + processed_pins: list[ConfigType] = [{CONF_PIN: value}] + else: + processed_pins = list(value) + + for i, pin_config in enumerate(processed_pins): + # now validate each item + validated_pin = WAKEUP_PIN_SCHEMA(pin_config) + validate_pin_number(validated_pin[CONF_PIN]) + processed_pins[i] = validated_pin + + return processed_pins + + +def validate_config(config: ConfigType) -> ConfigType: + # right now only BK72XX supports the list format for wakeup pins + if CORE.is_bk72xx: + if CONF_WAKEUP_PIN_MODE in config: + wakeup_pins = config.get(CONF_WAKEUP_PIN, []) + if len(wakeup_pins) > 1: + raise cv.Invalid( + "You need to remove the global wakeup_pin_mode and define it per pin" + ) + if wakeup_pins: + wakeup_pins[0][CONF_WAKEUP_PIN_MODE] = config.pop(CONF_WAKEUP_PIN_MODE) + elif ( + isinstance(config.get(CONF_WAKEUP_PIN), list) + and len(config[CONF_WAKEUP_PIN]) > 1 + ): + raise cv.Invalid( + "Your platform does not support providing multiple entries in wakeup_pin" + ) + + return config + + def _validate_ex1_wakeup_mode(value): if value == "ALL_LOW": esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value) if value == "ANY_LOW": esp32.only_on_variant( supported=[ + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32H2, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, - VARIANT_ESP32C6, - VARIANT_ESP32H2, ], msg_prefix="ANY_LOW", )(value) return value +def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod: + if not CORE.is_bk72xx: + return value + max_duration = core.TimePeriod(hours=36) + if value > max_duration: + raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX") + return value + + deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component) EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action) @@ -177,6 +244,13 @@ WAKEUP_CAUSES_SCHEMA = cv.Schema( } ) +WAKEUP_PIN_SCHEMA = cv.Schema( + { + cv.Required(CONF_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_WAKEUP_PIN_MODE): cv.enum(WAKEUP_PIN_MODES, upper=True), + } +) + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -185,14 +259,15 @@ CONFIG_SCHEMA = cv.All( cv.All(cv.only_on_esp32, WAKEUP_CAUSES_SCHEMA), cv.positive_time_period_milliseconds, ), - cv.Optional(CONF_SLEEP_DURATION): cv.positive_time_period_milliseconds, - cv.Optional(CONF_WAKEUP_PIN): cv.All( - cv.only_on_esp32, - pins.internal_gpio_input_pin_schema, - validate_pin_number, + cv.Optional(CONF_SLEEP_DURATION): cv.All( + cv.positive_time_period_milliseconds, + _validate_sleep_duration, ), + cv.Optional(CONF_WAKEUP_PIN): validate_wakeup_pin, cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All( - cv.only_on_esp32, cv.enum(WAKEUP_PIN_MODES), upper=True + cv.only_on([PLATFORM_ESP32, PLATFORM_BK72XX]), + cv.enum(WAKEUP_PIN_MODES), + upper=True, ), cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( cv.only_on_esp32, @@ -203,7 +278,8 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_PINS): cv.ensure_list( - pins.internal_gpio_input_pin_schema, validate_pin_number + pins.internal_gpio_input_pin_schema, + validate_pin_number_esp32, ), cv.Required(CONF_MODE): cv.All( cv.enum(EXT1_WAKEUP_MODES, upper=True), @@ -218,7 +294,9 @@ CONFIG_SCHEMA = cv.All( unsupported=[ VARIANT_ESP32C2, VARIANT_ESP32C3, + VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, ], msg_prefix="Wakeup from touch", @@ -227,7 +305,8 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]), + validate_config, ) @@ -238,8 +317,21 @@ async def to_code(config): if CONF_SLEEP_DURATION in config: cg.add(var.set_sleep_duration(config[CONF_SLEEP_DURATION])) if CONF_WAKEUP_PIN in config: - pin = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) - cg.add(var.set_wakeup_pin(pin)) + pins_as_list = config.get(CONF_WAKEUP_PIN, []) + if CORE.is_bk72xx: + cg.add(var.init_wakeup_pins_(len(pins_as_list))) + for item in pins_as_list: + cg.add( + var.add_wakeup_pin( + await cg.gpio_pin_expression(item[CONF_PIN]), + item.get( + CONF_WAKEUP_PIN_MODE, WakeupPinMode.WAKEUP_PIN_MODE_IGNORE + ), + ) + ) + else: + pin = await cg.gpio_pin_expression(pins_as_list[0][CONF_PIN]) + cg.add(var.set_wakeup_pin(pin)) if CONF_WAKEUP_PIN_MODE in config: cg.add(var.set_wakeup_pin_mode(config[CONF_WAKEUP_PIN_MODE])) if CONF_RUN_DURATION in config: @@ -294,7 +386,10 @@ DEEP_SLEEP_ENTER_SCHEMA = cv.All( cv.Schema( { cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable( - cv.positive_time_period_milliseconds + cv.All( + cv.positive_time_period_milliseconds, + _validate_sleep_duration, + ) ), # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep cv.Exclusive(CONF_UNTIL, "time"): cv.All( @@ -352,5 +447,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.ESP32_IDF, }, "deep_sleep_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "deep_sleep_bk72xx.cpp": {PlatformFramework.BK72XX_ARDUINO}, } ) diff --git a/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp b/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp new file mode 100644 index 0000000000..b5fadd7230 --- /dev/null +++ b/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp @@ -0,0 +1,64 @@ +#ifdef USE_BK72XX + +#include "deep_sleep_component.h" +#include "esphome/core/log.h" + +namespace esphome::deep_sleep { + +static const char *const TAG = "deep_sleep.bk72xx"; + +optional DeepSleepComponent::get_run_duration_() const { return this->run_duration_; } + +void DeepSleepComponent::dump_config_platform_() { + for (const WakeUpPinItem &item : this->wakeup_pins_) { + LOG_PIN(" Wakeup Pin: ", item.wakeup_pin); + } +} + +bool DeepSleepComponent::pin_prevents_sleep_(WakeUpPinItem &pinItem) const { + return (pinItem.wakeup_pin_mode == WAKEUP_PIN_MODE_KEEP_AWAKE && pinItem.wakeup_pin != nullptr && + !this->sleep_duration_.has_value() && (pinItem.wakeup_level == get_real_pin_state_(*pinItem.wakeup_pin))); +} + +bool DeepSleepComponent::prepare_to_sleep_() { + if (wakeup_pins_.size() > 0) { + for (WakeUpPinItem &item : this->wakeup_pins_) { + if (pin_prevents_sleep_(item)) { + // Defer deep sleep until inactive + if (!this->next_enter_deep_sleep_) { + this->status_set_warning(); + ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep..."); + } + this->next_enter_deep_sleep_ = true; + return false; + } + } + } + return true; +} + +void DeepSleepComponent::deep_sleep_() { + for (WakeUpPinItem &item : this->wakeup_pins_) { + if (item.wakeup_pin_mode == WAKEUP_PIN_MODE_INVERT_WAKEUP) { + if (item.wakeup_level == get_real_pin_state_(*item.wakeup_pin)) { + item.wakeup_level = !item.wakeup_level; + } + } + ESP_LOGI(TAG, "Wake-up on P%u %s (%d)", item.wakeup_pin->get_pin(), item.wakeup_level ? "HIGH" : "LOW", + static_cast(item.wakeup_pin_mode)); + } + + if (this->sleep_duration_.has_value()) + lt_deep_sleep_config_timer((*this->sleep_duration_ / 1000) & 0xFFFFFFFF); + + for (WakeUpPinItem &item : this->wakeup_pins_) { + lt_deep_sleep_config_gpio(1 << item.wakeup_pin->get_pin(), item.wakeup_level); + lt_deep_sleep_keep_floating_gpio(1 << item.wakeup_pin->get_pin(), true); + } + + lt_deep_sleep_enter(); +} + +} // namespace esphome::deep_sleep + +#endif // USE_BK72XX diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 80381e767c..3e6eda2257 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -19,7 +19,7 @@ namespace esphome { namespace deep_sleep { -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_BK72XX) /** The values of this enum define what should be done if deep sleep is set up with a wakeup pin on the ESP32 * and the scenario occurs that the wakeup pin is already in the wakeup state. @@ -33,7 +33,17 @@ enum WakeupPinMode { */ WAKEUP_PIN_MODE_INVERT_WAKEUP, }; +#endif +#if defined(USE_BK72XX) +struct WakeUpPinItem { + InternalGPIOPin *wakeup_pin; + WakeupPinMode wakeup_pin_mode; + bool wakeup_level; +}; +#endif // USE_BK72XX + +#ifdef USE_ESP32 #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) struct Ext1Wakeup { uint64_t mask; @@ -75,13 +85,20 @@ class DeepSleepComponent : public Component { void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); #endif // USE_ESP32 +#if defined(USE_BK72XX) + void init_wakeup_pins_(size_t capacity) { this->wakeup_pins_.init(capacity); } + void add_wakeup_pin(InternalGPIOPin *wakeup_pin, WakeupPinMode wakeup_pin_mode) { + this->wakeup_pins_.emplace_back(WakeUpPinItem{wakeup_pin, wakeup_pin_mode, !wakeup_pin->is_inverted()}); + } +#endif // USE_BK72XX + #if defined(USE_ESP32) #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); #endif #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ - !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) + !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2) void set_touch_wakeup(bool touch_wakeup); #endif @@ -114,7 +131,17 @@ class DeepSleepComponent : public Component { bool prepare_to_sleep_(); void deep_sleep_(); +#ifdef USE_BK72XX + bool pin_prevents_sleep_(WakeUpPinItem &pinItem) const; + bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); } +#endif // USE_BK72XX + optional sleep_duration_; + +#ifdef USE_BK72XX + FixedVector wakeup_pins_; +#endif // USE_BK72XX + #ifdef USE_ESP32 InternalGPIOPin *wakeup_pin_; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; @@ -124,8 +151,10 @@ class DeepSleepComponent : public Component { #endif optional touch_wakeup_; + optional wakeup_cause_to_run_duration_; #endif // USE_ESP32 + optional run_duration_; bool next_enter_deep_sleep_{false}; bool prevent_{false}; diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index b93d9ce601..833be8e76c 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -18,6 +18,7 @@ namespace deep_sleep { // | ESP32-C3 | | | | ✓ | // | ESP32-C5 | | (✓) | | (✓) | // | ESP32-C6 | | ✓ | | ✓ | +// | ESP32-C61 | | ✓ | | ✓ | // | ESP32-H2 | | ✓ | | | // // Notes: @@ -55,7 +56,7 @@ void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wa #endif #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ - !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) + !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2) void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } #endif @@ -121,8 +122,9 @@ void DeepSleepComponent::deep_sleep_() { } #endif - // GPIO wakeup - C2, C3, C6 only -#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) + // GPIO wakeup - C2, C3, C6, C61 only +#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ + defined(USE_ESP32_VARIANT_ESP32C61) if (this->wakeup_pin_ != nullptr) { const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin()); if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) { @@ -155,7 +157,7 @@ void DeepSleepComponent::deep_sleep_() { // Touch wakeup - ESP32, S2, S3 only #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \ - !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) + !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2) if (this->touch_wakeup_.has_value() && *(this->touch_wakeup_)) { esp_sleep_enable_touchpad_wakeup(); esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); diff --git a/esphome/components/demo/demo_alarm_control_panel.h b/esphome/components/demo/demo_alarm_control_panel.h index 9902d27882..f59434830b 100644 --- a/esphome/components/demo/demo_alarm_control_panel.h +++ b/esphome/components/demo/demo_alarm_control_panel.h @@ -33,7 +33,7 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { case ACP_STATE_ARMED_AWAY: if (this->get_requires_code_to_arm() && call.get_code().has_value()) { if (call.get_code().value() != "1234") { - this->status_momentary_error("Invalid code", 5000); + this->status_momentary_error("invalid_code", 5000); return; } } @@ -42,7 +42,7 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component { case ACP_STATE_DISARMED: if (this->get_requires_code() && call.get_code().has_value()) { if (call.get_code().value() != "1234") { - this->status_momentary_error("Invalid code", 5000); + this->status_momentary_error("invalid_code", 5000); return; } } diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp index 42074c80cf..8bb6ddf942 100644 --- a/esphome/components/dfrobot_sen0395/commands.cpp +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -179,8 +179,10 @@ uint8_t DetRangeCfgCommand::on_message(std::string &message) { ESP_LOGE(TAG, "Cannot configure range config. Sensor is not stopped!"); return 1; // Command done } else if (message == "Done") { - ESP_LOGI(TAG, "Updated detection area config:"); - ESP_LOGI(TAG, "Detection area 1 from %.02fm to %.02fm.", this->min1_, this->max1_); + ESP_LOGI(TAG, + "Updated detection area config:\n" + "Detection area 1 from %.02fm to %.02fm.", + this->min1_, this->max1_); if (this->min2_ >= 0 && this->max2_ >= 0) { ESP_LOGI(TAG, "Detection area 2 from %.02fm to %.02fm.", this->min2_, this->max2_); } @@ -209,9 +211,11 @@ uint8_t SetLatencyCommand::on_message(std::string &message) { ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!"); return 1; // Command done } else if (message == "Done") { - ESP_LOGI(TAG, "Updated output latency config:"); - ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.03f s.", this->delay_after_detection_); - ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.03f s.", this->delay_after_disappear_); + ESP_LOGI(TAG, + "Updated output latency config:\n" + "Signal that someone was detected is delayed by %.03f s.\n" + "Signal that nobody is detected anymore is delayed by %.03f s.", + this->delay_after_detection_, this->delay_after_disappear_); ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); return 1; // Command done } diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index cc0bf55a80..6cb204c8de 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -8,17 +8,23 @@ namespace dht { static const char *const TAG = "dht"; void DHT::setup() { - this->pin_->digital_write(true); - this->pin_->setup(); - this->pin_->digital_write(true); + this->t_pin_->digital_write(true); + this->t_pin_->setup(); +#ifdef USE_ESP32 + this->t_pin_->pin_mode(this->t_pin_->get_flags() | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN); +#endif + this->t_pin_->digital_write(true); } void DHT::dump_config() { - ESP_LOGCONFIG(TAG, "DHT:"); - LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, " %sModel: %s", this->is_auto_detect_ ? "Auto-detected " : "", - this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent"); - ESP_LOGCONFIG(TAG, " Internal pull-up: %s", ONOFF(this->pin_->get_flags() & gpio::FLAG_PULLUP)); + ESP_LOGCONFIG(TAG, + "DHT:\n" + " %sModel: %s\n" + " Internal pull-up: %s", + this->is_auto_detect_ ? "Auto-detected " : "", + this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent", + ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP)); + LOG_PIN(" Pin: ", this->t_pin_); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); @@ -72,21 +78,15 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r int8_t i = 0; uint8_t data[5] = {0, 0, 0, 0, 0}; - this->pin_->digital_write(false); - this->pin_->pin_mode(gpio::FLAG_OUTPUT); - this->pin_->digital_write(false); +#ifndef USE_ESP32 + this->pin_.pin_mode(gpio::FLAG_OUTPUT); +#endif + this->pin_.digital_write(false); if (this->model_ == DHT_MODEL_DHT11) { delayMicroseconds(18000); } else if (this->model_ == DHT_MODEL_SI7021) { -#ifdef USE_ESP8266 delayMicroseconds(500); - this->pin_->digital_write(true); - delayMicroseconds(40); -#else - delayMicroseconds(400); - this->pin_->digital_write(true); -#endif } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { delayMicroseconds(2000); } else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) { @@ -94,7 +94,12 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r } else { delayMicroseconds(800); } - this->pin_->pin_mode(this->pin_->get_flags()); + +#ifdef USE_ESP32 + this->pin_.digital_write(true); +#else + this->pin_.pin_mode(this->t_pin_->get_flags()); +#endif { InterruptLock lock; @@ -110,7 +115,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r uint32_t start_time = micros(); // Wait for rising edge - while (!this->pin_->digital_read()) { + while (!this->pin_.digital_read()) { if (micros() - start_time > 90) { if (i < 0) { error_code = 1; // line didn't clear @@ -127,7 +132,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r uint32_t end_time = start_time; // Wait for falling edge - while (this->pin_->digital_read()) { + while (this->pin_.digital_read()) { end_time = micros(); if (end_time - start_time > 90) { if (i < 0) { diff --git a/esphome/components/dht/dht.h b/esphome/components/dht/dht.h index 327e8a4f5c..9047dd2c96 100644 --- a/esphome/components/dht/dht.h +++ b/esphome/components/dht/dht.h @@ -38,7 +38,10 @@ class DHT : public PollingComponent { */ void set_dht_model(DHTModel model); - void set_pin(InternalGPIOPin *pin) { pin_ = pin; } + void set_pin(InternalGPIOPin *pin) { + this->t_pin_ = pin; + this->pin_ = pin->to_isr(); + } void set_model(DHTModel model) { model_ = model; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -54,7 +57,8 @@ class DHT : public PollingComponent { protected: bool read_sensor_(float *temperature, float *humidity, bool report_errors); - InternalGPIOPin *pin_; + InternalGPIOPin *t_pin_; + ISRInternalGPIOPin pin_; DHTModel model_{DHT_MODEL_AUTO_DETECT}; bool is_auto_detect_{false}; sensor::Sensor *temperature_sensor_{nullptr}; diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 1451d14e2e..ebc3c0a9f6 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -7,7 +7,6 @@ namespace esphome { namespace display { - static const char *const TAG = "display"; const Color COLOR_OFF(0, 0, 0, 0); @@ -16,6 +15,7 @@ const Color COLOR_ON(255, 255, 255, 255); void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void Display::clear() { this->fill(COLOR_OFF); } void Display::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } + void HOT Display::line(int x1, int y1, int x2, int y2, Color color) { const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1; const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; @@ -91,23 +91,27 @@ void HOT Display::horizontal_line(int x, int y, int width, Color color) { for (int i = x; i < x + width; i++) this->draw_pixel_at(i, y, color); } + void HOT Display::vertical_line(int x, int y, int height, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = y; i < y + height; i++) this->draw_pixel_at(x, i, color); } + void Display::rectangle(int x1, int y1, int width, int height, Color color) { this->horizontal_line(x1, y1, width, color); this->horizontal_line(x1, y1 + height - 1, width, color); this->vertical_line(x1, y1, height, color); this->vertical_line(x1 + width - 1, y1, height, color); } + void Display::filled_rectangle(int x1, int y1, int width, int height, Color color) { // Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses. for (int i = y1; i < y1 + height; i++) { this->horizontal_line(x1, i, width, color); } } + void HOT Display::circle(int center_x, int center_xy, int radius, Color color) { int dx = -radius; int dy = 0; @@ -131,6 +135,7 @@ void HOT Display::circle(int center_x, int center_xy, int radius, Color color) { } } while (dx <= 0); } + void Display::filled_circle(int center_x, int center_y, int radius, Color color) { int dx = -int32_t(radius); int dy = 0; @@ -157,6 +162,7 @@ void Display::filled_circle(int center_x, int center_y, int radius, Color color) } } while (dx <= 0); } + void Display::filled_ring(int center_x, int center_y, int radius1, int radius2, Color color) { int rmax = radius1 > radius2 ? radius1 : radius2; int rmin = radius1 < radius2 ? radius1 : radius2; @@ -213,6 +219,7 @@ void Display::filled_ring(int center_x, int center_y, int radius1, int radius2, } } while (dxmax <= 0); } + void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, int progress, Color color) { int rmax = radius1 > radius2 ? radius1 : radius2; int rmin = radius1 < radius2 ? radius1 : radius2; @@ -228,7 +235,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, // outer dots this->draw_pixel_at(center_x + dxmax, center_y - dymax, color); this->draw_pixel_at(center_x - dxmax, center_y - dymax, color); - if (dymin < rmin) { // side parts + if (dymin < rmin) { + // side parts int lhline_width = -(dxmax - dxmin) + 1; if (progress >= 50) { if (float(dymax) < float(-dxmax) * tan_a) { @@ -239,7 +247,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); // left if (!dymax) this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border - if (upd_dxmax > -dxmin) { // right + if (upd_dxmax > -dxmin) { + // right int rhline_width = (upd_dxmax + dxmin) + 1; this->horizontal_line(center_x - dxmin, center_y - dymax, rhline_width > lhline_width ? lhline_width : rhline_width, color); @@ -256,7 +265,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, if (lhline_width > 0) this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); } - } else { // top part + } else { + // top part int hline_width = 2 * (-dxmax) + 1; if (progress >= 50) { if (dymax < float(-dxmax) * tan_a) { @@ -300,11 +310,13 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, } } while (dxmax <= 0); } + void HOT Display::triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { this->line(x1, y1, x2, y2, color); this->line(x1, y1, x3, y3, color); this->line(x2, y2, x3, y3, color); } + void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3) { if (*y1 > *y2) { int x_temp = *x1, y_temp = *y1; @@ -322,6 +334,7 @@ void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3 = x_temp, *y3 = y_temp; } } + void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { // y2 must be equal to y3 (same horizontal line) @@ -333,7 +346,8 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int s1_dy = abs(y2 - y1); int s1_sign_x = ((x2 - x1) >= 0) ? 1 : -1; int s1_sign_y = ((y2 - y1) >= 0) ? 1 : -1; - if (s1_dy > s1_dx) { // swap values + if (s1_dy > s1_dx) { + // swap values int tmp = s1_dx; s1_dx = s1_dy; s1_dy = tmp; @@ -349,7 +363,8 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int s2_dy = abs(y3 - y1); int s2_sign_x = ((x3 - x1) >= 0) ? 1 : -1; int s2_sign_y = ((y3 - y1) >= 0) ? 1 : -1; - if (s2_dy > s2_dx) { // swap values + if (s2_dy > s2_dx) { + // swap values int tmp = s2_dx; s2_dx = s2_dy; s2_dy = tmp; @@ -402,20 +417,25 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, } } } + void Display::filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { // Sort the three points by y-coordinate ascending, so [x1,y1] is the topmost point this->sort_triangle_points_by_y_(&x1, &y1, &x2, &y2, &x3, &y3); - if (y2 == y3) { // Check for special case of a bottom-flat triangle + if (y2 == y3) { + // Check for special case of a bottom-flat triangle this->filled_flat_side_triangle_(x1, y1, x2, y2, x3, y3, color); - } else if (y1 == y2) { // Check for special case of a top-flat triangle + } else if (y1 == y2) { + // Check for special case of a top-flat triangle this->filled_flat_side_triangle_(x3, y3, x1, y1, x2, y2, color); - } else { // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle + } else { + // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle int x_temp = (int) (x1 + ((float) (y2 - y1) / (float) (y3 - y1)) * (x3 - x1)), y_temp = y2; this->filled_flat_side_triangle_(x1, y1, x2, y2, x_temp, y_temp, color); this->filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color); } } + void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *vertex_y, int center_x, int center_y, int radius, int edges, RegularPolygonVariation variation, float rotation_degrees) { @@ -447,7 +467,8 @@ void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPo int current_vertex_x, current_vertex_y; get_regular_polygon_vertex(current_vertex_id, ¤t_vertex_x, ¤t_vertex_y, x, y, radius, edges, variation, rotation_degrees); - if (current_vertex_id > 0) { // Start drawing after the 2nd vertex coordinates has been calculated + if (current_vertex_id > 0) { + // Start drawing after the 2nd vertex coordinates has been calculated if (drawing == DRAWING_FILLED) { this->filled_triangle(x, y, previous_vertex_x, previous_vertex_y, current_vertex_x, current_vertex_y, color); } else if (drawing == DRAWING_OUTLINE) { @@ -459,21 +480,26 @@ void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPo } } } + void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color, RegularPolygonDrawing drawing) { regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, drawing); } + void HOT Display::regular_polygon(int x, int y, int radius, int edges, Color color, RegularPolygonDrawing drawing) { regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, drawing); } + void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, float rotation_degrees, Color color) { regular_polygon(x, y, radius, edges, variation, rotation_degrees, color, DRAWING_FILLED); } + void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color) { regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, DRAWING_FILLED); } + void Display::filled_regular_polygon(int x, int y, int radius, int edges, Color color) { regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, DRAWING_FILLED); } @@ -584,15 +610,19 @@ void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, Te break; } } + void Display::print(int x, int y, BaseFont *font, Color color, const char *text, Color background) { this->print(x, y, font, color, TextAlign::TOP_LEFT, text, background); } + void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *text) { this->print(x, y, font, COLOR_ON, align, text); } + void Display::print(int x, int y, BaseFont *font, const char *text) { this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); } + void Display::printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, ...) { va_list arg; @@ -600,31 +630,37 @@ void Display::printf(int x, int y, BaseFont *font, Color color, Color background this->vprintf_(x, y, font, color, background, align, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, COLOR_OFF, align, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, align, format, arg); va_end(arg); } + void Display::printf(int x, int y, BaseFont *font, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, arg); va_end(arg); } + void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; } + void Display::set_pages(std::vector pages) { for (auto *page : pages) page->set_parent(this); @@ -637,6 +673,7 @@ void Display::set_pages(std::vector pages) { pages[pages.size() - 1]->set_next(pages[0]); this->show_page(pages[0]); } + void Display::show_page(DisplayPage *page) { this->previous_page_ = this->page_; this->page_ = page; @@ -645,8 +682,10 @@ void Display::show_page(DisplayPage *page) { t->process(this->previous_page_, this->page_); } } + void Display::show_next_page() { this->page_->show_next(); } void Display::show_prev_page() { this->page_->show_prev(); } + void Display::do_update_() { if (this->auto_clear_enabled_) { this->clear(); @@ -660,10 +699,12 @@ void Display::do_update_() { } this->clear_clipping_(); } + void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) this->trigger(from, to); } + void Display::strftime(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, ESPTime time) { char buffer[64]; @@ -671,15 +712,19 @@ void Display::strftime(int x, int y, BaseFont *font, Color color, Color backgrou if (ret > 0) this->print(x, y, font, color, align, buffer, background); } + void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) { this->strftime(x, y, font, color, COLOR_OFF, align, format, time); } + void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) { this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time); } + void Display::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, COLOR_OFF, align, format, time); } + void Display::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, time); } @@ -691,6 +736,7 @@ void Display::start_clipping(Rect rect) { } this->clipping_rectangle_.push_back(rect); } + void Display::end_clipping() { if (this->clipping_rectangle_.empty()) { ESP_LOGE(TAG, "clear: Clipping is not set."); @@ -698,6 +744,7 @@ void Display::end_clipping() { this->clipping_rectangle_.pop_back(); } } + void Display::extend_clipping(Rect add_rect) { if (this->clipping_rectangle_.empty()) { ESP_LOGE(TAG, "add: Clipping is not set."); @@ -705,6 +752,7 @@ void Display::extend_clipping(Rect add_rect) { this->clipping_rectangle_.back().extend(add_rect); } } + void Display::shrink_clipping(Rect add_rect) { if (this->clipping_rectangle_.empty()) { ESP_LOGE(TAG, "add: Clipping is not set."); @@ -712,6 +760,7 @@ void Display::shrink_clipping(Rect add_rect) { this->clipping_rectangle_.back().shrink(add_rect); } } + Rect Display::get_clipping() const { if (this->clipping_rectangle_.empty()) { return Rect(); @@ -719,7 +768,9 @@ Rect Display::get_clipping() const { return this->clipping_rectangle_.back(); } } + void Display::clear_clipping_() { this->clipping_rectangle_.clear(); } + bool Display::clip(int x, int y) { if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height()) return false; @@ -727,6 +778,7 @@ bool Display::clip(int x, int y) { return false; return true; } + bool Display::clamp_x_(int x, int w, int &min_x, int &max_x) { min_x = std::max(x, 0); max_x = std::min(x + w, this->get_width()); @@ -742,6 +794,7 @@ bool Display::clamp_x_(int x, int w, int &min_x, int &max_x) { return min_x < max_x; } + bool Display::clamp_y_(int y, int h, int &min_y, int &max_y) { min_y = std::max(y, 0); max_y = std::min(y + h, this->get_height()); @@ -766,15 +819,15 @@ void Display::test_card() { int w = get_width(), h = get_height(), image_w, image_h; this->clear(); this->show_test_card_ = false; + image_w = std::min(w - 20, 310); + image_h = std::min(h - 20, 255); + int shift_x = (w - image_w) / 2; + int shift_y = (h - image_h) / 2; + int line_w = (image_w - 6) / 6; + int image_c = image_w / 2; if (this->get_display_type() == DISPLAY_TYPE_COLOR) { Color r(255, 0, 0), g(0, 255, 0), b(0, 0, 255); - image_w = std::min(w - 20, 310); - image_h = std::min(h - 20, 255); - int shift_x = (w - image_w) / 2; - int shift_y = (h - image_h) / 2; - int line_w = (image_w - 6) / 6; - int image_c = image_w / 2; for (auto i = 0; i != image_h; i++) { int c = esp_scale(i, image_h); this->horizontal_line(shift_x + 0, shift_y + i, line_w, r.fade_to_white(c)); @@ -786,26 +839,26 @@ void Display::test_card() { this->horizontal_line(shift_x + image_w - (line_w * 2), shift_y + i, line_w, b.fade_to_white(c)); this->horizontal_line(shift_x + image_w - line_w, shift_y + i, line_w, b.fade_to_black(c)); } - this->rectangle(shift_x, shift_y, image_w, image_h, Color(127, 127, 0)); + } + this->rectangle(shift_x, shift_y, image_w, image_h, Color(127, 127, 0)); - uint16_t shift_r = shift_x + line_w - (8 * 3); - uint16_t shift_g = shift_x + image_c - (8 * 3); - uint16_t shift_b = shift_x + image_w - line_w - (8 * 3); - shift_y = h / 2 - (8 * 3); - for (auto i = 0; i < 8; i++) { - uint8_t ftr = progmem_read_byte(&TESTCARD_FONT[0][i]); - uint8_t ftg = progmem_read_byte(&TESTCARD_FONT[1][i]); - uint8_t ftb = progmem_read_byte(&TESTCARD_FONT[2][i]); - for (auto k = 0; k < 8; k++) { - if ((ftr & (1 << k)) != 0) { - this->filled_rectangle(shift_r + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); - } - if ((ftg & (1 << k)) != 0) { - this->filled_rectangle(shift_g + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); - } - if ((ftb & (1 << k)) != 0) { - this->filled_rectangle(shift_b + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); - } + uint16_t shift_r = shift_x + line_w - (8 * 3); + uint16_t shift_g = shift_x + image_c - (8 * 3); + uint16_t shift_b = shift_x + image_w - line_w - (8 * 3); + shift_y = h / 2 - (8 * 3); + for (auto i = 0; i < 8; i++) { + uint8_t ftr = progmem_read_byte(&TESTCARD_FONT[0][i]); + uint8_t ftg = progmem_read_byte(&TESTCARD_FONT[1][i]); + uint8_t ftb = progmem_read_byte(&TESTCARD_FONT[2][i]); + for (auto k = 0; k < 8; k++) { + if ((ftr & (1 << k)) != 0) { + this->filled_rectangle(shift_r + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); + } + if ((ftg & (1 << k)) != 0) { + this->filled_rectangle(shift_g + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); + } + if ((ftb & (1 << k)) != 0) { + this->filled_rectangle(shift_b + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF); } } } @@ -818,7 +871,9 @@ void Display::test_card() { } DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} + void DisplayPage::show() { this->parent_->show_page(this); } + void DisplayPage::show_next() { if (this->next_ == nullptr) { ESP_LOGE(TAG, "no next page"); @@ -826,6 +881,7 @@ void DisplayPage::show_next() { } this->next_->show(); } + void DisplayPage::show_prev() { if (this->prev_ == nullptr) { ESP_LOGE(TAG, "no previous page"); @@ -833,6 +889,7 @@ void DisplayPage::show_prev() { } this->prev_->show(); } + void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; } void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; } void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; } @@ -868,6 +925,5 @@ const LogString *text_align_to_string(TextAlign textalign) { return LOG_STR("UNKNOWN"); } } - } // namespace display } // namespace esphome diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index 8224adf3fe..08f758045e 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -54,6 +54,7 @@ bool MenuItemSelect::select_next() { if (this->select_var_ != nullptr) { this->select_var_->make_call().select_next(true).perform(); + this->on_value_(); changed = true; } @@ -65,6 +66,7 @@ bool MenuItemSelect::select_prev() { if (this->select_var_ != nullptr) { this->select_var_->make_call().select_previous(true).perform(); + this->on_value_(); changed = true; } diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 017a11673f..0ba68daf5d 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -4,7 +4,7 @@ from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_UART_ID -CODEOWNERS = ["@glmnet", "@zuidwijk"] +CODEOWNERS = ["@glmnet", "@zuidwijk", "@PolarGoose"] MULTI_CONF = True @@ -61,7 +61,6 @@ CONFIG_SCHEMA = cv.All( ): cv.positive_time_period_milliseconds, } ).extend(uart.UART_DEVICE_SCHEMA), - cv.only_with_arduino, ) @@ -83,7 +82,7 @@ async def to_code(config): cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) # DSMR Parser - cg.add_library("glmnet/Dsmr", "0.8") + cg.add_library("esphome/dsmr_parser", "1.0.0") # Crypto - cg.add_library("rweather/Crypto", "0.4.0") + cg.add_library("polargoose/Crypto-no-arduino", "0.4.0") diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index d99cf5e7a9..5c62aa93ab 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -1,5 +1,3 @@ -#ifdef USE_ARDUINO - #include "dsmr.h" #include "esphome/core/log.h" @@ -7,8 +5,7 @@ #include #include -namespace esphome { -namespace dsmr { +namespace esphome::dsmr { static const char *const TAG = "dsmr"; @@ -257,9 +254,9 @@ bool Dsmr::parse_telegram() { ESP_LOGV(TAG, "Trying to parse telegram"); this->stop_requesting_data_(); - ::dsmr::ParseResult res = - ::dsmr::P1Parser::parse(&data, this->telegram_, this->bytes_read_, false, - this->crc_check_); // Parse telegram according to data definition. Ignore unknown values. + const auto &res = dsmr_parser::P1Parser::parse( + data, this->telegram_, this->bytes_read_, false, + this->crc_check_); // Parse telegram according to data definition. Ignore unknown values. if (res.err) { // Parsing error, show it auto err_str = res.fullError(this->telegram_, this->telegram_ + this->bytes_read_); @@ -271,7 +268,7 @@ bool Dsmr::parse_telegram() { // publish the telegram, after publishing the sensors so it can also trigger action based on latest values if (this->s_telegram_ != nullptr) { - this->s_telegram_->publish_state(std::string(this->telegram_, this->bytes_read_)); + this->s_telegram_->publish_state(this->telegram_, this->bytes_read_); } return true; } @@ -329,7 +326,4 @@ void Dsmr::set_decryption_key(const std::string &decryption_key) { } } -} // namespace dsmr -} // namespace esphome - -#endif // USE_ARDUINO +} // namespace esphome::dsmr diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index 7304737b50..56ba75b5fa 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -1,24 +1,17 @@ #pragma once -#ifdef USE_ARDUINO - #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/uart/uart.h" #include "esphome/core/log.h" -#include "esphome/core/defines.h" - -// don't include because it puts everything in global namespace -#include -#include - +#include +#include #include -namespace esphome { -namespace dsmr { +namespace esphome::dsmr { -using namespace ::dsmr::fields; +using namespace dsmr_parser::fields; // DSMR_**_LIST generated by ESPHome and written in esphome/core/defines @@ -44,8 +37,8 @@ using namespace ::dsmr::fields; #define DSMR_DATA_SENSOR(s) s #define DSMR_COMMA , -using MyData = ::dsmr::ParsedData; +using MyData = dsmr_parser::ParsedData; class Dsmr : public Component, public uart::UARTDevice { public: @@ -140,7 +133,4 @@ class Dsmr : public Component, public uart::UARTDevice { std::vector decryption_key_{}; bool crc_check_; }; -} // namespace dsmr -} // namespace esphome - -#endif // USE_ARDUINO +} // namespace esphome::dsmr diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 0696fccdf7..7d69f79530 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -3,27 +3,34 @@ from esphome.components import sensor import esphome.config_validation as cv from esphome.const import ( CONF_ID, + DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DURATION, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_WATER, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_CUBIC_METER, + UNIT_HERTZ, + UNIT_KILOVOLT_AMPS, UNIT_KILOVOLT_AMPS_REACTIVE, UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, UNIT_KILOWATT, UNIT_KILOWATT_HOURS, + UNIT_SECOND, UNIT_VOLT, ) from . import CONF_DSMR_ID, Dsmr AUTO_LOAD = ["dsmr"] - +UNIT_GIGA_JOULE = "GJ" CONFIG_SCHEMA = cv.Schema( { @@ -46,6 +53,18 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), + cv.Optional("energy_delivered_tariff3"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_delivered_tariff4"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), cv.Optional("energy_returned_lux"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, accuracy_decimals=3, @@ -64,14 +83,82 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), + cv.Optional("energy_returned_tariff3"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_returned_tariff4"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_delivered_tariff1_ch"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_delivered_tariff2_ch"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_returned_tariff1_ch"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("energy_returned_tariff2_ch"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), cv.Optional("total_imported_energy"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, ), + cv.Optional("reactive_energy_delivered_tariff1"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=3, + ), + cv.Optional("reactive_energy_delivered_tariff2"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=3, + ), + cv.Optional("reactive_energy_delivered_tariff3"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=3, + ), + cv.Optional("reactive_energy_delivered_tariff4"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=3, + ), cv.Optional("total_exported_energy"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=3, ), + cv.Optional("reactive_energy_returned_tariff1"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=3, + ), + cv.Optional("reactive_energy_returned_tariff2"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=3, + ), + cv.Optional("reactive_energy_returned_tariff3"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=3, + ), + cv.Optional("reactive_energy_returned_tariff4"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=3, + ), cv.Optional("power_delivered"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, @@ -84,61 +171,195 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional("power_delivered_ch"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("power_returned_ch"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), cv.Optional("reactive_power_delivered"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_threshold"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_switch_position"): sensor.sensor_schema( accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_failures"): sensor.sensor_schema( accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_long_failures"): sensor.sensor_schema( accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_sags_l1"): sensor.sensor_schema( accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_sags_l2"): sensor.sensor_schema( accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_sags_l3"): sensor.sensor_schema( accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_swells_l1"): sensor.sensor_schema( accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_swells_l2"): sensor.sensor_schema( accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_swells_l3"): sensor.sensor_schema( accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_sag_time_l1"): sensor.sensor_schema( + unit_of_measurement=UNIT_SECOND, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_sag_time_l2"): sensor.sensor_schema( + unit_of_measurement=UNIT_SECOND, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_sag_time_l3"): sensor.sensor_schema( + unit_of_measurement=UNIT_SECOND, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_sag_l1"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_sag_l2"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_sag_l3"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_swell_time_l1"): sensor.sensor_schema( + unit_of_measurement=UNIT_SECOND, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_swell_time_l2"): sensor.sensor_schema( + unit_of_measurement=UNIT_SECOND, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_swell_time_l3"): sensor.sensor_schema( + unit_of_measurement=UNIT_SECOND, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_swell_l1"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_swell_l2"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_swell_l3"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("current_l1"): sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=1, + accuracy_decimals=3, device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("current_l2"): sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=1, + accuracy_decimals=3, device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("current_l3"): sensor.sensor_schema( unit_of_measurement=UNIT_AMPERE, - accuracy_decimals=1, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("current"): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("current_n"): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("current_sum"): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("current_fuse_l1"): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("current_fuse_l2"): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("current_fuse_l3"): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, device_class=DEVICE_CLASS_CURRENT, state_class=STATE_CLASS_MEASUREMENT, ), @@ -181,51 +402,93 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("voltage_l1"): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, + accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("voltage_l2"): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, + accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("voltage_l3"): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, - accuracy_decimals=1, + accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional("voltage_avg_l1"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_avg_l2"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_avg_l3"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("frequency"): sensor.sensor_schema( + unit_of_measurement=UNIT_HERTZ, + accuracy_decimals=3, + device_class=DEVICE_CLASS_FREQUENCY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("abs_power"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), cv.Optional("gas_delivered"): sensor.sensor_schema( unit_of_measurement=UNIT_CUBIC_METER, accuracy_decimals=3, @@ -244,6 +507,109 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_WATER, state_class=STATE_CLASS_TOTAL_INCREASING, ), + cv.Optional("thermal_delivered"): sensor.sensor_schema( + unit_of_measurement=UNIT_GIGA_JOULE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("sub_delivered"): sensor.sensor_schema( + unit_of_measurement=UNIT_CUBIC_METER, + accuracy_decimals=3, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("gas_device_type"): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("gas_valve_position"): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("thermal_device_type"): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("thermal_valve_position"): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("water_device_type"): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("water_valve_position"): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("sub_device_type"): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("sub_valve_position"): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("apparent_delivery_power"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("apparent_delivery_power_l1"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("apparent_delivery_power_l2"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("apparent_delivery_power_l3"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("apparent_return_power"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("apparent_return_power_l1"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("apparent_return_power_l2"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("apparent_return_power_l3"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("active_demand_power"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("active_demand_abs"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), cv.Optional( "active_energy_import_current_average_demand" ): sensor.sensor_schema( @@ -252,6 +618,90 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional( + "active_energy_export_current_average_demand" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "reactive_energy_import_current_average_demand" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "reactive_energy_export_current_average_demand" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "apparent_energy_import_current_average_demand" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "apparent_energy_export_current_average_demand" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("active_energy_import_last_completed_demand"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("active_energy_export_last_completed_demand"): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "reactive_energy_import_last_completed_demand" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "reactive_energy_export_last_completed_demand" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_REACTIVE_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "apparent_energy_import_last_completed_demand" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "apparent_energy_export_last_completed_demand" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOVOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), cv.Optional( "active_energy_import_maximum_demand_running_month" ): sensor.sensor_schema( @@ -268,6 +718,14 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional("fw_core_version"): sensor.sensor_schema( + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("fw_module_version"): sensor.sensor_schema( + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, + ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/dsmr/text_sensor.py b/esphome/components/dsmr/text_sensor.py index 3223d943be..4c7455a38f 100644 --- a/esphome/components/dsmr/text_sensor.py +++ b/esphome/components/dsmr/text_sensor.py @@ -18,11 +18,15 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional("electricity_failure_log"): text_sensor.text_sensor_schema(), cv.Optional("message_short"): text_sensor.text_sensor_schema(), cv.Optional("message_long"): text_sensor.text_sensor_schema(), + cv.Optional("equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("gas_equipment_id"): text_sensor.text_sensor_schema(), + cv.Optional("gas_equipment_id_be"): text_sensor.text_sensor_schema(), cv.Optional("thermal_equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("water_equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(), + cv.Optional("fw_core_checksum"): text_sensor.text_sensor_schema(), + cv.Optional("fw_module_checksum"): text_sensor.text_sensor_schema(), cv.Optional("telegram"): text_sensor.text_sensor_schema().extend( {cv.Optional(CONF_INTERNAL, default=True): cv.boolean} ), diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index c6eaf4e728..602e31db14 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -7,6 +7,9 @@ namespace ee895 { static const char *const TAG = "ee895"; +// Serial number is 16 bytes +static constexpr size_t EE895_SERIAL_NUMBER_SIZE = 16; + static const uint16_t CRC16_ONEWIRE_START = 0xFFFF; static const uint8_t FUNCTION_CODE_READ = 0x03; static const uint16_t SERIAL_NUMBER = 0x0000; @@ -26,7 +29,10 @@ void EE895Component::setup() { this->mark_failed(); return; } - ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(serial_number + 2, 16).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char serial_hex[format_hex_size(EE895_SERIAL_NUMBER_SIZE)]; +#endif + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex_to(serial_hex, serial_number + 2, EE895_SERIAL_NUMBER_SIZE)); } void EE895Component::dump_config() { diff --git a/esphome/components/emmeti/emmeti.cpp b/esphome/components/emmeti/emmeti.cpp index 3cb184f868..5286f962b8 100644 --- a/esphome/components/emmeti/emmeti.cpp +++ b/esphome/components/emmeti/emmeti.cpp @@ -153,8 +153,10 @@ void EmmetiClimate::reverse_add_(T val, size_t len, esphome::remote_base::Remote bool EmmetiClimate::check_checksum_(uint8_t checksum) { uint8_t expected = this->gen_checksum_(); - ESP_LOGV(TAG, "Expected checksum: %X", expected); - ESP_LOGV(TAG, "Checksum received: %X", checksum); + ESP_LOGV(TAG, + "Expected checksum: %X\n" + "Checksum received: %X", + expected, checksum); return checksum == expected; } @@ -264,8 +266,10 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, "Swing: %d", (curr_state.bitmap >> 1) & 0x01); - ESP_LOGD(TAG, "Sleep: %d", (curr_state.bitmap >> 2) & 0x01); + ESP_LOGD(TAG, + "Swing: %d\n" + "Sleep: %d", + (curr_state.bitmap >> 1) & 0x01, (curr_state.bitmap >> 2) & 0x01); for (size_t pos = 0; pos < 4; pos++) { if (data.expect_item(EMMETI_BIT_MARK, EMMETI_ONE_SPACE)) { @@ -291,10 +295,13 @@ bool EmmetiClimate::on_receive(remote_base::RemoteReceiveData data) { } } - ESP_LOGD(TAG, "Turbo: %d", (curr_state.bitmap >> 3) & 0x01); - ESP_LOGD(TAG, "Light: %d", (curr_state.bitmap >> 4) & 0x01); - ESP_LOGD(TAG, "Tree: %d", (curr_state.bitmap >> 5) & 0x01); - ESP_LOGD(TAG, "Blow: %d", (curr_state.bitmap >> 6) & 0x01); + ESP_LOGD(TAG, + "Turbo: %d\n" + "Light: %d\n" + "Tree: %d\n" + "Blow: %d", + (curr_state.bitmap >> 3) & 0x01, (curr_state.bitmap >> 4) & 0x01, (curr_state.bitmap >> 5) & 0x01, + (curr_state.bitmap >> 6) & 0x01); uint16_t control_data = 0; for (size_t pos = 0; pos < 11; pos++) { diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 381f098eb5..2c281ea2e6 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -104,10 +104,12 @@ void EndstopCover::loop() { } void EndstopCover::dump_config() { LOG_COVER("", "Endstop Cover", this); + ESP_LOGCONFIG(TAG, + " Open Duration: %.1fs\n" + " Close Duration: %.1fs", + this->open_duration_ / 1e3f, this->close_duration_ / 1e3f); LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_); - ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f); LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_); - ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f); } float EndstopCover::get_setup_priority() const { return setup_priority::DATA; } void EndstopCover::stop_prev_trigger_() { diff --git a/esphome/components/ens160_base/ens160_base.cpp b/esphome/components/ens160_base/ens160_base.cpp index 6ffaac9588..785b053f04 100644 --- a/esphome/components/ens160_base/ens160_base.cpp +++ b/esphome/components/ens160_base/ens160_base.cpp @@ -151,14 +151,16 @@ void ENS160Component::update() { } // verbose status logging - ESP_LOGV(TAG, "Status: ENS160 STATAS bit 0x%x", - (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS); - ESP_LOGV(TAG, "Status: ENS160 STATER bit 0x%x", - (ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER); - ESP_LOGV(TAG, "Status: ENS160 VALIDITY FLAG 0x%02x", (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2); - ESP_LOGV(TAG, "Status: ENS160 NEWDAT bit 0x%x", - (ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT); - ESP_LOGV(TAG, "Status: ENS160 NEWGPR bit 0x%x", + ESP_LOGV(TAG, + "Status: ENS160 STATAS bit 0x%x\n" + "Status: ENS160 STATER bit 0x%x\n" + "Status: ENS160 VALIDITY FLAG 0x%02x\n" + "Status: ENS160 NEWDAT bit 0x%x\n" + "Status: ENS160 NEWGPR bit 0x%x", + (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS, + (ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER, + (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2, + (ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT, (ENS160_DATA_STATUS_NEWGPR & (status_value)) == ENS160_DATA_STATUS_NEWGPR); data_ready = ENS160_DATA_STATUS_NEWDAT & status_value; diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 9ff393b397..b7e71a3cae 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -4,8 +4,10 @@ import pkgutil from esphome import core, pins import esphome.codegen as cg from esphome.components import display, spi +from esphome.components.display import CONF_SHOW_TEST_CARD, validate_rotation from esphome.components.mipi import flatten_sequence, map_sequence import esphome.config_validation as cv +from esphome.config_validation import update_interval from esphome.const import ( CONF_BUSY_PIN, CONF_CS_PIN, @@ -13,15 +15,25 @@ from esphome.const import ( CONF_DC_PIN, CONF_DIMENSIONS, CONF_ENABLE_PIN, + CONF_FULL_UPDATE_EVERY, CONF_HEIGHT, CONF_ID, CONF_INIT_SEQUENCE, CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, CONF_MODEL, + CONF_PAGES, CONF_RESET_DURATION, CONF_RESET_PIN, + CONF_ROTATION, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_UPDATE_INTERVAL, CONF_WIDTH, ) +from esphome.cpp_generator import RawExpression +from esphome.final_validate import full_config from . import models @@ -29,11 +41,13 @@ AUTO_LOAD = ["split_buffer"] DEPENDENCIES = ["spi"] CONF_INIT_SEQUENCE_ID = "init_sequence_id" +CONF_MINIMUM_UPDATE_INTERVAL = "minimum_update_interval" epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi") EPaperBase = epaper_spi_ns.class_( - "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer + "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.Display ) +Transform = epaper_spi_ns.enum("Transform") EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) @@ -52,10 +66,15 @@ DIMENSION_SCHEMA = cv.Schema( } ) +TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY} + def model_schema(config): model = MODELS[config[CONF_MODEL]] class_name = epaper_spi_ns.class_(model.class_name, EPaperBase) + minimum_update_interval = update_interval( + model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s") + ) cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required return ( display.FULL_DISPLAY_SCHEMA.extend( @@ -73,7 +92,18 @@ def model_schema(config): ) .extend( { + cv.Optional(CONF_ROTATION, default=0): validate_rotation, cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All( + update_interval, cv.Range(min=minimum_update_interval) + ), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ), + cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, cv.GenerateID(): cv.declare_id(class_name), cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), @@ -102,7 +132,7 @@ def customise_schema(config): """ config = cv.Schema( { - cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True, space="-"), }, extra=cv.ALLOW_EXTRA, )(config) @@ -111,9 +141,28 @@ def customise_schema(config): CONFIG_SCHEMA = customise_schema -FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( - "epaper_spi", require_miso=False, require_mosi=True -) + +def _final_validate(config): + spi.final_validate_device_schema( + "epaper_spi", require_miso=False, require_mosi=True + )(config) + + global_config = full_config.get() + from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN + + if CONF_LAMBDA not in config and CONF_PAGES not in config: + if LVGL_DOMAIN in global_config: + if CONF_UPDATE_INTERVAL not in config: + config[CONF_UPDATE_INTERVAL] = update_interval("never") + else: + # If no drawing methods are configured, and LVGL is not enabled, show a test card + config[CONF_SHOW_TEST_CARD] = True + elif CONF_UPDATE_INTERVAL not in config: + config[CONF_UPDATE_INTERVAL] = update_interval("1min") + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): @@ -137,7 +186,9 @@ async def to_code(config): init_sequence_length, ) - await display.register_display(var, config) + # Rotation is handled by setting the transform + display_config = {k: v for k, v in config.items() if k != CONF_ROTATION} + await display.register_display(var, display_config) await spi.register_spi_device(var, config) dc = await cg.gpio_pin_expression(config[CONF_DC_PIN]) @@ -148,11 +199,35 @@ async def to_code(config): config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) - if CONF_RESET_PIN in config: - reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN]) + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) cg.add(var.set_reset_pin(reset)) - if CONF_BUSY_PIN in config: - busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN]) + if busy_pin := config.get(CONF_BUSY_PIN): + busy = await cg.gpio_pin_expression(busy_pin) cg.add(var.set_busy_pin(busy)) + cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) if CONF_RESET_DURATION in config: cg.add(var.set_reset_duration(config[CONF_RESET_DURATION])) + if transform := config.get(CONF_TRANSFORM): + transform[CONF_SWAP_XY] = False + else: + transform = {x: model.get_default(x, False) for x in TRANSFORM_OPTIONS} + rotation = config[CONF_ROTATION] + if rotation == 180: + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + elif rotation == 90: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + elif rotation == 270: + transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] + transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform_str = "|".join( + { + str(getattr(Transform, x.upper())) + for x in TRANSFORM_OPTIONS + if transform.get(x) + } + ) + if transform_str: + cg.add(var.set_transform(RawExpression(transform_str))) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index cf6a0b0c3d..0b600feeae 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -7,11 +7,11 @@ namespace esphome::epaper_spi { static const char *const TAG = "epaper_spi"; +static constexpr size_t EPAPER_MAX_CMD_LOG_BYTES = 128; static constexpr const char *const EPAPER_STATE_STRINGS[] = { - "IDLE", "UPDATE", "RESET", "RESET_END", - - "SHOULD_WAIT", "INITIALISE", "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", + "IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE", + "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", }; const char *EPaperBase::epaper_state_to_string_() { @@ -22,7 +22,7 @@ const char *EPaperBase::epaper_state_to_string_() { void EPaperBase::setup() { if (!this->init_buffer_(this->buffer_length_)) { - this->mark_failed("Failed to initialise buffer"); + this->mark_failed(LOG_STR("Failed to initialise buffer")); return; } this->setup_pins_(); @@ -69,8 +69,11 @@ void EPaperBase::data(uint8_t value) { // The command is the first byte, length is the length of data only in the second byte, followed by the data. // [COMMAND, LENGTH, DATA...] void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) { - ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, - format_hex_pretty(ptr, length, '.', false).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(EPAPER_MAX_CMD_LOG_BYTES)]; + ESP_LOGV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, + format_hex_pretty_to(hex_buf, ptr, length, '.')); +#endif this->dc_pin_->digital_write(false); this->enable(); @@ -89,7 +92,7 @@ bool EPaperBase::is_idle_() const { return !this->busy_pin_->digital_read(); } -bool EPaperBase::reset_() const { +bool EPaperBase::reset() { if (this->reset_pin_ != nullptr) { if (this->state_ == EPaperState::RESET) { this->reset_pin_->digital_write(false); @@ -105,16 +108,16 @@ void EPaperBase::update() { ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_()); return; } - this->set_state_(EPaperState::RESET); + this->set_state_(EPaperState::UPDATE); this->enable_loop(); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + this->update_start_time_ = millis(); +#endif } void EPaperBase::wait_for_idle_(bool should_wait) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - if (should_wait) { - this->waiting_for_idle_start_ = millis(); - this->waiting_for_idle_last_print_ = this->waiting_for_idle_start_; - } + this->waiting_for_idle_start_ = millis(); #endif this->waiting_for_idle_ = should_wait; } @@ -138,7 +141,9 @@ void EPaperBase::loop() { if (this->waiting_for_idle_) { if (this->is_idle_()) { this->waiting_for_idle_ = false; - ESP_LOGV(TAG, "Screen now idle after %u ms", (unsigned) (millis() - this->waiting_for_idle_start_)); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_)); +#endif } else { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE if (now - this->waiting_for_idle_last_print_ >= 1000) { @@ -164,23 +169,27 @@ void EPaperBase::process_state_() { ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_()); switch (this->state_) { default: - ESP_LOGD(TAG, "Display is in unhandled state %s", epaper_state_to_string_()); - this->disable_loop(); + ESP_LOGE(TAG, "Display is in unhandled state %s", epaper_state_to_string_()); + this->set_state_(EPaperState::IDLE); break; case EPaperState::IDLE: this->disable_loop(); break; case EPaperState::RESET: case EPaperState::RESET_END: - if (this->reset_()) { - this->set_state_(EPaperState::UPDATE); + if (this->reset()) { + this->set_state_(EPaperState::INITIALISE); } else { - this->set_state_(EPaperState::RESET_END); + this->set_state_(EPaperState::RESET_END, this->reset_duration_); } break; case EPaperState::UPDATE: this->do_update_(); // Calls ESPHome (current page) lambda - this->set_state_(EPaperState::INITIALISE); + if (this->x_high_ < this->x_low_ || this->y_high_ < this->y_low_) { + this->set_state_(EPaperState::IDLE); + return; + } + this->set_state_(EPaperState::RESET); break; case EPaperState::INITIALISE: this->initialise_(); @@ -190,6 +199,10 @@ void EPaperBase::process_state_() { if (!this->transfer_data()) { return; // Not done yet, come back next loop } + this->x_low_ = this->width_; + this->x_high_ = 0; + this->y_low_ = this->height_; + this->y_high_ = 0; this->set_state_(EPaperState::POWER_ON); break; case EPaperState::POWER_ON: @@ -197,7 +210,8 @@ void EPaperBase::process_state_() { this->set_state_(EPaperState::REFRESH_SCREEN); break; case EPaperState::REFRESH_SCREEN: - this->refresh_screen(); + this->refresh_screen(this->update_count_ != 0); + this->update_count_ = (this->update_count_ + 1) % this->full_update_every_; this->set_state_(EPaperState::POWER_OFF); break; case EPaperState::POWER_OFF: @@ -207,6 +221,7 @@ void EPaperBase::process_state_() { case EPaperState::DEEP_SLEEP: this->deep_sleep(); this->set_state_(EPaperState::IDLE); + ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_); break; } } @@ -222,6 +237,9 @@ void EPaperBase::set_state_(EPaperState state, uint16_t delay) { } ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay, TRUEFALSE(this->waiting_for_idle_)); + if (state == EPaperState::IDLE) { + this->disable_loop(); + } } void EPaperBase::start_command_() { @@ -246,7 +264,7 @@ void EPaperBase::initialise_() { auto length = this->init_sequence_length_; while (index != length) { if (length - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } const uint8_t cmd = sequence[index++]; @@ -260,19 +278,73 @@ void EPaperBase::initialise_() { this->mark_failed(); return; } - ESP_LOGV(TAG, "Command %02X, length %d", cmd, num_args); this->cmd_data(cmd, sequence + index, num_args); index += num_args; } } } +/** + * Check and rotate coordinates based on the transform flags. + * @param x + * @param y + * @return false if the coordinates are out of bounds + */ +bool EPaperBase::rotate_coordinates_(int &x, int &y) { + if (!this->get_clipping().inside(x, y)) + return false; + if (this->transform_ & SWAP_XY) + std::swap(x, y); + if (this->transform_ & MIRROR_X) + x = this->width_ - x - 1; + if (this->transform_ & MIRROR_Y) + y = this->height_ - y - 1; + if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) + return false; + this->x_low_ = clamp_at_most(this->x_low_, x); + this->x_high_ = clamp_at_least(this->x_high_, x + 1); + this->y_low_ = clamp_at_most(this->y_low_, y); + this->y_high_ = clamp_at_least(this->y_high_, y + 1); + return true; +} + +/** + * Default implementation for monochrome displays where 8 pixels are packed to a byte. + * @param x + * @param y + * @param color + */ +void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) { + if (!rotate_coordinates_(x, y)) + return; + const size_t pixel_position = y * this->width_ + x; + const size_t byte_position = pixel_position / 8; + const uint8_t bit_position = pixel_position % 8; + const uint8_t pixel_bit = 0x80 >> bit_position; + const auto original = this->buffer_[byte_position]; + if ((color_to_bit(color) == 0)) { + this->buffer_[byte_position] = original & ~pixel_bit; + } else { + this->buffer_[byte_position] = original | pixel_bit; + } +} + void EPaperBase::dump_config() { LOG_DISPLAY("", "E-Paper SPI", this); - ESP_LOGCONFIG(TAG, " Model: %s", this->name_); + ESP_LOGCONFIG(TAG, + " Model: %s\n" + " SPI Data Rate: %uMHz\n" + " Full update every: %d\n" + " Swap X/Y: %s\n" + " Mirror X: %s\n" + " Mirror Y: %s", + this->name_, (unsigned) (this->data_rate_ / 1000000), this->full_update_every_, + YESNO(this->transform_ & SWAP_XY), YESNO(this->transform_ & MIRROR_X), + YESNO(this->transform_ & MIRROR_Y)); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index 4745ec7339..b587b07e8f 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -5,8 +5,6 @@ #include "esphome/components/split_buffer/split_buffer.h" #include "esphome/core/component.h" -#include - namespace esphome::epaper_spi { using namespace display; @@ -25,10 +23,16 @@ enum class EPaperState : uint8_t { DEEP_SLEEP, // deep sleep the display }; -static constexpr uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr uint8_t NONE = 0; +static constexpr uint8_t MIRROR_X = 1; +static constexpr uint8_t MIRROR_Y = 2; +static constexpr uint8_t SWAP_XY = 4; + +static constexpr uint32_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr size_t MAX_TRANSFER_SIZE = 128; static constexpr uint8_t DELAY_FLAG = 0xFF; -class EPaperBase : public DisplayBuffer, +class EPaperBase : public Display, public spi::SPIDevice { public: @@ -45,6 +49,8 @@ class EPaperBase : public DisplayBuffer, void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } + void set_transform(uint8_t transform) { this->transform_ = transform; } + void set_full_update_every(uint8_t full_update_every) { this->full_update_every_ = full_update_every; } void dump_config() override; void command(uint8_t value); @@ -60,20 +66,53 @@ class EPaperBase : public DisplayBuffer, DisplayType get_display_type() override { return this->display_type_; }; + // Default implementations for monochrome displays + static uint8_t color_to_bit(Color color) { + // It's always a shade of gray. Map to BLACK or WHITE. + // We split the luminance at a suitable point + if ((static_cast(color.r) + color.g + color.b) > 512) { + return 1; + } + return 0; + } + void fill(Color color) override { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + + auto pixel_color = color_to_bit(color) ? 0xFF : 0x00; + + // We store 8 pixels per byte + this->buffer_.fill(pixel_color); + this->x_high_ = this->width_; + this->y_high_ = this->height_; + this->x_low_ = 0; + this->y_low_ = 0; + } + + void clear() override { + // clear buffer to white, just like real paper. + this->fill(COLOR_ON); + } + protected: int get_height_internal() override { return this->height_; }; int get_width_internal() override { return this->width_; }; + int get_width() override { return this->transform_ & SWAP_XY ? this->height_ : this->width_; } + int get_height() override { return this->transform_ & SWAP_XY ? this->width_ : this->height_; } + void draw_pixel_at(int x, int y, Color color) override; void process_state_(); const char *epaper_state_to_string_(); bool is_idle_() const; void setup_pins_() const; - bool reset_() const; + virtual bool reset(); void initialise_(); void wait_for_idle_(bool should_wait); bool init_buffer_(size_t buffer_length); - - virtual int get_width_controller() { return this->get_width_internal(); }; + bool rotate_coordinates_(int &x, int &y); /** * Methods that must be implemented by concrete classes to control the display @@ -86,7 +125,7 @@ class EPaperBase : public DisplayBuffer, /** * Refresh the screen after data transfer */ - virtual void refresh_screen() = 0; + virtual void refresh_screen(bool partial) = 0; /** * Power the display on @@ -118,24 +157,31 @@ class EPaperBase : public DisplayBuffer, DisplayType display_type_; size_t buffer_length_{}; - size_t current_data_index_{0}; // used by data transfer to track progress - uint32_t reset_duration_{200}; -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - uint32_t transfer_start_time_{}; - uint32_t waiting_for_idle_last_print_{0}; - uint32_t waiting_for_idle_start_{0}; -#endif - + size_t current_data_index_{}; // used by data transfer to track progress + split_buffer::SplitBuffer buffer_{}; GPIOPin *dc_pin_{}; GPIOPin *busy_pin_{}; GPIOPin *reset_pin_{}; + bool waiting_for_idle_{}; + uint32_t delay_until_{}; + uint8_t transform_{}; + uint8_t update_count_{}; + // these values represent the bounds of the updated buffer. Note that x_high and y_high + // point to the pixel past the last one updated, i.e. may range up to width/height. + uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{}; - bool waiting_for_idle_{false}; - uint32_t delay_until_{0}; - - split_buffer::SplitBuffer buffer_; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + uint32_t waiting_for_idle_last_print_{}; + uint32_t waiting_for_idle_start_{}; +#endif +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + uint32_t update_start_time_{}; +#endif + // properties with specific initialisers go last EPaperState state_{EPaperState::IDLE}; + uint32_t reset_duration_{10}; + uint8_t full_update_every_{1}; }; } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp index 8e4cbdde2a..be243145fc 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp @@ -6,7 +6,6 @@ namespace esphome::epaper_spi { static constexpr const char *const TAG = "epaper_spi.6c"; -static constexpr size_t MAX_TRANSFER_SIZE = 128; static constexpr unsigned char GRAY_THRESHOLD = 50; enum E6Color { @@ -75,29 +74,35 @@ static uint8_t color_to_hex(Color color) { } void EPaperSpectraE6::power_on() { - ESP_LOGD(TAG, "Power on"); + ESP_LOGV(TAG, "Power on"); this->command(0x04); } void EPaperSpectraE6::power_off() { - ESP_LOGD(TAG, "Power off"); + ESP_LOGV(TAG, "Power off"); this->command(0x02); this->data(0x00); } -void EPaperSpectraE6::refresh_screen() { - ESP_LOGD(TAG, "Refresh"); +void EPaperSpectraE6::refresh_screen(bool partial) { + ESP_LOGV(TAG, "Refresh"); this->command(0x12); this->data(0x00); } void EPaperSpectraE6::deep_sleep() { - ESP_LOGD(TAG, "Deep sleep"); + ESP_LOGV(TAG, "Deep sleep"); this->command(0x07); this->data(0xA5); } void EPaperSpectraE6::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + EPaperBase::fill(color); + return; + } + auto pixel_color = color_to_hex(color); // We store 2 pixels per byte @@ -109,12 +114,11 @@ void EPaperSpectraE6::clear() { this->fill(COLOR_ON); } -void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) { - if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) +void HOT EPaperSpectraE6::draw_pixel_at(int x, int y, Color color) { + if (!this->rotate_coordinates_(x, y)) return; - auto pixel_bits = color_to_hex(color); - uint32_t pixel_position = x + y * this->get_width_controller(); + uint32_t pixel_position = x + y * this->get_width_internal(); uint32_t byte_position = pixel_position / 2; auto original = this->buffer_[byte_position]; if ((pixel_position & 1) != 0) { @@ -128,10 +132,6 @@ bool HOT EPaperSpectraE6::transfer_data() { const uint32_t start_time = App.get_loop_component_start_time(); const size_t buffer_length = this->buffer_length_; if (this->current_data_index_ == 0) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - this->transfer_start_time_ = millis(); -#endif - ESP_LOGV(TAG, "Start sending data at %ums", (unsigned) millis()); this->command(0x10); } @@ -160,7 +160,6 @@ bool HOT EPaperSpectraE6::transfer_data() { this->end_data_(); } this->current_data_index_ = 0; - ESP_LOGV(TAG, "Sent data in %" PRIu32 " ms", millis() - this->transfer_start_time_); return true; } } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h index 48356ad74b..b8dbf0b0c5 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -16,11 +16,11 @@ class EPaperSpectraE6 : public EPaperBase { void clear() override; protected: - void refresh_screen() override; + void refresh_screen(bool partial) override; void power_on() override; void power_off() override; void deep_sleep() override; - void draw_absolute_pixel_internal(int x, int y, Color color) override; + void draw_pixel_at(int x, int y, Color color) override; bool transfer_data() override; }; diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp b/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp new file mode 100644 index 0000000000..e4f04657ad --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_ssd1677.cpp @@ -0,0 +1,86 @@ +#include "epaper_spi_ssd1677.h" + +#include + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { +static constexpr const char *const TAG = "epaper_spi.ssd1677"; + +void EPaperSSD1677::refresh_screen(bool partial) { + ESP_LOGV(TAG, "Refresh screen"); + this->command(0x22); + this->data(partial ? 0xFF : 0xF7); + this->command(0x20); +} + +void EPaperSSD1677::deep_sleep() { + ESP_LOGV(TAG, "Deep sleep"); + this->command(0x10); +} + +bool EPaperSSD1677::reset() { + if (EPaperBase::reset()) { + this->command(0x12); + return true; + } + return false; +} + +bool HOT EPaperSSD1677::transfer_data() { + auto start_time = millis(); + if (this->current_data_index_ == 0) { + uint8_t data[4]{}; + // round to byte boundaries + this->x_low_ &= ~7; + this->y_low_ &= ~7; + this->x_high_ += 7; + this->x_high_ &= ~7; + this->y_high_ += 7; + this->y_high_ &= ~7; + data[0] = this->x_low_; + data[1] = this->x_low_ / 256; + data[2] = this->x_high_ - 1; + data[3] = (this->x_high_ - 1) / 256; + cmd_data(0x4E, data, 2); + cmd_data(0x44, data, sizeof(data)); + data[0] = this->y_low_; + data[1] = this->y_low_ / 256; + data[2] = this->y_high_ - 1; + data[3] = (this->y_high_ - 1) / 256; + cmd_data(0x4F, data, 2); + this->cmd_data(0x45, data, sizeof(data)); + // for monochrome, we still need to clear the red data buffer at least once to prevent it + // causing dirty pixels after partial refresh. + this->command(this->send_red_ ? 0x26 : 0x24); + this->current_data_index_ = this->y_low_; // actually current line + } + size_t row_length = (this->x_high_ - this->x_low_) / 8; + FixedVector bytes_to_send{}; + bytes_to_send.init(row_length); + ESP_LOGV(TAG, "Writing bytes at line %zu at %ums", this->current_data_index_, (unsigned) millis()); + this->start_data_(); + while (this->current_data_index_ != this->y_high_) { + size_t data_idx = (this->current_data_index_ * this->width_ + this->x_low_) / 8; + for (size_t i = 0; i != row_length; i++) { + bytes_to_send[i] = this->send_red_ ? 0 : this->buffer_[data_idx++]; + } + ++this->current_data_index_; + this->write_array(&bytes_to_send.front(), row_length); // NOLINT + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + this->end_data_(); + return false; + } + } + + this->end_data_(); + this->current_data_index_ = 0; + if (this->send_red_) { + this->send_red_ = false; + return false; + } + return true; +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_ssd1677.h b/esphome/components/epaper_spi/epaper_spi_ssd1677.h new file mode 100644 index 0000000000..47584d24c0 --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_ssd1677.h @@ -0,0 +1,25 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +class EPaperSSD1677 : public EPaperBase { + public: + EPaperSSD1677(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) { + this->buffer_length_ = width * height / 8; // 8 pixels per byte + } + + protected: + void refresh_screen(bool partial) override; + void power_on() override {} + void power_off() override{}; + void deep_sleep() override; + bool reset() override; + bool transfer_data() override; + bool send_red_{true}; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/spectra_e6.py b/esphome/components/epaper_spi/models/spectra_e6.py index 9f0b673d69..58015f486e 100644 --- a/esphome/components/epaper_spi/models/spectra_e6.py +++ b/esphome/components/epaper_spi/models/spectra_e6.py @@ -4,8 +4,8 @@ from . import EpaperModel class SpectraE6(EpaperModel): - def __init__(self, name, class_name="EPaperSpectraE6", **kwargs): - super().__init__(name, class_name, **kwargs) + def __init__(self, name, class_name="EPaperSpectraE6", **defaults): + super().__init__(name, class_name, **defaults) # fmt: off def get_init_sequence(self, config: dict): @@ -30,13 +30,17 @@ class SpectraE6(EpaperModel): return self.defaults.get(key, fallback) -spectra_e6 = SpectraE6("spectra-e6") +spectra_e6 = SpectraE6("spectra-e6", minimum_update_interval="30s") -spectra_e6.extend( - "Seeed-reTerminal-E1002", +spectra_e6_7p3 = spectra_e6.extend( + "7.3in-Spectra-E6", width=800, height=480, data_rate="20MHz", +) + +spectra_e6_7p3.extend( + "Seeed-reTerminal-E1002", cs_pin=10, dc_pin=11, reset_pin=12, diff --git a/esphome/components/epaper_spi/models/ssd1677.py b/esphome/components/epaper_spi/models/ssd1677.py new file mode 100644 index 0000000000..3eb53d650e --- /dev/null +++ b/esphome/components/epaper_spi/models/ssd1677.py @@ -0,0 +1,42 @@ +from esphome.const import CONF_DATA_RATE + +from . import EpaperModel + + +class SSD1677(EpaperModel): + def __init__(self, name, class_name="EPaperSSD1677", **kwargs): + if CONF_DATA_RATE not in kwargs: + kwargs[CONF_DATA_RATE] = "20MHz" + super().__init__(name, class_name, **kwargs) + + # fmt: off + def get_init_sequence(self, config: dict): + width, _height = self.get_dimensions(config) + return ( + (0x18, 0x80), # Select internal Temp sensor + (0x0C, 0xAE, 0xC7, 0xC3, 0xC0, 0x80), # inrush current level 2 + (0x01, (width - 1) % 256, (width - 1) // 256, 0x02), # Set column gate limit + (0x3C, 0x01), # Set border waveform + (0x11, 3), # Set transform + ) + + +ssd1677 = SSD1677("ssd1677") + +ssd1677.extend( + "seeed-ee04-mono-4.26", + width=800, + height=480, + mirror_x=True, + cs_pin=44, + dc_pin=10, + reset_pin=38, + busy_pin={ + "number": 4, + "inverted": False, + "mode": { + "input": True, + "pulldown": True, + }, + }, +) diff --git a/esphome/components/es8311/audio_dac.py b/esphome/components/es8311/audio_dac.py index 7d80cfd5fb..5941a81935 100644 --- a/esphome/components/es8311/audio_dac.py +++ b/esphome/components/es8311/audio_dac.py @@ -22,7 +22,6 @@ ES8311_BITS_PER_SAMPLE_ENUM = { es8311_mic_gain = es8311_ns.enum("ES8311MicGain") ES8311_MIC_GAIN_ENUM = { - "MIN": es8311_mic_gain.ES8311_MIC_GAIN_MIN, "0DB": es8311_mic_gain.ES8311_MIC_GAIN_0DB, "6DB": es8311_mic_gain.ES8311_MIC_GAIN_6DB, "12DB": es8311_mic_gain.ES8311_MIC_GAIN_12DB, @@ -31,7 +30,6 @@ ES8311_MIC_GAIN_ENUM = { "30DB": es8311_mic_gain.ES8311_MIC_GAIN_30DB, "36DB": es8311_mic_gain.ES8311_MIC_GAIN_36DB, "42DB": es8311_mic_gain.ES8311_MIC_GAIN_42DB, - "MAX": es8311_mic_gain.ES8311_MIC_GAIN_MAX, } diff --git a/esphome/components/es8388/es8388.cpp b/esphome/components/es8388/es8388.cpp index 69c16a9615..9deb29416f 100644 --- a/esphome/components/es8388/es8388.cpp +++ b/esphome/components/es8388/es8388.cpp @@ -116,9 +116,8 @@ void ES8388::setup() { if (this->dac_output_select_ != nullptr) { auto dac_power = this->get_dac_power(); if (dac_power.has_value()) { - auto dac_power_str = this->dac_output_select_->at(dac_power.value()); - if (dac_power_str.has_value()) { - this->dac_output_select_->publish_state(dac_power_str.value()); + if (this->dac_output_select_->has_index(dac_power.value())) { + this->dac_output_select_->publish_state(dac_power.value()); } else { ESP_LOGW(TAG, "Unknown DAC output power value: %d", dac_power.value()); } @@ -127,9 +126,8 @@ void ES8388::setup() { if (this->adc_input_mic_select_ != nullptr) { auto mic_input = this->get_mic_input(); if (mic_input.has_value()) { - auto mic_input_str = this->adc_input_mic_select_->at(mic_input.value()); - if (mic_input_str.has_value()) { - this->adc_input_mic_select_->publish_state(mic_input_str.value()); + if (this->adc_input_mic_select_->has_index(mic_input.value())) { + this->adc_input_mic_select_->publish_state(mic_input.value()); } else { ESP_LOGW(TAG, "Unknown ADC input mic value: %d", mic_input.value()); } @@ -210,9 +208,11 @@ bool ES8388::set_dac_output(DacOutputLine line) { return false; }; - ESP_LOGV(TAG, "Setting ES8388_DACPOWER to 0x%02X", dac_power); - ESP_LOGV(TAG, "Setting ES8388_DACCONTROL24 / ES8388_DACCONTROL25 to 0x%02X", reg_out1); - ESP_LOGV(TAG, "Setting ES8388_DACCONTROL26 / ES8388_DACCONTROL27 to 0x%02X", reg_out2); + ESP_LOGV(TAG, + "Setting ES8388_DACPOWER to 0x%02X\n" + "Setting ES8388_DACCONTROL24 / ES8388_DACCONTROL25 to 0x%02X\n" + "Setting ES8388_DACCONTROL26 / ES8388_DACCONTROL27 to 0x%02X", + dac_power, reg_out1, reg_out2); ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL24, reg_out1)); // LOUT1VOL ES8388_ERROR_CHECK(this->write_byte(ES8388_DACCONTROL25, reg_out1)); // ROUT1VOL @@ -225,7 +225,7 @@ bool ES8388::set_dac_output(DacOutputLine line) { optional ES8388::get_dac_power() { uint8_t dac_power; if (!this->read_byte(ES8388_DACPOWER, &dac_power)) { - this->status_momentary_warning("Failed to read ES8388_DACPOWER"); + this->status_momentary_warning("dacpower_read"); return {}; } switch (dac_power) { @@ -268,7 +268,7 @@ bool ES8388::set_adc_input_mic(AdcInputMicLine line) { optional ES8388::get_mic_input() { uint8_t mic_input; if (!this->read_byte(ES8388_ADCCONTROL2, &mic_input)) { - this->status_momentary_warning("Failed to read ES8388_ADCCONTROL2"); + this->status_momentary_warning("adccontrol2_read"); return {}; } switch (mic_input) { diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0f85e585f7..45fe8d1c26 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -4,6 +4,7 @@ import itertools import logging import os from pathlib import Path +import re from esphome import yaml_util import esphome.codegen as cg @@ -12,17 +13,20 @@ from esphome.const import ( CONF_ADVANCED, CONF_BOARD, CONF_COMPONENTS, + CONF_DISABLED, CONF_ESPHOME, CONF_FRAMEWORK, CONF_IGNORE_EFUSE_CUSTOM_MAC, CONF_IGNORE_EFUSE_MAC_CRC, CONF_LOG_LEVEL, CONF_NAME, + CONF_OTA, CONF_PATH, CONF_PLATFORM_VERSION, CONF_PLATFORMIO_OPTIONS, CONF_REF, CONF_REFRESH, + CONF_SAFE_MODE, CONF_SOURCE, CONF_TYPE, CONF_VARIANT, @@ -37,6 +41,7 @@ from esphome.const import ( __version__, ) from esphome.core import CORE, HexInt, TimePeriod +from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, write_file_if_changed from esphome.types import ConfigType @@ -58,6 +63,7 @@ from .const import ( # noqa VARIANT_ESP32C3, VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, @@ -78,7 +84,9 @@ CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" +CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" +CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" LOG_LEVELS_IDF = [ @@ -103,6 +111,21 @@ COMPILER_OPTIMIZATIONS = { "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", } +# ESP32 (original) chip revision options +# Setting minimum revision to 3.0 or higher: +# - Reduces flash size by excluding workaround code for older chip bugs +# - For PSRAM users: disables CONFIG_SPIRAM_CACHE_WORKAROUND, which saves significant +# IRAM by keeping C library functions in ROM instead of recompiling them +# See: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/chip_revision.html +ESP32_CHIP_REVISIONS = { + "0.0": "CONFIG_ESP32_REV_MIN_0", + "1.0": "CONFIG_ESP32_REV_MIN_1", + "1.1": "CONFIG_ESP32_REV_MIN_1_1", + "2.0": "CONFIG_ESP32_REV_MIN_2", + "3.0": "CONFIG_ESP32_REV_MIN_3", + "3.1": "CONFIG_ESP32_REV_MIN_3_1", +} + # Socket limit configuration for ESP-IDF # ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10 DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default @@ -115,24 +138,25 @@ ARDUINO_ALLOWED_VARIANTS = [ ] -def get_cpu_frequencies(*frequencies): - return [str(x) + "MHZ" for x in frequencies] +def get_cpu_frequencies(*frequencies: int) -> list[str]: + return [f"{frequency}MHZ" for frequency in frequencies] CPU_FREQUENCIES = { VARIANT_ESP32: get_cpu_frequencies(80, 160, 240), - VARIANT_ESP32S2: get_cpu_frequencies(80, 160, 240), - VARIANT_ESP32S3: get_cpu_frequencies(80, 160, 240), VARIANT_ESP32C2: get_cpu_frequencies(80, 120), VARIANT_ESP32C3: get_cpu_frequencies(80, 160), VARIANT_ESP32C5: get_cpu_frequencies(80, 160, 240), VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160), + VARIANT_ESP32C61: get_cpu_frequencies(80, 120, 160), VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96), VARIANT_ESP32P4: get_cpu_frequencies(40, 360, 400), + VARIANT_ESP32S2: get_cpu_frequencies(80, 160, 240), + VARIANT_ESP32S3: get_cpu_frequencies(80, 160, 240), } # Make sure not missed here if a new variant added. -assert all(v in CPU_FREQUENCIES for v in VARIANTS) +assert all(variant in CPU_FREQUENCIES for variant in VARIANTS) FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values())) @@ -246,10 +270,10 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): def add_idf_component( *, name: str, - repo: str = None, - ref: str = None, - path: str = None, - refresh: TimePeriod = None, + repo: str | None = None, + ref: str | None = None, + path: str | None = None, + refresh: TimePeriod | None = None, components: list[str] | None = None, submodules: list[str] | None = None, ): @@ -262,15 +286,32 @@ def add_idf_component( "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " "an issue to the external_component author and ask them to update it." ) + components_registry = CORE.data[KEY_ESP32][KEY_COMPONENTS] if components: for comp in components: - CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = { + existing = components_registry.get(comp) + if existing and existing.get(KEY_REF) != ref: + _LOGGER.warning( + "IDF component %s version conflict %s replaced by %s", + comp, + existing.get(KEY_REF), + ref, + ) + components_registry[comp] = { KEY_REPO: repo, KEY_REF: ref, KEY_PATH: f"{path}/{comp}" if path else comp, } else: - CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { + existing = components_registry.get(name) + if existing and existing.get(KEY_REF) != ref: + _LOGGER.warning( + "IDF component %s version conflict %s replaced by %s", + name, + existing.get(KEY_REF), + ref, + ) + components_registry[name] = { KEY_REPO: repo, KEY_REF: ref, KEY_PATH: path, @@ -313,7 +354,7 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" -def _is_framework_url(source: str) -> str: +def _is_framework_url(source: str) -> bool: # platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink import urllib.parse @@ -333,11 +374,12 @@ def _is_framework_url(source: str) -> str: # The default/recommended arduino framework version # - https://github.com/espressif/arduino-esp32/releases ARDUINO_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(3, 3, 2), - "latest": cv.Version(3, 3, 4), - "dev": cv.Version(3, 3, 4), + "recommended": cv.Version(3, 3, 5), + "latest": cv.Version(3, 3, 5), + "dev": cv.Version(3, 3, 5), } ARDUINO_PLATFORM_VERSION_LOOKUP = { + cv.Version(3, 3, 5): cv.Version(55, 3, 35), cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"), @@ -350,15 +392,33 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = { cv.Version(3, 1, 1): cv.Version(53, 3, 11), cv.Version(3, 1, 0): cv.Version(53, 3, 10), } +# Maps Arduino framework versions to a compatible ESP-IDF version +# These versions correspond to pioarduino/esp-idf releases +# See: https://github.com/pioarduino/esp-idf/releases +ARDUINO_IDF_VERSION_LOOKUP = { + cv.Version(3, 3, 5): cv.Version(5, 5, 2), + cv.Version(3, 3, 4): cv.Version(5, 5, 1), + cv.Version(3, 3, 3): cv.Version(5, 5, 1), + cv.Version(3, 3, 2): cv.Version(5, 5, 1), + cv.Version(3, 3, 1): cv.Version(5, 5, 1), + cv.Version(3, 3, 0): cv.Version(5, 5, 0), + cv.Version(3, 2, 1): cv.Version(5, 4, 2), + cv.Version(3, 2, 0): cv.Version(5, 4, 2), + cv.Version(3, 1, 3): cv.Version(5, 3, 2), + cv.Version(3, 1, 2): cv.Version(5, 3, 2), + cv.Version(3, 1, 1): cv.Version(5, 3, 1), + cv.Version(3, 1, 0): cv.Version(5, 3, 0), +} # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { - "recommended": cv.Version(5, 5, 1), - "latest": cv.Version(5, 5, 1), - "dev": cv.Version(5, 5, 1), + "recommended": cv.Version(5, 5, 2), + "latest": cv.Version(5, 5, 2), + "dev": cv.Version(5, 5, 2), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { + cv.Version(5, 5, 2): cv.Version(55, 3, 35), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), cv.Version(5, 4, 3): cv.Version(55, 3, 32), @@ -375,9 +435,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 31, "2"), - "latest": cv.Version(55, 3, 31, "2"), - "dev": cv.Version(55, 3, 31, "2"), + "recommended": cv.Version(55, 3, 35), + "latest": cv.Version(55, 3, 35), + "dev": cv.Version(55, 3, 35), } @@ -523,6 +583,16 @@ def final_validate(config): path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC], ) ) + if ( + config[CONF_VARIANT] != VARIANT_ESP32 + and advanced.get(CONF_MINIMUM_CHIP_REVISION) is not None + ): + errs.append( + cv.Invalid( + f"'{CONF_MINIMUM_CHIP_REVISION}' is only supported on {VARIANT_ESP32}", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION], + ) + ) if advanced[CONF_EXECUTE_FROM_PSRAM]: if config[CONF_VARIANT] != VARIANT_ESP32S3: errs.append( @@ -550,6 +620,20 @@ def final_validate(config): path=[CONF_FLASH_SIZE], ) ) + if advanced[CONF_ENABLE_OTA_ROLLBACK]: + # "disabled: false" means safe mode *is* enabled. + safe_mode_config = full_config.get(CONF_SAFE_MODE, {CONF_DISABLED: True}) + safe_mode_enabled = not safe_mode_config[CONF_DISABLED] + ota_enabled = CONF_OTA in full_config + # Both need to be enabled for rollback to work + if not (ota_enabled and safe_mode_enabled): + # But only warn if ota is even possible + if ota_enabled: + _LOGGER.warning( + "OTA rollback requires safe_mode, disabling rollback support" + ) + # disable the rollback feature anyway since it can't be used. + advanced[CONF_ENABLE_OTA_ROLLBACK] = False if errs: raise cv.MultipleInvalid(errs) @@ -566,6 +650,9 @@ CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram" CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" +CONF_FREERTOS_IN_IRAM = "freertos_in_iram" +CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram" +CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" # VFS requirement tracking @@ -592,6 +679,17 @@ def require_vfs_dir() -> None: CORE.data[KEY_VFS_DIR_REQUIRED] = True +def _parse_idf_component(value: str) -> ConfigType: + """Parse IDF component shorthand syntax like 'owner/component^version'""" + # Match operator followed by version-like string (digit or *) + if match := re.search(r"(~=|>=|<=|==|!=|>|<|\^|~)(\d|\*)", value): + return {CONF_NAME: value[: match.start()], CONF_REF: value[match.start() :]} + raise cv.Invalid( + f"Invalid IDF component shorthand '{value}'. " + f"Expected format: 'owner/componentversion' where is one of: ^, ~, ~=, ==, !=, >=, >, <=, <" + ) + + def _validate_idf_component(config: ConfigType) -> ConfigType: """Validate IDF component config and warn about deprecated options.""" if CONF_REFRESH in config: @@ -631,6 +729,9 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean, cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, + cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of( + *ESP32_CHIP_REVISIONS + ), # DHCP server is needed for WiFi AP mode. When WiFi component is used, # it will handle disabling DHCP server when AP is not configured. # Default to false (disabled) when WiFi is not used. @@ -651,22 +752,31 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, + cv.Optional(CONF_FREERTOS_IN_IRAM, default=False): cv.boolean, + cv.Optional(CONF_RINGBUF_IN_IRAM, default=False): cv.boolean, + cv.Optional(CONF_HEAP_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean, cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( min=8192, max=32768 ), + cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean, } ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( cv.All( - cv.Schema( - { - cv.Required(CONF_NAME): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.git_ref, - cv.Optional(CONF_REF): cv.string, - cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH): cv.All(cv.string, cv.source_refresh), - } + cv.Any( + cv.All(cv.string_strict, _parse_idf_component), + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.git_ref, + cv.Optional(CONF_REF): cv.string, + cv.Optional(CONF_PATH): cv.string, + cv.Optional(CONF_REFRESH): cv.All( + cv.string, cv.source_refresh + ), + } + ), ), _validate_idf_component, ) @@ -675,12 +785,14 @@ FRAMEWORK_SCHEMA = cv.Schema( ) +# Remove this class in 2026.7.0 class _FrameworkMigrationWarning: shown = False def _show_framework_migration_message(name: str, variant: str) -> None: - """Show a friendly message about framework migration when defaulting to Arduino.""" + """Show a message about the framework default change and how to switch back to Arduino.""" + # Remove this function in 2026.7.0 if _FrameworkMigrationWarning.shown: return _FrameworkMigrationWarning.shown = True @@ -690,44 +802,30 @@ def _show_framework_migration_message(name: str, variant: str) -> None: message = ( color( AnsiFore.BOLD_CYAN, - f"💡 IMPORTANT: {name} doesn't have a framework specified!", + f"💡 NOTICE: {name} does not have a framework specified.", ) + "\n\n" - + f"Currently, {variant} defaults to the Arduino framework.\n" - + color(AnsiFore.YELLOW, "This will change to ESP-IDF in ESPHome 2026.1.0.\n") + + f"Starting with ESPHome 2026.1.0, the default framework for {variant} is ESP-IDF.\n" + + "(We've been warning about this change since ESPHome 2025.8.0)\n" + "\n" - + "Note: Newer ESP32 variants (C6, H2, P4, etc.) already use ESP-IDF by default.\n" - + "\n" - + "Why change? ESP-IDF offers:\n" - + color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n") - + color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n") + + "Why we made this change:\n" + + color(AnsiFore.GREEN, " ✨ Up to 40% smaller firmware binaries\n") + color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n") - + color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n") - + color( - AnsiFore.GREEN, - " 🔧 Active development and testing by ESPHome developers\n", - ) + + color(AnsiFore.GREEN, " 🚀 Better performance and newer features\n") + + color(AnsiFore.GREEN, " 🔧 More actively maintained by ESPHome\n") + "\n" - + "Trade-offs:\n" - + color(AnsiFore.YELLOW, " 🔄 Some components need migration\n") + + "To continue using Arduino, add this to your YAML under 'esp32:':\n" + + color(AnsiFore.WHITE, " framework:\n") + + color(AnsiFore.WHITE, " type: arduino\n") + "\n" - + "What should I do?\n" - + color(AnsiFore.CYAN, " Option 1") - + ": Migrate to ESP-IDF (recommended)\n" - + " Add this to your YAML under 'esp32:':\n" - + color(AnsiFore.WHITE, " framework:\n") - + color(AnsiFore.WHITE, " type: esp-idf\n") + + "To silence this message with ESP-IDF, explicitly set:\n" + + color(AnsiFore.WHITE, " framework:\n") + + color(AnsiFore.WHITE, " type: esp-idf\n") + "\n" - + color(AnsiFore.CYAN, " Option 2") - + ": Keep using Arduino (still supported)\n" - + " Add this to your YAML under 'esp32:':\n" - + color(AnsiFore.WHITE, " framework:\n") - + color(AnsiFore.WHITE, " type: arduino\n") - + "\n" - + "Need help? Check out the migration guide:\n" + + "Migration guide: " + color( AnsiFore.BLUE, - "https://esphome.io/guides/esp32_arduino_to_idf.html", + "https://esphome.io/guides/esp32_arduino_to_idf/", ) ) _LOGGER.warning(message) @@ -739,13 +837,13 @@ def _set_default_framework(config): config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({}) if CONF_TYPE not in config[CONF_FRAMEWORK]: variant = config[CONF_VARIANT] + config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF + # Show migration message for variants that previously defaulted to Arduino + # Remove this message in 2026.7.0 if variant in ARDUINO_ALLOWED_VARIANTS: - config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO _show_framework_migration_message( config.get(CONF_NAME, "This device"), variant ) - else: - config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF return config @@ -851,11 +949,28 @@ def _configure_lwip_max_sockets(conf: dict) -> None: add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_yaml_idf_components(components: list[ConfigType]): + """Add IDF components from YAML config with final priority to override code-added components.""" + for component in components: + add_idf_component( + name=component[CONF_NAME], + repo=component.get(CONF_SOURCE), + ref=component.get(CONF_REF), + path=component.get(CONF_PATH), + ) + + async def to_code(config): cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) + cg.add_platformio_option( + "board_upload.maximum_size", + int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024, + ) cg.set_cpp_standard("gnu++20") cg.add_build_flag("-DUSE_ESP32") + cg.add_build_flag("-Wl,-z,noexecstack") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) variant = config[CONF_VARIANT] cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}") @@ -883,6 +998,12 @@ async def to_code(config): CORE.relative_internal_path(".espressif") ) + add_extra_script( + "pre", + "pre_build.py", + Path(__file__).parent / "pre_build.py.script", + ) + add_extra_script( "post", "post_build.py", @@ -907,29 +1028,27 @@ async def to_code(config): cg.add_platformio_option("framework", "arduino, espidf") cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") - cg.add_platformio_option( - "board_build.embed_txtfiles", - [ - "managed_components/espressif__esp_insights/server_certs/https_server.crt", - "managed_components/espressif__esp_rainmaker/server_certs/rmaker_mqtt_server.crt", - "managed_components/espressif__esp_rainmaker/server_certs/rmaker_claim_service_server.crt", - "managed_components/espressif__esp_rainmaker/server_certs/rmaker_ota_server.crt", - ], - ) cg.add_define( "USE_ARDUINO_VERSION_CODE", cg.RawExpression( f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" ), ) - add_idf_sdkconfig_option( - "CONFIG_ARDUINO_LOOP_STACK_SIZE", - conf[CONF_ADVANCED][CONF_LOOP_TASK_STACK_SIZE], - ) - add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) - add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) + + # Add IDF framework source for Arduino builds to ensure it uses the same version as + # the ESP-IDF framework + if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None: + cg.add_platformio_option( + "platform_packages", [_format_framework_espidf_version(idf_ver, None)] + ) + + # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency + if get_esp32_variant() == VARIANT_ESP32S2: + cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1") + cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0") + cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0") cg.add_build_flag("-Wno-nonnull-compare") @@ -937,6 +1056,16 @@ async def to_code(config): add_idf_sdkconfig_option( f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True ) + + # Set minimum chip revision for ESP32 variant + # Setting this to 3.0 or higher reduces flash size by excluding workaround code, + # and for PSRAM users saves significant IRAM by keeping C library functions in ROM. + if variant == VARIANT_ESP32: + min_rev = conf[CONF_ADVANCED].get(CONF_MINIMUM_CHIP_REVISION) + if min_rev is not None: + for rev, flag in ESP32_CHIP_REVISIONS.items(): + add_idf_sdkconfig_option(flag, rev == min_rev) + cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET") add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv") @@ -944,6 +1073,39 @@ async def to_code(config): # Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000) + # Place non-ISR FreeRTOS functions into flash instead of IRAM + # This saves up to 8KB of IRAM. ISR-safe functions (FromISR variants) stay in IRAM. + # In ESP-IDF 6.0 this becomes the default and CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH + # is removed (replaced by CONFIG_FREERTOS_IN_IRAM to restore old behavior). + # We enable this now to match IDF 6.0 behavior and catch any issues early. + # Users can set freertos_in_iram: true as an escape hatch if they encounter problems + # with code that incorrectly calls FreeRTOS functions from ISRs with cache disabled. + if conf[CONF_ADVANCED][CONF_FREERTOS_IN_IRAM]: + # IDF 5.x: don't set the flash option (keeps functions in IRAM) + # IDF 6.0+: will need CONFIG_FREERTOS_IN_IRAM=y to restore IRAM placement + add_idf_sdkconfig_option("CONFIG_FREERTOS_IN_IRAM", True) + else: + # IDF 5.x: explicitly place functions in flash + # IDF 6.0+: this is the default, option no longer exists + add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True) + + # Place ring buffer functions into flash instead of IRAM by default + # This saves IRAM. In ESP-IDF 6.0 flash placement becomes the default. + # Users can set ringbuf_in_iram: true as an escape hatch if they encounter issues. + if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM]: + # User requests ring buffer in IRAM + # IDF 6.0+: will need CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=n + add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH", False) + else: + # Place in flash to save IRAM (default) + add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True) + + # Place heap functions into flash to save IRAM (~4-6KB savings) + # Safe as long as heap functions are not called from ISRs (which they shouldn't be) + # Users can set heap_in_iram: true as an escape hatch if needed + if not conf[CONF_ADVANCED][CONF_HEAP_IN_IRAM]: + add_idf_sdkconfig_option("CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH", True) + # Setup watchdog add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True) @@ -1067,6 +1229,11 @@ async def to_code(config): "CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH", True ) + # Enable OTA rollback support + if advanced[CONF_ENABLE_OTA_ROLLBACK]: + add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True) + cg.add_define("USE_OTA_ROLLBACK") + cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE]) cg.add_define( @@ -1081,13 +1248,10 @@ async def to_code(config): for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) - for component in conf[CONF_COMPONENTS]: - add_idf_component( - name=component[CONF_NAME], - repo=component.get(CONF_SOURCE), - ref=component.get(CONF_REF), - path=component.get(CONF_PATH), - ) + # Components from YAML are added in a separate coroutine with FINAL priority + # Schedule it to run after all other components + if conf[CONF_COMPONENTS]: + CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS]) APP_PARTITION_SIZES = { @@ -1099,7 +1263,7 @@ APP_PARTITION_SIZES = { } -def get_arduino_partition_csv(flash_size): +def get_arduino_partition_csv(flash_size: str): app_partition_size = APP_PARTITION_SIZES[flash_size] eeprom_partition_size = 0x1000 # 4 KB spiffs_partition_size = 0xF000 # 60 KB @@ -1119,7 +1283,7 @@ spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size: """ -def get_idf_partition_csv(flash_size): +def get_idf_partition_csv(flash_size: str): app_partition_size = APP_PARTITION_SIZES[flash_size] return f"""\ diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index cbb314650a..8a7a9428db 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -4,6 +4,7 @@ from .const import ( VARIANT_ESP32C3, VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, @@ -17,6 +18,7 @@ STANDARD_BOARDS = { VARIANT_ESP32C3: "esp32-c3-devkitm-1", VARIANT_ESP32C5: "esp32-c5-devkitc-1", VARIANT_ESP32C6: "esp32-c6-devkitm-1", + VARIANT_ESP32C61: "esp32-c61-devkitc1-n8r2", VARIANT_ESP32H2: "esp32-h2-devkitm-1", VARIANT_ESP32P4: "esp32-p4-evboard", VARIANT_ESP32S2: "esp32-s2-kaluga-1", @@ -1216,6 +1218,28 @@ ESP32_BOARD_PINS = { "LED_BUILTINB": 4, }, "sensesiot_weizen": {}, + "seeed_xiao_esp32c6": { + "D0": 0, + "D1": 1, + "D2": 2, + "D3": 21, + "D4": 22, + "D5": 23, + "D6": 16, + "D7": 17, + "D8": 19, + "D9": 20, + "D10": 18, + "MTDO": 7, + "MTCK": 6, + "MTDI": 5, + "MTMS": 4, + "BOOT": 9, + "LED": 8, + "LED_BUILTIN": 8, + "RF_SWITCH_EN": 3, + "RF_ANT_SELECT": 14, + }, "sg-o_airMon": {}, "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16}, "tinypico": {}, @@ -1464,6 +1488,10 @@ BOARDS = { "name": "Arduino Nano ESP32", "variant": VARIANT_ESP32S3, }, + "arduino_nesso_n1": { + "name": "Arduino Nesso-N1", + "variant": VARIANT_ESP32C6, + }, "atd147_s3": { "name": "ArtronShop ATD1.47-S3", "variant": VARIANT_ESP32S3, @@ -1632,6 +1660,10 @@ BOARDS = { "name": "Espressif ESP32-C6-DevKitM-1", "variant": VARIANT_ESP32C6, }, + "esp32-c61-devkitc1-n8r2": { + "name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)", + "variant": VARIANT_ESP32C61, + }, "esp32-devkitlipo": { "name": "OLIMEX ESP32-DevKit-LiPo", "variant": VARIANT_ESP32, @@ -1649,11 +1681,15 @@ BOARDS = { "variant": VARIANT_ESP32H2, }, "esp32-p4": { - "name": "Espressif ESP32-P4 generic", + "name": "Espressif ESP32-P4 ES (pre rev.300) generic", "variant": VARIANT_ESP32P4, }, "esp32-p4-evboard": { - "name": "Espressif ESP32-P4 Function EV Board", + "name": "Espressif ESP32-P4 Function EV Board (ES pre rev.300)", + "variant": VARIANT_ESP32P4, + }, + "esp32-p4_r3": { + "name": "Espressif ESP32-P4 rev.300 generic", "variant": VARIANT_ESP32P4, }, "esp32-pico-devkitm-2": { @@ -2069,7 +2105,7 @@ BOARDS = { "variant": VARIANT_ESP32, }, "m5stack-tab5-p4": { - "name": "M5STACK Tab5 esp32-p4 Board", + "name": "M5STACK Tab5 esp32-p4 Board (ES pre rev.300)", "variant": VARIANT_ESP32P4, }, "m5stack-timer-cam": { diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index 9bef18847f..dfb736f615 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -13,36 +13,39 @@ KEY_SUBMODULES = "submodules" KEY_EXTRA_BUILD_FILES = "extra_build_files" VARIANT_ESP32 = "ESP32" -VARIANT_ESP32S2 = "ESP32S2" -VARIANT_ESP32S3 = "ESP32S3" VARIANT_ESP32C2 = "ESP32C2" VARIANT_ESP32C3 = "ESP32C3" VARIANT_ESP32C5 = "ESP32C5" VARIANT_ESP32C6 = "ESP32C6" +VARIANT_ESP32C61 = "ESP32C61" VARIANT_ESP32H2 = "ESP32H2" VARIANT_ESP32P4 = "ESP32P4" +VARIANT_ESP32S2 = "ESP32S2" +VARIANT_ESP32S3 = "ESP32S3" VARIANTS = [ VARIANT_ESP32, - VARIANT_ESP32S2, - VARIANT_ESP32S3, VARIANT_ESP32C2, VARIANT_ESP32C3, VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, VARIANT_ESP32P4, + VARIANT_ESP32S2, + VARIANT_ESP32S3, ] VARIANT_FRIENDLY = { VARIANT_ESP32: "ESP32", - VARIANT_ESP32S2: "ESP32-S2", - VARIANT_ESP32S3: "ESP32-S3", VARIANT_ESP32C2: "ESP32-C2", VARIANT_ESP32C3: "ESP32-C3", VARIANT_ESP32C5: "ESP32-C5", VARIANT_ESP32C6: "ESP32-C6", + VARIANT_ESP32C61: "ESP32-C61", VARIANT_ESP32H2: "ESP32-H2", VARIANT_ESP32P4: "ESP32-P4", + VARIANT_ESP32S2: "ESP32-S2", + VARIANT_ESP32S3: "ESP32-S3", } esp32_ns = cg.esphome_ns.namespace("esp32") diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 6215ff862f..09a45c14a6 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -4,25 +4,20 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" -#include -#include +#include +#include #include #include #include #include -#include +#include +#include -#include +void setup(); // NOLINT(readability-redundant-declaration) +void loop(); // NOLINT(readability-redundant-declaration) -#ifdef USE_ARDUINO -#include -#else -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) -#include -#endif -void setup(); -void loop(); -#endif +// Weak stub for initArduino - overridden when the Arduino component is present +extern "C" __attribute__((weak)) void initArduino() {} namespace esphome { @@ -41,29 +36,13 @@ void arch_restart() { void arch_init() { // Enable the task watchdog only on the loop task (from which we're currently running) -#if defined(USE_ESP_IDF) esp_task_wdt_add(nullptr); - // Idle task watchdog is disabled on ESP-IDF -#elif defined(USE_ARDUINO) - enableLoopWDT(); - // Disable idle task watchdog on the core we're using (Arduino pins the task to a core) -#if defined(CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0) && CONFIG_ARDUINO_RUNNING_CORE == 0 - disableCore0WDT(); -#endif -#if defined(CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1) && CONFIG_ARDUINO_RUNNING_CORE == 1 - disableCore1WDT(); -#endif -#endif - // If the bootloader was compiled with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE the current - // partition will get rolled back unless it is marked as valid. - esp_ota_img_states_t state; - const esp_partition_t *running = esp_ota_get_running_partition(); - if (esp_ota_get_state_partition(running, &state) == ESP_OK) { - if (state == ESP_OTA_IMG_PENDING_VERIFY) { - esp_ota_mark_app_valid_cancel_rollback(); - } - } + // Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled, + // in which case safe_mode will mark it valid after confirming successful boot. +#ifndef USE_OTA_ROLLBACK + esp_ota_mark_app_valid_cancel_rollback(); +#endif } void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } @@ -71,21 +50,10 @@ uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { uint32_t freq = 0; -#ifdef USE_ESP_IDF -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq); -#else - rtc_cpu_freq_config_t config; - rtc_clk_cpu_freq_get_config(&config); - freq = config.freq_mhz * 1000000U; -#endif -#elif defined(USE_ARDUINO) - freq = ESP.getCpuFreqMHz() * 1000000; -#endif return freq; } -#ifdef USE_ESP_IDF TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void loop_task(void *pv_params) { @@ -96,6 +64,7 @@ void loop_task(void *pv_params) { } extern "C" void app_main() { + initArduino(); esp32::setup_preferences(); #if CONFIG_FREERTOS_UNICORE xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle); @@ -103,11 +72,6 @@ extern "C" void app_main() { xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1); #endif } -#endif // USE_ESP_IDF - -#ifdef USE_ARDUINO -extern "C" void init() { esp32::setup_preferences(); } -#endif // USE_ARDUINO } // namespace esphome diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index a98245b889..4b53d3a172 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -97,10 +97,8 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi gpio_isr_handler_add(this->get_pin_num(), func, arg); } -std::string ESP32InternalGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%" PRIu32, static_cast(this->pin_)); - return buffer; +size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "GPIO%" PRIu32, static_cast(this->pin_)); } void ESP32InternalGPIOPin::setup() { diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index d30f4bdcba..3c13bd9b4f 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -24,7 +24,7 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return this->pin_; } diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 954891ea8d..c0803f40a8 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -29,6 +29,7 @@ from .const import ( VARIANT_ESP32C3, VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, @@ -40,6 +41,7 @@ from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_support from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports from .gpio_esp32_c5 import esp32_c5_validate_gpio_pin, esp32_c5_validate_supports from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports +from .gpio_esp32_c61 import esp32_c61_validate_gpio_pin, esp32_c61_validate_supports from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports @@ -110,6 +112,10 @@ _esp32_validations = { pin_validation=esp32_c6_validate_gpio_pin, usage_validation=esp32_c6_validate_supports, ), + VARIANT_ESP32C61: ESP32ValidationFunctions( + pin_validation=esp32_c61_validate_gpio_pin, + usage_validation=esp32_c61_validate_supports, + ), VARIANT_ESP32H2: ESP32ValidationFunctions( pin_validation=esp32_h2_validate_gpio_pin, usage_validation=esp32_h2_validate_supports, diff --git a/esphome/components/esp32/gpio_esp32_c5.py b/esphome/components/esp32/gpio_esp32_c5.py index ada426771c..fa2ce1a689 100644 --- a/esphome/components/esp32/gpio_esp32_c5.py +++ b/esphome/components/esp32/gpio_esp32_c5.py @@ -1,9 +1,12 @@ import logging import esphome.config_validation as cv -from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER +from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin +# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c5/include/hal/i2c_ll.h +_ESP32C5_I2C_LP_PINS = {"SDA": 2, "SCL": 3} + _ESP32C5_SPI_PSRAM_PINS = { 16: "SPICS0", 17: "SPIQ", @@ -43,3 +46,13 @@ def esp32_c5_validate_supports(value): check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER) return value + + +def esp32_c5_validate_lp_i2c(value): + lp_sda_pin = _ESP32C5_I2C_LP_PINS["SDA"] + lp_scl_pin = _ESP32C5_I2C_LP_PINS["SCL"] + if int(value[CONF_SDA]) != lp_sda_pin or int(value[CONF_SCL]) != lp_scl_pin: + raise cv.Invalid( + f"Low power i2c interface is only supported on GPIO{lp_sda_pin} SDA and GPIO{lp_scl_pin} SCL for ESP32-C5" + ) + return value diff --git a/esphome/components/esp32/gpio_esp32_c6.py b/esphome/components/esp32/gpio_esp32_c6.py index d466adb994..5d679dede2 100644 --- a/esphome/components/esp32/gpio_esp32_c6.py +++ b/esphome/components/esp32/gpio_esp32_c6.py @@ -1,9 +1,12 @@ import logging import esphome.config_validation as cv -from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER +from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin +# https://github.com/espressif/esp-idf/blob/master/components/esp_hal_i2c/esp32c6/include/hal/i2c_ll.h +_ESP32C6_I2C_LP_PINS = {"SDA": 6, "SCL": 7} + _ESP32C6_SPI_PSRAM_PINS = { 24: "SPICS0", 25: "SPIQ", @@ -43,3 +46,13 @@ def esp32_c6_validate_supports(value): check_strapping_pin(value, _ESP32C6_STRAPPING_PINS, _LOGGER) return value + + +def esp32_c6_validate_lp_i2c(value): + lp_sda_pin = _ESP32C6_I2C_LP_PINS["SDA"] + lp_scl_pin = _ESP32C6_I2C_LP_PINS["SCL"] + if int(value[CONF_SDA]) != lp_sda_pin or int(value[CONF_SCL]) != lp_scl_pin: + raise cv.Invalid( + f"Low power i2c interface is only supported on GPIO{lp_sda_pin} SDA and GPIO{lp_scl_pin} SCL for ESP32-C6" + ) + return value diff --git a/esphome/components/esp32/gpio_esp32_c61.py b/esphome/components/esp32/gpio_esp32_c61.py new file mode 100644 index 0000000000..77be42db3e --- /dev/null +++ b/esphome/components/esp32/gpio_esp32_c61.py @@ -0,0 +1,46 @@ +import logging + +import esphome.config_validation as cv +from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER +from esphome.pins import check_strapping_pin + +# GPIO14-17, GPIO19-21 are used for SPI flash/PSRAM +_ESP32C61_SPI_PSRAM_PINS = { + 14: "SPICS0", + 15: "SPICLK", + 16: "SPID", + 17: "SPIQ", + 19: "SPIWP", + 20: "SPIHD", + 21: "VDD_SPI", +} + +_ESP32C61_STRAPPING_PINS = {8, 9} + +_LOGGER = logging.getLogger(__name__) + + +def esp32_c61_validate_gpio_pin(value): + if value < 0 or value > 29: + raise cv.Invalid(f"Invalid pin number: {value} (must be 0-29)") + if value in _ESP32C61_SPI_PSRAM_PINS: + raise cv.Invalid( + f"This pin cannot be used on ESP32-C61s and is already used by the SPI/PSRAM interface (function: {_ESP32C61_SPI_PSRAM_PINS[value]})" + ) + + return value + + +def esp32_c61_validate_supports(value): + num = value[CONF_NUMBER] + mode = value[CONF_MODE] + is_input = mode[CONF_INPUT] + + if num < 0 or num > 29: + raise cv.Invalid(f"Invalid pin number: {num} (must be 0-29)") + if is_input: + # All ESP32-C61 pins support input mode + pass + + check_strapping_pin(value, _ESP32C61_STRAPPING_PINS, _LOGGER) + return value diff --git a/esphome/components/esp32/gpio_esp32_p4.py b/esphome/components/esp32/gpio_esp32_p4.py index 34d1b3139d..b98b567da2 100644 --- a/esphome/components/esp32/gpio_esp32_p4.py +++ b/esphome/components/esp32/gpio_esp32_p4.py @@ -1,9 +1,12 @@ import logging import esphome.config_validation as cv -from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER +from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin +# https://documentation.espressif.com/esp32-p4-chip-revision-v1.3_datasheet_en.pdf +_ESP32P4_LP_PINS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + _ESP32P4_USB_JTAG_PINS = {24, 25} _ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38} @@ -36,3 +39,14 @@ def esp32_p4_validate_supports(value): pass check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER) return value + + +def esp32_p4_validate_lp_i2c(value): + if ( + int(value[CONF_SDA]) not in _ESP32P4_LP_PINS + or int(value[CONF_SCL]) not in _ESP32P4_LP_PINS + ): + raise cv.Invalid( + f"Low power i2c interface for ESP32-P4 is only supported on low power interface GPIO{min(_ESP32P4_LP_PINS)} - GPIO{max(_ESP32P4_LP_PINS)}" + ) + return value diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index c995214232..5ef5860687 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -5,6 +5,7 @@ import json # noqa: E402 import os # noqa: E402 import pathlib # noqa: E402 import shutil # noqa: E402 +from glob import glob # noqa: E402 def merge_factory_bin(source, target, env): @@ -126,3 +127,14 @@ def esp32_copy_ota_bin(source, target, env): # Run merge first, then ota copy second env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821 + +# Find server certificates in managed components and generate .S files. +# Workaround for PlatformIO not processing target_add_binary_data() from managed component CMakeLists. +project_dir = env.subst("$PROJECT_DIR") +managed_components = os.path.join(project_dir, "managed_components") +if os.path.isdir(managed_components): + for cert_file in glob(os.path.join(managed_components, "**/server_certs/*.crt"), recursive=True): + try: + env.FileToAsm(cert_file, FILE_TYPE="TEXT") + except Exception as e: + print(f"Error processing {os.path.basename(cert_file)}: {e}") diff --git a/esphome/components/esp32/pre_build.py.script b/esphome/components/esp32/pre_build.py.script new file mode 100644 index 0000000000..af12275a0b --- /dev/null +++ b/esphome/components/esp32/pre_build.py.script @@ -0,0 +1,9 @@ +Import("env") # noqa: F821 + +# Remove custom_sdkconfig from the board config as it causes +# pioarduino to enable some strange hybrid build mode that breaks IDF +board = env.BoardConfig() +if "espidf.custom_sdkconfig" in board: + del board._manifest["espidf"]["custom_sdkconfig"] + if not board._manifest["espidf"]: + del board._manifest["espidf"] diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 7bdbb265ca..08439746b6 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -4,26 +4,30 @@ #include "esphome/core/log.h" #include "esphome/core/preferences.h" #include -#include #include -#include -#include +#include #include +#include namespace esphome { namespace esp32 { static const char *const TAG = "esp32.preferences"; +// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding +static constexpr size_t KEY_BUFFER_SIZE = 12; + struct NVSData { - std::string key; + uint32_t key; std::unique_ptr data; size_t len; void set_data(const uint8_t *src, size_t size) { - data = std::make_unique(size); - memcpy(data.get(), src, size); - len = size; + if (!this->data || this->len != size) { + this->data = std::make_unique(size); + this->len = size; + } + memcpy(this->data.get(), src, size); } }; @@ -31,27 +35,27 @@ static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-n class ESP32PreferenceBackend : public ESPPreferenceBackend { public: - std::string key; + uint32_t key; uint32_t nvs_handle; bool save(const uint8_t *data, size_t len) override { // try find in pending saves and update that for (auto &obj : s_pending_save) { - if (obj.key == key) { + if (obj.key == this->key) { obj.set_data(data, len); return true; } } NVSData save{}; - save.key = key; + save.key = this->key; save.set_data(data, len); s_pending_save.emplace_back(std::move(save)); - ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %zu", key.c_str(), len); + ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); return true; } bool load(uint8_t *data, size_t len) override { // try find in pending saves and load from that for (auto &obj : s_pending_save) { - if (obj.key == key) { + if (obj.key == this->key) { if (obj.len != len) { // size mismatch return false; @@ -61,22 +65,24 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { } } + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); size_t actual_len; - esp_err_t err = nvs_get_blob(nvs_handle, key.c_str(), nullptr, &actual_len); + esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len); if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key.c_str(), esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); return false; } if (actual_len != len) { ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); return false; } - err = nvs_get_blob(nvs_handle, key.c_str(), data, &len); + err = nvs_get_blob(this->nvs_handle, key_str, data, &len); if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); return false; } else { - ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key.c_str(), len); + ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len); } return true; } @@ -103,14 +109,12 @@ class ESP32Preferences : public ESPPreferences { } } ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - return make_preference(length, type); + return this->make_preference(length, type); } ESPPreferenceObject make_preference(size_t length, uint32_t type) override { auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->nvs_handle = nvs_handle; - - uint32_t keyval = type; - pref->key = str_sprintf("%" PRIu32, keyval); + pref->nvs_handle = this->nvs_handle; + pref->key = type; return ESPPreferenceObject(pref); } @@ -123,17 +127,19 @@ class ESP32Preferences : public ESPPreferences { // goal try write all pending saves even if one fails int cached = 0, written = 0, failed = 0; esp_err_t last_err = ESP_OK; - std::string last_key{}; + uint32_t last_key = 0; // go through vector from back to front (makes erase easier/more efficient) for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { const auto &save = s_pending_save[i]; - ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str()); - if (is_changed(nvs_handle, save)) { - esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.get(), save.len); - ESP_LOGV(TAG, "sync: key: %s, len: %zu", save.key.c_str(), save.len); + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); + ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); + if (this->is_changed_(this->nvs_handle, save, key_str)) { + esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len); + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", save.key.c_str(), save.len, esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err)); failed++; last_err = err; last_key = save.key; @@ -141,7 +147,7 @@ class ESP32Preferences : public ESPPreferences { } written++; } else { - ESP_LOGV(TAG, "NVS data not changed skipping %s len=%zu", save.key.c_str(), save.len); + ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len); cached++; } s_pending_save.erase(s_pending_save.begin() + i); @@ -149,12 +155,12 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, failed); if (failed > 0) { - ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%s", failed, esp_err_to_name(last_err), - last_key.c_str()); + ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err), + last_key); } // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes - esp_err_t err = nvs_commit(nvs_handle); + esp_err_t err = nvs_commit(this->nvs_handle); if (err != 0) { ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err)); return false; @@ -162,11 +168,13 @@ class ESP32Preferences : public ESPPreferences { return failed == 0; } - bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) { + + protected: + bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) { size_t actual_len; - esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len); + esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len); if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err)); return true; } // Check size first before allocating memory @@ -174,9 +182,9 @@ class ESP32Preferences : public ESPPreferences { return true; } auto stored_data = std::make_unique(actual_len); - err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.get(), &actual_len); + err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len); if (err != 0) { - ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); return true; } return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0; diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index ced7e3fec9..dcc3ce71cf 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -22,7 +22,6 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority import esphome.final_validate as fv DEPENDENCIES = ["esp32"] -AUTO_LOAD = ["socket"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] DOMAIN = "esp32_ble" diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index d0bfb6f843..87b5e2b738 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -24,7 +24,9 @@ extern "C" { #include #ifdef USE_ARDUINO -#include +// Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c). +// Without this, esp_bt_controller_init() fails with ESP_ERR_INVALID_STATE. +extern "C" bool btInUse() { return true; } // NOLINT(readability-identifier-naming) #endif namespace esphome::esp32_ble { @@ -165,12 +167,6 @@ void ESP32BLE::advertising_init_() { bool ESP32BLE::ble_setup_() { esp_err_t err; #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID -#ifdef USE_ARDUINO - if (!btStart()) { - ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); - return false; - } -#else if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { // start bt controller if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { @@ -195,7 +191,6 @@ bool ESP32BLE::ble_setup_() { return false; } } -#endif esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); #else @@ -256,29 +251,46 @@ bool ESP32BLE::ble_setup_() { } #endif - std::string name; - if (this->name_.has_value()) { - name = this->name_.value(); + // BLE device names are limited to 20 characters + // Buffer: 20 chars + null terminator + constexpr size_t ble_name_max_len = 21; + char name_buffer[ble_name_max_len]; + const char *device_name; + + if (this->name_ != nullptr) { if (App.is_name_add_mac_suffix_enabled()) { + // MAC address length: 12 hex chars + null terminator + constexpr size_t mac_address_len = 13; // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; - const std::string mac_addr = get_mac_address(); - const char *mac_suffix_ptr = mac_addr.c_str() + mac_address_suffix_len; - name = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len); + char mac_addr[mac_address_len]; + get_mac_address_into_buffer(mac_addr); + const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; + make_name_with_suffix_to(name_buffer, sizeof(name_buffer), this->name_, strlen(this->name_), '-', mac_suffix_ptr, + mac_address_suffix_len); + device_name = name_buffer; + } else { + device_name = this->name_; } } else { - name = App.get_name(); - if (name.length() > 20) { + const std::string &app_name = App.get_name(); + size_t name_len = app_name.length(); + if (name_len > 20) { if (App.is_name_add_mac_suffix_enabled()) { // Keep first 13 chars and last 7 chars (MAC suffix), remove middle - name.erase(13, name.length() - 20); + memcpy(name_buffer, app_name.c_str(), 13); + memcpy(name_buffer + 13, app_name.c_str() + name_len - 7, 7); } else { - name.resize(20); + memcpy(name_buffer, app_name.c_str(), 20); } + name_buffer[20] = '\0'; + } else { + memcpy(name_buffer, app_name.c_str(), name_len + 1); // Include null terminator } + device_name = name_buffer; } - err = esp_ble_gap_set_device_name(name.c_str()); + err = esp_ble_gap_set_device_name(device_name); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_set_device_name failed: %d", err); return false; @@ -299,22 +311,24 @@ bool ESP32BLE::ble_setup_() { bool ESP32BLE::ble_dismantle_() { esp_err_t err = esp_bluedroid_disable(); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err); - return false; + // ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine + if (err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err); + return false; + } + ESP_LOGD(TAG, "Already disabled"); } err = esp_bluedroid_deinit(); if (err != ESP_OK) { - ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err); - return false; + // ESP_ERR_INVALID_STATE means Bluedroid is already deinitialized, which is fine + if (err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err); + return false; + } + ESP_LOGD(TAG, "Already deinitialized"); } #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID -#ifdef USE_ARDUINO - if (!btStop()) { - ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status()); - return false; - } -#else if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_IDLE) { // stop bt controller if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) { @@ -338,7 +352,6 @@ bool ESP32BLE::ble_dismantle_() { return false; } } -#endif #else if (esp_hosted_bt_controller_disable() != ESP_OK) { ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed"); diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 2fb60bb822..1999c870f8 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -112,7 +112,7 @@ class ESP32BLE : public Component { void loop() override; void dump_config() override; float get_setup_priority() const override; - void set_name(const std::string &name) { this->name_ = name; } + void set_name(const char *name) { this->name_ = name; } #ifdef USE_ESP32_BLE_ADVERTISING void advertising_start(); @@ -191,13 +191,11 @@ class ESP32BLE : public Component { esphome::LockFreeQueue ble_events_; esphome::EventPool ble_event_pool_; - // optional (typically 16+ bytes on 32-bit, aligned to 4 bytes) - optional name_; - // 4-byte aligned members #ifdef USE_ESP32_BLE_ADVERTISING BLEAdvertising *advertising_{}; // 4 bytes (pointer) #endif + const char *name_{nullptr}; // 4 bytes (pointer to string literal in flash) esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) uint32_t advertising_cycle_time_{}; // 4 bytes @@ -214,17 +212,23 @@ extern ESP32BLE *global_ble; template class BLEEnabledCondition : public Condition { public: - bool check(const Ts &...x) override { return global_ble->is_active(); } + bool check(const Ts &...x) override { return global_ble != nullptr && global_ble->is_active(); } }; template class BLEEnableAction : public Action { public: - void play(const Ts &...x) override { global_ble->enable(); } + void play(const Ts &...x) override { + if (global_ble != nullptr) + global_ble->enable(); + } }; template class BLEDisableAction : public Action { public: - void play(const Ts &...x) override { global_ble->disable(); } + void play(const Ts &...x) override { + if (global_ble != nullptr) + global_ble->disable(); + } }; } // namespace esphome::esp32_ble diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index dcbb285e07..334780e3b8 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -39,36 +39,36 @@ ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) { ret.uuid_.uuid.uuid128[ESP_UUID_LEN_128 - 1 - i] = data[i]; return ret; } -ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { +ESPBTUUID ESPBTUUID::from_raw(const char *data, size_t length) { ESPBTUUID ret; - if (data.length() == 4) { + if (length == 4) { // 16-bit UUID as 4-character hex string - auto parsed = parse_hex(data); + auto parsed = parse_hex(data, length); if (parsed.has_value()) { ret.uuid_.len = ESP_UUID_LEN_16; ret.uuid_.uuid.uuid16 = parsed.value(); } - } else if (data.length() == 8) { + } else if (length == 8) { // 32-bit UUID as 8-character hex string - auto parsed = parse_hex(data); + auto parsed = parse_hex(data, length); if (parsed.has_value()) { ret.uuid_.len = ESP_UUID_LEN_32; ret.uuid_.uuid.uuid32 = parsed.value(); } - } else if (data.length() == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be - // investigated (lack of time) + } else if (length == 16) { // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be + // investigated (lack of time) ret.uuid_.len = ESP_UUID_LEN_128; - memcpy(ret.uuid_.uuid.uuid128, (uint8_t *) data.data(), 16); - } else if (data.length() == 36) { + memcpy(ret.uuid_.uuid.uuid128, reinterpret_cast(data), 16); + } else if (length == 36) { // If the length of the string is 36 bytes then we will assume it is a long hex string in // UUID format. ret.uuid_.len = ESP_UUID_LEN_128; int n = 0; - for (uint i = 0; i < data.length(); i += 2) { - if (data.c_str()[i] == '-') + for (size_t i = 0; i < length; i += 2) { + if (data[i] == '-') i++; - uint8_t msb = data.c_str()[i]; - uint8_t lsb = data.c_str()[i + 1]; + uint8_t msb = data[i]; + uint8_t lsb = data[i + 1]; if (msb > '9') msb -= 7; @@ -77,7 +77,7 @@ ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { ret.uuid_.uuid.uuid128[15 - n++] = ((msb & 0x0F) << 4) | (lsb & 0x0F); } } else { - ESP_LOGE(TAG, "ERROR: UUID value not 2, 4, 16 or 36 bytes - %s", data.c_str()); + ESP_LOGE(TAG, "ERROR: UUID value not 2, 4, 16 or 36 bytes - %s", data); } return ret; } @@ -143,9 +143,8 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const { return this->as_128bit() == uuid.as_128bit(); } esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } -std::string ESPBTUUID::to_string() const { - char buf[40]; // Enough for 128-bit UUID with dashes - char *pos = buf; +const char *ESPBTUUID::to_str(std::span output) const { + char *pos = output.data(); switch (this->uuid_.len) { case ESP_UUID_LEN_16: @@ -156,7 +155,7 @@ std::string ESPBTUUID::to_string() const { *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 4) & 0x0F); *pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 & 0x0F); *pos = '\0'; - return std::string(buf); + return output.data(); case ESP_UUID_LEN_32: *pos++ = '0'; @@ -165,7 +164,7 @@ std::string ESPBTUUID::to_string() const { *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid32 >> shift) & 0x0F); } *pos = '\0'; - return std::string(buf); + return output.data(); default: case ESP_UUID_LEN_128: @@ -179,9 +178,13 @@ std::string ESPBTUUID::to_string() const { } } *pos = '\0'; - return std::string(buf); + return output.data(); } - return ""; +} +std::string ESPBTUUID::to_string() const { + char buf[UUID_STR_LEN]; + this->to_str(buf); + return std::string(buf); } } // namespace esphome::esp32_ble diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index 4cf2d10abd..ae593955a4 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -7,11 +7,16 @@ #ifdef USE_ESP32 #ifdef USE_ESP32_BLE_UUID +#include +#include #include #include namespace esphome::esp32_ble { +/// Buffer size for UUID string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\0" +static constexpr size_t UUID_STR_LEN = 37; + class ESPBTUUID { public: ESPBTUUID(); @@ -23,7 +28,12 @@ class ESPBTUUID { static ESPBTUUID from_raw(const uint8_t *data); static ESPBTUUID from_raw_reversed(const uint8_t *data); - static ESPBTUUID from_raw(const std::string &data); + static ESPBTUUID from_raw(const char *data, size_t length); + static ESPBTUUID from_raw(const char *data) { return from_raw(data, strlen(data)); } + static ESPBTUUID from_raw(const std::string &data) { return from_raw(data.c_str(), data.length()); } + static ESPBTUUID from_raw(std::initializer_list data) { + return from_raw(reinterpret_cast(data.begin()), data.size()); + } static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); @@ -37,6 +47,7 @@ class ESPBTUUID { esp_bt_uuid_t get_uuid() const; std::string to_string() const; + const char *to_str(std::span output) const; protected: esp_bt_uuid_t uuid_; diff --git a/esphome/components/esp32_ble_client/ble_characteristic.cpp b/esphome/components/esp32_ble_client/ble_characteristic.cpp index 36229c23c3..e830702f11 100644 --- a/esphome/components/esp32_ble_client/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_client/ble_characteristic.cpp @@ -38,7 +38,7 @@ void BLECharacteristic::parse_descriptors() { } if (status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", - this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status); + this->service->client->get_connection_index(), this->service->client->address_str(), status); break; } if (count == 0) { @@ -50,8 +50,12 @@ void BLECharacteristic::parse_descriptors() { desc->handle = result.handle; desc->characteristic = this; this->descriptors.push_back(desc); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char uuid_buf[espbt::UUID_STR_LEN]; + desc->uuid.to_str(uuid_buf); ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(), - this->service->client->address_str().c_str(), desc->uuid.to_string().c_str(), desc->handle); + this->service->client->address_str(), uuid_buf, desc->handle); +#endif offset++; } } @@ -84,7 +88,7 @@ esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size, new_val, write_type, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "[%d] [%s] Error sending write value to BLE gattc server, status=%d", - this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status); + this->service->client->get_connection_index(), this->service->client->address_str(), status); } return status; } diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 18321ef91c..149fcc79d5 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -41,7 +41,7 @@ void BLEClientBase::setup() { } void BLEClientBase::set_state(espbt::ClientState st) { - ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st); + ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_, (int) st); ESPBTClient::set_state(st); } @@ -70,9 +70,9 @@ float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_B void BLEClientBase::dump_config() { ESP_LOGCONFIG(TAG, " Address: %s\n" - " Auto-Connect: %s", - this->address_str().c_str(), TRUEFALSE(this->auto_connect_)); - ESP_LOGCONFIG(TAG, " State: %s", espbt::client_state_to_string(this->state())); + " Auto-Connect: %s\n" + " State: %s", + this->address_str(), TRUEFALSE(this->auto_connect_), espbt::client_state_to_string(this->state())); if (this->status_ == ESP_GATT_NO_RESOURCES) { ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config."); } else if (this->status_ != ESP_GATT_OK) { @@ -104,12 +104,11 @@ void BLEClientBase::connect() { // Prevent duplicate connection attempts if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED || this->state_ == espbt::ClientState::ESTABLISHED) { - ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, - this->address_str_.c_str(), espbt::client_state_to_string(this->state_)); + ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_, + espbt::client_state_to_string(this->state_)); return; } - ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_.c_str(), - this->remote_addr_type_); + ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); this->paired_ = false; // Enable loop for state processing this->enable_loop(); @@ -135,13 +134,13 @@ esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda void BLEClientBase::disconnect() { if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) { - ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_.c_str(), + ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_, espbt::client_state_to_string(this->state_)); return; } if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) { ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_, - this->address_str_.c_str()); + this->address_str_); this->want_disconnect_ = true; return; } @@ -150,8 +149,7 @@ void BLEClientBase::disconnect() { void BLEClientBase::unconditional_disconnect() { // Disconnect without checking the state. - ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(), - this->conn_id_); + ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_); if (this->state_ == espbt::ClientState::DISCONNECTING) { this->log_error_("Already disconnecting"); return; @@ -192,24 +190,23 @@ void BLEClientBase::release_services() { } void BLEClientBase::log_event_(const char *name) { - ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name); + ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name); } void BLEClientBase::log_gattc_event_(const char *name) { - ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_.c_str(), name); + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name); } void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) { - ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, - status); + ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status); } void BLEClientBase::log_gattc_warning_(const char *operation, esp_err_t err) { - ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, err); + ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, err); } void BLEClientBase::log_connection_params_(const char *param_type) { - ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type); + ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_, param_type); } void BLEClientBase::handle_connection_result_(esp_err_t ret) { @@ -220,15 +217,15 @@ void BLEClientBase::handle_connection_result_(esp_err_t ret) { } void BLEClientBase::log_error_(const char *message) { - ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message); + ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } void BLEClientBase::log_error_(const char *message, int code) { - ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_.c_str(), message, code); + ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_, message, code); } void BLEClientBase::log_warning_(const char *message) { - ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message); + ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, @@ -264,13 +261,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_) return false; - ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_, - this->address_str_.c_str(), event, esp_gattc_if); + ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_, this->address_str_, + event, esp_gattc_if); switch (event) { case ESP_GATTC_REG_EVT: { if (param->reg.status == ESP_GATT_OK) { - ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_.c_str(), + ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_, this->app_id); this->gattc_if_ = esp_gattc_if; } else { @@ -292,7 +289,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // arriving after we've already transitioned to IDLE state. if (this->state_ == espbt::ClientState::IDLE) { ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_, - this->address_str_.c_str(), param->open.status); + this->address_str_, param->open.status); break; } @@ -301,7 +298,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // because it means we have a bad assumption about how the // ESP BT stack works. ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_, - this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status); + this->address_str_, espbt::client_state_to_string(this->state_), param->open.status); } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); @@ -318,7 +315,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } // MTU negotiation already started in ESP_GATTC_CONNECT_EVT this->set_state(espbt::ClientState::CONNECTED); - ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str()); + ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { // Cached connections already connected with medium parameters, no update needed // only set our state, subclients might have more stuff to do yet. @@ -354,8 +351,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->state_ == espbt::ClientState::CONNECTED) { this->log_warning_("Remote closed during discovery"); } else { - ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, - this->address_str_.c_str(), param->disconnect.reason); + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_, + param->disconnect.reason); } this->release_services(); this->set_state(espbt::ClientState::IDLE); @@ -366,12 +363,12 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (this->conn_id_ != param->cfg_mtu.conn_id) return false; if (param->cfg_mtu.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, - this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status); + ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, this->address_str_, + param->cfg_mtu.mtu, param->cfg_mtu.status); // No state change required here - disconnect event will follow if needed. break; } - ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_, param->cfg_mtu.status, param->cfg_mtu.mtu); this->mtu_ = param->cfg_mtu.mtu; break; @@ -414,15 +411,20 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium"); } else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) { #ifdef USE_ESP32_BLE_DEVICE +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE for (auto &svc : this->services_) { - ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(), - svc->uuid.to_string().c_str()); - ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, - this->address_str_.c_str(), svc->start_handle, svc->end_handle); + char uuid_buf[espbt::UUID_STR_LEN]; + svc->uuid.to_str(uuid_buf); + ESP_LOGV(TAG, + "[%d] [%s] Service UUID: %s\n" + "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", + this->connection_index_, this->address_str_, uuid_buf, this->connection_index_, this->address_str_, + svc->start_handle, svc->end_handle); } +#endif #endif } - ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str()); + ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_); this->state_ = espbt::ClientState::ESTABLISHED; break; } @@ -503,7 +505,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ default: // ideally would check all other events for matching conn_id - ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_.c_str(), event); + ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event); break; } return true; @@ -520,22 +522,21 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_ case ESP_GAP_BLE_SEC_REQ_EVT: if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr)) return; - ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event); + ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_, event); esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); break; // This event is sent once authentication has completed case ESP_GAP_BLE_AUTH_CMPL_EVT: if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr)) return; - esp_bd_addr_t bd_addr; - memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); - ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_.c_str(), - format_hex(bd_addr, 6).c_str()); + char addr_str[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(param->ble_security.auth_cmpl.bd_addr, addr_str); + ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_, addr_str); if (!param->ble_security.auth_cmpl.success) { this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason); } else { this->paired_ = true; - ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_.c_str(), + ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_, param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode); } break; @@ -598,7 +599,7 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { } } ESP_LOGW(TAG, "[%d] [%s] Cannot parse characteristic value of type 0x%x length %d", this->connection_index_, - this->address_str_.c_str(), value[0], length); + this->address_str_, value[0], length); return NAN; } diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 7f0ae3b83e..92c7444ee1 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -10,7 +10,6 @@ #endif #include -#include #include #include @@ -58,14 +57,12 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { this->remote_bda_[4] = (address >> 8) & 0xFF; this->remote_bda_[5] = (address >> 0) & 0xFF; if (address == 0) { - this->address_str_ = ""; + this->address_str_[0] = '\0'; } else { - char buf[18]; - format_mac_addr_upper(this->remote_bda_, buf); - this->address_str_ = buf; + format_mac_addr_upper(this->remote_bda_, this->address_str_); } } - const std::string &address_str() const { return this->address_str_; } + const char *address_str() const { return this->address_str_; } #ifdef USE_ESP32_BLE_DEVICE BLEService *get_service(espbt::ESPBTUUID uuid); @@ -104,7 +101,6 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { uint64_t address_{0}; // Group 2: Container types (grouped for memory optimization) - std::string address_str_{}; #ifdef USE_ESP32_BLE_DEVICE std::vector services_; #endif @@ -113,8 +109,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { int gattc_if_; esp_gatt_status_t status_{ESP_GATT_OK}; - // Group 4: Arrays (6 bytes) - esp_bd_addr_t remote_bda_; + // Group 4: Arrays + char address_str_[MAC_ADDRESS_PRETTY_BUFFER_SIZE]{}; + esp_bd_addr_t remote_bda_; // 6 bytes // Group 5: 2-byte types uint16_t conn_id_{UNSET_CONN_ID}; diff --git a/esphome/components/esp32_ble_client/ble_service.cpp b/esphome/components/esp32_ble_client/ble_service.cpp index accaad15e1..695f468c5b 100644 --- a/esphome/components/esp32_ble_client/ble_service.cpp +++ b/esphome/components/esp32_ble_client/ble_service.cpp @@ -51,7 +51,7 @@ void BLEService::parse_characteristics() { } if (status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->client->get_connection_index(), - this->client->address_str().c_str(), status); + this->client->address_str(), status); break; } if (count == 0) { @@ -64,9 +64,12 @@ void BLEService::parse_characteristics() { characteristic->handle = result.char_handle; characteristic->service = this; this->characteristics.push_back(characteristic); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char uuid_buf[espbt::UUID_STR_LEN]; + characteristic->uuid.to_str(uuid_buf); ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(), - this->client->address_str().c_str(), characteristic->uuid.to_string().c_str(), characteristic->handle, - characteristic->properties); + this->client->address_str(), uuid_buf, characteristic->handle, characteristic->properties); +#endif offset++; } } diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index 7627a58338..0482848ea0 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -109,7 +109,11 @@ void BLECharacteristic::do_create(BLEService *service) { esp_attr_control_t control; control.auto_rsp = ESP_GATT_RSP_BY_APP; - ESP_LOGV(TAG, "Creating characteristic - %s", this->uuid_.to_string().c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char uuid_buf[esp32_ble::UUID_STR_LEN]; + this->uuid_.to_str(uuid_buf); + ESP_LOGV(TAG, "Creating characteristic - %s", uuid_buf); +#endif esp_bt_uuid_t uuid = this->uuid_.get_uuid(); esp_err_t err = esp_ble_gatts_add_char(service->get_handle(), &uuid, static_cast(this->permissions_), diff --git a/esphome/components/esp32_ble_server/ble_descriptor.cpp b/esphome/components/esp32_ble_server/ble_descriptor.cpp index 2d053c09bd..4ffca7312b 100644 --- a/esphome/components/esp32_ble_server/ble_descriptor.cpp +++ b/esphome/components/esp32_ble_server/ble_descriptor.cpp @@ -34,7 +34,11 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) { esp_attr_control_t control; control.auto_rsp = ESP_GATT_AUTO_RSP; - ESP_LOGV(TAG, "Creating descriptor - %s", this->uuid_.to_string().c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char uuid_buf[esp32_ble::UUID_STR_LEN]; + this->uuid_.to_str(uuid_buf); + ESP_LOGV(TAG, "Creating descriptor - %s", uuid_buf); +#endif esp_bt_uuid_t uuid = this->uuid_.get_uuid(); esp_err_t err = esp_ble_gatts_add_char_descr(this->characteristic_->get_service()->get_handle(), &uuid, this->permissions_, &this->value_, &control); diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 0e58224a5a..2c13a8ac36 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -106,7 +106,11 @@ void BLEServer::restart_advertising_() { } BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) { - ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char uuid_buf[esp32_ble::UUID_STR_LEN]; + uuid.to_str(uuid_buf); + ESP_LOGV(TAG, "Creating BLE service - %s", uuid_buf); +#endif // Calculate the inst_id for the service uint8_t inst_id = 0; for (; inst_id < 0xFF; inst_id++) { @@ -115,7 +119,9 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n } } if (inst_id == 0xFF) { - ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", uuid.to_string().c_str()); + char warn_uuid_buf[esp32_ble::UUID_STR_LEN]; + uuid.to_str(warn_uuid_buf); + ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", warn_uuid_buf); return nullptr; } BLEService *service = // NOLINT(cppcoreguidelines-owning-memory) @@ -128,7 +134,11 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n } void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) { - ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid.to_string().c_str(), inst_id); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char uuid_buf[esp32_ble::UUID_STR_LEN]; + uuid.to_str(uuid_buf); + ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid_buf, inst_id); +#endif for (auto it = this->services_.begin(); it != this->services_.end(); ++it) { if (it->uuid == uuid && it->inst_id == inst_id) { it->service->do_delete(); @@ -137,7 +147,9 @@ void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) { return; } } - ESP_LOGW(TAG, "BLE service %s %d does not exist", uuid.to_string().c_str(), inst_id); + char warn_uuid_buf[esp32_ble::UUID_STR_LEN]; + uuid.to_str(warn_uuid_buf); + ESP_LOGW(TAG, "BLE service %s %d does not exist", warn_uuid_buf, inst_id); } BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) { diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 4e25434aad..37e74672ed 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -5,7 +5,7 @@ import logging from esphome import automation import esphome.codegen as cg -from esphome.components import esp32_ble +from esphome.components import esp32_ble, ota from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32_ble import ( IDF_MAX_CONNECTIONS, @@ -328,7 +328,7 @@ async def to_code(config): # Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now # configured in esp32_ble component based on max_connections setting - cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts + ota.request_ota_state_listeners() # To be notified when an OTA update starts cg.add_define("USE_ESP32_BLE_CLIENT") CORE.add_job(_add_ble_features) diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index bbf7992fa4..6d26040ccb 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -98,7 +98,13 @@ template class ESP32BLEStartScanAction : public Action { TEMPLATABLE_VALUE(bool, continuous) void play(const Ts &...x) override { this->parent_->set_scan_continuous(this->continuous_.value(x...)); - this->parent_->start_scan(); + // Only call start_scan() if scanner is IDLE + // For other states (STARTING, RUNNING, STOPPING, FAILED), the normal state + // machine flow will eventually transition back to IDLE, at which point + // loop() will see scan_continuous_ and restart scanning if it is true. + if (this->parent_->get_scanner_state() == ScannerState::IDLE) { + this->parent_->start_scan(); + } } protected: diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 8577f12a92..995755ac84 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -37,6 +37,9 @@ namespace esphome::esp32_ble_tracker { static const char *const TAG = "esp32_ble_tracker"; +// BLE advertisement max: 31 bytes adv data + 31 bytes scan response +static constexpr size_t BLE_ADV_MAX_LOG_BYTES = 62; + ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) const char *client_state_to_string(ClientState state) { @@ -71,21 +74,24 @@ void ESP32BLETracker::setup() { global_esp32_ble_tracker = this; -#ifdef USE_OTA - ota::get_global_ota_callback()->add_on_state_callback( - [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { - if (state == ota::OTA_STARTED) { - this->stop_scan(); -#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT - for (auto *client : this->clients_) { - client->disconnect(); - } -#endif - } - }); +#ifdef USE_OTA_STATE_LISTENER + ota::get_global_ota_callback()->add_global_state_listener(this); #endif } +#ifdef USE_OTA_STATE_LISTENER +void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + this->stop_scan(); +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + for (auto *client : this->clients_) { + client->disconnect(); + } +#endif + } +} +#endif + void ESP32BLETracker::loop() { if (!this->parent_->is_active()) { this->ble_was_disabled_ = true; @@ -185,7 +191,10 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); void ESP32BLETracker::stop_scan_() { if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) { - ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_)); + // If scanner is already idle, there's nothing to stop - this is not an error + if (this->scanner_state_ != ScannerState::IDLE) { + ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_)); + } return; } // Reset timeout state machine when stopping scan @@ -373,7 +382,9 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i void ESP32BLETracker::set_scanner_state_(ScannerState state) { this->scanner_state_ = state; - this->scanner_state_callbacks_.call(state); + for (auto *listener : this->scanner_state_listeners_) { + listener->on_scanner_state(state); + } } #ifdef USE_ESP32_BLE_DEVICE @@ -433,29 +444,38 @@ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_); } for (auto &uuid : this->service_uuids_) { - ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); + char uuid_buf[esp32_ble::UUID_STR_LEN]; + uuid.to_str(uuid_buf); + ESP_LOGVV(TAG, " Service UUID: %s", uuid_buf); } + char hex_buf[format_hex_pretty_size(BLE_ADV_MAX_LOG_BYTES)]; for (auto &data : this->manufacturer_datas_) { auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data); if (ibeacon.has_value()) { ESP_LOGVV(TAG, " Manufacturer iBeacon:"); - ESP_LOGVV(TAG, " UUID: %s", ibeacon.value().get_uuid().to_string().c_str()); + char uuid_buf[esp32_ble::UUID_STR_LEN]; + ibeacon.value().get_uuid().to_str(uuid_buf); + ESP_LOGVV(TAG, " UUID: %s", uuid_buf); ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major()); ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor()); ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power()); } else { - ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", data.uuid.to_string().c_str(), - format_hex_pretty(data.data).c_str()); + char uuid_buf[esp32_ble::UUID_STR_LEN]; + data.uuid.to_str(uuid_buf); + ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf, + format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); } } for (auto &data : this->service_datas_) { ESP_LOGVV(TAG, " Service data:"); - ESP_LOGVV(TAG, " UUID: %s", data.uuid.to_string().c_str()); - ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str()); + char uuid_buf[esp32_ble::UUID_STR_LEN]; + data.uuid.to_str(uuid_buf); + ESP_LOGVV(TAG, " UUID: %s", uuid_buf); + ESP_LOGVV(TAG, " Data: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); } ESP_LOGVV(TAG, " Adv data: %s", - format_hex_pretty(scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len).c_str()); + format_hex_pretty_to(hex_buf, scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len)); #endif } @@ -619,9 +639,8 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { } std::string ESPBTDevice::address_str() const { - char mac[18]; - format_mac_addr_upper(this->address_, mac); - return mac; + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + return this->address_str_to(buf); } uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); } @@ -637,8 +656,10 @@ void ESP32BLETracker::dump_config() { " Continuous Scanning: %s", this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f, this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_)); - ESP_LOGCONFIG(TAG, " Scanner State: %s", this->scanner_state_to_string_(this->scanner_state_)); - ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting, + ESP_LOGCONFIG(TAG, + " Scanner State: %s\n" + " Connecting: %d, discovered: %d, disconnecting: %d", + this->scanner_state_to_string_(this->scanner_state_), this->client_state_counts_.connecting, this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); if (this->scan_start_fail_count_) { ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_); @@ -654,7 +675,8 @@ void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { } this->already_discovered_.push_back(address); - ESP_LOGD(TAG, "Found device %s RSSI=%d", device.address_str().c_str(), device.get_rssi()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGD(TAG, "Found device %s RSSI=%d", device.address_str_to(addr_buf), device.get_rssi()); const char *address_type_s; switch (device.get_address_type()) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index f80f3e2670..f538a0eddc 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -6,6 +6,7 @@ #include "esphome/core/helpers.h" #include +#include #include #include @@ -22,6 +23,10 @@ #include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_scan_result.h" +#ifdef USE_OTA_STATE_LISTENER +#include "esphome/components/ota/ota_backend.h" +#endif + namespace esphome::esp32_ble_tracker { using namespace esp32_ble; @@ -69,6 +74,12 @@ class ESPBTDevice { std::string address_str() const; + /// Format MAC address into provided buffer, returns pointer to buffer for convenience + const char *address_str_to(std::span buf) const { + format_mac_addr_upper(this->address_, buf.data()); + return buf.data(); + } + uint64_t address_uint64() const; const uint8_t *address() const { return address_; } @@ -180,6 +191,16 @@ enum class ScannerState { STOPPING, }; +/** Listener interface for BLE scanner state changes. + * + * Components can implement this interface to receive scanner state updates + * without the overhead of std::function callbacks. + */ +class BLEScannerStateListener { + public: + virtual void on_scanner_state(ScannerState state) = 0; +}; + // Helper function to convert ClientState to string const char *client_state_to_string(ClientState state); @@ -231,6 +252,9 @@ class ESP32BLETracker : public Component, public GAPScanEventHandler, public GATTcEventHandler, public BLEStatusEventHandler, +#ifdef USE_OTA_STATE_LISTENER + public ota::OTAGlobalStateListener, +#endif public Parented { public: void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } @@ -264,8 +288,13 @@ class ESP32BLETracker : public Component, void gap_scan_event_handler(const BLEScanResult &scan_result) override; void ble_before_disabled_event_handler() override; - void add_scanner_state_callback(std::function &&callback) { - this->scanner_state_callbacks_.add(std::move(callback)); +#ifdef USE_OTA_STATE_LISTENER + void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; +#endif + + /// Add a listener for scanner state changes + void add_scanner_state_listener(BLEScannerStateListener *listener) { + this->scanner_state_listeners_.push_back(listener); } ScannerState get_scanner_state() const { return this->scanner_state_; } @@ -322,14 +351,14 @@ class ESP32BLETracker : public Component, return counts; } - // Group 1: Large objects (12+ bytes) - vectors and callback manager + // Group 1: Large objects (12+ bytes) - vectors #ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT StaticVector listeners_; #endif #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT StaticVector clients_; #endif - CallbackManager scanner_state_callbacks_; + std::vector scanner_state_listeners_; #ifdef USE_ESP32_BLE_DEVICE /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index d9d9bc0a56..db6244fb3f 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,8 +2,8 @@ import logging from esphome import automation, pins import esphome.codegen as cg -from esphome.components import i2c -from esphome.components.esp32 import add_idf_component +from esphome.components import i2c, socket +from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option from esphome.components.psram import DOMAIN as psram_domain import esphome.config_validation as cv from esphome.const import ( @@ -27,7 +27,7 @@ import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) -AUTO_LOAD = ["camera"] +AUTO_LOAD = ["camera", "socket"] DEPENDENCIES = ["esp32"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") @@ -186,7 +186,7 @@ CONFIG_SCHEMA = cv.All( { cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( - cv.frequency, cv.Range(min=8e6, max=20e6) + cv.frequency, cv.float_range(min=8e6, max=20e6) ), } ), @@ -324,6 +324,7 @@ SETTERS = { async def to_code(config): cg.add_define("USE_CAMERA") + socket.require_wake_loop_threadsafe() var = cg.new_Pvariable(config[CONF_ID]) await setup_entity(var, config, "camera") await cg.register_component(var, config) @@ -352,6 +353,8 @@ async def to_code(config): cg.add_define("USE_CAMERA") add_idf_component(name="espressif/esp32-camera", ref="2.1.1") + add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True) + add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False) for conf in config.get(CONF_ON_STREAM_START, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 38bd8d5822..5466d2e7ef 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -11,6 +11,10 @@ namespace esphome { namespace esp32_camera { static const char *const TAG = "esp32_camera"; +static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792; +#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE +static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000; +#endif /* ---------------- public API (derivated) ---------------- */ void ESP32Camera::setup() { @@ -39,12 +43,12 @@ void ESP32Camera::setup() { this->framebuffer_get_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task, - "framebuffer_task", // name - 1024, // stack size - this, // task pv params - 1, // priority - nullptr, // handle - 1 // core + "framebuffer_task", // name + FRAMEBUFFER_TASK_STACK_SIZE, // stack size + this, // task pv params + 1, // priority + nullptr, // handle + 1 // core ); } @@ -164,6 +168,19 @@ void ESP32Camera::dump_config() { } void ESP32Camera::loop() { + // Fast path: skip all work when truly idle + // (no current image, no pending requests, and not time for idle request yet) + const uint32_t now = App.get_loop_component_start_time(); + if (!this->current_image_ && !this->has_requested_image_()) { + // Only check idle interval when we're otherwise idle + if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { + this->last_idle_request_ = now; + this->request_image(camera::IDLE); + } else { + return; + } + } + // check if we can return the image if (this->can_return_image_()) { // return image @@ -172,13 +189,6 @@ void ESP32Camera::loop() { this->current_image_.reset(); } - // request idle image every idle_update_interval - const uint32_t now = App.get_loop_component_start_time(); - if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { - this->last_idle_request_ = now; - this->request_image(camera::IDLE); - } - // Check if we should fetch a new image if (!this->has_requested_image_()) return; @@ -204,8 +214,23 @@ void ESP32Camera::loop() { } this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); - ESP_LOGD(TAG, "Got Image: len=%u", fb->len); - this->new_image_callback_.call(this->current_image_); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Got Image: len=%u", fb->len); +#else + // Initialize log time on first frame to ensure accurate interval measurement + if (this->frame_count_ == 0) { + this->last_log_time_ = now; + } + this->frame_count_++; + if (now - this->last_log_time_ >= FRAME_LOG_INTERVAL_MS) { + ESP_LOGD(TAG, "Received %u images in last %us", this->frame_count_, FRAME_LOG_INTERVAL_MS / 1000); + this->last_log_time_ = now; + this->frame_count_ = 0; + } +#endif + for (auto *listener : this->listeners_) { + listener->on_camera_image(this->current_image_); + } this->last_update_ = now; this->single_requesters_ = 0; } @@ -357,21 +382,16 @@ void ESP32Camera::set_frame_buffer_location(camera_fb_location_t fb_location) { } /* ---------------- public API (specific) ---------------- */ -void ESP32Camera::add_image_callback(std::function)> &&callback) { - this->new_image_callback_.add(std::move(callback)); -} -void ESP32Camera::add_stream_start_callback(std::function &&callback) { - this->stream_start_callback_.add(std::move(callback)); -} -void ESP32Camera::add_stream_stop_callback(std::function &&callback) { - this->stream_stop_callback_.add(std::move(callback)); -} void ESP32Camera::start_stream(camera::CameraRequester requester) { - this->stream_start_callback_.call(); + for (auto *listener : this->listeners_) { + listener->on_stream_start(); + } this->stream_requesters_ |= (1U << requester); } void ESP32Camera::stop_stream(camera::CameraRequester requester) { - this->stream_stop_callback_.call(); + for (auto *listener : this->listeners_) { + listener->on_stream_stop(); + } this->stream_requesters_ &= ~(1U << requester); } void ESP32Camera::request_image(camera::CameraRequester requester) { this->single_requesters_ |= (1U << requester); } @@ -408,6 +428,12 @@ void ESP32Camera::framebuffer_task(void *pv) { while (true) { camera_fb_t *framebuffer = esp_camera_fb_get(); xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); + // Only wake the main loop if there's a pending request to consume the frame +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + if (that->has_requested_image_()) { + App.wake_loop_threadsafe(); + } +#endif // return is no-op for config with 1 fb xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); esp_camera_fb_return(framebuffer); diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 0e7f7c0ea6..e97eb27c70 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -2,6 +2,7 @@ #ifdef USE_ESP32 +#include #include #include #include @@ -165,9 +166,8 @@ class ESP32Camera : public camera::Camera { void request_image(camera::CameraRequester requester) override; void update_camera_parameters(); - void add_image_callback(std::function)> &&callback) override; - void add_stream_start_callback(std::function &&callback); - void add_stream_stop_callback(std::function &&callback); + /// Add a listener to receive camera events + void add_listener(camera::CameraListener *listener) override { this->listeners_.push_back(listener); } camera::CameraImageReader *create_image_reader() override; protected: @@ -206,48 +206,44 @@ class ESP32Camera : public camera::Camera { esp_err_t init_error_{ESP_OK}; std::shared_ptr current_image_; - uint8_t single_requesters_{0}; - uint8_t stream_requesters_{0}; + std::atomic single_requesters_{0}; + std::atomic stream_requesters_{0}; QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; - CallbackManager)> new_image_callback_{}; - CallbackManager stream_start_callback_{}; - CallbackManager stream_stop_callback_{}; + std::vector listeners_; uint32_t last_idle_request_{0}; uint32_t last_update_{0}; +#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE + uint32_t last_log_time_{0}; + uint16_t frame_count_{0}; +#endif #ifdef USE_I2C i2c::InternalI2CBus *i2c_bus_{nullptr}; #endif // USE_I2C }; -class ESP32CameraImageTrigger : public Trigger { +class ESP32CameraImageTrigger : public Trigger, public camera::CameraListener { public: - explicit ESP32CameraImageTrigger(ESP32Camera *parent) { - parent->add_image_callback([this](const std::shared_ptr &image) { - CameraImageData camera_image_data{}; - camera_image_data.length = image->get_data_length(); - camera_image_data.data = image->get_data_buffer(); - this->trigger(camera_image_data); - }); + explicit ESP32CameraImageTrigger(ESP32Camera *parent) { parent->add_listener(this); } + void on_camera_image(const std::shared_ptr &image) override { + CameraImageData camera_image_data{}; + camera_image_data.length = image->get_data_length(); + camera_image_data.data = image->get_data_buffer(); + this->trigger(camera_image_data); } }; -class ESP32CameraStreamStartTrigger : public Trigger<> { +class ESP32CameraStreamStartTrigger : public Trigger<>, public camera::CameraListener { public: - explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { - parent->add_stream_start_callback([this]() { this->trigger(); }); - } - - protected: + explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { parent->add_listener(this); } + void on_stream_start() override { this->trigger(); } }; -class ESP32CameraStreamStopTrigger : public Trigger<> { - public: - explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) { - parent->add_stream_stop_callback([this]() { this->trigger(); }); - } - protected: +class ESP32CameraStreamStopTrigger : public Trigger<>, public camera::CameraListener { + public: + explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) { parent->add_listener(this); } + void on_stream_stop() override { this->trigger(); } }; } // namespace esp32_camera diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.cpp b/esphome/components/esp32_camera_web_server/camera_web_server.cpp index 1b81989296..f49578c425 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.cpp +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -67,12 +67,14 @@ void CameraWebServer::setup() { httpd_register_uri_handler(this->httpd_, &uri); - camera::Camera::instance()->add_image_callback([this](std::shared_ptr image) { - if (this->running_ && image->was_requested_by(camera::WEB_REQUESTER)) { - this->image_ = std::move(image); - xSemaphoreGive(this->semaphore_); - } - }); + camera::Camera::instance()->add_listener(this); +} + +void CameraWebServer::on_camera_image(const std::shared_ptr &image) { + if (this->running_ && image->was_requested_by(camera::WEB_REQUESTER)) { + this->image_ = image; + xSemaphoreGive(this->semaphore_); + } } void CameraWebServer::on_shutdown() { diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.h b/esphome/components/esp32_camera_web_server/camera_web_server.h index e70246745c..ad7b29fb11 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.h +++ b/esphome/components/esp32_camera_web_server/camera_web_server.h @@ -18,7 +18,7 @@ namespace esp32_camera_web_server { enum Mode { STREAM, SNAPSHOT }; -class CameraWebServer : public Component { +class CameraWebServer : public Component, public camera::CameraListener { public: CameraWebServer(); ~CameraWebServer(); @@ -31,6 +31,9 @@ class CameraWebServer : public Component { void set_mode(Mode mode) { this->mode_ = mode; } void loop() override; + /// CameraListener interface + void on_camera_image(const std::shared_ptr &image) override; + protected: std::shared_ptr wait_for_image_(); esp_err_t handler_(struct httpd_req *req); diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py index dfa98b2eff..0899a0dc2b 100644 --- a/esphome/components/esp32_can/canbus.py +++ b/esphome/components/esp32_can/canbus.py @@ -4,14 +4,17 @@ from esphome import pins import esphome.codegen as cg from esphome.components import canbus from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed -from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import ( +from esphome.components.esp32 import ( VARIANT_ESP32, VARIANT_ESP32C3, + VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + get_esp32_variant, ) import esphome.config_validation as cv from esphome.const import ( @@ -57,16 +60,22 @@ CAN_SPEEDS_ESP32_S2 = { CAN_SPEEDS_ESP32_S3 = {**CAN_SPEEDS_ESP32_S2} CAN_SPEEDS_ESP32_C3 = {**CAN_SPEEDS_ESP32_S2} +CAN_SPEEDS_ESP32_C5 = {**CAN_SPEEDS_ESP32_S2} CAN_SPEEDS_ESP32_C6 = {**CAN_SPEEDS_ESP32_S2} +CAN_SPEEDS_ESP32_C61 = {**CAN_SPEEDS_ESP32_S2} CAN_SPEEDS_ESP32_H2 = {**CAN_SPEEDS_ESP32_S2} +CAN_SPEEDS_ESP32_P4 = {**CAN_SPEEDS_ESP32_S2} CAN_SPEEDS = { VARIANT_ESP32: CAN_SPEEDS_ESP32, + VARIANT_ESP32C3: CAN_SPEEDS_ESP32_C3, + VARIANT_ESP32C5: CAN_SPEEDS_ESP32_C5, + VARIANT_ESP32C6: CAN_SPEEDS_ESP32_C6, + VARIANT_ESP32C61: CAN_SPEEDS_ESP32_C61, + VARIANT_ESP32H2: CAN_SPEEDS_ESP32_H2, + VARIANT_ESP32P4: CAN_SPEEDS_ESP32_P4, VARIANT_ESP32S2: CAN_SPEEDS_ESP32_S2, VARIANT_ESP32S3: CAN_SPEEDS_ESP32_S3, - VARIANT_ESP32C3: CAN_SPEEDS_ESP32_C3, - VARIANT_ESP32C6: CAN_SPEEDS_ESP32_C6, - VARIANT_ESP32H2: CAN_SPEEDS_ESP32_H2, } diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index cdef7b1930..d50964187d 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -16,8 +16,9 @@ static const char *const TAG = "esp32_can"; static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config) { switch (bitrate) { -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3) || \ - defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \ + defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) case canbus::CAN_1KBPS: *t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_1KBITS(); return true; diff --git a/esphome/components/esp32_dac/output.py b/esphome/components/esp32_dac/output.py index cf4f12c46d..daace596d3 100644 --- a/esphome/components/esp32_dac/output.py +++ b/esphome/components/esp32_dac/output.py @@ -1,8 +1,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import output -from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import VARIANT_ESP32, VARIANT_ESP32S2 +from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index fde75517eb..9c9d1d4bb4 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -93,9 +93,9 @@ async def to_code(config): framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" if framework_ver >= cv.Version(5, 5, 0): - esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5") + esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.2") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3") - esp32.add_idf_component(name="espressif/esp_hosted", ref="2.6.1") + esp32.add_idf_component(name="espressif/esp_hosted", ref="2.7.0") else: esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") diff --git a/esphome/components/esp32_hosted/update/__init__.py b/esphome/components/esp32_hosted/update/__init__.py index 040f989a64..fff0d3623a 100644 --- a/esphome/components/esp32_hosted/update/__init__.py +++ b/esphome/components/esp32_hosted/update/__init__.py @@ -40,8 +40,8 @@ CONFIG_SCHEMA = cv.All( ), esp32.only_on_variant( supported=[ - esp32.const.VARIANT_ESP32H2, - esp32.const.VARIANT_ESP32P4, + esp32.VARIANT_ESP32H2, + esp32.VARIANT_ESP32P4, ] ), ) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index adbcc5bf11..3598a2e69c 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -22,6 +22,11 @@ constexpr size_t CHUNK_SIZE = 1500; void Esp32HostedUpdate::setup() { this->update_info_.title = "ESP32 Hosted Coprocessor"; + // if wifi is not present, connect to the coprocessor +#ifndef USE_WIFI + esp_hosted_connect_to_slave(); // NOLINT +#endif + // get coprocessor version esp_hosted_coprocessor_fwver_t ver_info; if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { @@ -36,11 +41,13 @@ void Esp32HostedUpdate::setup() { if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) { esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset); if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) { - ESP_LOGD(TAG, "Firmware version: %s", app_desc->version); - ESP_LOGD(TAG, "Project name: %s", app_desc->project_name); - ESP_LOGD(TAG, "Build date: %s", app_desc->date); - ESP_LOGD(TAG, "Build time: %s", app_desc->time); - ESP_LOGD(TAG, "IDF version: %s", app_desc->idf_ver); + ESP_LOGD(TAG, + "Firmware version: %s\n" + "Project name: %s\n" + "Build date: %s\n" + "Build time: %s\n" + "IDF version: %s", + app_desc->version, app_desc->project_name, app_desc->date, app_desc->time, app_desc->idf_ver); this->update_info_.latest_version = app_desc->version; if (this->update_info_.latest_version != this->update_info_.current_version) { this->state_ = update::UPDATE_STATE_AVAILABLE; @@ -83,12 +90,13 @@ void Esp32HostedUpdate::perform(bool force) { return; } - sha256::SHA256 hasher; + // ESP32-S3 hardware SHA acceleration requires 32-byte DMA alignment (IDF 5.5.x+) + alignas(32) sha256::SHA256 hasher; hasher.init(); hasher.add(this->firmware_data_, this->firmware_size_); hasher.calculate(); if (!hasher.equals_bytes(this->firmware_sha256_.data())) { - this->status_set_error("SHA256 verification failed"); + this->status_set_error(LOG_STR("SHA256 verification failed")); this->publish_state(); return; } @@ -105,7 +113,7 @@ void Esp32HostedUpdate::perform(bool force) { if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err)); this->state_ = prev_state; - this->status_set_error("Failed to begin OTA"); + this->status_set_error(LOG_STR("Failed to begin OTA")); this->publish_state(); return; } @@ -121,7 +129,7 @@ void Esp32HostedUpdate::perform(bool force) { ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err)); esp_hosted_slave_ota_end(); // NOLINT this->state_ = prev_state; - this->status_set_error("Failed to write OTA data"); + this->status_set_error(LOG_STR("Failed to write OTA data")); this->publish_state(); return; } @@ -134,7 +142,7 @@ void Esp32HostedUpdate::perform(bool force) { if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err)); this->state_ = prev_state; - this->status_set_error("Failed to end OTA"); + this->status_set_error(LOG_STR("Failed to end OTA")); this->publish_state(); return; } @@ -144,7 +152,7 @@ void Esp32HostedUpdate::perform(bool force) { if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err)); this->state_ = prev_state; - this->status_set_error("Failed to activate OTA"); + this->status_set_error(LOG_STR("Failed to activate OTA")); this->publish_state(); return; } diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 1a7194da81..ad2f057163 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -3,7 +3,7 @@ import esphome.codegen as cg from esphome.components import binary_sensor, esp32_ble, improv_base, output from esphome.components.esp32_ble import BTLoggers import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_START, CONF_ON_STATE, CONF_TRIGGER_ID AUTO_LOAD = ["esp32_ble_server", "improv_base"] CODEOWNERS = ["@jesserockz"] @@ -15,11 +15,14 @@ CONF_BLE_SERVER_ID = "ble_server_id" CONF_IDENTIFY_DURATION = "identify_duration" CONF_ON_PROVISIONED = "on_provisioned" CONF_ON_PROVISIONING = "on_provisioning" -CONF_ON_START = "on_start" CONF_ON_STOP = "on_stop" CONF_STATUS_INDICATOR = "status_indicator" CONF_WIFI_TIMEOUT = "wifi_timeout" +# Default WiFi timeout - aligned with WiFi component ap_timeout +# Allows sufficient time to try all BSSIDs before starting provisioning mode +DEFAULT_WIFI_TIMEOUT = "90s" + improv_ns = cg.esphome_ns.namespace("improv") Error = improv_ns.enum("Error") @@ -59,7 +62,7 @@ CONFIG_SCHEMA = ( CONF_AUTHORIZED_DURATION, default="1min" ): cv.positive_time_period_milliseconds, cv.Optional( - CONF_WIFI_TIMEOUT, default="1min" + CONF_WIFI_TIMEOUT, default=DEFAULT_WIFI_TIMEOUT ): cv.positive_time_period_milliseconds, cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( { diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 398b1d4251..1a19472c87 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -4,6 +4,7 @@ #include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble_server/ble_2902.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -14,6 +15,7 @@ namespace esp32_improv { using namespace bytebuffer; static const char *const TAG = "esp32_improv.component"; +static constexpr size_t IMPROV_MAX_LOG_BYTES = 128; static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; static constexpr uint16_t STOP_ADVERTISING_DELAY = 10000; // Delay (ms) before stopping service to allow BLE clients to read the final state @@ -127,6 +129,7 @@ void ESP32ImprovComponent::loop() { // Set initial state based on whether we have an authorizer this->set_state_(this->get_initial_state_(), false); this->set_error_(improv::ERROR_NONE); + this->should_start_ = false; // Clear flag after starting ESP_LOGD(TAG, "Service started!"); } } @@ -313,7 +316,11 @@ void ESP32ImprovComponent::dump_config() { void ESP32ImprovComponent::process_incoming_data_() { uint8_t length = this->incoming_data_[1]; - ESP_LOGV(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(IMPROV_MAX_LOG_BYTES)]; + ESP_LOGV(TAG, "Processing bytes - %s", + format_hex_pretty_to(hex_buf, this->incoming_data_.data(), this->incoming_data_.size())); +#endif if (this->incoming_data_.size() - 3 == length) { this->set_error_(improv::ERROR_NONE); improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_); @@ -391,9 +398,12 @@ void ESP32ImprovComponent::check_wifi_connection_() { #ifdef USE_ESP32_IMPROV_NEXT_URL // Add next_url if configured (should be first per Improv BLE spec) - std::string next_url = this->get_formatted_next_url_(); - if (!next_url.empty()) { - url_strings[url_count++] = std::move(next_url); + { + char url_buffer[384]; + size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer)); + if (len > 0) { + url_strings[url_count++] = std::string(url_buffer, len); + } } #endif @@ -402,8 +412,12 @@ void ESP32ImprovComponent::check_wifi_connection_() { #ifdef USE_WEBSERVER for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { if (ip.is_ip4()) { - char url_buffer[64]; - snprintf(url_buffer, sizeof(url_buffer), "http://%s:%d", ip.str().c_str(), USE_WEBSERVER_PORT); + // "http://" (7) + IPv4 max (15) + ":" (1) + port max (5) + null = 29 + char url_buffer[32]; + memcpy(url_buffer, "http://", 7); // NOLINT(bugprone-not-null-terminated-result) - str_to null-terminates + ip.str_to(url_buffer + 7); + size_t len = strlen(url_buffer); + snprintf(url_buffer + len, sizeof(url_buffer) - len, ":%d", USE_WEBSERVER_PORT); url_strings[url_count++] = url_buffer; break; } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 989552ea56..8f4cfd7958 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -45,6 +45,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { void start(); void stop(); bool is_active() const { return this->state_ != improv::STATE_STOPPED; } + bool should_start() const { return this->should_start_; } #ifdef USE_ESP32_IMPROV_STATE_CALLBACK void add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/esp32_rmt/__init__.py b/esphome/components/esp32_rmt/__init__.py index 1e72185e3e..272c7c81ba 100644 --- a/esphome/components/esp32_rmt/__init__.py +++ b/esphome/components/esp32_rmt/__init__.py @@ -9,7 +9,7 @@ def validate_clock_resolution(): cv.only_on_esp32(value) value = cv.int_(value) variant = esp32.get_esp32_variant() - if variant == esp32.const.VARIANT_ESP32H2 and value > 32000000: + if variant == esp32.VARIANT_ESP32H2 and value > 32000000: raise cv.Invalid( f"ESP32 variant {variant} has a max clock_resolution of 32000000." ) diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 2c7963b366..4ca0b998b1 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -98,7 +98,7 @@ void ESP32RMTLEDStripLightOutput::setup() { channel.trans_queue_depth = 1; channel.flags.io_loop_back = 0; channel.flags.io_od_mode = 0; - channel.flags.invert_out = 0; + channel.flags.invert_out = this->invert_out_; channel.flags.with_dma = this->use_dma_; channel.intr_priority = 0; if (rmt_new_tx_channel(&channel, &this->channel_) != ESP_OK) { diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h index 72ce659b4f..6f3aea9878 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.h +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -49,6 +49,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { } void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_inverted(bool inverted) { this->invert_out_ = inverted; } void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; } void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; } @@ -93,6 +94,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { bool is_wrgb_{false}; bool use_dma_{false}; bool use_psram_{false}; + bool invert_out_{false}; RGBOrder rgb_order_{ORDER_RGB}; diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index ac4f0b2e92..3be3c758f1 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -8,9 +8,11 @@ from esphome.components.const import CONF_USE_PSRAM import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, + CONF_INVERTED, CONF_IS_RGBW, CONF_MAX_REFRESH_RATE, CONF_NUM_LEDS, + CONF_NUMBER, CONF_OUTPUT_ID, CONF_PIN, CONF_RGB_ORDER, @@ -71,19 +73,19 @@ CONFIG_SCHEMA = cv.All( light.ADDRESSABLE_LIGHT_SCHEMA.extend( { cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), - cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), cv.SplitDefault( CONF_RMT_SYMBOLS, esp32=192, - esp32_s2=192, - esp32_s3=192, - esp32_p4=192, esp32_c3=96, esp32_c5=96, esp32_c6=96, esp32_h2=96, + esp32_p4=192, + esp32_s2=192, + esp32_s3=192, ): cv.int_range(min=2), cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), @@ -91,7 +93,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_IS_WRGB, default=False): cv.boolean, cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( - supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4] + supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] ), cv.boolean, ), @@ -132,7 +134,9 @@ async def to_code(config): await cg.register_component(var, config) cg.add(var.set_num_leds(config[CONF_NUM_LEDS])) - cg.add(var.set_pin(config[CONF_PIN])) + cg.add(var.set_pin(config[CONF_PIN][CONF_NUMBER])) + if config[CONF_PIN][CONF_INVERTED]: + cg.add(var.set_inverted(True)) if CONF_MAX_REFRESH_RATE in config: cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index b6cb19ebb1..c54ed8b9ea 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -1,10 +1,11 @@ import esphome.codegen as cg from esphome.components import esp32 -from esphome.components.esp32 import get_esp32_variant, gpio -from esphome.components.esp32.const import ( +from esphome.components.esp32 import ( VARIANT_ESP32, VARIANT_ESP32S2, VARIANT_ESP32S3, + get_esp32_variant, + gpio, ) import esphome.config_validation as cv from esphome.const import ( @@ -255,9 +256,9 @@ CONFIG_SCHEMA = cv.All( cv.has_none_or_all_keys(CONF_WATERPROOF_GUARD_RING, CONF_WATERPROOF_SHIELD_DRIVER), esp32.only_on_variant( supported=[ - esp32.const.VARIANT_ESP32, - esp32.const.VARIANT_ESP32S2, - esp32.const.VARIANT_ESP32S3, + esp32.VARIANT_ESP32, + esp32.VARIANT_ESP32S2, + esp32.VARIANT_ESP32S3, ] ), validate_variant_vars, diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index a74f9ee8ce..c7b5d5c130 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -23,11 +23,18 @@ from esphome.helpers import copy_file_if_changed from .boards import BOARDS, ESP8266_LD_SCRIPTS from .const import ( CONF_EARLY_PIN_INIT, + CONF_ENABLE_SERIAL, + CONF_ENABLE_SERIAL1, CONF_RESTORE_FROM_FLASH, KEY_BOARD, KEY_ESP8266, KEY_FLASH_SIZE, KEY_PIN_INITIAL_STATES, + KEY_SERIAL1_REQUIRED, + KEY_SERIAL_REQUIRED, + KEY_WAVEFORM_REQUIRED, + enable_serial, + enable_serial1, esp8266_ns, ) from .gpio import PinInitialState, add_pin_initial_states_array @@ -170,6 +177,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of( *BUILD_FLASH_MODES, lower=True ), + cv.Optional(CONF_ENABLE_SERIAL): cv.boolean, + cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean, } ), set_core_data, @@ -191,7 +200,13 @@ async def to_code(config): cg.add_define(ThreadModel.SINGLE) cg.add_platformio_option( - "extra_scripts", ["pre:testing_mode.py", "post:post_build.py"] + "extra_scripts", + [ + "pre:testing_mode.py", + "pre:exclude_updater.py", + "pre:exclude_waveform.py", + "post:post_build.py", + ], ) conf = config[CONF_FRAMEWORK] @@ -224,6 +239,12 @@ async def to_code(config): if config[CONF_EARLY_PIN_INIT]: cg.add_define("USE_ESP8266_EARLY_PIN_INIT") + # Allow users to force-enable Serial objects for use in lambdas or external libraries + if config.get(CONF_ENABLE_SERIAL): + enable_serial() + if config.get(CONF_ENABLE_SERIAL1): + enable_serial1() + # Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when # out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make # new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of @@ -263,10 +284,43 @@ async def to_code(config): cg.add_platformio_option("board_build.ldscript", ld_script) CORE.add_job(add_pin_initial_states_array) + CORE.add_job(finalize_waveform_config) + CORE.add_job(finalize_serial_config) + + +@coroutine_with_priority(CoroPriority.WORKAROUNDS) +async def finalize_waveform_config() -> None: + """Add waveform stubs define if waveform is not required. + + This runs at WORKAROUNDS priority (-999) to ensure all components + have had a chance to call require_waveform() first. + """ + if not CORE.data.get(KEY_ESP8266, {}).get(KEY_WAVEFORM_REQUIRED, False): + # No component needs waveform - enable stubs and exclude Arduino waveform code + # Use build flag (visible to both C++ code and PlatformIO script) + cg.add_build_flag("-DUSE_ESP8266_WAVEFORM_STUBS") + + +@coroutine_with_priority(CoroPriority.WORKAROUNDS) +async def finalize_serial_config() -> None: + """Exclude unused Arduino Serial objects from the build. + + This runs at WORKAROUNDS priority (-999) to ensure all components + have had a chance to call enable_serial() or enable_serial1() first. + + The Arduino ESP8266 core defines two global Serial objects (32 bytes each). + By adding NO_GLOBAL_SERIAL or NO_GLOBAL_SERIAL1 build flags, we prevent + unused Serial objects from being linked, saving 32 bytes each. + """ + esp8266_data = CORE.data.get(KEY_ESP8266, {}) + if not esp8266_data.get(KEY_SERIAL_REQUIRED, False): + cg.add_build_flag("-DNO_GLOBAL_SERIAL") + if not esp8266_data.get(KEY_SERIAL1_REQUIRED, False): + cg.add_build_flag("-DNO_GLOBAL_SERIAL1") # Called by writer.py -def copy_files(): +def copy_files() -> None: dir = Path(__file__).parent post_build_file = dir / "post_build.py.script" copy_file_if_changed( @@ -278,3 +332,13 @@ def copy_files(): testing_mode_file, CORE.relative_build_path("testing_mode.py"), ) + exclude_updater_file = dir / "exclude_updater.py.script" + copy_file_if_changed( + exclude_updater_file, + CORE.relative_build_path("exclude_updater.py"), + ) + exclude_waveform_file = dir / "exclude_waveform.py.script" + copy_file_if_changed( + exclude_waveform_file, + CORE.relative_build_path("exclude_waveform.py"), + ) diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index b718306b01..229ac61f24 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -1,11 +1,67 @@ import esphome.codegen as cg +from esphome.core import CORE KEY_ESP8266 = "esp8266" KEY_BOARD = "board" KEY_PIN_INITIAL_STATES = "pin_initial_states" CONF_RESTORE_FROM_FLASH = "restore_from_flash" CONF_EARLY_PIN_INIT = "early_pin_init" +CONF_ENABLE_SERIAL = "enable_serial" +CONF_ENABLE_SERIAL1 = "enable_serial1" KEY_FLASH_SIZE = "flash_size" +KEY_WAVEFORM_REQUIRED = "waveform_required" +KEY_SERIAL_REQUIRED = "serial_required" +KEY_SERIAL1_REQUIRED = "serial1_required" # esp8266 namespace is already defined by arduino, manually prefix esphome esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266") + + +def require_waveform() -> None: + """Mark that Arduino waveform/PWM support is required. + + Call this from components that need the Arduino waveform generator + (startWaveform, stopWaveform, analogWrite, Tone, Servo). + + If no component calls this, the waveform code is excluded from the build + to save ~596 bytes of RAM and 464 bytes of flash. + + Example: + from esphome.components.esp8266.const import require_waveform + + async def to_code(config): + require_waveform() + """ + CORE.data.setdefault(KEY_ESP8266, {})[KEY_WAVEFORM_REQUIRED] = True + + +def enable_serial() -> None: + """Mark that Arduino Serial (UART0) is required. + + Call this from components that use the global Serial object. + If no component calls this, Serial is excluded from the build + to save 32 bytes of RAM. + + Example: + from esphome.components.esp8266.const import enable_serial + + async def to_code(config): + enable_serial() + """ + CORE.data.setdefault(KEY_ESP8266, {})[KEY_SERIAL_REQUIRED] = True + + +def enable_serial1() -> None: + """Mark that Arduino Serial1 (UART1) is required. + + Call this from components that use the global Serial1 object. + If no component calls this, Serial1 is excluded from the build + to save 32 bytes of RAM. + + Example: + from esphome.components.esp8266.const import enable_serial1 + + async def to_code(config): + enable_serial1() + """ + CORE.data.setdefault(KEY_ESP8266, {})[KEY_SERIAL1_REQUIRED] = True diff --git a/esphome/components/esp8266/core.h b/esphome/components/esp8266/core.h index ac33305669..1abe67be86 100644 --- a/esphome/components/esp8266/core.h +++ b/esphome/components/esp8266/core.h @@ -7,8 +7,6 @@ extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16]; extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16]; -namespace esphome { -namespace esp8266 {} // namespace esp8266 -} // namespace esphome +namespace esphome::esp8266 {} // namespace esphome::esp8266 #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/exclude_updater.py.script b/esphome/components/esp8266/exclude_updater.py.script new file mode 100644 index 0000000000..69331e3b03 --- /dev/null +++ b/esphome/components/esp8266/exclude_updater.py.script @@ -0,0 +1,21 @@ +# pylint: disable=E0602 +Import("env") # noqa + +import os + +# Filter out Updater.cpp from the Arduino core build +# This saves 228 bytes of .bss by not instantiating the global Update object +# ESPHome uses its own native OTA backend instead + + +def filter_updater_from_core(env, node): + """Filter callback to exclude Updater.cpp from framework build.""" + path = node.get_path() + if path.endswith("Updater.cpp"): + print(f"ESPHome: Excluding {os.path.basename(path)} from build (using native OTA backend)") + return None + return node + + +# Apply the filter to framework sources +env.AddBuildMiddleware(filter_updater_from_core, "**/cores/esp8266/Updater.cpp") diff --git a/esphome/components/esp8266/exclude_waveform.py.script b/esphome/components/esp8266/exclude_waveform.py.script new file mode 100644 index 0000000000..35d6bc31f6 --- /dev/null +++ b/esphome/components/esp8266/exclude_waveform.py.script @@ -0,0 +1,50 @@ +# pylint: disable=E0602 +Import("env") # noqa + +import os + +# Filter out waveform/PWM code from the Arduino core build +# This saves ~596 bytes of RAM and 464 bytes of flash by not +# instantiating the waveform generator state structures (wvfState + pwmState). +# +# The waveform code is used by: analogWrite, Tone, Servo, and direct +# startWaveform/stopWaveform calls. ESPHome's esp8266_pwm component +# calls require_waveform() to keep this code when needed. +# +# When excluded, we provide stub implementations of stopWaveform() and +# _stopPWM() since digitalWrite() calls these unconditionally. + + +def has_define_flag(env, name): + """Check if a define exists in the build flags.""" + define_flag = f"-D{name}" + # Check BUILD_FLAGS (where ESPHome puts its defines) + for flag in env.get("BUILD_FLAGS", []): + if flag == define_flag or flag.startswith(f"{define_flag}="): + return True + # Also check CPPDEFINES list (parsed defines) + for define in env.get("CPPDEFINES", []): + if isinstance(define, tuple): + if define[0] == name: + return True + elif define == name: + return True + return False + +# USE_ESP8266_WAVEFORM_STUBS is defined when no component needs waveform +if has_define_flag(env, "USE_ESP8266_WAVEFORM_STUBS"): + + def filter_waveform_from_core(env, node): + """Filter callback to exclude waveform files from framework build.""" + path = node.get_path() + filename = os.path.basename(path) + if filename in ( + "core_esp8266_waveform_pwm.cpp", + "core_esp8266_waveform_phase.cpp", + ): + print(f"ESPHome: Excluding {filename} from build (waveform not required)") + return None + return node + + # Apply the filter to framework sources + env.AddBuildMiddleware(filter_waveform_from_core, "**/cores/esp8266/*.cpp") diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index ee3683c67d..7a5ee08984 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -3,17 +3,18 @@ #include "gpio.h" #include "esphome/core/log.h" -namespace esphome { -namespace esp8266 { +namespace esphome::esp8266 { static const char *const TAG = "esp8266"; static int flags_to_mode(gpio::Flags flags, uint8_t pin) { - if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone) - return INPUT; - } else if (flags == gpio::FLAG_OUTPUT) { + if (flags == gpio::FLAG_OUTPUT || flags == (gpio::FLAG_OUTPUT | gpio::FLAG_INPUT)) { return OUTPUT; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + } + if (flags == gpio::FLAG_INPUT) { + return INPUT; + } + if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { if (pin == 16) { // GPIO16 doesn't have a pullup, so pinMode would fail. // However, sometimes this method is called with pullup mode anyway @@ -22,13 +23,14 @@ static int flags_to_mode(gpio::Flags flags, uint8_t pin) { return INPUT; } return INPUT_PULLUP; - } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { - return INPUT_PULLDOWN_16; - } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { - return OUTPUT_OPEN_DRAIN; - } else { - return 0; } + if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { + return INPUT_PULLDOWN_16; + } + if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { + return OUTPUT_OPEN_DRAIN; + } + return INPUT; } struct ISRPinArg { @@ -96,10 +98,8 @@ void ESP8266GPIOPin::pin_mode(gpio::Flags flags) { pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT } -std::string ESP8266GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); - return buffer; +size_t ESP8266GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "GPIO%u", this->pin_); } bool ESP8266GPIOPin::digital_read() { @@ -110,9 +110,11 @@ void ESP8266GPIOPin::digital_write(bool value) { } void ESP8266GPIOPin::detach_interrupt() const { detachInterrupt(pin_); } -} // namespace esp8266 +} // namespace esphome::esp8266 -using namespace esp8266; +namespace esphome { + +using esp8266::ISRPinArg; bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { auto *arg = reinterpret_cast(this->arg_); diff --git a/esphome/components/esp8266/gpio.h b/esphome/components/esp8266/gpio.h index a1b6d79b3b..ff149abfbe 100644 --- a/esphome/components/esp8266/gpio.h +++ b/esphome/components/esp8266/gpio.h @@ -5,8 +5,7 @@ #include "esphome/core/hal.h" #include -namespace esphome { -namespace esp8266 { +namespace esphome::esp8266 { class ESP8266GPIOPin : public InternalGPIOPin { public: @@ -18,7 +17,7 @@ class ESP8266GPIOPin : public InternalGPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return pin_; } @@ -33,7 +32,6 @@ class ESP8266GPIOPin : public InternalGPIOPin { gpio::Flags flags_{}; }; -} // namespace esp8266 -} // namespace esphome +} // namespace esphome::esp8266 #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index a26e9cc498..47987b4a95 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -1,7 +1,6 @@ #ifdef USE_ESP8266 #include -#include extern "C" { #include "spi_flash.h" } @@ -15,24 +14,34 @@ extern "C" { #include #include -namespace esphome { -namespace esp8266 { +namespace esphome::esp8266 { static const char *const TAG = "esp8266.preferences"; -static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static const uint32_t ESP_RTC_USER_MEM_START = 0x60001200; +static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; +static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; +static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; + +// RTC memory layout for preferences: +// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 96-127) +// - Normal region: RTC words 32-127 (mapped from preference offset 0-95) +static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot +static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 96; // Words 32-127 for normal prefs +static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 128 + +// Maximum preference size in words (limited by uint8_t length_words field) +static constexpr uint32_t MAX_PREFERENCE_WORDS = 255; + #define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) -static const uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; -static const uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; #ifdef USE_ESP8266_PREFERENCES_FLASH -static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; +static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; #else -static const uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; +static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; #endif static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { @@ -118,6 +127,10 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) { return true; } +// Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data) +// This handles virtually all real-world preferences without heap allocation +static constexpr size_t PREF_BUFFER_WORDS = 16; + class ESP8266PreferenceBackend : public ESPPreferenceBackend { public: uint32_t type = 0; @@ -126,36 +139,54 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend { bool in_flash = false; bool save(const uint8_t *data, size_t len) override { - if (bytes_to_words(len) != length_words) { + if (bytes_to_words(len) != this->length_words) return false; - } - size_t buffer_size = static_cast(length_words) + 1; - std::unique_ptr buffer(new uint32_t[buffer_size]()); // Note the () for zero-initialization - memcpy(buffer.get(), data, len); - buffer[length_words] = calculate_crc(buffer.get(), buffer.get() + length_words, type); - if (in_flash) { - return save_to_flash(offset, buffer.get(), buffer_size); + const size_t buffer_size = static_cast(this->length_words) + 1; + uint32_t stack_buffer[PREF_BUFFER_WORDS]; + std::unique_ptr heap_buffer; + uint32_t *buffer; + + if (buffer_size <= PREF_BUFFER_WORDS) { + buffer = stack_buffer; + } else { + heap_buffer = make_unique(buffer_size); + buffer = heap_buffer.get(); } - return save_to_rtc(offset, buffer.get(), buffer_size); + memset(buffer, 0, buffer_size * sizeof(uint32_t)); + + memcpy(buffer, data, len); + buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type); + + return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size) + : save_to_rtc(this->offset, buffer, buffer_size); } + bool load(uint8_t *data, size_t len) override { - if (bytes_to_words(len) != length_words) { + if (bytes_to_words(len) != this->length_words) return false; + + const size_t buffer_size = static_cast(this->length_words) + 1; + uint32_t stack_buffer[PREF_BUFFER_WORDS]; + std::unique_ptr heap_buffer; + uint32_t *buffer; + + if (buffer_size <= PREF_BUFFER_WORDS) { + buffer = stack_buffer; + } else { + heap_buffer = make_unique(buffer_size); + buffer = heap_buffer.get(); } - size_t buffer_size = static_cast(length_words) + 1; - std::unique_ptr buffer(new uint32_t[buffer_size]()); - bool ret = in_flash ? load_from_flash(offset, buffer.get(), buffer_size) - : load_from_rtc(offset, buffer.get(), buffer_size); + + bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size) + : load_from_rtc(this->offset, buffer, buffer_size); if (!ret) return false; - uint32_t crc = calculate_crc(buffer.get(), buffer.get() + length_words, type); - if (buffer[length_words] != crc) { + if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type)) return false; - } - memcpy(data, buffer.get(), len); + memcpy(data, buffer, len); return true; } }; @@ -176,50 +207,42 @@ class ESP8266Preferences : public ESPPreferences { } ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - uint32_t length_words = bytes_to_words(length); - if (length_words > 255) { - ESP_LOGE(TAG, "Preference too large: %" PRIu32 " words > 255", length_words); + const uint32_t length_words = bytes_to_words(length); + if (length_words > MAX_PREFERENCE_WORDS) { + ESP_LOGE(TAG, "Preference too large: %u words", static_cast(length_words)); return {}; } + + const uint32_t total_words = length_words + 1; // +1 for CRC + uint16_t offset; + if (in_flash) { - uint32_t start = current_flash_offset; - uint32_t end = start + length_words + 1; - if (end > ESP8266_FLASH_STORAGE_SIZE) + if (this->current_flash_offset + total_words > ESP8266_FLASH_STORAGE_SIZE) return {}; - auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->offset = static_cast(start); - pref->type = type; - pref->length_words = static_cast(length_words); - pref->in_flash = true; - current_flash_offset = end; - return {pref}; + offset = static_cast(this->current_flash_offset); + this->current_flash_offset += total_words; + } else { + uint32_t start = this->current_offset; + bool in_normal = start < RTC_NORMAL_REGION_WORDS; + // Normal: offset 0-95 maps to RTC offset 32-127 + // Eboot: offset 96-127 maps to RTC offset 0-31 + if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) { + // start is in normal but end is not -> switch to Eboot + this->current_offset = start = RTC_NORMAL_REGION_WORDS; + in_normal = false; + } + if (start + total_words > PREF_TOTAL_WORDS) + return {}; // Doesn't fit in RTC memory + // Convert preference offset to RTC memory offset + offset = static_cast(in_normal ? start + RTC_EBOOT_REGION_WORDS : start - RTC_NORMAL_REGION_WORDS); + this->current_offset = start + total_words; } - uint32_t start = current_offset; - uint32_t end = start + length_words + 1; - bool in_normal = start < 96; - // Normal: offset 0-95 maps to RTC offset 32 - 127, - // Eboot: offset 96-127 maps to RTC offset 0 - 31 words - if (in_normal && end > 96) { - // start is in normal but end is not -> switch to Eboot - current_offset = start = 96; - end = start + length_words + 1; - in_normal = false; - } - - if (end > 128) { - // Doesn't fit in data, return uninitialized preference obj. - return {}; - } - - uint32_t rtc_offset = in_normal ? start + 32 : start - 96; - auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->offset = static_cast(rtc_offset); + pref->offset = offset; pref->type = type; pref->length_words = static_cast(length_words); - pref->in_flash = false; - current_offset += length_words + 1; + pref->in_flash = in_flash; return pref; } @@ -284,10 +307,10 @@ void setup_preferences() { } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } -} // namespace esp8266 +} // namespace esphome::esp8266 +namespace esphome { ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - } // namespace esphome #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/preferences.h b/esphome/components/esp8266/preferences.h index edec915794..16cf80a129 100644 --- a/esphome/components/esp8266/preferences.h +++ b/esphome/components/esp8266/preferences.h @@ -2,13 +2,11 @@ #ifdef USE_ESP8266 -namespace esphome { -namespace esp8266 { +namespace esphome::esp8266 { void setup_preferences(); void preferences_prevent_write(bool prevent); -} // namespace esp8266 -} // namespace esphome +} // namespace esphome::esp8266 #endif // USE_ESP8266 diff --git a/esphome/components/esp8266/waveform_stubs.cpp b/esphome/components/esp8266/waveform_stubs.cpp new file mode 100644 index 0000000000..686e03c6a9 --- /dev/null +++ b/esphome/components/esp8266/waveform_stubs.cpp @@ -0,0 +1,34 @@ +#ifdef USE_ESP8266_WAVEFORM_STUBS + +// Stub implementations for Arduino waveform/PWM functions. +// +// When the waveform generator is not needed (no esp8266_pwm component), +// we exclude core_esp8266_waveform_pwm.cpp from the build to save ~596 bytes +// of RAM and 464 bytes of flash. +// +// These stubs satisfy calls from the Arduino GPIO code when the real +// waveform implementation is excluded. They must be in the global namespace +// with C linkage to match the Arduino core function declarations. + +#include + +// Empty namespace to satisfy linter - actual stubs must be at global scope +namespace esphome::esp8266 {} // namespace esphome::esp8266 + +extern "C" { + +// Called by Arduino GPIO code to stop any waveform on a pin +int stopWaveform(uint8_t pin) { + (void) pin; + return 1; // Success (no waveform to stop) +} + +// Called by Arduino GPIO code to stop any PWM on a pin +bool _stopPWM(uint8_t pin) { + (void) pin; + return false; // No PWM was running +} + +} // extern "C" + +#endif // USE_ESP8266_WAVEFORM_STUBS diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.cpp b/esphome/components/esp8266_pwm/esp8266_pwm.cpp index 0aaef597d3..cc6bfbc8a8 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.cpp +++ b/esphome/components/esp8266_pwm/esp8266_pwm.cpp @@ -18,9 +18,11 @@ void ESP8266PWM::setup() { this->turn_off(); } void ESP8266PWM::dump_config() { - ESP_LOGCONFIG(TAG, "ESP8266 PWM:"); + ESP_LOGCONFIG(TAG, + "ESP8266 PWM:\n" + " Frequency: %.1f Hz", + this->frequency_); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); LOG_FLOAT_OUTPUT(this); } void HOT ESP8266PWM::write_state(float state) { diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py index 1404ef8ac3..a78831c516 100644 --- a/esphome/components/esp8266_pwm/output.py +++ b/esphome/components/esp8266_pwm/output.py @@ -1,6 +1,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import output +from esphome.components.esp8266.const import require_waveform import esphome.config_validation as cv from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_NUMBER, CONF_PIN @@ -16,7 +17,7 @@ def valid_pwm_pin(value): esp8266_pwm_ns = cg.esphome_ns.namespace("esp8266_pwm") ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Component) SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action) -validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6)) +validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6)) CONFIG_SCHEMA = cv.All( output.FLOAT_OUTPUT_SCHEMA.extend( @@ -34,7 +35,9 @@ CONFIG_SCHEMA = cv.All( ) -async def to_code(config): +async def to_code(config) -> None: + require_waveform() + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await output.register_output(var, config) diff --git a/esphome/components/esp_ldo/__init__.py b/esphome/components/esp_ldo/__init__.py index 38e684c537..f136dd149b 100644 --- a/esphome/components/esp_ldo/__init__.py +++ b/esphome/components/esp_ldo/__init__.py @@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All( } ) ), - cv.only_with_esp_idf, + cv.only_on_esp32, only_on_variant(supported=[VARIANT_ESP32P4]), ) diff --git a/esphome/components/esp_ldo/esp_ldo.cpp b/esphome/components/esp_ldo/esp_ldo.cpp index eb04670d7e..2eee855b46 100644 --- a/esphome/components/esp_ldo/esp_ldo.cpp +++ b/esphome/components/esp_ldo/esp_ldo.cpp @@ -14,16 +14,18 @@ void EspLdo::setup() { config.flags.adjustable = this->adjustable_; auto err = esp_ldo_acquire_channel(&config, &this->handle_); if (err != ESP_OK) { - auto msg = str_sprintf("Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); - this->mark_failed(msg.c_str()); + ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_); + this->mark_failed(LOG_STR("Failed to acquire LDO channel")); } else { ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_); } } void EspLdo::dump_config() { - ESP_LOGCONFIG(TAG, "ESP LDO Channel %d:", this->channel_); - ESP_LOGCONFIG(TAG, " Voltage: %fV", this->voltage_); - ESP_LOGCONFIG(TAG, " Adjustable: %s", YESNO(this->adjustable_)); + ESP_LOGCONFIG(TAG, + "ESP LDO Channel %d:\n" + " Voltage: %fV\n" + " Adjustable: %s", + this->channel_, this->voltage_, YESNO(this->adjustable_)); } void EspLdo::adjust_voltage(float voltage) { diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index e56e85b231..2f637d714d 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -16,7 +16,7 @@ from esphome.const import ( CONF_SAFE_MODE, CONF_VERSION, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import coroutine_with_priority from esphome.coroutine import CoroPriority import esphome.final_validate as fv from esphome.types import ConfigType @@ -28,17 +28,7 @@ CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] -def supports_sha256() -> bool: - """Check if the current platform supports SHA256 for OTA authentication.""" - return bool(CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2040 or CORE.is_libretiny) - - -def AUTO_LOAD() -> list[str]: - """Conditionally auto-load sha256 only on platforms that support it.""" - base_components = ["md5", "socket"] - if supports_sha256(): - return base_components + ["sha256"] - return base_components +AUTO_LOAD = ["sha256", "socket"] esphome = cg.esphome_ns.namespace("esphome") @@ -155,11 +145,6 @@ async def to_code(config: ConfigType) -> None: if config.get(CONF_PASSWORD): cg.add(var.set_auth_password(config[CONF_PASSWORD])) cg.add_define("USE_OTA_PASSWORD") - # Only include hash algorithms when password is configured - cg.add_define("USE_OTA_MD5") - # Only include SHA256 support on platforms that have it - if supports_sha256(): - cg.add_define("USE_OTA_SHA256") cg.add_define("USE_OTA_VERSION", config[CONF_VERSION]) await cg.register_component(var, config) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index eb6c61a69b..dfa637f701 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -1,17 +1,12 @@ #include "ota_esphome.h" #ifdef USE_OTA #ifdef USE_OTA_PASSWORD -#ifdef USE_OTA_MD5 -#include "esphome/components/md5/md5.h" -#endif -#ifdef USE_OTA_SHA256 #include "esphome/components/sha256/sha256.h" #endif -#endif #include "esphome/components/network/util.h" +#include "esphome/components/socket/socket.h" #include "esphome/components/ota/ota_backend.h" -#include "esphome/components/ota/ota_backend_arduino_esp32.h" -#include "esphome/components/ota/ota_backend_arduino_esp8266.h" +#include "esphome/components/ota/ota_backend_esp8266.h" #include "esphome/components/ota/ota_backend_arduino_libretiny.h" #include "esphome/components/ota/ota_backend_arduino_rp2040.h" #include "esphome/components/ota/ota_backend_esp_idf.h" @@ -32,20 +27,7 @@ static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer -#ifdef USE_OTA_PASSWORD -#ifdef USE_OTA_MD5 -static constexpr size_t MD5_HEX_SIZE = 32; // MD5 hash as hex string (16 bytes * 2) -#endif -#ifdef USE_OTA_SHA256 -static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2) -#endif -#endif // USE_OTA_PASSWORD - void ESPHomeOTAComponent::setup() { -#ifdef USE_OTA_STATE_CALLBACK - ota::register_ota_platform(this); -#endif - this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections if (this->server_ == nullptr) { this->log_socket_error_(LOG_STR("creation")); @@ -113,15 +95,7 @@ void ESPHomeOTAComponent::loop() { } static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; -#ifdef USE_OTA_SHA256 static const uint8_t FEATURE_SUPPORTS_SHA256_AUTH = 0x02; -#endif - -// Temporary flag to allow MD5 downgrade for ~3 versions (until 2026.1.0) -// This allows users to downgrade via OTA if they encounter issues after updating. -// Without this, users would need to do a serial flash to downgrade. -// TODO: Remove this flag and all associated code in 2026.1.0 -#define ALLOW_OTA_DOWNGRADE_MD5 void ESPHomeOTAComponent::handle_handshake_() { /// Handle the OTA handshake and authentication. @@ -298,8 +272,8 @@ void ESPHomeOTAComponent::handle_data_() { // accidentally trigger the update process. this->log_start_(LOG_STR("update")); this->status_set_warning(); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_STARTED, 0.0f, 0); #endif // This will block for a few seconds as it locks flash @@ -358,8 +332,8 @@ void ESPHomeOTAComponent::handle_data_() { last_progress = now; float percentage = (total * 100.0f) / ota_size; ESP_LOGD(TAG, "Progress: %0.1f%%", percentage); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0); #endif // feed watchdog and give other tasks a chance to run this->yield_and_feed_watchdog_(); @@ -388,8 +362,8 @@ void ESPHomeOTAComponent::handle_data_() { delay(10); ESP_LOGI(TAG, "Update complete"); this->status_clear_warning(); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0); #endif delay(100); // NOLINT App.safe_reboot(); @@ -402,9 +376,9 @@ error: this->backend_->abort(); } - this->status_momentary_error("onerror", 5000); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_ERROR, 0.0f, static_cast(error_code)); + this->status_momentary_error("err", 5000); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif } @@ -414,14 +388,14 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) { while (len - at > 0) { uint32_t now = millis(); if (now - start > OTA_SOCKET_TIMEOUT_DATA) { - ESP_LOGW(TAG, "Timeout reading %d bytes", len); + ESP_LOGW(TAG, "Timeout reading %zu bytes", len); return false; } ssize_t read = this->client_->read(buf + at, len - at); if (read == -1) { if (!this->would_block_(errno)) { - ESP_LOGW(TAG, "Read err %d bytes, errno %d", len, errno); + ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, errno); return false; } } else if (read == 0) { @@ -441,14 +415,14 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) { while (len - at > 0) { uint32_t now = millis(); if (now - start > OTA_SOCKET_TIMEOUT_DATA) { - ESP_LOGW(TAG, "Timeout writing %d bytes", len); + ESP_LOGW(TAG, "Timeout writing %zu bytes", len); return false; } ssize_t written = this->client_->write(buf + at, len - at); if (written == -1) { if (!this->would_block_(errno)) { - ESP_LOGW(TAG, "Write err %d bytes, errno %d", len, errno); + ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, errno); return false; } } else { @@ -470,7 +444,9 @@ void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) { void ESPHomeOTAComponent::log_read_error_(const LogString *what) { ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); } void ESPHomeOTAComponent::log_start_(const LogString *phase) { - ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), this->client_->getpeername().c_str()); + char peername[socket::SOCKADDR_STR_LEN]; + this->client_->getpeername_to(peername); + ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), peername); } void ESPHomeOTAComponent::log_remote_closed_(const LogString *during) { @@ -552,26 +528,8 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() { void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); } bool ESPHomeOTAComponent::select_auth_type_() { -#ifdef USE_OTA_SHA256 bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0; -#ifdef ALLOW_OTA_DOWNGRADE_MD5 - // Allow fallback to MD5 if client doesn't support SHA256 - if (client_supports_sha256) { - this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; - return true; - } -#ifdef USE_OTA_MD5 - this->log_auth_warning_(LOG_STR("Using deprecated MD5")); - this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; - return true; -#else - this->log_auth_warning_(LOG_STR("SHA256 required")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; -#endif // USE_OTA_MD5 - -#else // !ALLOW_OTA_DOWNGRADE_MD5 // Require SHA256 if (!client_supports_sha256) { this->log_auth_warning_(LOG_STR("SHA256 required")); @@ -580,20 +538,6 @@ bool ESPHomeOTAComponent::select_auth_type_() { } this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH; return true; -#endif // ALLOW_OTA_DOWNGRADE_MD5 - -#else // !USE_OTA_SHA256 -#ifdef USE_OTA_MD5 - // Only MD5 available - this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH; - return true; -#else - // No auth methods available - this->log_auth_warning_(LOG_STR("No auth methods available")); - this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID); - return false; -#endif // USE_OTA_MD5 -#endif // USE_OTA_SHA256 } bool ESPHomeOTAComponent::handle_auth_send_() { @@ -617,31 +561,14 @@ bool ESPHomeOTAComponent::handle_auth_send_() { // [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce // [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash - // Declare both hash objects in same stack frame, use pointer to select. - // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3 - // hardware SHA acceleration - the object must exist in this stack frame for all operations. - // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope. -#ifdef USE_OTA_SHA256 - sha256::SHA256 sha_hasher; -#endif -#ifdef USE_OTA_MD5 - md5::MD5Digest md5_hasher; -#endif - HashBase *hasher = nullptr; + // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame + // (no passing to other functions). All hash operations must happen in this function. + // NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for + // hardware SHA acceleration DMA operations. + alignas(32) sha256::SHA256 hasher; -#ifdef USE_OTA_SHA256 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - hasher = &sha_hasher; - } -#endif -#ifdef USE_OTA_MD5 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { - hasher = &md5_hasher; - } -#endif - - const size_t hex_size = hasher->get_size() * 2; - const size_t nonce_len = hasher->get_size() / 4; + const size_t hex_size = hasher.get_size() * 2; + const size_t nonce_len = hasher.get_size() / 4; const size_t auth_buf_size = 1 + 3 * hex_size; this->auth_buf_ = std::make_unique(auth_buf_size); this->auth_buf_pos_ = 0; @@ -653,22 +580,17 @@ bool ESPHomeOTAComponent::handle_auth_send_() { return false; } - hasher->init(); - hasher->add(buf, nonce_len); - hasher->calculate(); + hasher.init(); + hasher.add(buf, nonce_len); + hasher.calculate(); this->auth_buf_[0] = this->auth_type_; - hasher->get_hex(buf); + hasher.get_hex(buf); -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too - memcpy(log_buf, buf, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf); -#endif + ESP_LOGV(TAG, "Auth: Nonce is %.*s", hex_size, buf); } // Try to write auth_type + nonce - size_t hex_size = this->get_auth_hex_size_(); + constexpr size_t hex_size = SHA256_HEX_SIZE; const size_t to_write = 1 + hex_size; size_t remaining = to_write - this->auth_buf_pos_; @@ -690,7 +612,7 @@ bool ESPHomeOTAComponent::handle_auth_send_() { } bool ESPHomeOTAComponent::handle_auth_read_() { - size_t hex_size = this->get_auth_hex_size_(); + constexpr size_t hex_size = SHA256_HEX_SIZE; const size_t to_read = hex_size * 2; // CNonce + Response // Try to read remaining bytes (CNonce + Response) @@ -715,55 +637,27 @@ bool ESPHomeOTAComponent::handle_auth_read_() { const char *cnonce = nonce + hex_size; const char *response = cnonce + hex_size; - // CRITICAL ESP32-S3: Hash objects must stay in same stack frame (no passing to other functions). - // Declare both hash objects in same stack frame, use pointer to select. - // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3 - // hardware SHA acceleration - the object must exist in this stack frame for all operations. - // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope. -#ifdef USE_OTA_SHA256 - sha256::SHA256 sha_hasher; -#endif -#ifdef USE_OTA_MD5 - md5::MD5Digest md5_hasher; -#endif - HashBase *hasher = nullptr; + // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame + // (no passing to other functions). All hash operations must happen in this function. + // NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for + // hardware SHA acceleration DMA operations. + alignas(32) sha256::SHA256 hasher; -#ifdef USE_OTA_SHA256 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - hasher = &sha_hasher; - } -#endif -#ifdef USE_OTA_MD5 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) { - hasher = &md5_hasher; - } -#endif - - hasher->init(); - hasher->add(this->password_.c_str(), this->password_.length()); - hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) - hasher->calculate(); + hasher.init(); + hasher.add(this->password_.c_str(), this->password_.length()); + hasher.add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer) + hasher.calculate(); + ESP_LOGV(TAG, "Auth: CNonce is %.*s", hex_size, cnonce); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too - // Log CNonce - memcpy(log_buf, cnonce, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf); - - // Log computed hash - hasher->get_hex(log_buf); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Result is %s", log_buf); - - // Log received response - memcpy(log_buf, response, hex_size); - log_buf[hex_size] = '\0'; - ESP_LOGV(TAG, "Auth: Response is %s", log_buf); + char computed_hash[SHA256_HEX_SIZE + 1]; // Buffer for hex-encoded hash (max expected length + null terminator) + hasher.get_hex(computed_hash); + ESP_LOGV(TAG, "Auth: Result is %.*s", hex_size, computed_hash); #endif + ESP_LOGV(TAG, "Auth: Response is %.*s", hex_size, response); // Compare response - bool matches = hasher->equals_hex(response); + bool matches = hasher.equals_hex(response); if (!matches) { this->log_auth_warning_(LOG_STR("Password mismatch")); @@ -777,21 +671,6 @@ bool ESPHomeOTAComponent::handle_auth_read_() { return true; } -size_t ESPHomeOTAComponent::get_auth_hex_size_() const { -#ifdef USE_OTA_SHA256 - if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) { - return SHA256_HEX_SIZE; - } -#endif -#ifdef USE_OTA_MD5 - return MD5_HEX_SIZE; -#else -#ifndef USE_OTA_SHA256 -#error "Either USE_OTA_MD5 or USE_OTA_SHA256 must be defined when USE_OTA_PASSWORD is enabled" -#endif -#endif -} - void ESPHomeOTAComponent::cleanup_auth_() { this->auth_buf_ = nullptr; this->auth_buf_pos_ = 0; diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 057461e6a4..e199b7e406 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -44,10 +44,10 @@ class ESPHomeOTAComponent : public ota::OTAComponent { void handle_handshake_(); void handle_data_(); #ifdef USE_OTA_PASSWORD + static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2) bool handle_auth_send_(); bool handle_auth_read_(); bool select_auth_type_(); - size_t get_auth_hex_size_() const; void cleanup_auth_(); void log_auth_warning_(const LogString *msg); #endif // USE_OTA_PASSWORD @@ -80,6 +80,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { #ifdef USE_OTA_PASSWORD std::string password_; + std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD std::unique_ptr server_; @@ -93,7 +94,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent { uint8_t handshake_buf_pos_{0}; uint8_t ota_features_{0}; #ifdef USE_OTA_PASSWORD - std::unique_ptr auth_buf_; uint8_t auth_buf_pos_{0}; uint8_t auth_type_{0}; // Store auth type to know which hasher to use #endif // USE_OTA_PASSWORD diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index cc2c02d4c0..1f5ca1104a 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -66,11 +66,17 @@ CONF_WAIT_FOR_SENT = "wait_for_sent" MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes +def validate_channel(value): + if value is None: + raise cv.Invalid("channel is required if wifi is not configured") + return wifi.validate_channel(value) + + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(ESPNowComponent), - cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): wifi.validate_channel, + cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): validate_channel, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_AUTO_ADD_PEER, default=False): cv.boolean, cv.Optional(CONF_PEERS): cv.ensure_list(cv.mac_address), diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index d2f136d1c7..991803d870 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -6,10 +6,12 @@ #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include #include +#include #include #include #include @@ -62,18 +64,6 @@ static const LogString *espnow_error_to_str(esp_err_t error) { } } -std::string peer_str(uint8_t *peer) { - if (peer == nullptr || peer[0] == 0) { - return "[Not Set]"; - } else if (memcmp(peer, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { - return "[Broadcast]"; - } else if (memcmp(peer, ESPNOW_MULTICAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { - return "[Multicast]"; - } else { - return format_mac_address_pretty(peer); - } -} - #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status) #else @@ -138,11 +128,13 @@ void ESPNowComponent::dump_config() { ESP_LOGCONFIG(TAG, " Disabled"); return; } + char own_addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(this->own_address_, own_addr_buf); ESP_LOGCONFIG(TAG, " Own address: %s\n" " Version: v%" PRIu32 "\n" " Wi-Fi channel: %d", - format_mac_address_pretty(this->own_address_).c_str(), version, this->wifi_channel_); + own_addr_buf, version, this->wifi_channel_); #ifdef USE_WIFI ESP_LOGCONFIG(TAG, " Wi-Fi enabled: %s", YESNO(this->is_wifi_enabled())); #endif @@ -157,6 +149,12 @@ bool ESPNowComponent::is_wifi_enabled() { } void ESPNowComponent::setup() { +#ifndef USE_WIFI + // Initialize LwIP stack for wake_loop_threadsafe() socket support + // When WiFi component is present, it handles esp_netif_init() + ESP_ERROR_CHECK(esp_netif_init()); +#endif + if (this->enable_on_boot_) { this->enable_(); } else { @@ -292,9 +290,13 @@ void ESPNowComponent::loop() { // Intentionally left as if instead of else in case the peer is added above if (esp_now_is_peer_exist(info.src_addr)) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - ESP_LOGV(TAG, "<<< [%s -> %s] %s", format_mac_address_pretty(info.src_addr).c_str(), - format_mac_address_pretty(info.des_addr).c_str(), - format_hex_pretty(packet->packet_.receive.data, packet->packet_.receive.size).c_str()); + char src_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + char dst_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + char hex_buf[format_hex_pretty_size(ESP_NOW_MAX_DATA_LEN)]; + format_mac_addr_upper(info.src_addr, src_buf); + format_mac_addr_upper(info.des_addr, dst_buf); + ESP_LOGV(TAG, "<<< [%s -> %s] %s", src_buf, dst_buf, + format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size)); #endif if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) { for (auto *handler : this->broadcasted_handlers_) { @@ -312,8 +314,9 @@ void ESPNowComponent::loop() { } case ESPNowPacket::SENT: { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - ESP_LOGV(TAG, ">>> [%s] %s", format_mac_address_pretty(packet->packet_.sent.address).c_str(), - LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status))); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(packet->packet_.sent.address, addr_buf); + ESP_LOGV(TAG, ">>> [%s] %s", addr_buf, LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status))); #endif if (this->current_send_packet_ != nullptr) { this->current_send_packet_->callback_(packet->packet_.sent.status); @@ -400,8 +403,9 @@ void ESPNowComponent::send_() { this->current_send_packet_ = packet; esp_err_t err = esp_now_send(packet->address_, packet->data_, packet->size_); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to send packet to %s - %s", format_mac_address_pretty(packet->address_).c_str(), - LOG_STR_ARG(espnow_error_to_str(err))); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(packet->address_, addr_buf); + ESP_LOGE(TAG, "Failed to send packet to %s - %s", addr_buf, LOG_STR_ARG(espnow_error_to_str(err))); if (packet->callback_ != nullptr) { packet->callback_(err); } @@ -430,8 +434,9 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) { esp_err_t err = esp_now_add_peer(&peer_info); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to add peer %s - %s", format_mac_address_pretty(peer).c_str(), - LOG_STR_ARG(espnow_error_to_str(err))); + char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(peer, peer_buf); + ESP_LOGE(TAG, "Failed to add peer %s - %s", peer_buf, LOG_STR_ARG(espnow_error_to_str(err))); this->status_momentary_warning("peer-add-failed"); return err; } @@ -459,8 +464,9 @@ esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) { if (esp_now_is_peer_exist(peer)) { esp_err_t err = esp_now_del_peer(peer); if (err != ESP_OK) { - ESP_LOGE(TAG, "Failed to delete peer %s - %s", format_mac_address_pretty(peer).c_str(), - LOG_STR_ARG(espnow_error_to_str(err))); + char peer_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(peer, peer_buf); + ESP_LOGE(TAG, "Failed to delete peer %s - %s", peer_buf, LOG_STR_ARG(espnow_error_to_str(err))); this->status_momentary_warning("peer-del-failed"); return err; } diff --git a/esphome/components/espnow/packet_transport/espnow_transport.cpp b/esphome/components/espnow/packet_transport/espnow_transport.cpp index d30e9447a0..3d16f28c7d 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.cpp +++ b/esphome/components/espnow/packet_transport/espnow_transport.cpp @@ -13,7 +13,7 @@ static const char *const TAG = "espnow.transport"; bool ESPNowTransport::should_send() { return this->parent_ != nullptr && !this->parent_->is_failed(); } void ESPNowTransport::setup() { - packet_transport::PacketTransport::setup(); + PacketTransport::setup(); if (this->parent_ == nullptr) { ESP_LOGE(TAG, "ESPNow component not set"); @@ -21,20 +21,17 @@ void ESPNowTransport::setup() { return; } - ESP_LOGI(TAG, "Registering ESP-NOW handlers"); - ESP_LOGI(TAG, "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], this->peer_address_[1], - this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], this->peer_address_[5]); + ESP_LOGI(TAG, + "Registering ESP-NOW handlers\n" + "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", + this->peer_address_[0], this->peer_address_[1], this->peer_address_[2], this->peer_address_[3], + this->peer_address_[4], this->peer_address_[5]); // Register received handler - this->parent_->register_received_handler(static_cast(this)); + this->parent_->register_received_handler(this); // Register broadcasted handler - this->parent_->register_broadcasted_handler(static_cast(this)); -} - -void ESPNowTransport::update() { - packet_transport::PacketTransport::update(); - this->updated_ = true; + this->parent_->register_broadcasted_handler(this); } void ESPNowTransport::send_packet(const std::vector &buf) const { diff --git a/esphome/components/espnow/packet_transport/espnow_transport.h b/esphome/components/espnow/packet_transport/espnow_transport.h index 3629fad2cd..d85119db7d 100644 --- a/esphome/components/espnow/packet_transport/espnow_transport.h +++ b/esphome/components/espnow/packet_transport/espnow_transport.h @@ -18,7 +18,6 @@ class ESPNowTransport : public packet_transport::PacketTransport, public ESPNowBroadcastedHandler { public: void setup() override; - void update() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void set_peer_address(peer_address_t address) { diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 2f02d227d7..f140f395e4 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -3,16 +3,17 @@ import logging from esphome import pins import esphome.codegen as cg from esphome.components.esp32 import ( - add_idf_component, - add_idf_sdkconfig_option, - get_esp32_variant, -) -from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + add_idf_component, + add_idf_sdkconfig_option, + get_esp32_variant, ) from esphome.components.network import ip_address_literal from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface @@ -219,10 +220,6 @@ BASE_SCHEMA = cv.Schema( cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, - cv.Optional("enable_mdns"): cv.invalid( - "This option has been removed. Please use the [disabled] option under the " - "new mdns component instead." - ), cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, } ).extend(cv.COMPONENT_SCHEMA) @@ -303,7 +300,14 @@ def _final_validate_spi(config): return if spi_configs := fv.full_config.get().get(CONF_SPI): variant = get_esp32_variant() - if variant in (VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3): + if variant in ( + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + ): spi_host = "SPI2_HOST" else: spi_host = "SPI3_HOST" @@ -383,6 +387,7 @@ async def to_code(config): cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) if CONF_MANUAL_IP in config: + cg.add_define("USE_ETHERNET_MANUAL_IP") cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) # Add compile-time define for PHY types with specific code @@ -425,10 +430,13 @@ def _final_validate_rmii_pins(config: ConfigType) -> None: # Check all used pins against RMII reserved pins for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values(): - for pin_path, _, pin_config in pin_list: + for pin_path, pin_device, pin_config in pin_list: pin_num = pin_config.get(CONF_NUMBER) if pin_num not in rmii_pins: continue + # Skip if pin is not directly on ESP, but at some expander (device set to something else than 'None') + if pin_device is not None: + continue # Found a conflict - show helpful error message pin_function = rmii_pins[pin_num] component_path = ".".join(str(p) for p in pin_path) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index cad963b299..896c5cc874 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -1,5 +1,6 @@ #include "ethernet_component.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -39,6 +40,9 @@ namespace ethernet { static const char *const TAG = "ethernet"; +// PHY register size for hex logging +static constexpr size_t PHY_REG_SIZE = 2; + EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) { @@ -87,8 +91,8 @@ void EthernetComponent::setup() { .intr_flags = 0, }; -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ - defined(USE_ESP32_VARIANT_ESP32C6) +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \ + defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) auto host = SPI2_HOST; #else auto host = SPI3_HOST; @@ -553,11 +557,14 @@ void EthernetComponent::start_connect_() { } esp_netif_ip_info_t info; +#ifdef USE_ETHERNET_MANUAL_IP if (this->manual_ip_.has_value()) { info.ip = this->manual_ip_->static_ip; info.gw = this->manual_ip_->gateway; info.netmask = this->manual_ip_->subnet; - } else { + } else +#endif + { info.ip.addr = 0; info.gw.addr = 0; info.netmask.addr = 0; @@ -578,6 +585,7 @@ void EthernetComponent::start_connect_() { err = esp_netif_set_ip_info(this->eth_netif_, &info); ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); +#ifdef USE_ETHERNET_MANUAL_IP if (this->manual_ip_.has_value()) { LwIPLock lock; if (this->manual_ip_->dns1.is_set()) { @@ -590,7 +598,9 @@ void EthernetComponent::start_connect_() { d = this->manual_ip_->dns2; dns_setserver(1, &d); } - } else { + } else +#endif + { err = esp_netif_dhcpc_start(this->eth_netif_); if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { ESPHL_ERROR_CHECK(err, "DHCPC start error"); @@ -638,6 +648,12 @@ void EthernetComponent::dump_connect_params_() { dns_ip2 = dns_getserver(1); } + // Use stack buffers for IP address formatting to avoid heap allocations + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " IP Address: %s\n" " Hostname: '%s'\n" @@ -645,9 +661,9 @@ void EthernetComponent::dump_connect_params_() { " Gateway: %s\n" " DNS1: %s\n" " DNS2: %s", - network::IPAddress(&ip.ip).str().c_str(), App.get_name().c_str(), - network::IPAddress(&ip.netmask).str().c_str(), network::IPAddress(&ip.gw).str().c_str(), - network::IPAddress(dns_ip1).str().c_str(), network::IPAddress(dns_ip2).str().c_str()); + network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(), + network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf), + network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf)); #if USE_NETWORK_IPV6 struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; @@ -659,12 +675,13 @@ void EthernetComponent::dump_connect_params_() { } #endif /* USE_NETWORK_IPV6 */ + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " MAC Address: %s\n" " Is Full Duplex: %s\n" " Link Speed: %u", - this->get_eth_mac_address_pretty().c_str(), YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), - this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); + this->get_eth_mac_address_pretty_into_buffer(mac_buf), + YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10); } #ifdef USE_ETHERNET_SPI @@ -688,7 +705,9 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } #endif void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } +#ifdef USE_ETHERNET_MANUAL_IP void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } +#endif // set_use_address() is guaranteed to be called during component setup by Python code generation, // so use_address_ will always be valid when get_use_address() is called - no fallback needed. @@ -703,11 +722,16 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { } std::string EthernetComponent::get_eth_mac_address_pretty() { + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + return std::string(this->get_eth_mac_address_pretty_into_buffer(buf)); +} + +const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer( + std::span buf) { uint8_t mac[6]; get_eth_mac_address_raw(mac); - char buf[18]; - format_mac_addr_upper(mac, buf); - return std::string(buf); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); } eth_duplex_t EthernetComponent::get_duplex_mode() { @@ -753,7 +777,10 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { uint32_t phy_control_2; err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); - ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)]; +#endif + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); /* * Bit 7 is `RMII Reference Clock Select`. Default is `0`. @@ -770,7 +797,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) { ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed"); err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2)); ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed"); - ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str()); + ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", + format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE)); } } #endif // USE_ETHERNET_KSZ8081 @@ -785,8 +813,10 @@ void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister regi ESPHL_ERROR_CHECK(err, "Select PHY Register page failed"); } - ESP_LOGD(TAG, "Writing to PHY Register Address: 0x%02" PRIX32, register_data.address); - ESP_LOGD(TAG, "Writing to PHY Register Value: 0x%04" PRIX32, register_data.value); + ESP_LOGD(TAG, + "Writing to PHY Register Address: 0x%02" PRIX32 "\n" + "Writing to PHY Register Value: 0x%04" PRIX32, + register_data.address, register_data.value); err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value); ESPHL_ERROR_CHECK(err, "Writing PHY Register failed"); diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index f1f0ac9cb8..490a9d026e 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/network/ip_address.h" #ifdef USE_ESP32 @@ -82,7 +83,9 @@ class EthernetComponent : public Component { void add_phy_register(PHYRegister register_value); #endif void set_type(EthernetType type); +#ifdef USE_ETHERNET_MANUAL_IP void set_manual_ip(const ManualIP &manual_ip); +#endif void set_fixed_mac(const std::array &mac) { this->fixed_mac_ = mac; } network::IPAddresses get_ip_addresses(); @@ -91,6 +94,7 @@ class EthernetComponent : public Component { void set_use_address(const char *use_address); void get_eth_mac_address_raw(uint8_t *mac); std::string get_eth_mac_address_pretty(); + const char *get_eth_mac_address_pretty_into_buffer(std::span buf); eth_duplex_t get_duplex_mode(); eth_speed_t get_link_speed(); bool powerdown(); @@ -137,7 +141,9 @@ class EthernetComponent : public Component { uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; #endif +#ifdef USE_ETHERNET_MANUAL_IP optional manual_ip_{}; +#endif uint32_t connect_begin_; // Group all uint8_t types together (enums and bools) diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp index 329fb9113a..35e18c7de5 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.cpp @@ -3,8 +3,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ethernet_info { +namespace esphome::ethernet_info { static const char *const TAG = "ethernet_info"; @@ -12,7 +11,6 @@ void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IP void DNSAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo DNS Address", this); } void MACAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo MAC Address", this); } -} // namespace ethernet_info -} // namespace esphome +} // namespace esphome::ethernet_info #endif // USE_ESP32 diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h index 2adc08e31e..5b858b772f 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -6,8 +6,7 @@ #ifdef USE_ESP32 -namespace esphome { -namespace ethernet_info { +namespace esphome::ethernet_info { class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor { public: @@ -15,12 +14,15 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS auto ips = ethernet::global_eth_component->get_ip_addresses(); if (ips != this->last_ips_) { this->last_ips_ = ips; - this->publish_state(ips[0].str()); + char buf[network::IP_ADDRESS_BUFFER_SIZE]; + ips[0].str_to(buf); + this->publish_state(buf); uint8_t sensor = 0; for (auto &ip : ips) { if (ip.is_set()) { if (this->ip_sensors_[sensor] != nullptr) { - this->ip_sensors_[sensor]->publish_state(ip.str()); + ip.str_to(buf); + this->ip_sensors_[sensor]->publish_state(buf); } sensor++; } @@ -40,31 +42,39 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS class DNSAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor { public: void update() override { - auto dns_one = ethernet::global_eth_component->get_dns_address(0); - auto dns_two = ethernet::global_eth_component->get_dns_address(1); + auto dns1 = ethernet::global_eth_component->get_dns_address(0); + auto dns2 = ethernet::global_eth_component->get_dns_address(1); - std::string dns_results = dns_one.str() + " " + dns_two.str(); - - if (dns_results != this->last_results_) { - this->last_results_ = dns_results; - this->publish_state(dns_results); + if (dns1 != this->last_dns1_ || dns2 != this->last_dns2_) { + this->last_dns1_ = dns1; + this->last_dns2_ = dns2; + // IP_ADDRESS_BUFFER_SIZE (40) = max IP (39) + null; space reuses first null's slot + char buf[network::IP_ADDRESS_BUFFER_SIZE * 2]; + dns1.str_to(buf); + size_t len1 = strlen(buf); + buf[len1] = ' '; + dns2.str_to(buf + len1 + 1); + this->publish_state(buf); } } float get_setup_priority() const override { return setup_priority::ETHERNET; } void dump_config() override; protected: - std::string last_results_; + network::IPAddress last_dns1_; + network::IPAddress last_dns2_; }; class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor { public: - void setup() override { this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty()); } + void setup() override { + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty_into_buffer(buf)); + } float get_setup_priority() const override { return setup_priority::ETHERNET; } void dump_config() override; }; -} // namespace ethernet_info -} // namespace esphome +} // namespace esphome::ethernet_info #endif // USE_ESP32 diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index e4b2e0b845..0d5850d339 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -50,7 +50,7 @@ class Event : public EntityBase, public EntityBase_DeviceClass { void add_on_event_callback(std::function &&callback); protected: - CallbackManager event_callback_; + LazyCallbackManager event_callback_; FixedVector types_; private: diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp index 9ec41cce30..61b601328a 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.cpp +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -148,10 +148,13 @@ void EzoPMP::read_command_result_() { char current_char = response_buffer[i]; if (current_char == '\0') { - ESP_LOGV(TAG, "Read Response from device: %s", (char *) response_buffer); - ESP_LOGV(TAG, "First Component: %s", (char *) first_parameter_buffer); - ESP_LOGV(TAG, "Second Component: %s", (char *) second_parameter_buffer); - ESP_LOGV(TAG, "Third Component: %s", (char *) third_parameter_buffer); + ESP_LOGV(TAG, + "Read Response from device: %s\n" + "First Component: %s\n" + "Second Component: %s\n" + "Third Component: %s", + (char *) response_buffer, (char *) first_parameter_buffer, (char *) second_parameter_buffer, + (char *) third_parameter_buffer); break; } diff --git a/esphome/components/factory_reset/__init__.py b/esphome/components/factory_reset/__init__.py index f3cefe6970..5784d09ce6 100644 --- a/esphome/components/factory_reset/__init__.py +++ b/esphome/components/factory_reset/__init__.py @@ -50,7 +50,9 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(FactoryResetComponent), cv.Optional(CONF_MAX_DELAY, default="10s"): cv.All( cv.positive_time_period_seconds, - cv.Range(min=cv.TimePeriod(milliseconds=1000)), + cv.Range( + min=cv.TimePeriod(seconds=1), max=cv.TimePeriod(seconds=65535) + ), ), cv.Optional(CONF_RESETS_REQUIRED): cv.positive_not_null_int, cv.Optional(CONF_ON_INCREMENT): validate_automation( @@ -82,7 +84,7 @@ async def to_code(config): var = cg.new_Pvariable( config[CONF_ID], reset_count, - config[CONF_MAX_DELAY].total_milliseconds, + config[CONF_MAX_DELAY].total_seconds, ) await cg.register_component(var, config) for conf in config.get(CONF_ON_INCREMENT, []): diff --git a/esphome/components/factory_reset/factory_reset.cpp b/esphome/components/factory_reset/factory_reset.cpp index c900759d90..2e3f802343 100644 --- a/esphome/components/factory_reset/factory_reset.cpp +++ b/esphome/components/factory_reset/factory_reset.cpp @@ -8,8 +8,7 @@ #if !defined(USE_RP2040) && !defined(USE_HOST) -namespace esphome { -namespace factory_reset { +namespace esphome::factory_reset { static const char *const TAG = "factory_reset"; static const uint32_t POWER_CYCLES_KEY = 0xFA5C0DE; @@ -31,12 +30,12 @@ static bool was_power_cycled() { void FactoryResetComponent::dump_config() { uint8_t count = 0; this->flash_.load(&count); - ESP_LOGCONFIG(TAG, "Factory Reset by Reset:"); ESP_LOGCONFIG(TAG, - " Max interval between resets %" PRIu32 " seconds\n" + "Factory Reset by Reset:\n" + " Max interval between resets: %u seconds\n" " Current count: %u\n" " Factory reset after %u resets", - this->max_interval_ / 1000, count, this->required_count_); + this->max_interval_, count, this->required_count_); } void FactoryResetComponent::save_(uint8_t count) { @@ -61,8 +60,8 @@ void FactoryResetComponent::setup() { } this->save_(count); ESP_LOGD(TAG, "Power on reset detected, incremented count to %u", count); - this->set_timeout(this->max_interval_, [this]() { - ESP_LOGD(TAG, "No reset in the last %" PRIu32 " seconds, resetting count", this->max_interval_ / 1000); + this->set_timeout(static_cast(this->max_interval_) * 1000, [this]() { + ESP_LOGD(TAG, "No reset in the last %u seconds, resetting count", this->max_interval_); this->save_(0); // reset count }); } else { @@ -70,7 +69,6 @@ void FactoryResetComponent::setup() { } } -} // namespace factory_reset -} // namespace esphome +} // namespace esphome::factory_reset #endif // !defined(USE_RP2040) && !defined(USE_HOST) diff --git a/esphome/components/factory_reset/factory_reset.h b/esphome/components/factory_reset/factory_reset.h index 80942b29bd..990bb2edb6 100644 --- a/esphome/components/factory_reset/factory_reset.h +++ b/esphome/components/factory_reset/factory_reset.h @@ -9,12 +9,11 @@ #include #endif -namespace esphome { -namespace factory_reset { +namespace esphome::factory_reset { class FactoryResetComponent : public Component { public: - FactoryResetComponent(uint8_t required_count, uint32_t max_interval) - : required_count_(required_count), max_interval_(max_interval) {} + FactoryResetComponent(uint8_t required_count, uint16_t max_interval) + : max_interval_(max_interval), required_count_(required_count) {} void dump_config() override; void setup() override; @@ -26,9 +25,9 @@ class FactoryResetComponent : public Component { ~FactoryResetComponent() = default; void save_(uint8_t count); ESPPreferenceObject flash_{}; // saves the number of fast power cycles - uint8_t required_count_; // The number of boot attempts before fast boot is enabled - uint32_t max_interval_; // max interval between power cycles CallbackManager increment_callback_{}; + uint16_t max_interval_; // max interval between power cycles in seconds + uint8_t required_count_; // The number of boot attempts before fast boot is enabled }; class FastBootTrigger : public Trigger { @@ -37,7 +36,6 @@ class FastBootTrigger : public Trigger { parent->add_increment_callback([this](uint8_t current, uint8_t target) { this->trigger(current, target); }); } }; -} // namespace factory_reset -} // namespace esphome +} // namespace esphome::factory_reset #endif // !defined(USE_RP2040) && !defined(USE_HOST) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index d37825a651..0ffb60e50d 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -19,22 +19,28 @@ const LogString *fan_direction_to_string(FanDirection direction) { } } -FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { return this->set_preset_mode(preset_mode.c_str()); } +FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { + return this->set_preset_mode(preset_mode.data(), preset_mode.size()); +} FanCall &FanCall::set_preset_mode(const char *preset_mode) { - if (preset_mode == nullptr || strlen(preset_mode) == 0) { + return this->set_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0); +} + +FanCall &FanCall::set_preset_mode(const char *preset_mode, size_t len) { + if (preset_mode == nullptr || len == 0) { this->preset_mode_ = nullptr; return *this; } // Find and validate pointer from traits immediately auto traits = this->parent_.get_traits(); - const char *validated_mode = traits.find_preset_mode(preset_mode); + const char *validated_mode = traits.find_preset_mode(preset_mode, len); if (validated_mode != nullptr) { this->preset_mode_ = validated_mode; // Store pointer from traits } else { // Preset mode not found in traits - log warning and don't set - ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), preset_mode); + ESP_LOGW(TAG, "%s: Preset mode '%.*s' not supported", this->parent_.get_name().c_str(), (int) len, preset_mode); this->preset_mode_ = nullptr; } return *this; @@ -140,7 +146,13 @@ FanCall Fan::turn_off() { return this->make_call().set_state(false); } FanCall Fan::toggle() { return this->make_call().set_state(!this->state); } FanCall Fan::make_call() { return FanCall(*this); } -const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_traits().find_preset_mode(preset_mode); } +const char *Fan::find_preset_mode_(const char *preset_mode) { + return this->find_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0); +} + +const char *Fan::find_preset_mode_(const char *preset_mode, size_t len) { + return this->get_traits().find_preset_mode(preset_mode, len); +} bool Fan::set_preset_mode_(const char *preset_mode) { if (preset_mode == nullptr) { @@ -167,8 +179,10 @@ void Fan::add_on_state_callback(std::function &&callback) { this->state_ void Fan::publish_state() { auto traits = this->get_traits(); - ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); - ESP_LOGD(TAG, " State: %s", ONOFF(this->state)); + ESP_LOGD(TAG, + "'%s' - Sending state:\n" + " State: %s", + this->name_.c_str(), ONOFF(this->state)); if (traits.supports_speed()) { ESP_LOGD(TAG, " Speed: %d", this->speed); } diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index e38a80dbbe..7c79fda83e 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -72,6 +72,7 @@ class FanCall { optional get_direction() const { return this->direction_; } FanCall &set_preset_mode(const std::string &preset_mode); FanCall &set_preset_mode(const char *preset_mode); + FanCall &set_preset_mode(const char *preset_mode, size_t len); const char *get_preset_mode() const { return this->preset_mode_; } bool has_preset_mode() const { return this->preset_mode_ != nullptr; } @@ -152,8 +153,9 @@ class Fan : public EntityBase { void clear_preset_mode_(); /// Find and return the matching preset mode pointer from traits, or nullptr if not found. const char *find_preset_mode_(const char *preset_mode); + const char *find_preset_mode_(const char *preset_mode, size_t len); - CallbackManager state_callback_{}; + LazyCallbackManager state_callback_{}; ESPPreferenceObject rtc_; FanRestoreMode restore_mode_; diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 24987fe984..c0c5f34c50 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -47,10 +47,13 @@ class FanTraits { bool supports_preset_modes() const { return !this->preset_modes_.empty(); } /// Find and return the matching preset mode pointer from supported modes, or nullptr if not found. const char *find_preset_mode(const char *preset_mode) const { - if (preset_mode == nullptr) + return this->find_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0); + } + const char *find_preset_mode(const char *preset_mode, size_t len) const { + if (preset_mode == nullptr || len == 0) return nullptr; for (const char *mode : this->preset_modes_) { - if (strcmp(mode, preset_mode) == 0) { + if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') { return mode; // Return pointer from traits } } diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp index a7342bc828..f7f7f8d02c 100644 --- a/esphome/components/gcja5/gcja5.cpp +++ b/esphome/components/gcja5/gcja5.cpp @@ -95,11 +95,13 @@ void GCJA5Component::parse_data_() { if (!this->first_status_log_) { this->first_status_log_ = true; - ESP_LOGI(TAG, "GCJA5 Status"); - ESP_LOGI(TAG, "Overall Status : %i", (status >> 6) & 0x03); - ESP_LOGI(TAG, "PD Status : %i", (status >> 4) & 0x03); - ESP_LOGI(TAG, "LD Status : %i", (status >> 2) & 0x03); - ESP_LOGI(TAG, "Fan Status : %i", (status >> 0) & 0x03); + ESP_LOGI(TAG, + "GCJA5 Status\n" + "Overall Status : %i\n" + "PD Status : %i\n" + "LD Status : %i\n" + "Fan Status : %i", + (status >> 6) & 0x03, (status >> 4) & 0x03, (status >> 2) & 0x03, (status >> 0) & 0x03); } } diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 6c218f03d9..617e2138fb 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -36,20 +36,20 @@ void GDK101Component::setup() { uint8_t data[2]; // first, reset the sensor if (!this->reset_sensor_(data)) { - this->status_set_error("Reset failed!"); + this->status_set_error(LOG_STR("Reset failed!")); this->mark_failed(); return; } // sensor should acknowledge success of the reset procedure if (data[0] != 1) { - this->status_set_error("Reset not acknowledged!"); + this->status_set_error(LOG_STR("Reset not acknowledged!")); this->mark_failed(); return; } delay(10); // read firmware version if (!this->read_fw_version_(data)) { - this->status_set_error("Failed to read firmware version"); + this->status_set_error(LOG_STR("Failed to read firmware version")); this->mark_failed(); return; } diff --git a/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp index e2a64b6877..38328c4b03 100644 --- a/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp +++ b/esphome/components/gl_r01_i2c/gl_r01_i2c.cpp @@ -27,8 +27,10 @@ void GLR01I2CComponent::setup() { } void GLR01I2CComponent::dump_config() { - ESP_LOGCONFIG(TAG, "GL-R01 I2C:"); - ESP_LOGCONFIG(TAG, " Firmware Version: 0x%04X", this->version_); + ESP_LOGCONFIG(TAG, + "GL-R01 I2C:\n" + " Firmware Version: 0x%04X", + this->version_); LOG_I2C_DEVICE(this); LOG_SENSOR(" ", "Distance", this); } diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index a872cf7015..2135189bd5 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_SPEED, DEVICE_CLASS_SPEED, STATE_CLASS_MEASUREMENT, + STATE_CLASS_MEASUREMENT_ANGLE, UNIT_DEGREES, UNIT_KILOMETER_PER_HOUR, UNIT_METER, @@ -21,6 +22,7 @@ CONF_HDOP = "hdop" ICON_ALTIMETER = "mdi:altimeter" ICON_COMPASS = "mdi:compass" +ICON_CIRCLE_DOUBLE = "mdi:circle-double" ICON_LATITUDE = "mdi:latitude" ICON_LONGITUDE = "mdi:longitude" ICON_SATELLITE = "mdi:satellite-variant" @@ -50,7 +52,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_DEGREES, icon=ICON_LONGITUDE, accuracy_decimals=6, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_MEASUREMENT_ANGLE, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( unit_of_measurement=UNIT_KILOMETER_PER_HOUR, @@ -63,7 +65,7 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_DEGREES, icon=ICON_COMPASS, accuracy_decimals=2, - state_class=STATE_CLASS_MEASUREMENT, + state_class=STATE_CLASS_MEASUREMENT_ANGLE, ), cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_METER, @@ -72,11 +74,14 @@ CONFIG_SCHEMA = cv.All( state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SATELLITES): sensor.sensor_schema( + # no unit_of_measurement icon=ICON_SATELLITE, accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HDOP): sensor.sensor_schema( + # no unit_of_measurement + icon=ICON_CIRCLE_DOUBLE, accuracy_decimals=3, state_class=STATE_CLASS_MEASUREMENT, ), diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index 88bb306408..e3b9119108 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -337,7 +337,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of return; /// Plot border - if (this->border_) { + if (legend_->border_) { int w = legend_->width_; int h = legend_->height_; buff->horizontal_line(x_offset, y_offset, w, color); diff --git a/esphome/components/gree/__init__.py b/esphome/components/gree/__init__.py index e69de29bb2..2dd9ac0f1c 100644 --- a/esphome/components/gree/__init__.py +++ b/esphome/components/gree/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +gree_ns = cg.esphome_ns.namespace("gree") diff --git a/esphome/components/gree/climate.py b/esphome/components/gree/climate.py index 057ba67b94..0892155fd2 100644 --- a/esphome/components/gree/climate.py +++ b/esphome/components/gree/climate.py @@ -3,11 +3,11 @@ from esphome.components import climate_ir import esphome.config_validation as cv from esphome.const import CONF_MODEL +from . import gree_ns + CODEOWNERS = ["@orestismers"] AUTO_LOAD = ["climate_ir"] - -gree_ns = cg.esphome_ns.namespace("gree") GreeClimate = gree_ns.class_("GreeClimate", climate_ir.ClimateIR) Model = gree_ns.enum("Model") @@ -23,7 +23,7 @@ MODELS = { CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend( { - cv.Required(CONF_MODEL): cv.enum(MODELS), + cv.Required(CONF_MODEL): cv.enum(MODELS, lower=True), } ) diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index e0cacb4f1e..b8cf8a39a8 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -16,13 +16,28 @@ void GreeClimate::set_model(Model model) { this->model_ = model; } +void GreeClimate::set_mode_bit(uint8_t bit_mask, bool enabled) { + if (enabled) { + this->mode_bits_ |= bit_mask; + } else { + this->mode_bits_ &= ~bit_mask; + } + this->transmit_state(); +} + void GreeClimate::transmit_state() { uint8_t remote_state[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00}; remote_state[0] = this->fan_speed_() | this->operation_mode_(); remote_state[1] = this->temperature_(); - if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF || this->model_ == GREE_YAG) { + if (this->model_ == GREE_YAN) { + remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO, LIGHT, HEALTH, X-FAN + remote_state[3] = 0x50; // bits 4..7 always 0101 + remote_state[4] = this->vertical_swing_(); + } + + if (this->model_ == GREE_YX1FF || this->model_ == GREE_YAG) { remote_state[2] = 0x60; remote_state[3] = 0x50; remote_state[4] = this->vertical_swing_(); @@ -41,7 +56,7 @@ void GreeClimate::transmit_state() { } if (this->model_ == GREE_YAA || this->model_ == GREE_YAC || this->model_ == GREE_YAC1FB9) { - remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO,LIGHT,HEALTH,X-FAN + remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO, LIGHT, HEALTH, X-FAN remote_state[3] = 0x50; // bits 4..7 always 0101 remote_state[6] = 0x20; // YAA1FB, FAA1FB1, YB1F2 bits 4..7 always 0010 @@ -52,6 +67,13 @@ void GreeClimate::transmit_state() { } } + if (this->model_ == GREE_YAN || this->model_ == GREE_YAA || this->model_ == GREE_YAC || + this->model_ == GREE_YAC1FB9) { + // Merge the mode bits into remote_state[2] + // Clear the mode bits (bits 4-7) and OR in the current mode_bits_ + remote_state[2] = (remote_state[2] & 0x0F) | this->mode_bits_; + } + if (this->model_ == GREE_YX1FF) { if (this->fan_speed_() == GREE_FAN_TURBO) { remote_state[2] |= GREE_FAN_TURBO_BIT; diff --git a/esphome/components/gree/gree.h b/esphome/components/gree/gree.h index f91d78cabd..24453750ae 100644 --- a/esphome/components/gree/gree.h +++ b/esphome/components/gree/gree.h @@ -2,80 +2,79 @@ #include "esphome/components/climate_ir/climate_ir.h" -namespace esphome { -namespace gree { +namespace esphome::gree { // Values for GREE IR Controllers // Temperature -const uint8_t GREE_TEMP_MIN = 16; // Celsius -const uint8_t GREE_TEMP_MAX = 30; // Celsius +static constexpr uint8_t GREE_TEMP_MIN = 16; // Celsius +static constexpr uint8_t GREE_TEMP_MAX = 30; // Celsius // Modes -const uint8_t GREE_MODE_AUTO = 0x00; -const uint8_t GREE_MODE_COOL = 0x01; -const uint8_t GREE_MODE_HEAT = 0x04; -const uint8_t GREE_MODE_DRY = 0x02; -const uint8_t GREE_MODE_FAN = 0x03; +static constexpr uint8_t GREE_MODE_AUTO = 0x00; +static constexpr uint8_t GREE_MODE_COOL = 0x01; +static constexpr uint8_t GREE_MODE_HEAT = 0x04; +static constexpr uint8_t GREE_MODE_DRY = 0x02; +static constexpr uint8_t GREE_MODE_FAN = 0x03; -const uint8_t GREE_MODE_OFF = 0x00; -const uint8_t GREE_MODE_ON = 0x08; +static constexpr uint8_t GREE_MODE_OFF = 0x00; +static constexpr uint8_t GREE_MODE_ON = 0x08; // Fan Speed -const uint8_t GREE_FAN_AUTO = 0x00; -const uint8_t GREE_FAN_1 = 0x10; -const uint8_t GREE_FAN_2 = 0x20; -const uint8_t GREE_FAN_3 = 0x30; +static constexpr uint8_t GREE_FAN_AUTO = 0x00; +static constexpr uint8_t GREE_FAN_1 = 0x10; +static constexpr uint8_t GREE_FAN_2 = 0x20; +static constexpr uint8_t GREE_FAN_3 = 0x30; // IR Transmission -const uint32_t GREE_IR_FREQUENCY = 38000; -const uint32_t GREE_HEADER_MARK = 9000; -const uint32_t GREE_HEADER_SPACE = 4000; -const uint32_t GREE_BIT_MARK = 620; -const uint32_t GREE_ONE_SPACE = 1600; -const uint32_t GREE_ZERO_SPACE = 540; -const uint32_t GREE_MESSAGE_SPACE = 19000; +static constexpr uint32_t GREE_IR_FREQUENCY = 38000; +static constexpr uint32_t GREE_HEADER_MARK = 9000; +static constexpr uint32_t GREE_HEADER_SPACE = 4000; +static constexpr uint32_t GREE_BIT_MARK = 620; +static constexpr uint32_t GREE_ONE_SPACE = 1600; +static constexpr uint32_t GREE_ZERO_SPACE = 540; +static constexpr uint32_t GREE_MESSAGE_SPACE = 19000; // Timing specific for YAC features (I-Feel mode) -const uint32_t GREE_YAC_HEADER_MARK = 6000; -const uint32_t GREE_YAC_HEADER_SPACE = 3000; -const uint32_t GREE_YAC_BIT_MARK = 650; +static constexpr uint32_t GREE_YAC_HEADER_MARK = 6000; +static constexpr uint32_t GREE_YAC_HEADER_SPACE = 3000; +static constexpr uint32_t GREE_YAC_BIT_MARK = 650; // Timing specific to YAC1FB9 -const uint32_t GREE_YAC1FB9_HEADER_SPACE = 4500; -const uint32_t GREE_YAC1FB9_MESSAGE_SPACE = 19980; +static constexpr uint32_t GREE_YAC1FB9_HEADER_SPACE = 4500; +static constexpr uint32_t GREE_YAC1FB9_MESSAGE_SPACE = 19980; // State Frame size -const uint8_t GREE_STATE_FRAME_SIZE = 8; +static constexpr uint8_t GREE_STATE_FRAME_SIZE = 8; // Only available on YAN // Vertical air directions. Note that these cannot be set on all heat pumps -const uint8_t GREE_VDIR_AUTO = 0x00; -const uint8_t GREE_VDIR_MANUAL = 0x00; -const uint8_t GREE_VDIR_SWING = 0x01; -const uint8_t GREE_VDIR_UP = 0x02; -const uint8_t GREE_VDIR_MUP = 0x03; -const uint8_t GREE_VDIR_MIDDLE = 0x04; -const uint8_t GREE_VDIR_MDOWN = 0x05; -const uint8_t GREE_VDIR_DOWN = 0x06; +static constexpr uint8_t GREE_VDIR_AUTO = 0x00; +static constexpr uint8_t GREE_VDIR_MANUAL = 0x00; +static constexpr uint8_t GREE_VDIR_SWING = 0x01; +static constexpr uint8_t GREE_VDIR_UP = 0x02; +static constexpr uint8_t GREE_VDIR_MUP = 0x03; +static constexpr uint8_t GREE_VDIR_MIDDLE = 0x04; +static constexpr uint8_t GREE_VDIR_MDOWN = 0x05; +static constexpr uint8_t GREE_VDIR_DOWN = 0x06; // Only available on YAC/YAG // Horizontal air directions. Note that these cannot be set on all heat pumps -const uint8_t GREE_HDIR_AUTO = 0x00; -const uint8_t GREE_HDIR_MANUAL = 0x00; -const uint8_t GREE_HDIR_SWING = 0x01; -const uint8_t GREE_HDIR_LEFT = 0x02; -const uint8_t GREE_HDIR_MLEFT = 0x03; -const uint8_t GREE_HDIR_MIDDLE = 0x04; -const uint8_t GREE_HDIR_MRIGHT = 0x05; -const uint8_t GREE_HDIR_RIGHT = 0x06; +static constexpr uint8_t GREE_HDIR_AUTO = 0x00; +static constexpr uint8_t GREE_HDIR_MANUAL = 0x00; +static constexpr uint8_t GREE_HDIR_SWING = 0x01; +static constexpr uint8_t GREE_HDIR_LEFT = 0x02; +static constexpr uint8_t GREE_HDIR_MLEFT = 0x03; +static constexpr uint8_t GREE_HDIR_MIDDLE = 0x04; +static constexpr uint8_t GREE_HDIR_MRIGHT = 0x05; +static constexpr uint8_t GREE_HDIR_RIGHT = 0x06; // Only available on YX1FF // Turbo (high) fan mode + sleep preset mode -const uint8_t GREE_FAN_TURBO = 0x80; -const uint8_t GREE_FAN_TURBO_BIT = 0x10; -const uint8_t GREE_PRESET_NONE = 0x00; -const uint8_t GREE_PRESET_SLEEP = 0x01; -const uint8_t GREE_PRESET_SLEEP_BIT = 0x80; +static constexpr uint8_t GREE_FAN_TURBO = 0x80; +static constexpr uint8_t GREE_FAN_TURBO_BIT = 0x10; +static constexpr uint8_t GREE_PRESET_NONE = 0x00; +static constexpr uint8_t GREE_PRESET_SLEEP = 0x01; +static constexpr uint8_t GREE_PRESET_SLEEP_BIT = 0x80; // Model codes enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG }; @@ -90,6 +89,7 @@ class GreeClimate : public climate_ir::ClimateIR { climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} void set_model(Model model); + void set_mode_bit(uint8_t bit_mask, bool enabled); protected: // Transmit via IR the state of this climate controller. @@ -103,7 +103,7 @@ class GreeClimate : public climate_ir::ClimateIR { uint8_t preset_(); Model model_{}; + uint8_t mode_bits_{0}; // Combined mode bits for remote_state[2] }; -} // namespace gree -} // namespace esphome +} // namespace esphome::gree diff --git a/esphome/components/gree/switch/__init__.py b/esphome/components/gree/switch/__init__.py new file mode 100644 index 0000000000..111fea65d2 --- /dev/null +++ b/esphome/components/gree/switch/__init__.py @@ -0,0 +1,74 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import CONF_LIGHT, DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG +import esphome.final_validate as fv + +from .. import gree_ns +from ..climate import CONF_MODEL, GreeClimate + +CODEOWNERS = ["@nagyrobi"] + +GreeModeBitSwitch = gree_ns.class_("GreeModeBitSwitch", switch.Switch, cg.Component) + +CONF_TURBO = "turbo" +CONF_HEALTH = "health" +CONF_XFAN = "xfan" +CONF_GREE_ID = "gree_id" + +# Switch configurations: (config_key, display_name, bit_mask, icon) +SWITCH_CONFIGS = ( + (CONF_TURBO, "Gree Turbo Switch", 0x10, "mdi:car-turbocharger"), + (CONF_LIGHT, "Gree Light Switch", 0x20, "mdi:led-outline"), + (CONF_HEALTH, "Gree Health Switch", 0x40, "mdi:pine-tree"), + (CONF_XFAN, "Gree X-FAN Switch", 0x80, "mdi:wall-sconce-flat"), +) + +SUPPORTED_MODELS = { + "yan", + "yaa", + "yac", + "yac1fb9", +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_GREE_ID): cv.use_id(GreeClimate), + **{ + cv.Optional(key): switch.switch_schema( + GreeModeBitSwitch, + icon=icon, + default_restore_mode="RESTORE_DEFAULT_OFF", + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + ) + for key, _, _, icon in SWITCH_CONFIGS + }, + } +) + + +def _validate_model(config): + full_config = fv.full_config.get() + climate_path = full_config.get_path_for_id(config[CONF_GREE_ID])[:-1] + climate_conf = full_config.get_config_for_path(climate_path) + if climate_conf[CONF_MODEL] not in SUPPORTED_MODELS: + raise cv.Invalid( + "Gree switches are only supported for the " + + ", ".join(SUPPORTED_MODELS) + + " models" + ) + + +FINAL_VALIDATE_SCHEMA = _validate_model + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_GREE_ID]) + + for conf_key, name, bit_mask, _ in SWITCH_CONFIGS: + if switch_conf := config.get(conf_key): + sw = cg.new_Pvariable(switch_conf[cv.CONF_ID], name, bit_mask) + await switch.register_switch(sw, switch_conf) + await cg.register_component(sw, switch_conf) + await cg.register_parented(sw, parent) diff --git a/esphome/components/gree/switch/gree_switch.cpp b/esphome/components/gree/switch/gree_switch.cpp new file mode 100644 index 0000000000..13f14e5453 --- /dev/null +++ b/esphome/components/gree/switch/gree_switch.cpp @@ -0,0 +1,24 @@ +#include "gree_switch.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace gree { + +static const char *const TAG = "gree.switch"; + +void GreeModeBitSwitch::setup() { + auto initial = this->get_initial_state_with_restore_mode(); + if (initial.has_value()) { + this->write_state(*initial); + } +} + +void GreeModeBitSwitch::dump_config() { log_switch(TAG, " ", this->name_, this); } + +void GreeModeBitSwitch::write_state(bool state) { + this->parent_->set_mode_bit(this->bit_mask_, state); + this->publish_state(state); +} + +} // namespace gree +} // namespace esphome diff --git a/esphome/components/gree/switch/gree_switch.h b/esphome/components/gree/switch/gree_switch.h new file mode 100644 index 0000000000..239ac4bf17 --- /dev/null +++ b/esphome/components/gree/switch/gree_switch.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" +#include "esphome/components/gree/gree.h" + +namespace esphome { +namespace gree { + +class GreeModeBitSwitch : public switch_::Switch, public Component, public Parented { + public: + GreeModeBitSwitch(const char *name, uint8_t bit_mask) : name_(name), bit_mask_(bit_mask) {} + + void setup() override; + void dump_config() override; + void write_state(bool state) override; + + protected: + const char *name_; + uint8_t bit_mask_; +}; + +} // namespace gree +} // namespace esphome diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 992a86cc21..b11880a042 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -79,13 +79,13 @@ void GT911Touchscreen::setup_internal_() { } } if (err != i2c::ERROR_OK) { - this->mark_failed("Calibration error"); + this->mark_failed(LOG_STR("Calibration error")); return; } } if (err != i2c::ERROR_OK) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } this->setup_done_ = true; diff --git a/esphome/components/hc8/__init__.py b/esphome/components/hc8/__init__.py new file mode 100644 index 0000000000..e1028456b0 --- /dev/null +++ b/esphome/components/hc8/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@omartijn"] diff --git a/esphome/components/hc8/hc8.cpp b/esphome/components/hc8/hc8.cpp new file mode 100644 index 0000000000..4d0f77df1b --- /dev/null +++ b/esphome/components/hc8/hc8.cpp @@ -0,0 +1,100 @@ +#include "hc8.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +namespace esphome::hc8 { + +static const char *const TAG = "hc8"; +static const std::array HC8_COMMAND_GET_PPM{0x64, 0x69, 0x03, 0x5E, 0x4E}; +static const std::array HC8_COMMAND_CALIBRATE_PREAMBLE{0x11, 0x03, 0x03}; + +void HC8Component::setup() { + // send an initial query to the device, this will + // get it out of "active output mode", where it + // generates data every second + this->write_array(HC8_COMMAND_GET_PPM); + this->flush(); + + // ensure the buffer is empty + while (this->available()) + this->read(); +} + +void HC8Component::update() { + uint32_t now_ms = App.get_loop_component_start_time(); + uint32_t warmup_ms = this->warmup_seconds_ * 1000; + if (now_ms < warmup_ms) { + ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000); + this->status_set_warning(); + return; + } + + while (this->available()) + this->read(); + + this->write_array(HC8_COMMAND_GET_PPM); + this->flush(); + + // the sensor is a bit slow in responding, so trying to + // read immediately after sending a query will timeout + this->set_timeout(50, [this]() { + std::array response; + if (!this->read_array(response.data(), response.size())) { + ESP_LOGW(TAG, "Reading data from HC8 failed!"); + this->status_set_warning(); + return; + } + + if (response[0] != 0x64 || response[1] != 0x69) { + ESP_LOGW(TAG, "Invalid preamble from HC8!"); + this->status_set_warning(); + return; + } + + if (crc16(response.data(), 12) != encode_uint16(response[13], response[12])) { + ESP_LOGW(TAG, "HC8 Checksum mismatch"); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + + const uint16_t ppm = encode_uint16(response[5], response[4]); + ESP_LOGD(TAG, "HC8 Received CO₂=%uppm", ppm); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(ppm); + }); +} + +void HC8Component::calibrate(uint16_t baseline) { + ESP_LOGD(TAG, "HC8 Calibrating baseline to %uppm", baseline); + + std::array command{}; + std::copy(begin(HC8_COMMAND_CALIBRATE_PREAMBLE), end(HC8_COMMAND_CALIBRATE_PREAMBLE), begin(command)); + command[3] = baseline >> 8; + command[4] = baseline; + command[5] = 0; + + // the last byte is a checksum over the data + for (uint8_t i = 0; i < 5; ++i) + command[5] -= command[i]; + + this->write_array(command); + this->flush(); +} + +float HC8Component::get_setup_priority() const { return setup_priority::DATA; } + +void HC8Component::dump_config() { + ESP_LOGCONFIG(TAG, + "HC8:\n" + " Warmup time: %" PRIu32 " s", + this->warmup_seconds_); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + this->check_uart_settings(9600); +} + +} // namespace esphome::hc8 diff --git a/esphome/components/hc8/hc8.h b/esphome/components/hc8/hc8.h new file mode 100644 index 0000000000..7711fb8c97 --- /dev/null +++ b/esphome/components/hc8/hc8.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome::hc8 { + +class HC8Component : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override; + + void setup() override; + void update() override; + void dump_config() override; + + void calibrate(uint16_t baseline); + + void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + void set_warmup_seconds(uint32_t seconds) { warmup_seconds_ = seconds; } + + protected: + sensor::Sensor *co2_sensor_{nullptr}; + uint32_t warmup_seconds_{0}; +}; + +template class HC8CalibrateAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, baseline) + + void play(const Ts &...x) override { this->parent_->calibrate(this->baseline_.value(x...)); } +}; + +} // namespace esphome::hc8 diff --git a/esphome/components/hc8/sensor.py b/esphome/components/hc8/sensor.py new file mode 100644 index 0000000000..90698b2661 --- /dev/null +++ b/esphome/components/hc8/sensor.py @@ -0,0 +1,79 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import sensor, uart +import esphome.config_validation as cv +from esphome.const import ( + CONF_BASELINE, + CONF_CO2, + CONF_ID, + DEVICE_CLASS_CARBON_DIOXIDE, + ICON_MOLECULE_CO2, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, +) + +DEPENDENCIES = ["uart"] + +CONF_WARMUP_TIME = "warmup_time" + +hc8_ns = cg.esphome_ns.namespace("hc8") +HC8Component = hc8_ns.class_("HC8Component", cg.PollingComponent, uart.UARTDevice) +HC8CalibrateAction = hc8_ns.class_("HC8CalibrateAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HC8Component), + cv.Optional(CONF_CO2): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + CONF_WARMUP_TIME, default="75s" + ): cv.positive_time_period_seconds, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "hc8", + baud_rate=9600, + require_rx=True, + require_tx=True, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if co2 := config.get(CONF_CO2): + sens = await sensor.new_sensor(co2) + cg.add(var.set_co2_sensor(sens)) + + cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME])) + + +CALIBRATION_ACTION_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(HC8Component), + cv.Required(CONF_BASELINE): cv.templatable(cv.uint16_t), + } +) + + +@automation.register_action( + "hc8.calibrate", HC8CalibrateAction, CALIBRATION_ACTION_SCHEMA +) +async def hc8_calibration_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_BASELINE], args, cg.uint16) + cg.add(var.set_baseline(template_)) + return var diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index f4d2ca6c1d..67447a3123 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -181,6 +181,11 @@ void HeatpumpIRClimate::transmit_state() { power_mode_cmd = POWER_ON; operating_mode_cmd = MODE_HEAT; break; + // Map HEAT_COOL to hardware AUTO mode (automatic heat/cool changeover based on temperature). + // In hardware AUTO mode, the device automatically switches between heating and cooling + // based on the current temperature versus the target temperature. + // See https://github.com/esphome/esphome/issues/11161 for further discussion. + case climate::CLIMATE_MODE_HEAT_COOL: case climate::CLIMATE_MODE_AUTO: power_mode_cmd = POWER_ON; operating_mode_cmd = MODE_AUTO; diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index ab15a2340d..c0f14c7105 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -8,6 +8,9 @@ namespace esphome::hlk_fm22x { static const char *const TAG = "hlk_fm22x"; +// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name) +static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36; + void HlkFm22xComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); this->set_enrolling_(false); @@ -142,7 +145,10 @@ void HlkFm22xComponent::recv_command_() { data.push_back(byte); } - ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)]; + ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size())); +#endif byte = this->read(); if (byte != checksum) { diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index 5ecc715ea1..9c981d3c44 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -189,7 +189,7 @@ template class EnrollmentAction : public Action, public P TEMPLATABLE_VALUE(std::string, name) TEMPLATABLE_VALUE(uint8_t, direction) - void play(Ts... x) override { + void play(const Ts &...x) override { auto name = this->name_.value(x...); auto direction = (HlkFm22xFaceDirection) this->direction_.value(x...); this->parent_->enroll_face(name, direction); @@ -200,7 +200,7 @@ template class DeleteAction : public Action, public Paren public: TEMPLATABLE_VALUE(int16_t, face_id) - void play(Ts... x) override { + void play(const Ts &...x) override { auto face_id = this->face_id_.value(x...); this->parent_->delete_face(face_id); } @@ -208,17 +208,17 @@ template class DeleteAction : public Action, public Paren template class DeleteAllAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->delete_all_faces(); } + void play(const Ts &...x) override { this->parent_->delete_all_faces(); } }; template class ScanAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->scan_face(); } + void play(const Ts &...x) override { this->parent_->scan_face(); } }; template class ResetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->reset(); } + void play(const Ts &...x) override { this->parent_->reset(); } }; } // namespace esphome::hlk_fm22x diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index 73696bd2a5..f037ee9d8b 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -33,15 +33,15 @@ void HLW8012Component::setup() { } } void HLW8012Component::dump_config() { - ESP_LOGCONFIG(TAG, "HLW8012:"); - LOG_PIN(" SEL Pin: ", this->sel_pin_) - LOG_PIN(" CF Pin: ", this->cf_pin_) - LOG_PIN(" CF1 Pin: ", this->cf1_pin_) ESP_LOGCONFIG(TAG, + "HLW8012:\n" " Change measurement mode every %" PRIu32 "\n" " Current resistor: %.1f mΩ\n" " Voltage Divider: %.1f", this->change_mode_every_, this->current_resistor_ * 1000.0f, this->voltage_divider_); + LOG_PIN(" SEL Pin: ", this->sel_pin_); + LOG_PIN(" CF Pin: ", this->cf_pin_); + LOG_PIN(" CF1 Pin: ", this->cf1_pin_); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_); diff --git a/esphome/components/hlw8032/__init__.py b/esphome/components/hlw8032/__init__.py new file mode 100644 index 0000000000..4908e10037 --- /dev/null +++ b/esphome/components/hlw8032/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@rici4kubicek"] diff --git a/esphome/components/hlw8032/hlw8032.cpp b/esphome/components/hlw8032/hlw8032.cpp new file mode 100644 index 0000000000..55e6664a8b --- /dev/null +++ b/esphome/components/hlw8032/hlw8032.cpp @@ -0,0 +1,194 @@ +#include "hlw8032.h" +#include "esphome/core/log.h" +#include + +namespace esphome::hlw8032 { + +static const char *const TAG = "hlw8032"; + +static constexpr uint8_t STATE_REG_OFFSET = 0; +static constexpr uint8_t VOLTAGE_PARAM_OFFSET = 2; +static constexpr uint8_t VOLTAGE_REG_OFFSET = 5; +static constexpr uint8_t CURRENT_PARAM_OFFSET = 8; +static constexpr uint8_t CURRENT_REG_OFFSET = 11; +static constexpr uint8_t POWER_PARAM_OFFSET = 14; +static constexpr uint8_t POWER_REG_OFFSET = 17; +static constexpr uint8_t DATA_UPDATE_REG_OFFSET = 20; +static constexpr uint8_t CHECKSUM_REG_OFFSET = 23; +static constexpr uint8_t PARAM_REG_USABLE_BIT = (1 << 0); +static constexpr uint8_t POWER_OVERFLOW_BIT = (1 << 1); +static constexpr uint8_t CURRENT_OVERFLOW_BIT = (1 << 2); +static constexpr uint8_t VOLTAGE_OVERFLOW_BIT = (1 << 3); +static constexpr uint8_t HAVE_POWER_BIT = (1 << 4); +static constexpr uint8_t HAVE_CURRENT_BIT = (1 << 5); +static constexpr uint8_t HAVE_VOLTAGE_BIT = (1 << 6); +static constexpr uint8_t CHECK_REG = 0x5A; +static constexpr uint8_t STATE_REG_CORRECTION_FUNC_NORMAL = 0x55; +static constexpr uint8_t STATE_REG_CORRECTION_FUNC_FAIL = 0xAA; +static constexpr uint8_t STATE_REG_CORRECTION_MASK = 0xF0; +static constexpr uint8_t STATE_REG_OVERFLOW_MASK = 0xF; +static constexpr uint8_t PACKET_LENGTH = 24; + +void HLW8032Component::loop() { + while (this->available()) { + uint8_t data = this->read(); + if (!this->header_found_) { + if ((data == STATE_REG_CORRECTION_FUNC_NORMAL) || (data == STATE_REG_CORRECTION_FUNC_FAIL) || + (data & STATE_REG_CORRECTION_MASK) == STATE_REG_CORRECTION_MASK) { + this->header_found_ = true; + this->raw_data_[0] = data; + } + } else if (data == CHECK_REG) { + this->raw_data_[1] = data; + this->raw_data_index_ = 2; + this->check_ = 0; + } else if (this->raw_data_index_ >= 2 && this->raw_data_index_ < PACKET_LENGTH) { + this->raw_data_[this->raw_data_index_++] = data; + if (this->raw_data_index_ < PACKET_LENGTH) { + this->check_ += data; + } else if (this->raw_data_index_ == PACKET_LENGTH) { + if (this->check_ == this->raw_data_[CHECKSUM_REG_OFFSET]) { + this->parse_data_(); + } else { + ESP_LOGW(TAG, "Invalid checksum: 0x%02X != 0x%02X", this->check_, this->raw_data_[CHECKSUM_REG_OFFSET]); + } + this->raw_data_index_ = 0; + this->header_found_ = false; + memset(this->raw_data_, 0, PACKET_LENGTH); + } + } + } +} + +uint32_t HLW8032Component::read_uint24_(uint8_t offset) { + return encode_uint24(this->raw_data_[offset], this->raw_data_[offset + 1], this->raw_data_[offset + 2]); +} + +void HLW8032Component::parse_data_() { + // Parse header + uint8_t state_reg = this->raw_data_[STATE_REG_OFFSET]; + + if (state_reg == STATE_REG_CORRECTION_FUNC_FAIL) { + ESP_LOGE(TAG, "The chip's function of error correction fails."); + return; + } + + // Parse data frame + uint32_t voltage_parameter = this->read_uint24_(VOLTAGE_PARAM_OFFSET); + uint32_t voltage_reg = this->read_uint24_(VOLTAGE_REG_OFFSET); + uint32_t current_parameter = this->read_uint24_(CURRENT_PARAM_OFFSET); + uint32_t current_reg = this->read_uint24_(CURRENT_REG_OFFSET); + uint32_t power_parameter = this->read_uint24_(POWER_PARAM_OFFSET); + uint32_t power_reg = this->read_uint24_(POWER_REG_OFFSET); + uint8_t data_update_register = this->raw_data_[DATA_UPDATE_REG_OFFSET]; + + bool have_power = data_update_register & HAVE_POWER_BIT; + bool have_current = data_update_register & HAVE_CURRENT_BIT; + bool have_voltage = data_update_register & HAVE_VOLTAGE_BIT; + + bool power_cycle_exceeds_range = false; + bool parameter_regs_usable = true; + + if ((state_reg & STATE_REG_CORRECTION_MASK) == STATE_REG_CORRECTION_MASK) { + if (state_reg & STATE_REG_OVERFLOW_MASK) { + if (state_reg & VOLTAGE_OVERFLOW_BIT) { + have_voltage = false; + } + if (state_reg & CURRENT_OVERFLOW_BIT) { + have_current = false; + } + if (state_reg & POWER_OVERFLOW_BIT) { + have_power = false; + } + if (state_reg & PARAM_REG_USABLE_BIT) { + parameter_regs_usable = false; + } + + ESP_LOGW(TAG, + "Reports: (0x%02X)\n" + " Voltage REG overflows: %s\n" + " Current REG overflows: %s\n" + " Power REG overflows: %s\n" + " Voltage/Current/Power Parameter REGs not usable: %s\n", + state_reg, YESNO(!have_voltage), YESNO(!have_current), YESNO(!have_power), + YESNO(!parameter_regs_usable)); + + if (!parameter_regs_usable) { + return; + } + } + power_cycle_exceeds_range = have_power; + } + + ESP_LOGVV(TAG, + "Parsed data:\n" + " Voltage: Parameter REG 0x%06" PRIX32 ", REG 0x%06" PRIX32 "\n" + " Current: Parameter REG 0x%06" PRIX32 ", REG 0x%06" PRIX32 "\n" + " Power: Parameter REG 0x%06" PRIX32 ", REG 0x%06" PRIX32 "\n" + " Data Update: REG 0x%02" PRIX8 "\n", + voltage_parameter, voltage_reg, current_parameter, current_reg, power_parameter, power_reg, + data_update_register); + + const float current_multiplier = 1 / (this->current_resistor_ * 1000); + + float voltage = 0.0f; + if (have_voltage && voltage_reg) { + voltage = float(voltage_parameter) * this->voltage_divider_ / float(voltage_reg); + } + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(voltage); + } + + float power = 0.0f; + if (have_power && power_reg && !power_cycle_exceeds_range) { + power = (float(power_parameter) / float(power_reg)) * this->voltage_divider_ * current_multiplier; + } + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(power); + } + + float current = 0.0f; + if (have_current && current_reg) { + current = float(current_parameter) * current_multiplier / float(current_reg); + } + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(current); + } + + float pf = NAN; + const float apparent_power = voltage * current; + if (have_voltage && have_current) { + if (have_power || power_cycle_exceeds_range) { + if (apparent_power > 0) { + pf = power / apparent_power; + if (pf < 0 || pf > 1) { + ESP_LOGD(TAG, "Impossible power factor: %.4f not in interval [0, 1]", pf); + pf = NAN; + } + } else if (apparent_power == 0 && power == 0) { + // No load, report ideal power factor + pf = 1.0f; + } + } + } + if (this->apparent_power_sensor_ != nullptr) { + this->apparent_power_sensor_->publish_state(apparent_power); + } + if (this->power_factor_sensor_ != nullptr) { + this->power_factor_sensor_->publish_state(pf); + } +} + +void HLW8032Component::dump_config() { + ESP_LOGCONFIG(TAG, + "Configuration:\n" + " Current resistor: %.1f mΩ\n" + " Voltage Divider: %.3f", + this->current_resistor_ * 1000.0f, this->voltage_divider_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); + LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_); + LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_); +} +} // namespace esphome::hlw8032 diff --git a/esphome/components/hlw8032/hlw8032.h b/esphome/components/hlw8032/hlw8032.h new file mode 100644 index 0000000000..d4c7dbd26c --- /dev/null +++ b/esphome/components/hlw8032/hlw8032.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome::hlw8032 { + +class HLW8032Component : public Component, public uart::UARTDevice { + public: + void loop() override; + void dump_config() override; + + void set_current_resistor(float current_resistor) { this->current_resistor_ = current_resistor; } + void set_voltage_divider(float voltage_divider) { this->voltage_divider_ = voltage_divider; } + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } + void set_apparent_power_sensor(sensor::Sensor *apparent_power_sensor) { + this->apparent_power_sensor_ = apparent_power_sensor; + } + void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { + this->power_factor_sensor_ = power_factor_sensor; + } + + protected: + void parse_data_(); + uint32_t read_uint24_(uint8_t offset); + + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *apparent_power_sensor_{nullptr}; + sensor::Sensor *power_factor_sensor_{nullptr}; + + float current_resistor_{0.001f}; + float voltage_divider_{1.720f}; + uint8_t raw_data_[24]{}; + uint8_t check_{0}; + uint8_t raw_data_index_{0}; + bool header_found_{false}; +}; + +} // namespace esphome::hlw8032 diff --git a/esphome/components/hlw8032/sensor.py b/esphome/components/hlw8032/sensor.py new file mode 100644 index 0000000000..96800e46f4 --- /dev/null +++ b/esphome/components/hlw8032/sensor.py @@ -0,0 +1,93 @@ +import esphome.codegen as cg +from esphome.components import sensor, uart +import esphome.config_validation as cv +from esphome.const import ( + CONF_APPARENT_POWER, + CONF_CURRENT, + CONF_CURRENT_RESISTOR, + CONF_ID, + CONF_POWER, + CONF_POWER_FACTOR, + CONF_VOLTAGE, + CONF_VOLTAGE_DIVIDER, + DEVICE_CLASS_APPARENT_POWER, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_VOLT, + UNIT_VOLT_AMPS, + UNIT_WATT, +) + +DEPENDENCIES = ["uart"] + +hlw8032_ns = cg.esphome_ns.namespace("hlw8032") +HLW8032Component = hlw8032_ns.class_("HLW8032Component", cg.Component, uart.UARTDevice) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(HLW8032Component), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_APPARENT_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, + cv.Optional(CONF_VOLTAGE_DIVIDER, default=1.720): cv.positive_float, + } +).extend(uart.UART_DEVICE_SCHEMA) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "hlw8032", baud_rate=4800, require_rx=True, data_bits=8, parity="EVEN" +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if voltage_config := config.get(CONF_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) + cg.add(var.set_voltage_sensor(sens)) + if current_config := config.get(CONF_CURRENT): + sens = await sensor.new_sensor(current_config) + cg.add(var.set_current_sensor(sens)) + if power_config := config.get(CONF_POWER): + sens = await sensor.new_sensor(power_config) + cg.add(var.set_power_sensor(sens)) + if apparent_power_config := config.get(CONF_APPARENT_POWER): + sens = await sensor.new_sensor(apparent_power_config) + cg.add(var.set_apparent_power_sensor(sens)) + if power_factor_config := config.get(CONF_POWER_FACTOR): + sens = await sensor.new_sensor(power_factor_config) + cg.add(var.set_power_factor_sensor(sens)) + cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR])) + cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER])) diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h deleted file mode 100644 index aa01060d2c..0000000000 --- a/esphome/components/hm3301/aqi_calculator.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once -#include -#include "abstract_aqi_calculator.h" -// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf - -namespace esphome { -namespace hm3301 { - -class AQICalculator : public AbstractAQICalculator { - public: - uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { - int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); - int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); - - return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; - } - - protected: - static const int AMOUNT_OF_LEVELS = 6; - - int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; - - int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55}, - {56, 125}, {126, 225}, {226, INT_MAX}}; - - int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, - {255, 354}, {355, 424}, {425, INT_MAX}}; - - int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { - int grid_index = get_grid_index_(value, array); - int aqi_lo = index_grid_[grid_index][0]; - int aqi_hi = index_grid_[grid_index][1]; - int conc_lo = array[grid_index][0]; - int conc_hi = array[grid_index][1]; - - return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; - } - - int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { - for (int i = 0; i < AMOUNT_OF_LEVELS; i++) { - if (value >= array[i][0] && value <= array[i][1]) { - return i; - } - } - return -1; - } -}; - -} // namespace hm3301 -} // namespace esphome diff --git a/esphome/components/hm3301/caqi_calculator.h b/esphome/components/hm3301/caqi_calculator.h deleted file mode 100644 index 3f338776d8..0000000000 --- a/esphome/components/hm3301/caqi_calculator.h +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -#include "esphome/core/log.h" -#include "abstract_aqi_calculator.h" - -namespace esphome { -namespace hm3301 { - -class CAQICalculator : public AbstractAQICalculator { - public: - uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { - int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); - int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); - - return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; - } - - protected: - static const int AMOUNT_OF_LEVELS = 5; - - int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; - - int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}}; - - int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}}; - - int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { - int grid_index = get_grid_index_(value, array); - if (grid_index == -1) { - return -1; - } - - int aqi_lo = index_grid_[grid_index][0]; - int aqi_hi = index_grid_[grid_index][1]; - int conc_lo = array[grid_index][0]; - int conc_hi = array[grid_index][1]; - - return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; - } - - int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { - for (int i = 0; i < AMOUNT_OF_LEVELS; i++) { - if (value >= array[i][0] && value <= array[i][1]) { - return i; - } - } - return -1; - } -}; - -} // namespace hm3301 -} // namespace esphome diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index a19d9dd09f..00fb85397c 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -63,7 +63,7 @@ void HM3301Component::update() { int16_t aqi_value = -1; if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) { - AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); + aqi::AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value); } diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index 6779b4e195..e155ed6b4b 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -3,7 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" -#include "aqi_calculator_factory.h" +#include "esphome/components/aqi/aqi_calculator_factory.h" namespace esphome { namespace hm3301 { @@ -19,7 +19,7 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; } void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; } - void set_aqi_calculation_type(AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; } + void set_aqi_calculation_type(aqi::AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; } void setup() override; void dump_config() override; @@ -41,8 +41,8 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *pm_10_0_sensor_{nullptr}; sensor::Sensor *aqi_sensor_{nullptr}; - AQICalculatorType aqi_calc_type_; - AQICalculatorFactory aqi_calculator_factory_ = AQICalculatorFactory(); + aqi::AQICalculatorType aqi_calc_type_; + aqi::AQICalculatorFactory aqi_calculator_factory_ = aqi::AQICalculatorFactory(); bool validate_checksum_(const uint8_t *data); uint16_t get_sensor_value_(const uint8_t *data, uint8_t i); diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 5eb1773518..9546ae1c3c 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -1,5 +1,8 @@ +import logging + import esphome.codegen as cg from esphome.components import i2c, sensor +from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_AQI, CONF_CALCULATION_TYPE import esphome.config_validation as cv from esphome.const import ( CONF_ID, @@ -15,24 +18,19 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, ) +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["aqi"] CODEOWNERS = ["@freekode"] hm3301_ns = cg.esphome_ns.namespace("hm3301") HM3301Component = hm3301_ns.class_( "HM3301Component", cg.PollingComponent, i2c.I2CDevice ) -AQICalculatorType = hm3301_ns.enum("AQICalculatorType") -CONF_AQI = "aqi" -CONF_CALCULATION_TYPE = "calculation_type" UNIT_INDEX = "index" -AQI_CALCULATION_TYPE = { - "CAQI": AQICalculatorType.CAQI_TYPE, - "AQI": AQICalculatorType.AQI_TYPE, -} - def _validate(config): if CONF_AQI in config and CONF_PM_2_5 not in config: @@ -105,7 +103,12 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_PM_10_0]) cg.add(var.set_pm_10_0_sensor(sens)) + # Remove before 2026.12.0 if CONF_AQI in config: + _LOGGER.warning( + "The 'aqi' option in hm3301 is deprecated, " + "please use the standalone 'aqi' sensor platform instead." + ) sens = await sensor.new_sensor(config[CONF_AQI]) cg.add(var.set_aqi_sensor(sens)) cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE])) diff --git a/esphome/components/hmac_md5/__init__.py b/esphome/components/hmac_md5/__init__.py index fe245c0cfd..e37eb9b116 100644 --- a/esphome/components/hmac_md5/__init__.py +++ b/esphome/components/hmac_md5/__init__.py @@ -1,2 +1,6 @@ +import esphome.config_validation as cv + AUTO_LOAD = ["md5"] CODEOWNERS = ["@dwmw2"] + +CONFIG_SCHEMA = cv.Schema({}) diff --git a/esphome/components/hmac_md5/hmac_md5.h b/esphome/components/hmac_md5/hmac_md5.h index b83b9d5421..fb9479e3af 100644 --- a/esphome/components/hmac_md5/hmac_md5.h +++ b/esphome/components/hmac_md5/hmac_md5.h @@ -30,7 +30,7 @@ class HmacMD5 { void get_bytes(uint8_t *output); /// Retrieve the HMAC-MD5 digest as hex characters. - /// The output must be able to hold 32 bytes or more. + /// The output must be able to hold 33 bytes or more (32 hex chars + null terminator). void get_hex(char *output); /// Compare the digest against a provided byte-encoded digest (16 bytes). diff --git a/esphome/components/hmac_sha256/__init__.py b/esphome/components/hmac_sha256/__init__.py new file mode 100644 index 0000000000..158d740dc5 --- /dev/null +++ b/esphome/components/hmac_sha256/__init__.py @@ -0,0 +1,6 @@ +import esphome.config_validation as cv + +AUTO_LOAD = ["sha256"] +CODEOWNERS = ["@dwmw2"] + +CONFIG_SCHEMA = cv.Schema({}) diff --git a/esphome/components/hmac_sha256/hmac_sha256.cpp b/esphome/components/hmac_sha256/hmac_sha256.cpp new file mode 100644 index 0000000000..cf5daf63af --- /dev/null +++ b/esphome/components/hmac_sha256/hmac_sha256.cpp @@ -0,0 +1,102 @@ +#include +#include +#include "hmac_sha256.h" +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST) +#include "esphome/core/helpers.h" + +namespace esphome::hmac_sha256 { + +constexpr size_t SHA256_DIGEST_SIZE = 32; + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + +HmacSHA256::~HmacSHA256() { mbedtls_md_free(&this->ctx_); } + +void HmacSHA256::init(const uint8_t *key, size_t len) { + mbedtls_md_init(&this->ctx_); + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + mbedtls_md_setup(&this->ctx_, md_info, 1); // 1 = HMAC mode + mbedtls_md_hmac_starts(&this->ctx_, key, len); +} + +void HmacSHA256::add(const uint8_t *data, size_t len) { mbedtls_md_hmac_update(&this->ctx_, data, len); } + +void HmacSHA256::calculate() { mbedtls_md_hmac_finish(&this->ctx_, this->digest_); } + +void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); } + +void HmacSHA256::get_hex(char *output) { + for (size_t i = 0; i < SHA256_DIGEST_SIZE; i++) { + sprintf(output + (i * 2), "%02x", this->digest_[i]); + } +} + +bool HmacSHA256::equals_bytes(const uint8_t *expected) { + return memcmp(this->digest_, expected, SHA256_DIGEST_SIZE) == 0; +} + +bool HmacSHA256::equals_hex(const char *expected) { + char hex_output[SHA256_DIGEST_SIZE * 2 + 1]; + this->get_hex(hex_output); + hex_output[SHA256_DIGEST_SIZE * 2] = '\0'; + return strncmp(hex_output, expected, SHA256_DIGEST_SIZE * 2) == 0; +} + +#else + +HmacSHA256::~HmacSHA256() = default; + +// HMAC block size for SHA256 (RFC 2104) +constexpr size_t HMAC_BLOCK_SIZE = 64; + +void HmacSHA256::init(const uint8_t *key, size_t len) { + uint8_t ipad[HMAC_BLOCK_SIZE], opad[HMAC_BLOCK_SIZE]; + + memset(ipad, 0, sizeof(ipad)); + if (len > HMAC_BLOCK_SIZE) { + sha256::SHA256 keysha256; + keysha256.init(); + keysha256.add(key, len); + keysha256.calculate(); + keysha256.get_bytes(ipad); + } else { + memcpy(ipad, key, len); + } + memcpy(opad, ipad, sizeof(opad)); + + for (size_t i = 0; i < HMAC_BLOCK_SIZE; i++) { + ipad[i] ^= 0x36; + opad[i] ^= 0x5c; + } + + this->ihash_.init(); + this->ihash_.add(ipad, sizeof(ipad)); + + this->ohash_.init(); + this->ohash_.add(opad, sizeof(opad)); +} + +void HmacSHA256::add(const uint8_t *data, size_t len) { this->ihash_.add(data, len); } + +void HmacSHA256::calculate() { + uint8_t ibytes[32]; + + this->ihash_.calculate(); + this->ihash_.get_bytes(ibytes); + + this->ohash_.add(ibytes, sizeof(ibytes)); + this->ohash_.calculate(); +} + +void HmacSHA256::get_bytes(uint8_t *output) { this->ohash_.get_bytes(output); } + +void HmacSHA256::get_hex(char *output) { this->ohash_.get_hex(output); } + +bool HmacSHA256::equals_bytes(const uint8_t *expected) { return this->ohash_.equals_bytes(expected); } + +bool HmacSHA256::equals_hex(const char *expected) { return this->ohash_.equals_hex(expected); } + +#endif // USE_ESP32 || USE_LIBRETINY + +} // namespace esphome::hmac_sha256 +#endif diff --git a/esphome/components/hmac_sha256/hmac_sha256.h b/esphome/components/hmac_sha256/hmac_sha256.h new file mode 100644 index 0000000000..85622cac46 --- /dev/null +++ b/esphome/components/hmac_sha256/hmac_sha256.h @@ -0,0 +1,59 @@ +#pragma once + +#include "esphome/core/defines.h" +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST) + +#include + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#include "mbedtls/md.h" +#else +#include "esphome/components/sha256/sha256.h" +#endif + +namespace esphome::hmac_sha256 { + +class HmacSHA256 { + public: + HmacSHA256() = default; + ~HmacSHA256(); + + /// Initialize a new HMAC-SHA256 digest computation. + void init(const uint8_t *key, size_t len); + void init(const char *key, size_t len) { this->init((const uint8_t *) key, len); } + void init(const std::string &key) { this->init(key.c_str(), key.length()); } + + /// Add bytes of data for the digest. + void add(const uint8_t *data, size_t len); + void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } + + /// Compute the digest, based on the provided data. + void calculate(); + + /// Retrieve the HMAC-SHA256 digest as bytes. + /// The output must be able to hold 32 bytes or more. + void get_bytes(uint8_t *output); + + /// Retrieve the HMAC-SHA256 digest as hex characters. + /// The output must be able to hold 65 bytes or more (64 hex chars + null terminator). + void get_hex(char *output); + + /// Compare the digest against a provided byte-encoded digest (32 bytes). + bool equals_bytes(const uint8_t *expected); + + /// Compare the digest against a provided hex-encoded digest (64 bytes). + bool equals_hex(const char *expected); + + protected: +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + static constexpr size_t SHA256_DIGEST_SIZE = 32; + mbedtls_md_context_t ctx_{}; + uint8_t digest_[SHA256_DIGEST_SIZE]{}; +#else + sha256::SHA256 ihash_; + sha256::SHA256 ohash_; +#endif +}; + +} // namespace esphome::hmac_sha256 +#endif diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp index a36fcb204a..b0d9135822 100644 --- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp +++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp @@ -1,6 +1,7 @@ #include "homeassistant_binary_sensor.h" -#include "esphome/core/log.h" #include "esphome/components/api/api_server.h" +#include "esphome/core/log.h" +#include "esphome/core/string_ref.h" namespace esphome { namespace homeassistant { @@ -8,38 +9,36 @@ namespace homeassistant { static const char *const TAG = "homeassistant.binary_sensor"; void HomeassistantBinarySensor::setup() { - api::global_api_server->subscribe_home_assistant_state( - this->entity_id_, this->attribute_, [this](const std::string &state) { - auto val = parse_on_off(state.c_str()); - switch (val) { - case PARSE_NONE: - case PARSE_TOGGLE: - ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str()); - break; - case PARSE_ON: - case PARSE_OFF: - bool new_state = val == PARSE_ON; - if (this->attribute_.has_value()) { - ESP_LOGD(TAG, "'%s::%s': Got attribute state %s", this->entity_id_.c_str(), - this->attribute_.value().c_str(), ONOFF(new_state)); - } else { - ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state)); - } - if (this->initial_) { - this->publish_initial_state(new_state); - } else { - this->publish_state(new_state); - } - break; + api::global_api_server->subscribe_home_assistant_state(this->entity_id_, this->attribute_, [this](StringRef state) { + auto val = parse_on_off(state.c_str()); + switch (val) { + case PARSE_NONE: + case PARSE_TOGGLE: + ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str()); + break; + case PARSE_ON: + case PARSE_OFF: + bool new_state = val == PARSE_ON; + if (this->attribute_ != nullptr) { + ESP_LOGD(TAG, "'%s::%s': Got attribute state %s", this->entity_id_, this->attribute_, ONOFF(new_state)); + } else { + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, ONOFF(new_state)); } - this->initial_ = false; - }); + if (this->initial_) { + this->publish_initial_state(new_state); + } else { + this->publish_state(new_state); + } + break; + } + this->initial_ = false; + }); } void HomeassistantBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Homeassistant Binary Sensor", this); - ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); - if (this->attribute_.has_value()) { - ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str()); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_); + if (this->attribute_ != nullptr) { + ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_); } } float HomeassistantBinarySensor::get_setup_priority() const { return setup_priority::AFTER_WIFI; } diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h index 7026496295..9aec61a370 100644 --- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h +++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h @@ -8,15 +8,15 @@ namespace homeassistant { class HomeassistantBinarySensor : public binary_sensor::BinarySensor, public Component { public: - void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; } - void set_attribute(const std::string &attribute) { attribute_ = attribute; } + void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; } + void set_attribute(const char *attribute) { this->attribute_ = attribute; } void setup() override; void dump_config() override; float get_setup_priority() const override; protected: - std::string entity_id_; - optional attribute_; + const char *entity_id_{nullptr}; + const char *attribute_{nullptr}; bool initial_{true}; }; diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index 9963f3431d..82387a81e9 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -3,50 +3,51 @@ #include "esphome/components/api/api_pb2.h" #include "esphome/components/api/api_server.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" namespace esphome { namespace homeassistant { static const char *const TAG = "homeassistant.number"; -void HomeassistantNumber::state_changed_(const std::string &state) { - auto number_value = parse_number(state); +void HomeassistantNumber::state_changed_(StringRef state) { + auto number_value = parse_number(state.c_str()); if (!number_value.has_value()) { - ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str()); + ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_, state.c_str()); this->publish_state(NAN); return; } if (this->state == number_value.value()) { return; } - ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), state.c_str()); + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, state.c_str()); this->publish_state(number_value.value()); } -void HomeassistantNumber::min_retrieved_(const std::string &min) { - auto min_value = parse_number(min); +void HomeassistantNumber::min_retrieved_(StringRef min) { + auto min_value = parse_number(min.c_str()); if (!min_value.has_value()) { - ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_.c_str(), min.c_str()); + ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_, min.c_str()); return; } ESP_LOGD(TAG, "'%s': Min retrieved: %s", get_name().c_str(), min.c_str()); this->traits.set_min_value(min_value.value()); } -void HomeassistantNumber::max_retrieved_(const std::string &max) { - auto max_value = parse_number(max); +void HomeassistantNumber::max_retrieved_(StringRef max) { + auto max_value = parse_number(max.c_str()); if (!max_value.has_value()) { - ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_.c_str(), max.c_str()); + ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_, max.c_str()); return; } ESP_LOGD(TAG, "'%s': Max retrieved: %s", get_name().c_str(), max.c_str()); this->traits.set_max_value(max_value.value()); } -void HomeassistantNumber::step_retrieved_(const std::string &step) { - auto step_value = parse_number(step); +void HomeassistantNumber::step_retrieved_(StringRef step) { + auto step_value = parse_number(step.c_str()); if (!step_value.has_value()) { - ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_.c_str(), step.c_str()); + ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_, step.c_str()); return; } ESP_LOGD(TAG, "'%s': Step Retrieved %s", get_name().c_str(), step.c_str()); @@ -55,22 +56,19 @@ void HomeassistantNumber::step_retrieved_(const std::string &step) { void HomeassistantNumber::setup() { api::global_api_server->subscribe_home_assistant_state( - this->entity_id_, nullopt, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1)); + this->entity_id_, nullptr, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1)); api::global_api_server->get_home_assistant_state( - this->entity_id_, optional("min"), - std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1)); + this->entity_id_, "min", std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1)); api::global_api_server->get_home_assistant_state( - this->entity_id_, optional("max"), - std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1)); + this->entity_id_, "max", std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1)); api::global_api_server->get_home_assistant_state( - this->entity_id_, optional("step"), - std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1)); + this->entity_id_, "step", std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1)); } void HomeassistantNumber::dump_config() { LOG_NUMBER("", "Homeassistant Number", this); - ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_); } float HomeassistantNumber::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } @@ -88,15 +86,15 @@ void HomeassistantNumber::control(float value) { static constexpr auto VALUE_KEY = StringRef::from_lit("value"); api::HomeassistantActionRequest resp; - resp.set_service(SERVICE_NAME); + resp.service = SERVICE_NAME; resp.data.init(2); auto &entity_id = resp.data.emplace_back(); - entity_id.set_key(ENTITY_ID_KEY); + entity_id.key = ENTITY_ID_KEY; entity_id.value = this->entity_id_; auto &entity_value = resp.data.emplace_back(); - entity_value.set_key(VALUE_KEY); + entity_value.key = VALUE_KEY; entity_value.value = to_string(value); api::global_api_server->send_homeassistant_action(resp); diff --git a/esphome/components/homeassistant/number/homeassistant_number.h b/esphome/components/homeassistant/number/homeassistant_number.h index 0860b4e91c..275d2d5f03 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.h +++ b/esphome/components/homeassistant/number/homeassistant_number.h @@ -1,31 +1,29 @@ #pragma once -#include -#include - #include "esphome/components/number/number.h" #include "esphome/core/component.h" +#include "esphome/core/string_ref.h" namespace esphome { namespace homeassistant { class HomeassistantNumber : public number::Number, public Component { public: - void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; } + void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; } void setup() override; void dump_config() override; float get_setup_priority() const override; protected: - void state_changed_(const std::string &state); - void min_retrieved_(const std::string &min); - void max_retrieved_(const std::string &max); - void step_retrieved_(const std::string &step); + void state_changed_(StringRef state); + void min_retrieved_(StringRef min); + void max_retrieved_(StringRef max); + void step_retrieved_(StringRef step); void control(float value) override; - std::string entity_id_; + const char *entity_id_{nullptr}; }; } // namespace homeassistant } // namespace esphome diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp index 35e660f7c1..66300ebba5 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.cpp @@ -1,6 +1,7 @@ #include "homeassistant_sensor.h" -#include "esphome/core/log.h" #include "esphome/components/api/api_server.h" +#include "esphome/core/log.h" +#include "esphome/core/string_ref.h" namespace esphome { namespace homeassistant { @@ -8,29 +9,27 @@ namespace homeassistant { static const char *const TAG = "homeassistant.sensor"; void HomeassistantSensor::setup() { - api::global_api_server->subscribe_home_assistant_state( - this->entity_id_, this->attribute_, [this](const std::string &state) { - auto val = parse_number(state); - if (!val.has_value()) { - ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str()); - this->publish_state(NAN); - return; - } + api::global_api_server->subscribe_home_assistant_state(this->entity_id_, this->attribute_, [this](StringRef state) { + auto val = parse_number(state.c_str()); + if (!val.has_value()) { + ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_, state.c_str()); + this->publish_state(NAN); + return; + } - if (this->attribute_.has_value()) { - ESP_LOGD(TAG, "'%s::%s': Got attribute state %.2f", this->entity_id_.c_str(), - this->attribute_.value().c_str(), *val); - } else { - ESP_LOGD(TAG, "'%s': Got state %.2f", this->entity_id_.c_str(), *val); - } - this->publish_state(*val); - }); + if (this->attribute_ != nullptr) { + ESP_LOGD(TAG, "'%s::%s': Got attribute state %.2f", this->entity_id_, this->attribute_, *val); + } else { + ESP_LOGD(TAG, "'%s': Got state %.2f", this->entity_id_, *val); + } + this->publish_state(*val); + }); } void HomeassistantSensor::dump_config() { LOG_SENSOR("", "Homeassistant Sensor", this); - ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); - if (this->attribute_.has_value()) { - ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str()); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_); + if (this->attribute_ != nullptr) { + ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_); } } float HomeassistantSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } diff --git a/esphome/components/homeassistant/sensor/homeassistant_sensor.h b/esphome/components/homeassistant/sensor/homeassistant_sensor.h index 53b288d7d4..d89fc069ff 100644 --- a/esphome/components/homeassistant/sensor/homeassistant_sensor.h +++ b/esphome/components/homeassistant/sensor/homeassistant_sensor.h @@ -8,15 +8,15 @@ namespace homeassistant { class HomeassistantSensor : public sensor::Sensor, public Component { public: - void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; } - void set_attribute(const std::string &attribute) { attribute_ = attribute; } + void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; } + void set_attribute(const char *attribute) { this->attribute_ = attribute; } void setup() override; void dump_config() override; float get_setup_priority() const override; protected: - std::string entity_id_; - optional attribute_; + const char *entity_id_{nullptr}; + const char *attribute_{nullptr}; }; } // namespace homeassistant diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.cpp b/esphome/components/homeassistant/switch/homeassistant_switch.cpp index 27d3705fc2..79d17eb290 100644 --- a/esphome/components/homeassistant/switch/homeassistant_switch.cpp +++ b/esphome/components/homeassistant/switch/homeassistant_switch.cpp @@ -1,6 +1,7 @@ #include "homeassistant_switch.h" #include "esphome/components/api/api_server.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" namespace esphome { namespace homeassistant { @@ -10,7 +11,7 @@ static const char *const TAG = "homeassistant.switch"; using namespace esphome::switch_; void HomeassistantSwitch::setup() { - api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullopt, [this](const std::string &state) { + api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr, [this](StringRef state) { auto val = parse_on_off(state.c_str()); switch (val) { case PARSE_NONE: @@ -20,7 +21,7 @@ void HomeassistantSwitch::setup() { case PARSE_ON: case PARSE_OFF: bool new_state = val == PARSE_ON; - ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state)); + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, ONOFF(new_state)); this->publish_state(new_state); break; } @@ -29,7 +30,7 @@ void HomeassistantSwitch::setup() { void HomeassistantSwitch::dump_config() { LOG_SWITCH("", "Homeassistant Switch", this); - ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_); } float HomeassistantSwitch::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } @@ -46,14 +47,14 @@ void HomeassistantSwitch::write_state(bool state) { api::HomeassistantActionRequest resp; if (state) { - resp.set_service(SERVICE_ON); + resp.service = SERVICE_ON; } else { - resp.set_service(SERVICE_OFF); + resp.service = SERVICE_OFF; } resp.data.init(1); auto &entity_id_kv = resp.data.emplace_back(); - entity_id_kv.set_key(ENTITY_ID_KEY); + entity_id_kv.key = ENTITY_ID_KEY; entity_id_kv.value = this->entity_id_; api::global_api_server->send_homeassistant_action(resp); diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.h b/esphome/components/homeassistant/switch/homeassistant_switch.h index a4da257960..c180b7f98a 100644 --- a/esphome/components/homeassistant/switch/homeassistant_switch.h +++ b/esphome/components/homeassistant/switch/homeassistant_switch.h @@ -8,14 +8,14 @@ namespace homeassistant { class HomeassistantSwitch : public switch_::Switch, public Component { public: - void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; } + void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; } void setup() override; void dump_config() override; float get_setup_priority() const override; protected: void write_state(bool state) override; - std::string entity_id_; + const char *entity_id_{nullptr}; }; } // namespace homeassistant diff --git a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp index 9b933fbbbe..109574e0c8 100644 --- a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp +++ b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.cpp @@ -1,6 +1,7 @@ #include "homeassistant_text_sensor.h" -#include "esphome/core/log.h" #include "esphome/components/api/api_server.h" +#include "esphome/core/log.h" +#include "esphome/core/string_ref.h" namespace esphome { namespace homeassistant { @@ -8,22 +9,20 @@ namespace homeassistant { static const char *const TAG = "homeassistant.text_sensor"; void HomeassistantTextSensor::setup() { - api::global_api_server->subscribe_home_assistant_state( - this->entity_id_, this->attribute_, [this](const std::string &state) { - if (this->attribute_.has_value()) { - ESP_LOGD(TAG, "'%s::%s': Got attribute state '%s'", this->entity_id_.c_str(), - this->attribute_.value().c_str(), state.c_str()); - } else { - ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_.c_str(), state.c_str()); - } - this->publish_state(state); - }); + api::global_api_server->subscribe_home_assistant_state(this->entity_id_, this->attribute_, [this](StringRef state) { + if (this->attribute_ != nullptr) { + ESP_LOGD(TAG, "'%s::%s': Got attribute state '%s'", this->entity_id_, this->attribute_, state.c_str()); + } else { + ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_, state.c_str()); + } + this->publish_state(state.c_str(), state.size()); + }); } void HomeassistantTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Homeassistant Text Sensor", this); - ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); - if (this->attribute_.has_value()) { - ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str()); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_); + if (this->attribute_ != nullptr) { + ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_); } } float HomeassistantTextSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } diff --git a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h index ce6b2c2c3f..4d66c65a17 100644 --- a/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h +++ b/esphome/components/homeassistant/text_sensor/homeassistant_text_sensor.h @@ -8,15 +8,15 @@ namespace homeassistant { class HomeassistantTextSensor : public text_sensor::TextSensor, public Component { public: - void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; } - void set_attribute(const std::string &attribute) { attribute_ = attribute; } + void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; } + void set_attribute(const char *attribute) { this->attribute_ = attribute; } void setup() override; void dump_config() override; float get_setup_priority() const override; protected: - std::string entity_id_; - optional attribute_; + const char *entity_id_{nullptr}; + const char *attribute_{nullptr}; }; } // namespace homeassistant diff --git a/esphome/components/honeywellabp/honeywellabp.cpp b/esphome/components/honeywellabp/honeywellabp.cpp index 4c00f034aa..c204325dfc 100644 --- a/esphome/components/honeywellabp/honeywellabp.cpp +++ b/esphome/components/honeywellabp/honeywellabp.cpp @@ -35,8 +35,10 @@ uint8_t HONEYWELLABPSensor::readsensor_() { pressure_count_ = ((uint16_t) (buf_[0]) << 8 & 0x3F00) | ((uint16_t) (buf_[1]) & 0xFF); // 11 - bit temperature is all of byte 2 (lowest 8 bits) and the first three bits of byte 3 temperature_count_ = (((uint16_t) (buf_[2]) << 3) & 0x7F8) | (((uint16_t) (buf_[3]) >> 5) & 0x7); - ESP_LOGV(TAG, "Sensor pressure_count_ %d", pressure_count_); - ESP_LOGV(TAG, "Sensor temperature_count_ %d", temperature_count_); + ESP_LOGV(TAG, + "Sensor pressure_count_ %d\n" + "Sensor temperature_count_ %d", + pressure_count_, temperature_count_); } return status_; } diff --git a/esphome/components/host/gpio.cpp b/esphome/components/host/gpio.cpp index e46f158513..f99b82bcc2 100644 --- a/esphome/components/host/gpio.cpp +++ b/esphome/components/host/gpio.cpp @@ -25,11 +25,7 @@ void HostGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::Interr } void HostGPIOPin::pin_mode(gpio::Flags flags) { ESP_LOGD(TAG, "Setting pin %d mode to %02X", pin_, (uint32_t) flags); } -std::string HostGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); - return buffer; -} +size_t HostGPIOPin::dump_summary(char *buffer, size_t len) const { return snprintf(buffer, len, "GPIO%u", this->pin_); } bool HostGPIOPin::digital_read() { return inverted_; } void HostGPIOPin::digital_write(bool value) { diff --git a/esphome/components/host/gpio.h b/esphome/components/host/gpio.h index ae677291b9..ea6b13f436 100644 --- a/esphome/components/host/gpio.h +++ b/esphome/components/host/gpio.h @@ -17,7 +17,7 @@ class HostGPIOPin : public InternalGPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return pin_; } diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index b7d3be63fe..cde6886109 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -7,6 +7,8 @@ namespace hte501 { static const char *const TAG = "hte501"; +static constexpr size_t HTE501_SERIAL_NUMBER_SIZE = 7; + void HTE501Component::setup() { uint8_t address[] = {0x70, 0x29}; uint8_t identification[9]; @@ -16,7 +18,10 @@ void HTE501Component::setup() { this->mark_failed(); return; } - ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(identification + 0, 7).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char serial_hex[format_hex_size(HTE501_SERIAL_NUMBER_SIZE)]; +#endif + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex_to(serial_hex, identification, HTE501_SERIAL_NUMBER_SIZE)); } void HTE501Component::dump_config() { diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index f4fa448c5b..b133aa69b2 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -69,9 +69,6 @@ def validate_url(value): def validate_ssl_verification(config): error_message = "" - if CORE.is_esp32 and not CORE.using_esp_idf and config[CONF_VERIFY_SSL]: - error_message = "ESPHome supports certificate verification only via ESP-IDF" - if CORE.is_rp2040 and config[CONF_VERIFY_SSL]: error_message = "ESPHome does not support certificate verification on RP2040" @@ -93,9 +90,9 @@ def validate_ssl_verification(config): def _declare_request_class(value): if CORE.is_host: return cv.declare_id(HttpRequestHost)(value) - if CORE.using_esp_idf: + if CORE.is_esp32: return cv.declare_id(HttpRequestIDF)(value) - if CORE.is_esp8266 or CORE.is_esp32 or CORE.is_rp2040: + if CORE.is_esp8266 or CORE.is_rp2040: return cv.declare_id(HttpRequestArduino)(value) return NotImplementedError @@ -121,11 +118,11 @@ CONFIG_SCHEMA = cv.All( cv.positive_not_null_time_period, cv.positive_time_period_milliseconds, ), - cv.SplitDefault(CONF_BUFFER_SIZE_RX, esp32_idf=512): cv.All( - cv.uint16_t, cv.only_with_esp_idf + cv.SplitDefault(CONF_BUFFER_SIZE_RX, esp32=512): cv.All( + cv.uint16_t, cv.only_on_esp32 ), - cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32_idf=512): cv.All( - cv.uint16_t, cv.only_with_esp_idf + cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32=512): cv.All( + cv.uint16_t, cv.only_on_esp32 ), cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.All( cv.file_, @@ -158,25 +155,20 @@ async def to_code(config): cg.add(var.set_watchdog_timeout(timeout_ms)) if CORE.is_esp32: - if CORE.using_esp_idf: - cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX])) - cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) + cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX])) + cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) - esp32.add_idf_sdkconfig_option( - "CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", - config.get(CONF_VERIFY_SSL), - ) - esp32.add_idf_sdkconfig_option( - "CONFIG_ESP_TLS_INSECURE", - not config.get(CONF_VERIFY_SSL), - ) - esp32.add_idf_sdkconfig_option( - "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", - not config.get(CONF_VERIFY_SSL), - ) - else: - cg.add_library("NetworkClientSecure", None) - cg.add_library("HTTPClient", None) + if config.get(CONF_VERIFY_SSL): + esp32.add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) + + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_TLS_INSECURE", + not config.get(CONF_VERIFY_SSL), + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", + not config.get(CONF_VERIFY_SSL), + ) if CORE.is_esp8266: cg.add_library("ESP8266HTTPClient", None) if CORE.is_rp2040 and CORE.using_arduino: @@ -327,13 +319,15 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( { "http_request_host.cpp": {PlatformFramework.HOST_NATIVE}, "http_request_arduino.cpp": { - PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2040_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, - "http_request_idf.cpp": {PlatformFramework.ESP32_IDF}, + "http_request_idf.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, } ) diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 806354baf1..11dde4715a 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -4,8 +4,7 @@ #include -namespace esphome { -namespace http_request { +namespace esphome::http_request { static const char *const TAG = "http_request"; @@ -42,5 +41,4 @@ std::string HttpContainer::get_response_header(const std::string &header_name) { } } -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 8a82a44d7d..1b5fd9f00e 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -15,8 +15,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace http_request { +namespace esphome::http_request { struct Header { std::string name; @@ -255,6 +254,9 @@ template class HttpRequestSendAction : public Action { size_t read_index = 0; while (container->get_bytes_read() < max_length) { int read = container->read(buf + read_index, std::min(max_length - read_index, 512)); + if (read <= 0) { + break; + } App.feed_wdt(); yield(); read_index += read; @@ -302,5 +304,4 @@ template class HttpRequestSendAction : public Action { size_t max_response_buffer_size_{SIZE_MAX}; }; -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index c64a7be554..a653942b18 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -1,6 +1,6 @@ #include "http_request_arduino.h" -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) #include "esphome/components/network/util.h" #include "esphome/components/watchdog/watchdog.h" @@ -9,8 +9,7 @@ #include "esphome/core/defines.h" #include "esphome/core/log.h" -namespace esphome { -namespace http_request { +namespace esphome::http_request { static const char *const TAG = "http_request.arduino"; @@ -75,8 +74,6 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur container->client_.setInsecure(); } bool status = container->client_.begin(url.c_str()); -#elif defined(USE_ESP32) - bool status = container->client_.begin(url.c_str()); #endif App.feed_wdt(); @@ -90,9 +87,6 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur container->client_.setReuse(true); container->client_.setTimeout(this->timeout_); -#if defined(USE_ESP32) - container->client_.setConnectTimeout(this->timeout_); -#endif if (this->useragent_ != nullptr) { container->client_.setUserAgent(this->useragent_); @@ -177,7 +171,6 @@ void HttpContainerArduino::end() { this->client_.end(); } -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request -#endif // USE_ARDUINO +#endif // USE_ARDUINO && !USE_ESP32 diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index b736bb56d1..d9b5af9d81 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -2,9 +2,9 @@ #include "http_request.h" -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) -#if defined(USE_ESP32) || defined(USE_RP2040) +#if defined(USE_RP2040) #include #include #endif @@ -15,8 +15,7 @@ #endif #endif -namespace esphome { -namespace http_request { +namespace esphome::http_request { class HttpRequestArduino; class HttpContainerArduino : public HttpContainer { @@ -36,7 +35,6 @@ class HttpRequestArduino : public HttpRequestComponent { const std::set &collect_headers) override; }; -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request -#endif // USE_ARDUINO +#endif // USE_ARDUINO && !USE_ESP32 diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index 402affc1d1..b94570be12 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -12,8 +12,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" -namespace esphome { -namespace http_request { +namespace esphome::http_request { static const char *const TAG = "http_request.host"; @@ -139,7 +138,6 @@ void HttpContainerHost::end() { this->bytes_read_ = 0; } -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request #endif // USE_HOST diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 886ba94938..32e149e6a3 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -2,8 +2,8 @@ #ifdef USE_HOST #include "http_request.h" -namespace esphome { -namespace http_request { + +namespace esphome::http_request { class HttpRequestHost; class HttpContainerHost : public HttpContainer { @@ -27,7 +27,6 @@ class HttpRequestHost : public HttpRequestComponent { const char *ca_path_{}; }; -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request #endif // USE_HOST diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 34a3fb87eb..725a9c1c1e 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -1,6 +1,6 @@ #include "http_request_idf.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/components/network/util.h" #include "esphome/components/watchdog/watchdog.h" @@ -14,8 +14,7 @@ #include "esp_task_wdt.h" -namespace esphome { -namespace http_request { +namespace esphome::http_request { static const char *const TAG = "http_request.idf"; @@ -245,7 +244,6 @@ void HttpContainerIDF::feed_wdt() { } } -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index e51b3aaebc..4dc4736423 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -2,15 +2,14 @@ #include "http_request.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include #include #include -namespace esphome { -namespace http_request { +namespace esphome::http_request { class HttpContainerIDF : public HttpContainer { public: @@ -48,7 +47,6 @@ class HttpRequestIDF : public HttpRequestComponent { static esp_err_t http_event_handler(esp_http_client_event_t *evt); }; -} // namespace http_request -} // namespace esphome +} // namespace esphome::http_request -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 4d9e868c74..2a7db9137f 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -7,8 +7,7 @@ #include "esphome/components/md5/md5.h" #include "esphome/components/watchdog/watchdog.h" #include "esphome/components/ota/ota_backend.h" -#include "esphome/components/ota/ota_backend_arduino_esp32.h" -#include "esphome/components/ota/ota_backend_arduino_esp8266.h" +#include "esphome/components/ota/ota_backend_esp8266.h" #include "esphome/components/ota/ota_backend_arduino_rp2040.h" #include "esphome/components/ota/ota_backend_esp_idf.h" @@ -17,12 +16,6 @@ namespace http_request { static const char *const TAG = "http_request.ota"; -void OtaHttpRequestComponent::setup() { -#ifdef USE_OTA_STATE_CALLBACK - ota::register_ota_platform(this); -#endif -} - void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); }; void OtaHttpRequestComponent::set_md5_url(const std::string &url) { @@ -49,24 +42,24 @@ void OtaHttpRequestComponent::flash() { } ESP_LOGI(TAG, "Starting update"); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_STARTED, 0.0f, 0); #endif auto ota_status = this->do_ota_(); switch (ota_status) { case ota::OTA_RESPONSE_OK: -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, ota_status); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_COMPLETED, 100.0f, ota_status); #endif delay(10); App.safe_reboot(); break; default: -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_ERROR, 0.0f, ota_status); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_ERROR, 0.0f, ota_status); #endif this->md5_computed_.clear(); // will be reset at next attempt this->md5_expected_.clear(); // will be reset at next attempt @@ -112,9 +105,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() { // we will compute MD5 on the fly for verification -- Arduino OTA seems to ignore it md5_receive.init(); - ESP_LOGV(TAG, "MD5Digest initialized"); - - ESP_LOGV(TAG, "OTA backend begin"); + ESP_LOGV(TAG, "MD5Digest initialized\n" + "OTA backend begin"); auto backend = ota::make_ota_backend(); auto error_code = backend->begin(container->content_length); if (error_code != ota::OTA_RESPONSE_OK) { @@ -133,11 +125,18 @@ uint8_t OtaHttpRequestComponent::do_ota_() { App.feed_wdt(); yield(); - if (bufsize < 0) { - ESP_LOGE(TAG, "Stream closed"); - this->cleanup_(std::move(backend), container); - return OTA_CONNECTION_ERROR; - } else if (bufsize > 0 && bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) { + // Exit loop if no data available (stream closed or end of data) + if (bufsize <= 0) { + if (bufsize < 0) { + ESP_LOGE(TAG, "Stream closed with error"); + this->cleanup_(std::move(backend), container); + return OTA_CONNECTION_ERROR; + } + // bufsize == 0: no more data available, exit loop + break; + } + + if (bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) { // add read bytes to MD5 md5_receive.add(buf, bufsize); @@ -159,8 +158,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() { last_progress = now; float percentage = container->get_bytes_read() * 100.0f / container->content_length; ESP_LOGD(TAG, "Progress: %0.1f%%", percentage); -#ifdef USE_OTA_STATE_CALLBACK - this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0); +#ifdef USE_OTA_STATE_LISTENER + this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0); #endif } } // while @@ -248,6 +247,9 @@ bool OtaHttpRequestComponent::http_get_md5_() { int read_len = 0; while (container->get_bytes_read() < MD5_SIZE) { read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE); + if (read_len <= 0) { + break; + } App.feed_wdt(); yield(); } diff --git a/esphome/components/http_request/ota/ota_http_request.h b/esphome/components/http_request/ota/ota_http_request.h index 6a86b4ab43..8735189e99 100644 --- a/esphome/components/http_request/ota/ota_http_request.h +++ b/esphome/components/http_request/ota/ota_http_request.h @@ -24,7 +24,6 @@ enum OtaHttpRequestError : uint8_t { class OtaHttpRequestComponent : public ota::OTAComponent, public Parented { public: - void setup() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } diff --git a/esphome/components/http_request/update/__init__.py b/esphome/components/http_request/update/__init__.py index abb4b2a430..d84d80109a 100644 --- a/esphome/components/http_request/update/__init__.py +++ b/esphome/components/http_request/update/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import update +from esphome.components import ota, update import esphome.config_validation as cv from esphome.const import CONF_SOURCE @@ -38,6 +38,6 @@ async def to_code(config): cg.add(var.set_source_url(config[CONF_SOURCE])) - cg.add_define("USE_OTA_STATE_CALLBACK") + ota.request_ota_state_listeners() await cg.register_component(var, config) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 06aa6da6a4..82b391e01f 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -20,22 +20,26 @@ static const char *const TAG = "http_request.update"; static const size_t MAX_READ_SIZE = 256; -void HttpRequestUpdate::setup() { - this->ota_parent_->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t err) { - if (state == ota::OTAState::OTA_IN_PROGRESS) { - this->state_ = update::UPDATE_STATE_INSTALLING; - this->update_info_.has_progress = true; - this->update_info_.progress = progress; - this->publish_state(); - } else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) { - this->state_ = update::UPDATE_STATE_AVAILABLE; - this->status_set_error("Failed to install firmware"); - this->publish_state(); - } - }); +void HttpRequestUpdate::setup() { this->ota_parent_->add_state_listener(this); } + +void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) { + if (state == ota::OTAState::OTA_IN_PROGRESS) { + this->state_ = update::UPDATE_STATE_INSTALLING; + this->update_info_.has_progress = true; + this->update_info_.progress = progress; + this->publish_state(); + } else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) { + this->state_ = update::UPDATE_STATE_AVAILABLE; + this->status_set_error(LOG_STR("Failed to install firmware")); + this->publish_state(); + } } void HttpRequestUpdate::update() { + if (!network::is_connected()) { + ESP_LOGD(TAG, "Network not connected, skipping update check"); + return; + } #ifdef USE_ESP32 xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); #else @@ -49,18 +53,19 @@ void HttpRequestUpdate::update_task(void *params) { auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { - std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); }); UPDATE_RETURN; } RAMAllocator allocator; uint8_t *data = allocator.allocate(container->content_length); if (data == nullptr) { - std::string msg = str_sprintf("Failed to allocate %zu bytes for manifest", container->content_length); + ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer( + [this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); }); container->end(); UPDATE_RETURN; } @@ -71,6 +76,11 @@ void HttpRequestUpdate::update_task(void *params) { yield(); + if (read_bytes <= 0) { + // Network error or connection closed - break to avoid infinite loop + break; + } + read_index += read_bytes; } @@ -83,35 +93,36 @@ void HttpRequestUpdate::update_task(void *params) { container.reset(); // Release ownership of the container's shared_ptr valid = json::parse_json(response, [this_update](JsonObject root) -> bool { - if (!root["name"].is() || !root["version"].is() || !root["builds"].is()) { + if (!root[ESPHOME_F("name")].is() || !root[ESPHOME_F("version")].is() || + !root[ESPHOME_F("builds")].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - this_update->update_info_.title = root["name"].as(); - this_update->update_info_.latest_version = root["version"].as(); + this_update->update_info_.title = root[ESPHOME_F("name")].as(); + this_update->update_info_.latest_version = root[ESPHOME_F("version")].as(); - for (auto build : root["builds"].as()) { - if (!build["chipFamily"].is()) { + for (auto build : root[ESPHOME_F("builds")].as()) { + if (!build[ESPHOME_F("chipFamily")].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - if (build["chipFamily"] == ESPHOME_VARIANT) { - if (!build["ota"].is()) { + if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) { + if (!build[ESPHOME_F("ota")].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - JsonObject ota = build["ota"].as(); - if (!ota["path"].is() || !ota["md5"].is()) { + JsonObject ota = build[ESPHOME_F("ota")].as(); + if (!ota[ESPHOME_F("path")].is() || !ota[ESPHOME_F("md5")].is()) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - this_update->update_info_.firmware_url = ota["path"].as(); - this_update->update_info_.md5 = ota["md5"].as(); + this_update->update_info_.firmware_url = ota[ESPHOME_F("path")].as(); + this_update->update_info_.md5 = ota[ESPHOME_F("md5")].as(); - if (ota["summary"].is()) - this_update->update_info_.summary = ota["summary"].as(); - if (ota["release_url"].is()) - this_update->update_info_.release_url = ota["release_url"].as(); + if (ota[ESPHOME_F("summary")].is()) + this_update->update_info_.summary = ota[ESPHOME_F("summary")].as(); + if (ota[ESPHOME_F("release_url")].is()) + this_update->update_info_.release_url = ota[ESPHOME_F("release_url")].as(); return true; } @@ -121,9 +132,9 @@ void HttpRequestUpdate::update_task(void *params) { } if (!valid) { - std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); // Defer to main loop to avoid race condition on component_state_ read-modify-write - this_update->defer([this_update, msg]() { this_update->status_set_error(msg.c_str()); }); + this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); }); UPDATE_RETURN; } diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index e05fdb0cc2..cf34ace18e 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -14,7 +14,7 @@ namespace esphome { namespace http_request { -class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { +class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent, public ota::OTAStateListener { public: void setup() override; void update() override; @@ -29,6 +29,8 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void on_ota_state(ota::OTAState state, float progress, uint8_t error) override; + protected: HttpRequestComponent *request_parent_; OtaHttpRequestComponent *ota_parent_; diff --git a/esphome/components/hub75/__init__.py b/esphome/components/hub75/__init__.py new file mode 100644 index 0000000000..cd5441f749 --- /dev/null +++ b/esphome/components/hub75/__init__.py @@ -0,0 +1,6 @@ +from esphome.cpp_generator import MockObj + +CODEOWNERS = ["@stuartparmenter"] + +# Use fully-qualified namespace to avoid collision with external hub75 library's global ::hub75 namespace +hub75_ns = MockObj("::esphome::hub75", "::") diff --git a/esphome/components/hub75/boards/__init__.py b/esphome/components/hub75/boards/__init__.py new file mode 100644 index 0000000000..52f8864c60 --- /dev/null +++ b/esphome/components/hub75/boards/__init__.py @@ -0,0 +1,80 @@ +"""Board presets for HUB75 displays. + +Each board preset defines standard pin mappings for HUB75 controller boards. +""" + +from dataclasses import dataclass, field +import importlib +import pkgutil +from typing import ClassVar + + +class BoardRegistry: + """Global registry for board configurations.""" + + _boards: ClassVar[dict[str, "BoardConfig"]] = {} + + @classmethod + def register(cls, board: "BoardConfig") -> None: + """Register a board configuration.""" + cls._boards[board.name] = board + + @classmethod + def get_boards(cls) -> dict[str, "BoardConfig"]: + """Return all registered boards.""" + return cls._boards + + +@dataclass +class BoardConfig: + """Board configuration storing HUB75 pin mappings.""" + + name: str + r1_pin: int + g1_pin: int + b1_pin: int + r2_pin: int + g2_pin: int + b2_pin: int + a_pin: int + b_pin: int + c_pin: int + d_pin: int + e_pin: int | None + lat_pin: int + oe_pin: int + clk_pin: int + ignore_strapping_pins: tuple[str, ...] = () # e.g., ("a_pin", "clk_pin") + + # Derived field for pin lookup + pins: dict[str, int | None] = field(default_factory=dict, init=False, repr=False) + + def __post_init__(self): + """Initialize derived fields and register board.""" + self.name = self.name.lower() + self.pins = { + "r1": self.r1_pin, + "g1": self.g1_pin, + "b1": self.b1_pin, + "r2": self.r2_pin, + "g2": self.g2_pin, + "b2": self.b2_pin, + "a": self.a_pin, + "b": self.b_pin, + "c": self.c_pin, + "d": self.d_pin, + "e": self.e_pin, + "lat": self.lat_pin, + "oe": self.oe_pin, + "clk": self.clk_pin, + } + BoardRegistry.register(self) + + def get_pin(self, pin_name: str) -> int | None: + """Get pin number for a given pin name.""" + return self.pins.get(pin_name) + + +# Dynamically import all board definition modules +for module_info in pkgutil.iter_modules(__path__): + importlib.import_module(f".{module_info.name}", package=__package__) diff --git a/esphome/components/hub75/boards/adafruit.py b/esphome/components/hub75/boards/adafruit.py new file mode 100644 index 0000000000..e27eeb9379 --- /dev/null +++ b/esphome/components/hub75/boards/adafruit.py @@ -0,0 +1,23 @@ +"""Adafruit Matrix Portal board definitions.""" + +from . import BoardConfig + +# Adafruit Matrix Portal S3 +BoardConfig( + "adafruit-matrix-portal-s3", + r1_pin=42, + g1_pin=41, + b1_pin=40, + r2_pin=38, + g2_pin=39, + b2_pin=37, + a_pin=45, + b_pin=36, + c_pin=48, + d_pin=35, + e_pin=21, + lat_pin=47, + oe_pin=14, + clk_pin=2, + ignore_strapping_pins=("a_pin",), # GPIO45 is a strapping pin +) diff --git a/esphome/components/hub75/boards/apollo.py b/esphome/components/hub75/boards/apollo.py new file mode 100644 index 0000000000..4b8b2c1f0a --- /dev/null +++ b/esphome/components/hub75/boards/apollo.py @@ -0,0 +1,41 @@ +"""Apollo Automation M1 board definitions.""" + +from . import BoardConfig + +# Apollo Automation M1 Rev4 +BoardConfig( + "apollo-automation-m1-rev4", + r1_pin=42, + g1_pin=41, + b1_pin=40, + r2_pin=38, + g2_pin=39, + b2_pin=37, + a_pin=45, + b_pin=36, + c_pin=48, + d_pin=35, + e_pin=21, + lat_pin=47, + oe_pin=14, + clk_pin=2, +) + +# Apollo Automation M1 Rev6 +BoardConfig( + "apollo-automation-m1-rev6", + r1_pin=1, + g1_pin=5, + b1_pin=6, + r2_pin=7, + g2_pin=13, + b2_pin=9, + a_pin=16, + b_pin=48, + c_pin=47, + d_pin=21, + e_pin=38, + lat_pin=8, + oe_pin=4, + clk_pin=18, +) diff --git a/esphome/components/hub75/boards/huidu.py b/esphome/components/hub75/boards/huidu.py new file mode 100644 index 0000000000..52744d397e --- /dev/null +++ b/esphome/components/hub75/boards/huidu.py @@ -0,0 +1,22 @@ +"""Huidu board definitions.""" + +from . import BoardConfig + +# Huidu HD-WF2 +BoardConfig( + "huidu-hd-wf2", + r1_pin=2, + g1_pin=6, + b1_pin=10, + r2_pin=3, + g2_pin=7, + b2_pin=11, + a_pin=39, + b_pin=38, + c_pin=37, + d_pin=36, + e_pin=21, + lat_pin=33, + oe_pin=35, + clk_pin=34, +) diff --git a/esphome/components/hub75/boards/trinity.py b/esphome/components/hub75/boards/trinity.py new file mode 100644 index 0000000000..bfad779ad0 --- /dev/null +++ b/esphome/components/hub75/boards/trinity.py @@ -0,0 +1,24 @@ +"""ESP32 Trinity board definitions.""" + +from . import BoardConfig + +# ESP32 Trinity +# https://esp32trinity.com/ +# Pin assignments from: https://github.com/witnessmenow/ESP32-Trinity/blob/master/FAQ.md +BoardConfig( + "esp32-trinity", + r1_pin=25, + g1_pin=26, + b1_pin=27, + r2_pin=14, + g2_pin=12, + b2_pin=13, + a_pin=23, + b_pin=19, + c_pin=5, + d_pin=17, + e_pin=18, + lat_pin=4, + oe_pin=15, + clk_pin=16, +) diff --git a/esphome/components/hub75/display.py b/esphome/components/hub75/display.py new file mode 100644 index 0000000000..40202e52ca --- /dev/null +++ b/esphome/components/hub75/display.py @@ -0,0 +1,626 @@ +from typing import Any + +from esphome import automation, pins +import esphome.codegen as cg +from esphome.components import display +from esphome.components.esp32 import add_idf_component +import esphome.config_validation as cv +from esphome.const import ( + CONF_AUTO_CLEAR_ENABLED, + CONF_BIT_DEPTH, + CONF_BOARD, + CONF_BRIGHTNESS, + CONF_CLK_PIN, + CONF_GAMMA_CORRECT, + CONF_ID, + CONF_LAMBDA, + CONF_OE_PIN, + CONF_ROTATION, + CONF_UPDATE_INTERVAL, +) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +import esphome.final_validate as fv +from esphome.types import ConfigType + +from . import boards, hub75_ns + +DEPENDENCIES = ["esp32"] +CODEOWNERS = ["@stuartparmenter"] + +# Load all board presets +BOARDS = boards.BoardRegistry.get_boards() + +# Constants +CONF_HUB75_ID = "hub75_id" + +# Panel dimensions +CONF_PANEL_WIDTH = "panel_width" +CONF_PANEL_HEIGHT = "panel_height" + +# Multi-panel layout +CONF_LAYOUT_ROWS = "layout_rows" +CONF_LAYOUT_COLS = "layout_cols" +CONF_LAYOUT = "layout" + +# Panel hardware +CONF_SCAN_WIRING = "scan_wiring" +CONF_SHIFT_DRIVER = "shift_driver" + +# RGB pins +CONF_R1_PIN = "r1_pin" +CONF_G1_PIN = "g1_pin" +CONF_B1_PIN = "b1_pin" +CONF_R2_PIN = "r2_pin" +CONF_G2_PIN = "g2_pin" +CONF_B2_PIN = "b2_pin" + +# Address pins +CONF_A_PIN = "a_pin" +CONF_B_PIN = "b_pin" +CONF_C_PIN = "c_pin" +CONF_D_PIN = "d_pin" +CONF_E_PIN = "e_pin" + +# Control pins +CONF_LAT_PIN = "lat_pin" + +NEVER = 4294967295 # uint32_t max - value used when update_interval is "never" + +# Pin mapping from config keys to board keys +PIN_MAPPING = { + CONF_R1_PIN: "r1", + CONF_G1_PIN: "g1", + CONF_B1_PIN: "b1", + CONF_R2_PIN: "r2", + CONF_G2_PIN: "g2", + CONF_B2_PIN: "b2", + CONF_A_PIN: "a", + CONF_B_PIN: "b", + CONF_C_PIN: "c", + CONF_D_PIN: "d", + CONF_E_PIN: "e", + CONF_LAT_PIN: "lat", + CONF_OE_PIN: "oe", + CONF_CLK_PIN: "clk", +} + +# Required pins (E pin is optional) +REQUIRED_PINS = [key for key in PIN_MAPPING if key != CONF_E_PIN] + +# Configuration +CONF_CLOCK_SPEED = "clock_speed" +CONF_LATCH_BLANKING = "latch_blanking" +CONF_CLOCK_PHASE = "clock_phase" +CONF_DOUBLE_BUFFER = "double_buffer" +CONF_MIN_REFRESH_RATE = "min_refresh_rate" + +# Map to hub75 library enums (in global namespace) +Hub75ShiftDriver = cg.global_ns.enum("Hub75ShiftDriver", is_class=True) +SHIFT_DRIVERS = { + "GENERIC": Hub75ShiftDriver.GENERIC, + "FM6126A": Hub75ShiftDriver.FM6126A, + "ICN2038S": Hub75ShiftDriver.ICN2038S, + "FM6124": Hub75ShiftDriver.FM6124, + "MBI5124": Hub75ShiftDriver.MBI5124, + "DP3246": Hub75ShiftDriver.DP3246, +} + +Hub75PanelLayout = cg.global_ns.enum("Hub75PanelLayout", is_class=True) +PANEL_LAYOUTS = { + "HORIZONTAL": Hub75PanelLayout.HORIZONTAL, + "TOP_LEFT_DOWN": Hub75PanelLayout.TOP_LEFT_DOWN, + "TOP_RIGHT_DOWN": Hub75PanelLayout.TOP_RIGHT_DOWN, + "BOTTOM_LEFT_UP": Hub75PanelLayout.BOTTOM_LEFT_UP, + "BOTTOM_RIGHT_UP": Hub75PanelLayout.BOTTOM_RIGHT_UP, + "TOP_LEFT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_LEFT_DOWN_ZIGZAG, + "TOP_RIGHT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_RIGHT_DOWN_ZIGZAG, + "BOTTOM_LEFT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_LEFT_UP_ZIGZAG, + "BOTTOM_RIGHT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG, +} + +Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True) +SCAN_PATTERNS = { + "STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN, + "FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH, + "FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH, + "FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH, +} + +Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True) +CLOCK_SPEEDS = { + "8MHZ": Hub75ClockSpeed.HZ_8M, + "10MHZ": Hub75ClockSpeed.HZ_10M, + "16MHZ": Hub75ClockSpeed.HZ_16M, + "20MHZ": Hub75ClockSpeed.HZ_20M, +} + +Hub75Rotation = cg.global_ns.enum("Hub75Rotation", is_class=True) +ROTATIONS = { + 0: Hub75Rotation.ROTATE_0, + 90: Hub75Rotation.ROTATE_90, + 180: Hub75Rotation.ROTATE_180, + 270: Hub75Rotation.ROTATE_270, +} + +HUB75Display = hub75_ns.class_("HUB75Display", cg.PollingComponent, display.Display) +Hub75Config = cg.global_ns.struct("Hub75Config") +Hub75Pins = cg.global_ns.struct("Hub75Pins") +SetBrightnessAction = hub75_ns.class_("SetBrightnessAction", automation.Action) + + +def _merge_board_pins(config: ConfigType) -> ConfigType: + """Merge board preset pins with explicit pin overrides.""" + board_name = config.get(CONF_BOARD) + + if board_name is None: + # No board specified - validate that all required pins are present + errs = [ + cv.Invalid( + f"Required pin '{pin_name}' is missing. " + f"Either specify a board preset or provide all pin mappings manually.", + path=[pin_name], + ) + for pin_name in REQUIRED_PINS + if pin_name not in config + ] + + if errs: + raise cv.MultipleInvalid(errs) + + # E_PIN is optional + return config + + # Get board configuration + if board_name not in BOARDS: + raise cv.Invalid( + f"Unknown board '{board_name}'. Available boards: {', '.join(sorted(BOARDS.keys()))}" + ) + + board = BOARDS[board_name] + + # Merge board pins with explicit overrides + # Explicit pins in config take precedence over board defaults + for conf_key, board_key in PIN_MAPPING.items(): + if conf_key in config or (board_pin := board.get_pin(board_key)) is None: + continue + # Create pin config + pin_config = {"number": board_pin} + if conf_key in board.ignore_strapping_pins: + pin_config["ignore_strapping_warning"] = True + + # Validate through pin schema to add required fields (id, etc.) + config[conf_key] = pins.gpio_output_pin_schema(pin_config) + + return config + + +def _validate_config(config: ConfigType) -> ConfigType: + """Validate driver and layout requirements.""" + errs: list[cv.Invalid] = [] + + # MBI5124 requires inverted clock phase + driver = config.get(CONF_SHIFT_DRIVER, "GENERIC") + if driver == "MBI5124" and not config.get(CONF_CLOCK_PHASE, False): + errs.append( + cv.Invalid( + "MBI5124 shift driver requires 'clock_phase: true' to be set", + path=[CONF_CLOCK_PHASE], + ) + ) + + # Prevent conflicting min_refresh_rate + update_interval configuration + # min_refresh_rate is auto-calculated from update_interval unless using LVGL mode + update_interval = config.get(CONF_UPDATE_INTERVAL) + if CONF_MIN_REFRESH_RATE in config and update_interval is not None: + # Handle both integer (NEVER) and time object cases + interval_ms = ( + update_interval + if isinstance(update_interval, int) + else update_interval.total_milliseconds + ) + if interval_ms != NEVER: + errs.append( + cv.Invalid( + "Cannot set both 'min_refresh_rate' and 'update_interval' (except 'never'). " + "Refresh rate is auto-calculated from update_interval. " + "Remove 'min_refresh_rate' or use 'update_interval: never' for LVGL mode.", + path=[CONF_MIN_REFRESH_RATE], + ) + ) + + # Validate layout configuration (validate effective config including C++ defaults) + layout = config.get(CONF_LAYOUT, "HORIZONTAL") + layout_rows = config.get(CONF_LAYOUT_ROWS, 1) + layout_cols = config.get(CONF_LAYOUT_COLS, 1) + is_zigzag = "ZIGZAG" in layout + + # Single panel (1x1) should use HORIZONTAL + if layout_rows == 1 and layout_cols == 1 and layout != "HORIZONTAL": + errs.append( + cv.Invalid( + f"Single panel (layout_rows=1, layout_cols=1) should use 'layout: HORIZONTAL' (got {layout})", + path=[CONF_LAYOUT], + ) + ) + + # HORIZONTAL layout requires single row + if layout == "HORIZONTAL" and layout_rows != 1: + errs.append( + cv.Invalid( + f"HORIZONTAL layout requires 'layout_rows: 1' (got {layout_rows}). " + "For multi-row grids, use TOP_LEFT_DOWN or other grid layouts.", + path=[CONF_LAYOUT_ROWS], + ) + ) + + # Grid layouts (non-HORIZONTAL) require more than one panel + if layout != "HORIZONTAL" and layout_rows == 1 and layout_cols == 1: + errs.append( + cv.Invalid( + f"Grid layout '{layout}' requires multiple panels (layout_rows > 1 or layout_cols > 1)", + path=[CONF_LAYOUT], + ) + ) + + # Serpentine layouts (non-ZIGZAG) require multiple rows + # Serpentine physically rotates alternate rows upside down (Y-coordinate inversion) + # Single-row chains should use HORIZONTAL or ZIGZAG variants + if not is_zigzag and layout != "HORIZONTAL" and layout_rows == 1: + errs.append( + cv.Invalid( + f"Serpentine layout '{layout}' requires layout_rows > 1 " + f"(got layout_rows={layout_rows}). " + "Serpentine wiring physically rotates alternate rows upside down. " + "For single-row chains, use 'layout: HORIZONTAL' or add '_ZIGZAG' suffix.", + path=[CONF_LAYOUT_ROWS], + ) + ) + + # ZIGZAG layouts require actual grid (both rows AND cols > 1) + if is_zigzag and (layout_rows == 1 or layout_cols == 1): + errs.append( + cv.Invalid( + f"ZIGZAG layout '{layout}' requires both layout_rows > 1 AND layout_cols > 1 " + f"(got rows={layout_rows}, cols={layout_cols}). " + "For single row/column chains, use non-zigzag layouts or HORIZONTAL.", + path=[CONF_LAYOUT], + ) + ) + + if errs: + raise cv.MultipleInvalid(errs) + + return config + + +def _final_validate(config: ConfigType) -> ConfigType: + """Validate requirements when using HUB75 display.""" + # Local imports to avoid circular dependencies + from esphome.components.esp32 import get_esp32_variant + from esphome.components.esp32.const import VARIANT_ESP32P4 + from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN + from esphome.components.psram import DOMAIN as PSRAM_DOMAIN + + full_config = fv.full_config.get() + errs: list[cv.Invalid] = [] + + # ESP32-P4 requires PSRAM + variant = get_esp32_variant() + if variant == VARIANT_ESP32P4 and PSRAM_DOMAIN not in full_config: + errs.append( + cv.Invalid( + "HUB75 display on ESP32-P4 requires PSRAM. Add 'psram:' to your configuration.", + path=[CONF_ID], + ) + ) + + # LVGL-specific validation + if LVGL_DOMAIN in full_config: + # Check update_interval (converted from "never" to NEVER constant) + update_interval = config.get(CONF_UPDATE_INTERVAL) + if update_interval is not None: + # Handle both integer (NEVER) and time object cases + interval_ms = ( + update_interval + if isinstance(update_interval, int) + else update_interval.total_milliseconds + ) + if interval_ms != NEVER: + errs.append( + cv.Invalid( + "HUB75 display with LVGL must have 'update_interval: never'. " + "LVGL manages its own refresh timing.", + path=[CONF_UPDATE_INTERVAL], + ) + ) + + # Check auto_clear_enabled + auto_clear = config[CONF_AUTO_CLEAR_ENABLED] + if auto_clear is not False: + errs.append( + cv.Invalid( + f"HUB75 display with LVGL must have 'auto_clear_enabled: false' (got '{auto_clear}'). " + "LVGL manages screen clearing.", + path=[CONF_AUTO_CLEAR_ENABLED], + ) + ) + + # Check double_buffer (C++ default: false) + double_buffer = config.get(CONF_DOUBLE_BUFFER, False) + if double_buffer is not False: + errs.append( + cv.Invalid( + f"HUB75 display with LVGL must have 'double_buffer: false' (got '{double_buffer}'). " + "LVGL uses its own buffering strategy.", + path=[CONF_DOUBLE_BUFFER], + ) + ) + + if errs: + raise cv.MultipleInvalid(errs) + + return config + + +FINAL_VALIDATE_SCHEMA = cv.Schema(_final_validate) + + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(HUB75Display), + # Override rotation - store Hub75Rotation directly (driver handles rotation) + cv.Optional(CONF_ROTATION): cv.enum(ROTATIONS, int=True), + # Board preset (optional - provides default pin mappings) + cv.Optional(CONF_BOARD): cv.one_of(*BOARDS.keys(), lower=True), + # Panel dimensions + cv.Required(CONF_PANEL_WIDTH): cv.positive_int, + cv.Required(CONF_PANEL_HEIGHT): cv.positive_int, + # Multi-panel layout + cv.Optional(CONF_LAYOUT_ROWS): cv.positive_int, + cv.Optional(CONF_LAYOUT_COLS): cv.positive_int, + cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"), + # Panel hardware configuration + cv.Optional(CONF_SCAN_WIRING): cv.enum( + SCAN_PATTERNS, upper=True, space="_" + ), + cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True), + # Display configuration + cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean, + cv.Optional(CONF_BRIGHTNESS): cv.int_range(min=0, max=255), + cv.Optional(CONF_BIT_DEPTH): cv.int_range(min=4, max=12), + cv.Optional(CONF_GAMMA_CORRECT): cv.enum( + {"LINEAR": 0, "CIE1931": 1, "GAMMA_2_2": 2}, upper=True + ), + cv.Optional(CONF_MIN_REFRESH_RATE): cv.int_range(min=40, max=200), + # RGB data pins + cv.Optional(CONF_R1_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_G1_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_B1_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_R2_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_G2_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_B2_PIN): pins.gpio_output_pin_schema, + # Address pins + cv.Optional(CONF_A_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_B_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_C_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_D_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_E_PIN): pins.gpio_output_pin_schema, + # Control pins + cv.Optional(CONF_LAT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_OE_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_CLK_PIN): pins.gpio_output_pin_schema, + # Timing configuration + cv.Optional(CONF_CLOCK_SPEED): cv.enum(CLOCK_SPEEDS, upper=True), + cv.Optional(CONF_LATCH_BLANKING): cv.positive_int, + cv.Optional(CONF_CLOCK_PHASE): cv.boolean, + } + ), + _merge_board_pins, + _validate_config, +) + + +DEFAULT_REFRESH_RATE = 60 # Hz + + +def _calculate_min_refresh_rate(config: ConfigType) -> int: + """Calculate minimum refresh rate for the display. + + Priority: + 1. Explicit min_refresh_rate setting (user override) + 2. Derived from update_interval (ms to Hz conversion) + 3. Default 60 Hz (for LVGL or unspecified interval) + """ + if CONF_MIN_REFRESH_RATE in config: + return config[CONF_MIN_REFRESH_RATE] + + update_interval = config.get(CONF_UPDATE_INTERVAL) + if update_interval is None: + return DEFAULT_REFRESH_RATE + + # update_interval can be TimePeriod object or NEVER constant (int) + interval_ms = ( + update_interval + if isinstance(update_interval, int) + else update_interval.total_milliseconds + ) + + # "never" or zero means external refresh (e.g., LVGL) + if interval_ms in (NEVER, 0): + return DEFAULT_REFRESH_RATE + + # Convert ms interval to Hz, clamped to valid range [40, 200] + return max(40, min(200, int(round(1000 / interval_ms)))) + + +def _build_pins_struct( + pin_expressions: dict[str, Any], e_pin_num: int | cg.RawExpression +) -> cg.StructInitializer: + """Build Hub75Pins struct from pin expressions.""" + + def pin_cast(pin): + return cg.RawExpression(f"static_cast({pin.get_pin()})") + + return cg.StructInitializer( + Hub75Pins, + ("r1", pin_cast(pin_expressions["r1"])), + ("g1", pin_cast(pin_expressions["g1"])), + ("b1", pin_cast(pin_expressions["b1"])), + ("r2", pin_cast(pin_expressions["r2"])), + ("g2", pin_cast(pin_expressions["g2"])), + ("b2", pin_cast(pin_expressions["b2"])), + ("a", pin_cast(pin_expressions["a"])), + ("b", pin_cast(pin_expressions["b"])), + ("c", pin_cast(pin_expressions["c"])), + ("d", pin_cast(pin_expressions["d"])), + ("e", e_pin_num), + ("lat", pin_cast(pin_expressions["lat"])), + ("oe", pin_cast(pin_expressions["oe"])), + ("clk", pin_cast(pin_expressions["clk"])), + ) + + +def _append_config_fields( + config: ConfigType, + field_mapping: list[tuple[str, str]], + config_fields: list[tuple[str, Any]], +) -> None: + """Append config fields from mapping if present in config.""" + for conf_key, struct_field in field_mapping: + if conf_key in config: + config_fields.append((struct_field, config[conf_key])) + + +def _build_config_struct( + config: ConfigType, pins_struct: cg.StructInitializer, min_refresh: int +) -> cg.StructInitializer: + """Build Hub75Config struct from config. + + Fields must be added in declaration order (see hub75_types.h) to satisfy + C++ designated initializer requirements. The order is: + 1. fields_before_pins (panel_width through layout) + 2. rotation + 3. pins + 4. output_clock_speed + 5. min_refresh_rate + 6. fields_after_min_refresh (latch_blanking through brightness) + """ + fields_before_pins = [ + (CONF_PANEL_WIDTH, "panel_width"), + (CONF_PANEL_HEIGHT, "panel_height"), + # scan_pattern - auto-calculated, not set + (CONF_SCAN_WIRING, "scan_wiring"), + (CONF_SHIFT_DRIVER, "shift_driver"), + (CONF_LAYOUT_ROWS, "layout_rows"), + (CONF_LAYOUT_COLS, "layout_cols"), + (CONF_LAYOUT, "layout"), + ] + fields_after_min_refresh = [ + (CONF_LATCH_BLANKING, "latch_blanking"), + (CONF_DOUBLE_BUFFER, "double_buffer"), + (CONF_CLOCK_PHASE, "clk_phase_inverted"), + (CONF_BRIGHTNESS, "brightness"), + ] + + config_fields: list[tuple[str, Any]] = [] + + _append_config_fields(config, fields_before_pins, config_fields) + + # Rotation - config already contains Hub75Rotation enum from cv.enum + if CONF_ROTATION in config: + config_fields.append(("rotation", config[CONF_ROTATION])) + + config_fields.append(("pins", pins_struct)) + + if CONF_CLOCK_SPEED in config: + config_fields.append(("output_clock_speed", config[CONF_CLOCK_SPEED])) + + config_fields.append(("min_refresh_rate", min_refresh)) + + _append_config_fields(config, fields_after_min_refresh, config_fields) + + return cg.StructInitializer(Hub75Config, *config_fields) + + +async def to_code(config: ConfigType) -> None: + add_idf_component( + name="esphome/esp-hub75", + ref="0.2.2", + ) + + # Set compile-time configuration via defines + if CONF_BIT_DEPTH in config: + cg.add_define("HUB75_BIT_DEPTH", config[CONF_BIT_DEPTH]) + + if CONF_GAMMA_CORRECT in config: + cg.add_define("HUB75_GAMMA_MODE", config[CONF_GAMMA_CORRECT]) + + # Await all pin expressions + pin_expressions = { + "r1": await cg.gpio_pin_expression(config[CONF_R1_PIN]), + "g1": await cg.gpio_pin_expression(config[CONF_G1_PIN]), + "b1": await cg.gpio_pin_expression(config[CONF_B1_PIN]), + "r2": await cg.gpio_pin_expression(config[CONF_R2_PIN]), + "g2": await cg.gpio_pin_expression(config[CONF_G2_PIN]), + "b2": await cg.gpio_pin_expression(config[CONF_B2_PIN]), + "a": await cg.gpio_pin_expression(config[CONF_A_PIN]), + "b": await cg.gpio_pin_expression(config[CONF_B_PIN]), + "c": await cg.gpio_pin_expression(config[CONF_C_PIN]), + "d": await cg.gpio_pin_expression(config[CONF_D_PIN]), + "lat": await cg.gpio_pin_expression(config[CONF_LAT_PIN]), + "oe": await cg.gpio_pin_expression(config[CONF_OE_PIN]), + "clk": await cg.gpio_pin_expression(config[CONF_CLK_PIN]), + } + + # E pin is optional + if CONF_E_PIN in config: + e_pin = await cg.gpio_pin_expression(config[CONF_E_PIN]) + e_pin_num = cg.RawExpression(f"static_cast({e_pin.get_pin()})") + else: + e_pin_num = -1 + + # Build structs + min_refresh = _calculate_min_refresh_rate(config) + pins_struct = _build_pins_struct(pin_expressions, e_pin_num) + hub75_config = _build_config_struct(config, pins_struct, min_refresh) + + # Rotation is handled by the hub75 driver (config_.rotation already set above). + # Force rotation to 0 for ESPHome's Display base class to avoid double-rotation. + if CONF_ROTATION in config: + config[CONF_ROTATION] = 0 + + # Create display and register + var = cg.new_Pvariable(config[CONF_ID], hub75_config) + await display.register_display(var, config) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + + +@automation.register_action( + "hub75.set_brightness", + SetBrightnessAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(HUB75Display), + cv.Required(CONF_BRIGHTNESS): cv.templatable(cv.int_range(min=0, max=255)), + }, + key=CONF_BRIGHTNESS, + ), +) +async def hub75_set_brightness_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, cg.uint8) + cg.add(var.set_brightness(template_)) + return var diff --git a/esphome/components/hub75/hub75.cpp b/esphome/components/hub75/hub75.cpp new file mode 100644 index 0000000000..cf8661b2b3 --- /dev/null +++ b/esphome/components/hub75/hub75.cpp @@ -0,0 +1,206 @@ +#include "hub75_component.h" +#include "esphome/core/application.h" + +#ifdef USE_ESP32 + +namespace esphome::hub75 { + +static const char *const TAG = "hub75"; + +// ======================================== +// Constructor +// ======================================== + +HUB75Display::HUB75Display(const Hub75Config &config) : config_(config) { + // Initialize runtime state from config + this->brightness_ = config.brightness; + this->enabled_ = (config.brightness > 0); +} + +// ======================================== +// Core Component methods +// ======================================== + +void HUB75Display::setup() { + ESP_LOGCONFIG(TAG, "Setting up HUB75Display..."); + + // Create driver with pre-configured config + driver_ = new Hub75Driver(config_); + if (!driver_->begin()) { + ESP_LOGE(TAG, "Failed to initialize HUB75 driver!"); + return; + } + + this->enabled_ = true; +} + +void HUB75Display::dump_config() { + LOG_DISPLAY("", "HUB75", this); + + ESP_LOGCONFIG(TAG, + " Panel: %dx%d pixels\n" + " Layout: %dx%d panels\n" + " Virtual Display: %dx%d pixels", + config_.panel_width, config_.panel_height, config_.layout_cols, config_.layout_rows, + config_.panel_width * config_.layout_cols, config_.panel_height * config_.layout_rows); + + ESP_LOGCONFIG(TAG, + " Scan Wiring: %d\n" + " Shift Driver: %d", + static_cast(config_.scan_wiring), static_cast(config_.shift_driver)); + + ESP_LOGCONFIG(TAG, + " Pins: R1:%i, G1:%i, B1:%i, R2:%i, G2:%i, B2:%i\n" + " Pins: A:%i, B:%i, C:%i, D:%i, E:%i\n" + " Pins: LAT:%i, OE:%i, CLK:%i", + config_.pins.r1, config_.pins.g1, config_.pins.b1, config_.pins.r2, config_.pins.g2, config_.pins.b2, + config_.pins.a, config_.pins.b, config_.pins.c, config_.pins.d, config_.pins.e, config_.pins.lat, + config_.pins.oe, config_.pins.clk); + + ESP_LOGCONFIG(TAG, + " Clock Speed: %u MHz\n" + " Latch Blanking: %i\n" + " Clock Phase: %s\n" + " Min Refresh Rate: %i Hz\n" + " Bit Depth: %i\n" + " Double Buffer: %s", + static_cast(config_.output_clock_speed) / 1000000, config_.latch_blanking, + TRUEFALSE(config_.clk_phase_inverted), config_.min_refresh_rate, HUB75_BIT_DEPTH, + YESNO(config_.double_buffer)); +} + +// ======================================== +// Display/PollingComponent methods +// ======================================== + +void HUB75Display::update() { + if (!driver_) [[unlikely]] + return; + if (!this->enabled_) [[unlikely]] + return; + + this->do_update_(); + + if (config_.double_buffer) { + driver_->flip_buffer(); + } +} + +void HUB75Display::fill(Color color) { + if (!driver_) [[unlikely]] + return; + if (!this->enabled_) [[unlikely]] + return; + + // Start with full display rect + display::Rect fill_rect(0, 0, this->get_width_internal(), this->get_height_internal()); + + // Apply clipping using Rect::shrink() to intersect + display::Rect clip = this->get_clipping(); + if (clip.is_set()) { + fill_rect.shrink(clip); + if (!fill_rect.is_set()) + return; // Completely clipped + } + + // Fast path: black filling entire display + if (!color.is_on() && fill_rect.x == 0 && fill_rect.y == 0 && fill_rect.w == this->get_width_internal() && + fill_rect.h == this->get_height_internal()) { + driver_->clear(); + return; + } + + driver_->fill(fill_rect.x, fill_rect.y, fill_rect.w, fill_rect.h, color.r, color.g, color.b); +} + +void HOT HUB75Display::draw_pixel_at(int x, int y, Color color) { + if (!driver_) [[unlikely]] + return; + if (!this->enabled_) [[unlikely]] + return; + + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]] + return; + + if (!this->get_clipping().inside(x, y)) + return; + + driver_->set_pixel(x, y, color.r, color.g, color.b); + App.feed_wdt(); +} + +void HOT HUB75Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order, + ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { + if (!driver_) [[unlikely]] + return; + if (!this->enabled_) [[unlikely]] + return; + + // Map ESPHome enums to hub75 enums + Hub75PixelFormat format; + Hub75ColorOrder color_order = Hub75ColorOrder::RGB; + int bytes_per_pixel; + + // Determine format based on bitness + if (bitness == ColorBitness::COLOR_BITNESS_565) { + format = Hub75PixelFormat::RGB565; + bytes_per_pixel = 2; + } else if (bitness == ColorBitness::COLOR_BITNESS_888) { +#ifdef USE_LVGL +#if LV_COLOR_DEPTH == 32 + // 32-bit: 4 bytes per pixel with padding byte (LVGL mode) + format = Hub75PixelFormat::RGB888_32; + bytes_per_pixel = 4; + + // Map ESPHome ColorOrder to Hub75ColorOrder + // ESPHome ColorOrder is typically BGR for little-endian 32-bit + color_order = (order == ColorOrder::COLOR_ORDER_RGB) ? Hub75ColorOrder::RGB : Hub75ColorOrder::BGR; +#elif LV_COLOR_DEPTH == 24 + // 24-bit: 3 bytes per pixel, tightly packed + format = Hub75PixelFormat::RGB888; + bytes_per_pixel = 3; + // Note: 24-bit is always RGB order in LVGL +#else + ESP_LOGE(TAG, "Unsupported LV_COLOR_DEPTH: %d", LV_COLOR_DEPTH); + return; +#endif +#else + // Non-LVGL mode: standard 24-bit RGB888 + format = Hub75PixelFormat::RGB888; + bytes_per_pixel = 3; + color_order = (order == ColorOrder::COLOR_ORDER_RGB) ? Hub75ColorOrder::RGB : Hub75ColorOrder::BGR; +#endif + } else { + ESP_LOGE(TAG, "Unsupported bitness: %d", static_cast(bitness)); + return; + } + + // Check if buffer is tightly packed (no stride) + const int stride_px = x_offset + w + x_pad; + const bool is_packed = (x_offset == 0 && x_pad == 0 && y_offset == 0); + + if (is_packed) { + // Tightly packed buffer - single bulk call for best performance + driver_->draw_pixels(x_start, y_start, w, h, ptr, format, color_order, big_endian); + } else { + // Buffer has stride (padding between rows) - draw row by row + for (int yy = 0; yy < h; ++yy) { + const size_t row_offset = ((y_offset + yy) * stride_px + x_offset) * bytes_per_pixel; + const uint8_t *row_ptr = ptr + row_offset; + + driver_->draw_pixels(x_start, y_start + yy, w, 1, row_ptr, format, color_order, big_endian); + } + } +} + +void HUB75Display::set_brightness(uint8_t brightness) { + this->brightness_ = brightness; + this->enabled_ = (brightness > 0); + if (this->driver_ != nullptr) { + this->driver_->set_brightness(brightness); + } +} + +} // namespace esphome::hub75 + +#endif diff --git a/esphome/components/hub75/hub75_component.h b/esphome/components/hub75/hub75_component.h new file mode 100644 index 0000000000..ab7e3fc5b1 --- /dev/null +++ b/esphome/components/hub75/hub75_component.h @@ -0,0 +1,63 @@ +#pragma once + +#ifdef USE_ESP32 + +#include + +#include "esphome/components/display/display_buffer.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "hub75.h" // hub75 library + +namespace esphome::hub75 { + +using esphome::display::ColorBitness; +using esphome::display::ColorOrder; + +class HUB75Display : public display::Display { + public: + // Constructor accepting config + explicit HUB75Display(const Hub75Config &config); + + // Core Component methods + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + + // Display/PollingComponent methods + void update() override; + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + void fill(Color color) override; + void draw_pixel_at(int x, int y, Color color) override; + void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; + + // Brightness control (runtime mutable) + void set_brightness(uint8_t brightness); + + protected: + // Display internal methods + int get_width_internal() override { return this->driver_ != nullptr ? this->driver_->get_width() : 0; } + int get_height_internal() override { return this->driver_ != nullptr ? this->driver_->get_height() : 0; } + + // Member variables + Hub75Driver *driver_{nullptr}; + Hub75Config config_; // Immutable configuration + + // Runtime state (mutable) + uint8_t brightness_{128}; + bool enabled_{false}; +}; + +template class SetBrightnessAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, brightness) + + void play(const Ts &...x) override { this->parent_->set_brightness(this->brightness_.value(x...)); } +}; + +} // namespace esphome::hub75 + +#endif diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 6308923759..19efda0b49 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -2,6 +2,23 @@ import logging from esphome import pins import esphome.codegen as cg +from esphome.components import esp32 +from esphome.components.esp32 import ( + VARIANT_ESP32, + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32H2, + VARIANT_ESP32P4, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + get_esp32_variant, +) +from esphome.components.esp32.gpio_esp32_c5 import esp32_c5_validate_lp_i2c +from esphome.components.esp32.gpio_esp32_c6 import esp32_c6_validate_lp_i2c +from esphome.components.esp32.gpio_esp32_p4 import esp32_p4_validate_lp_i2c from esphome.components.zephyr import ( zephyr_add_overlay, zephyr_add_prj_conf, @@ -16,6 +33,7 @@ from esphome.const import ( CONF_I2C, CONF_I2C_ID, CONF_ID, + CONF_LOW_POWER_MODE, CONF_SCAN, CONF_SCL, CONF_SDA, @@ -40,6 +58,25 @@ IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component) ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component) I2CDevice = i2c_ns.class_("I2CDevice") +ESP32_I2C_CAPABILITIES = { + # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/soc_caps.h + VARIANT_ESP32: {"NUM": 2, "HP": 2}, + VARIANT_ESP32C2: {"NUM": 1, "HP": 1}, + VARIANT_ESP32C3: {"NUM": 1, "HP": 1}, + VARIANT_ESP32C5: {"NUM": 2, "HP": 1, "LP": 1}, + VARIANT_ESP32C6: {"NUM": 2, "HP": 1, "LP": 1}, + VARIANT_ESP32C61: {"NUM": 1, "HP": 1}, + VARIANT_ESP32H2: {"NUM": 2, "HP": 2}, + VARIANT_ESP32P4: {"NUM": 3, "HP": 2, "LP": 1}, + VARIANT_ESP32S2: {"NUM": 2, "HP": 2}, + VARIANT_ESP32S3: {"NUM": 2, "HP": 2}, +} +VALIDATE_LP_I2C = { + VARIANT_ESP32C5: esp32_c5_validate_lp_i2c, + VARIANT_ESP32C6: esp32_c6_validate_lp_i2c, + VARIANT_ESP32P4: esp32_p4_validate_lp_i2c, +} +LP_I2C_VARIANT = list(VALIDATE_LP_I2C.keys()) CONF_SDA_PULLUP_ENABLED = "sda_pullup_enabled" CONF_SCL_PULLUP_ENABLED = "scl_pullup_enabled" @@ -47,18 +84,20 @@ MULTI_CONF = True def _bus_declare_type(value): + if CORE.is_esp32: + return cv.declare_id(IDFI2CBus)(value) if CORE.using_arduino: return cv.declare_id(ArduinoI2CBus)(value) - if CORE.using_esp_idf: - return cv.declare_id(IDFI2CBus)(value) if CORE.using_zephyr: return cv.declare_id(ZephyrI2CBus)(value) raise NotImplementedError def validate_config(config): - if CORE.using_esp_idf: - return cv.require_framework_version(esp_idf=cv.Version(5, 4, 2))(config) + if CORE.is_esp32: + return cv.require_framework_version( + esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1) + )(config) return config @@ -67,12 +106,12 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): _bus_declare_type, cv.Optional(CONF_SDA, default="SDA"): pins.internal_gpio_pin_number, - cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean + cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32=True): cv.All( + cv.only_on_esp32, cv.boolean ), cv.Optional(CONF_SCL, default="SCL"): pins.internal_gpio_pin_number, - cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( - cv.only_with_esp_idf, cv.boolean + cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32=True): cv.All( + cv.only_on_esp32, cv.boolean ), cv.SplitDefault( CONF_FREQUENCY, @@ -82,13 +121,20 @@ CONFIG_SCHEMA = cv.All( nrf52="100kHz", ): cv.All( cv.frequency, - cv.Range(min=0, min_included=False), + cv.float_range(min=0, min_included=False), ), cv.Optional(CONF_TIMEOUT): cv.All( cv.only_with_framework(["arduino", "esp-idf"]), cv.positive_time_period, ), cv.Optional(CONF_SCAN, default=True): cv.boolean, + cv.Optional(CONF_LOW_POWER_MODE): cv.All( + cv.only_on_esp32, + esp32.only_on_variant( + supported=LP_I2C_VARIANT, msg_prefix="Low power i2c" + ), + cv.boolean, + ), } ).extend(cv.COMPONENT_SCHEMA), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_NRF52]), @@ -100,6 +146,31 @@ def _final_validate(config): full_config = fv.full_config.get()[CONF_I2C] if CORE.using_zephyr and len(full_config) > 1: raise cv.Invalid("Second i2c is not implemented on Zephyr yet") + if CORE.is_esp32 and get_esp32_variant() in ESP32_I2C_CAPABILITIES: + variant = get_esp32_variant() + max_num = ESP32_I2C_CAPABILITIES[variant]["NUM"] + if len(full_config) > max_num: + raise cv.Invalid( + f"The maximum number of i2c interfaces for {variant} is {max_num}" + ) + if variant in LP_I2C_VARIANT: + max_lp_num = ESP32_I2C_CAPABILITIES[variant]["LP"] + max_hp_num = ESP32_I2C_CAPABILITIES[variant]["HP"] + lp_num = sum( + CONF_LOW_POWER_MODE in conf and conf[CONF_LOW_POWER_MODE] + for conf in full_config + ) + hp_num = len(full_config) - lp_num + if CONF_LOW_POWER_MODE in config and config[CONF_LOW_POWER_MODE]: + VALIDATE_LP_I2C[variant](config) + if lp_num > max_lp_num: + raise cv.Invalid( + f"The maximum number of low power i2c interfaces for {variant} is {max_lp_num}" + ) + if hp_num > max_hp_num: + raise cv.Invalid( + f"The maximum number of high power i2c interfaces for {variant} is {max_hp_num}" + ) FINAL_VALIDATE_SCHEMA = _final_validate @@ -151,8 +222,10 @@ async def to_code(config): cg.add(var.set_scan(config[CONF_SCAN])) if CONF_TIMEOUT in config: cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds))) - if CORE.using_arduino: + if CORE.using_arduino and not CORE.is_esp32: cg.add_library("Wire", None) + if CONF_LOW_POWER_MODE in config: + cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE]))) def i2c_device_schema(default_address): @@ -164,10 +237,6 @@ def i2c_device_schema(default_address): """ schema = { cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus), - cv.Optional("multiplexer"): cv.invalid( - "This option has been removed, please see " - "the tca9584a docs for the updated way to use multiplexers" - ), } if default_address is None: schema[cv.Required(CONF_ADDRESS)] = cv.i2c_address @@ -181,7 +250,7 @@ async def register_i2c_device(var, config): Sets the i2c bus to use and the i2c address. - This is a coroutine, you need to await it with a 'yield' expression! + This is a coroutine, you need to await it with an 'await' expression! """ parent = await cg.get_variable(config[CONF_I2C_ID]) cg.add(var.set_i2c_bus(parent)) @@ -248,14 +317,16 @@ def final_validate_device_schema( FILTER_SOURCE_FILES = filter_source_files_from_platform( { "i2c_bus_arduino.cpp": { - PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2040_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, - "i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "i2c_bus_esp_idf.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, "i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 221423418b..e728830147 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) #include "i2c_bus_arduino.h" #include @@ -12,19 +12,13 @@ namespace i2c { static const char *const TAG = "i2c.arduino"; +// Maximum bytes to log in hex format (truncates larger transfers) +static constexpr size_t I2C_MAX_LOG_BYTES = 32; + void ArduinoI2CBus::setup() { recover_(); -#if defined(USE_ESP32) - static uint8_t next_bus_num = 0; - if (next_bus_num == 0) { - wire_ = &Wire; - } else { - wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory) - } - this->port_ = next_bus_num; - next_bus_num++; -#elif defined(USE_ESP8266) +#if defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) static bool first = true; @@ -54,10 +48,7 @@ void ArduinoI2CBus::set_pins_and_clock_() { wire_->begin(static_cast(sda_pin_), static_cast(scl_pin_)); #endif if (timeout_ > 0) { // if timeout specified in yaml -#if defined(USE_ESP32) - // https://github.com/espressif/arduino-esp32/blob/master/libraries/Wire/src/Wire.cpp - wire_->setTimeOut(timeout_ / 1000); // unit: ms -#elif defined(USE_ESP8266) +#if defined(USE_ESP8266) // https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h wire_->setClockStretchLimit(timeout_); // unit: us #elif defined(USE_RP2040) @@ -76,9 +67,7 @@ void ArduinoI2CBus::dump_config() { " Frequency: %u Hz", this->sda_pin_, this->scl_pin_, this->frequency_); if (timeout_ > 0) { -#if defined(USE_ESP32) - ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); -#elif defined(USE_ESP8266) +#if defined(USE_ESP8266) ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_); #elif defined(USE_RP2040) ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); @@ -121,7 +110,10 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe return ERROR_NOT_INITIALIZED; } - ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(I2C_MAX_LOG_BYTES)]; + ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty_to(hex_buf, write_buffer, write_count)); +#endif uint8_t status = 0; if (write_count != 0 || read_count == 0) { @@ -275,4 +267,4 @@ void ArduinoI2CBus::recover_() { } // namespace i2c } // namespace esphome -#endif // USE_ESP_IDF +#endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index b441828353..2d69e7684c 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ARDUINO +#if defined(USE_ARDUINO) && !defined(USE_ESP32) #include #include "esphome/core/component.h" @@ -29,7 +29,7 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { void set_frequency(uint32_t frequency) { frequency_ = frequency; } void set_timeout(uint32_t timeout) { timeout_ = timeout; } - int get_port() const override { return this->port_; } + int get_port() const override { return 0; } private: void recover_(); @@ -37,7 +37,6 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { RecoveryCode recovery_result_; protected: - int8_t port_{-1}; TwoWire *wire_; uint8_t sda_pin_; uint8_t scl_pin_; @@ -49,4 +48,4 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { } // namespace i2c } // namespace esphome -#endif // USE_ARDUINO +#endif // defined(USE_ARDUINO) && !defined(USE_ESP32) diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index bf50ea0586..191c849aa3 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "i2c_bus_esp_idf.h" @@ -15,14 +15,14 @@ namespace i2c { static const char *const TAG = "i2c.idf"; +// Maximum bytes to log in hex format (truncates larger transfers) +static constexpr size_t I2C_MAX_LOG_BYTES = 32; + void IDFI2CBus::setup() { - static i2c_port_t next_port = I2C_NUM_0; - this->port_ = next_port; - if (this->port_ == I2C_NUM_MAX) { - ESP_LOGE(TAG, "No more than %u buses supported", I2C_NUM_MAX); - this->mark_failed(); - return; - } + static i2c_port_t next_hp_port = I2C_NUM_0; +#if SOC_LP_I2C_SUPPORTED + static i2c_port_t next_lp_port = LP_I2C_NUM_0; +#endif if (this->timeout_ > 13000) { ESP_LOGW(TAG, "Using max allowed timeout: 13 ms"); @@ -31,23 +31,35 @@ void IDFI2CBus::setup() { this->recover_(); - next_port = (i2c_port_t) (next_port + 1); - i2c_master_bus_config_t bus_conf{}; memset(&bus_conf, 0, sizeof(bus_conf)); bus_conf.sda_io_num = gpio_num_t(sda_pin_); bus_conf.scl_io_num = gpio_num_t(scl_pin_); - bus_conf.i2c_port = this->port_; bus_conf.glitch_ignore_cnt = 7; #if SOC_LP_I2C_SUPPORTED - if (this->port_ < SOC_HP_I2C_NUM) { - bus_conf.clk_source = I2C_CLK_SRC_DEFAULT; - } else { + if (this->lp_mode_) { + if ((next_lp_port - LP_I2C_NUM_0) == SOC_LP_I2C_NUM) { + ESP_LOGE(TAG, "No more than %u LP buses supported", SOC_LP_I2C_NUM); + this->mark_failed(); + return; + } + this->port_ = next_lp_port; + next_lp_port = (i2c_port_t) (next_lp_port + 1); bus_conf.lp_source_clk = LP_I2C_SCLK_DEFAULT; - } -#else - bus_conf.clk_source = I2C_CLK_SRC_DEFAULT; + } else { #endif + if (next_hp_port == SOC_HP_I2C_NUM) { + ESP_LOGE(TAG, "No more than %u HP buses supported", SOC_HP_I2C_NUM); + this->mark_failed(); + return; + } + this->port_ = next_hp_port; + next_hp_port = (i2c_port_t) (next_hp_port + 1); + bus_conf.clk_source = I2C_CLK_SRC_DEFAULT; +#if SOC_LP_I2C_SUPPORTED + } +#endif + bus_conf.i2c_port = this->port_; bus_conf.flags.enable_internal_pullup = sda_pullup_enabled_ || scl_pullup_enabled_; esp_err_t err = i2c_new_master_bus(&bus_conf, &this->bus_); if (err != ESP_OK) { @@ -138,7 +150,10 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s jobs[num_jobs++].write.total_bytes = 1; } else { if (write_count != 0) { - ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(I2C_MAX_LOG_BYTES)]; + ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty_to(hex_buf, write_buffer, write_count)); +#endif jobs[num_jobs++].command = I2C_MASTER_CMD_START; jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; jobs[num_jobs].write.ack_check = true; @@ -299,4 +314,4 @@ void IDFI2CBus::recover_() { } // namespace i2c } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index f565be4535..84f4616967 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/component.h" #include "i2c_bus.h" @@ -30,6 +30,9 @@ class IDFI2CBus : public InternalI2CBus, public Component { void set_scl_pullup_enabled(bool scl_pullup_enabled) { this->scl_pullup_enabled_ = scl_pullup_enabled; } void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } void set_timeout(uint32_t timeout) { this->timeout_ = timeout; } +#if SOC_LP_I2C_SUPPORTED + void set_lp_mode(bool lp_mode) { this->lp_mode_ = lp_mode; } +#endif int get_port() const override { return this->port_; } @@ -48,9 +51,12 @@ class IDFI2CBus : public InternalI2CBus, public Component { uint32_t frequency_{}; uint32_t timeout_ = 0; bool initialized_ = false; +#if SOC_LP_I2C_SUPPORTED + bool lp_mode_ = false; +#endif }; } // namespace i2c } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/i2c/i2c_bus_zephyr.cpp b/esphome/components/i2c/i2c_bus_zephyr.cpp index 658dcee35c..1eb9944dcb 100644 --- a/esphome/components/i2c/i2c_bus_zephyr.cpp +++ b/esphome/components/i2c/i2c_bus_zephyr.cpp @@ -8,6 +8,22 @@ namespace esphome::i2c { static const char *const TAG = "i2c.zephyr"; +static const char *get_speed(uint32_t dev_config) { + switch (I2C_SPEED_GET(dev_config)) { + case I2C_SPEED_STANDARD: + return "100 kHz"; + case I2C_SPEED_FAST: + return "400 kHz"; + case I2C_SPEED_FAST_PLUS: + return "1 MHz"; + case I2C_SPEED_HIGH: + return "3.4 MHz"; + case I2C_SPEED_ULTRA: + return "5 MHz"; + } + return "unknown"; +} + void ZephyrI2CBus::setup() { if (!device_is_ready(this->i2c_dev_)) { ESP_LOGE(TAG, "I2C dev is not ready."); @@ -31,21 +47,6 @@ void ZephyrI2CBus::setup() { } void ZephyrI2CBus::dump_config() { - auto get_speed = [](uint32_t dev_config) { - switch (I2C_SPEED_GET(dev_config)) { - case I2C_SPEED_STANDARD: - return "100 kHz"; - case I2C_SPEED_FAST: - return "400 kHz"; - case I2C_SPEED_FAST_PLUS: - return "1 MHz"; - case I2C_SPEED_HIGH: - return "3.4 MHz"; - case I2C_SPEED_ULTRA: - return "5 MHz"; - } - return "unknown"; - }; ESP_LOGCONFIG(TAG, "I2C Bus:\n" " SDA Pin: GPIO%u\n" diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 907429ee0e..d3128c5f4c 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -1,15 +1,17 @@ from esphome import pins import esphome.codegen as cg -from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant -from esphome.components.esp32.const import ( +from esphome.components.esp32 import ( VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + add_idf_sdkconfig_option, + get_esp32_variant, ) import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE @@ -68,13 +70,14 @@ I2S_ROLE_OPTIONS = { # https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h (SOC_I2S_NUM) I2S_PORTS = { VARIANT_ESP32: 2, - VARIANT_ESP32S2: 1, - VARIANT_ESP32S3: 2, VARIANT_ESP32C3: 1, VARIANT_ESP32C5: 1, VARIANT_ESP32C6: 1, + VARIANT_ESP32C61: 1, VARIANT_ESP32H2: 1, VARIANT_ESP32P4: 3, + VARIANT_ESP32S2: 1, + VARIANT_ESP32S3: 2, } i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t") @@ -229,6 +232,8 @@ def validate_use_legacy(value): if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino): raise cv.Invalid("Arduino supports only the legacy i2s driver") _set_use_legacy_driver(value[CONF_USE_LEGACY]) + elif CORE.using_arduino: + _set_use_legacy_driver(True) return value @@ -258,8 +263,7 @@ def _final_validate(_): def use_legacy(): - legacy_driver = _get_use_legacy_driver() - return not (CORE.using_esp_idf and not legacy_driver) + return _get_use_legacy_driver() FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index 316ce7c48b..35c42e1b06 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -40,7 +40,7 @@ INTERNAL_DAC_OPTIONS = { EXTERNAL_DAC_OPTIONS = [CONF_MONO, CONF_STEREO] -NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] +NO_INTERNAL_DAC_VARIANTS = [esp32.VARIANT_ESP32S2] I2C_COMM_FMT_OPTIONS = ["lsb", "msb"] diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index f919199c60..dd23673db5 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -37,8 +37,8 @@ I2SAudioMicrophone = i2s_audio_ns.class_( "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component ) -INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32] -PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3] +INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32] +PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3] def _validate_esp32_variant(config): diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 98322d3a18..2e009a1de1 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -62,7 +62,7 @@ I2C_COMM_FMT_OPTIONS = { "pcm_long": i2s_comm_format_t.I2S_COMM_FORMAT_PCM_LONG, } -INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32] +INTERNAL_DAC_VARIANTS = [esp32.VARIANT_ESP32] def _set_num_channels_from_config(config): diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 2a3d0edca7..a3eff901d3 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -131,6 +131,13 @@ float ILI9XXXDisplay::get_setup_priority() const { return setup_priority::HARDWA void ILI9XXXDisplay::fill(Color color) { if (!this->check_buffer_()) return; + + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + uint16_t new_color = 0; this->x_low_ = 0; this->y_low_ = 0; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index bf25a7cd92..a7b788bf91 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -374,23 +374,6 @@ def is_svg_file(file): return " 500 or height > 500): _LOGGER.warning( diff --git a/esphome/components/improv_base/improv_base.cpp b/esphome/components/improv_base/improv_base.cpp index 2091390f95..d0340344a6 100644 --- a/esphome/components/improv_base/improv_base.cpp +++ b/esphome/components/improv_base/improv_base.cpp @@ -1,5 +1,6 @@ #include "improv_base.h" +#include #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" @@ -13,37 +14,54 @@ static constexpr size_t DEVICE_NAME_PLACEHOLDER_LEN = sizeof(DEVICE_NAME_PLACEHO static constexpr const char IP_ADDRESS_PLACEHOLDER[] = "{{ip_address}}"; static constexpr size_t IP_ADDRESS_PLACEHOLDER_LEN = sizeof(IP_ADDRESS_PLACEHOLDER) - 1; -static void replace_all_in_place(std::string &str, const char *placeholder, size_t placeholder_len, - const std::string &replacement) { - size_t pos = 0; - const size_t replacement_len = replacement.length(); - while ((pos = str.find(placeholder, pos)) != std::string::npos) { - str.replace(pos, placeholder_len, replacement); - pos += replacement_len; +/// Copy src to dest, returning pointer past last written char. Stops at end or if src is null. +static char *copy_to_buffer(char *dest, const char *end, const char *src) { + if (src == nullptr) { + return dest; } + while (*src != '\0' && dest < end) { + *dest++ = *src++; + } + return dest; } -std::string ImprovBase::get_formatted_next_url_() { - if (this->next_url_.empty()) { - return ""; +size_t ImprovBase::get_formatted_next_url_(char *buffer, size_t buffer_size) { + if (this->next_url_ == nullptr || buffer_size == 0) { + if (buffer_size > 0) { + buffer[0] = '\0'; + } + return 0; } - std::string formatted_url = this->next_url_; - - // Replace all occurrences of {{device_name}} - replace_all_in_place(formatted_url, DEVICE_NAME_PLACEHOLDER, DEVICE_NAME_PLACEHOLDER_LEN, App.get_name()); - - // Replace all occurrences of {{ip_address}} + // Get IP address once for replacement + const char *ip_str = nullptr; + char ip_buffer[network::IP_ADDRESS_BUFFER_SIZE]; for (auto &ip : network::get_ip_addresses()) { if (ip.is_ip4()) { - replace_all_in_place(formatted_url, IP_ADDRESS_PLACEHOLDER, IP_ADDRESS_PLACEHOLDER_LEN, ip.str()); + ip.str_to(ip_buffer); + ip_str = ip_buffer; break; } } - // Note: {{esphome_version}} is replaced at code generation time in Python + const char *device_name = App.get_name().c_str(); + char *out = buffer; + const char *end = buffer + buffer_size - 1; - return formatted_url; + // Note: {{esphome_version}} is replaced at code generation time in Python + for (const char *p = this->next_url_; *p != '\0' && out < end;) { + if (strncmp(p, DEVICE_NAME_PLACEHOLDER, DEVICE_NAME_PLACEHOLDER_LEN) == 0) { + out = copy_to_buffer(out, end, device_name); + p += DEVICE_NAME_PLACEHOLDER_LEN; + } else if (ip_str != nullptr && strncmp(p, IP_ADDRESS_PLACEHOLDER, IP_ADDRESS_PLACEHOLDER_LEN) == 0) { + out = copy_to_buffer(out, end, ip_str); + p += IP_ADDRESS_PLACEHOLDER_LEN; + } else { + *out++ = *p++; + } + } + *out = '\0'; + return out - buffer; } #endif diff --git a/esphome/components/improv_base/improv_base.h b/esphome/components/improv_base/improv_base.h index e4138479df..ebc8f38d60 100644 --- a/esphome/components/improv_base/improv_base.h +++ b/esphome/components/improv_base/improv_base.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include "esphome/core/defines.h" namespace esphome { @@ -9,13 +9,14 @@ namespace improv_base { class ImprovBase { public: #if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) - void set_next_url(const std::string &next_url) { this->next_url_ = next_url; } + void set_next_url(const char *next_url) { this->next_url_ = next_url; } #endif protected: #if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) - std::string get_formatted_next_url_(); - std::string next_url_; + /// Format next_url_ into buffer, replacing placeholders. Returns length written. + size_t get_formatted_next_url_(char *buffer, size_t buffer_size); + const char *next_url_{nullptr}; #endif }; diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index fb2b541707..9a2ac2f40f 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -1,7 +1,6 @@ import esphome.codegen as cg from esphome.components import improv_base -from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import VARIANT_ESP32S3 +from esphome.components.esp32 import VARIANT_ESP32S3, get_esp32_variant from esphome.components.logger import USB_CDC import esphome.config_validation as cv from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER @@ -27,7 +26,7 @@ def validate_logger(config): logger_conf = fv.full_config.get()[CONF_LOGGER] if logger_conf[CONF_BAUD_RATE] == 0: raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") - if CORE.using_esp_idf and ( + if CORE.is_esp32 and ( logger_conf[CONF_HARDWARE_UART] == USB_CDC and get_esp32_variant() == VARIANT_ESP32S3 ): diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 70260eeab3..936ff414b1 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -70,9 +70,10 @@ optional ImprovSerialComponent::read_byte_() { case logger::UART_SELECTION_UART0: case logger::UART_SELECTION_UART1: #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ - !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) + !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) case logger::UART_SELECTION_UART2: -#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 +#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32C6 && !USE_ESP32_VARIANT_ESP32C61 && + // !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 if (this->uart_num_ >= 0) { size_t available; uart_get_buffered_data_len(this->uart_num_, &available); @@ -137,7 +138,7 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) case logger::UART_SELECTION_UART0: case logger::UART_SELECTION_UART1: #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ - !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) + !defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) case logger::UART_SELECTION_UART2: #endif uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len); @@ -181,8 +182,12 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) std::vector ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { std::vector urls; #ifdef USE_IMPROV_SERIAL_NEXT_URL - if (!this->next_url_.empty()) { - urls.push_back(this->get_formatted_next_url_()); + { + char url_buffer[384]; + size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer)); + if (len > 0) { + urls.emplace_back(url_buffer, len); + } } #endif #ifdef USE_WEBSERVER diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index 057247f376..dd8f5e4719 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -11,8 +11,8 @@ #ifdef USE_ESP32 #include -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S3) || \ - defined(USE_ESP32_VARIANT_ESP32H2) +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \ + defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32S3) #include #include #endif diff --git a/esphome/components/ina260/ina260.cpp b/esphome/components/ina260/ina260.cpp index 9dd922cec2..4d6acf400c 100644 --- a/esphome/components/ina260/ina260.cpp +++ b/esphome/components/ina260/ina260.cpp @@ -61,13 +61,13 @@ void INA260Component::setup() { } void INA260Component::dump_config() { - ESP_LOGCONFIG(TAG, "INA260:"); + ESP_LOGCONFIG(TAG, + "INA260:\n" + " Manufacture ID: 0x%x\n" + " Device ID: 0x%x", + this->manufacture_id_, this->device_id_); LOG_I2C_DEVICE(this); LOG_UPDATE_INTERVAL(this); - - ESP_LOGCONFIG(TAG, " Manufacture ID: 0x%x", this->manufacture_id_); - ESP_LOGCONFIG(TAG, " Device ID: 0x%x", this->device_id_); - LOG_SENSOR(" ", "Bus Voltage", this->bus_voltage_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_); LOG_SENSOR(" ", "Power", this->power_sensor_); diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 4ab02703e8..7185d21810 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -364,8 +364,10 @@ bool INA2XX::configure_shunt_() { ESP_LOGW(TAG, "Shunt value too high"); } this->shunt_cal_ &= 0x7FFF; - ESP_LOGV(TAG, "Given Rshunt=%f Ohm and Max_current=%.3f", this->shunt_resistance_ohm_, this->max_current_a_); - ESP_LOGV(TAG, "New CURRENT_LSB=%f, SHUNT_CAL=%u", this->current_lsb_, this->shunt_cal_); + ESP_LOGV(TAG, + "Given Rshunt=%f Ohm and Max_current=%.3f\n" + "New CURRENT_LSB=%f, SHUNT_CAL=%u", + this->shunt_resistance_ohm_, this->max_current_a_, this->current_lsb_, this->shunt_cal_); return this->write_unsigned_16_(RegisterMap::REG_SHUNT_CAL, this->shunt_cal_); } diff --git a/esphome/components/inkplate/display.py b/esphome/components/inkplate/display.py index 89518dcfab..47c8c898e5 100644 --- a/esphome/components/inkplate/display.py +++ b/esphome/components/inkplate/display.py @@ -6,10 +6,12 @@ import esphome.config_validation as cv from esphome.const import ( CONF_FULL_UPDATE_EVERY, CONF_ID, + CONF_IGNORE_STRAPPING_WARNING, CONF_LAMBDA, CONF_MIRROR_X, CONF_MIRROR_Y, CONF_MODEL, + CONF_NUMBER, CONF_OE_PIN, CONF_PAGES, CONF_TRANSFORM, @@ -101,14 +103,21 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_SPV_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_VCOM_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_WAKEUP_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_CL_PIN, default=0): pins.internal_gpio_output_pin_schema, - cv.Optional(CONF_LE_PIN, default=2): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_CL_PIN, + default={CONF_NUMBER: 0, CONF_IGNORE_STRAPPING_WARNING: True}, + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_LE_PIN, + default={CONF_NUMBER: 2, CONF_IGNORE_STRAPPING_WARNING: True}, + ): pins.internal_gpio_output_pin_schema, # Data pins cv.Optional( CONF_DISPLAY_DATA_0_PIN, default=4 ): pins.internal_gpio_output_pin_schema, cv.Optional( - CONF_DISPLAY_DATA_1_PIN, default=5 + CONF_DISPLAY_DATA_1_PIN, + default={CONF_NUMBER: 5, CONF_IGNORE_STRAPPING_WARNING: True}, ): pins.internal_gpio_output_pin_schema, cv.Optional( CONF_DISPLAY_DATA_2_PIN, default=18 diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index f96fb6905e..c921c643fa 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -293,6 +293,13 @@ void Inkplate::fill(Color color) { ESP_LOGV(TAG, "Fill called"); uint32_t start_time = millis(); + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); + return; + } + if (this->greyscale_) { uint8_t fill = ((color.red * 2126 / 10000) + (color.green * 7152 / 10000) + (color.blue * 722 / 10000)) >> 5; memset(this->buffer_, (fill << 4) | fill, this->get_buffer_length_()); diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp index 28ac55d6de..34d7baf880 100644 --- a/esphome/components/internal_temperature/internal_temperature.cpp +++ b/esphome/components/internal_temperature/internal_temperature.cpp @@ -7,9 +7,10 @@ extern "C" { uint8_t temprature_sens_read(); } -#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ - defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \ - defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32P4) +#elif defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || \ + defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \ + defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) #include "driver/temperature_sensor.h" #endif // USE_ESP32_VARIANT #endif // USE_ESP32 @@ -27,9 +28,9 @@ namespace internal_temperature { static const char *const TAG = "internal_temperature"; #ifdef USE_ESP32 -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ - defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ - defined(USE_ESP32_VARIANT_ESP32P4) +#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ + defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ + defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) static temperature_sensor_handle_t tsensNew = NULL; #endif // USE_ESP32_VARIANT #endif // USE_ESP32 @@ -43,9 +44,10 @@ void InternalTemperatureSensor::update() { ESP_LOGV(TAG, "Raw temperature value: %d", raw); temperature = (raw - 32) / 1.8f; success = (raw != 128); -#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ - defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \ - defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32P4) +#elif defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || \ + defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \ + defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) esp_err_t result = temperature_sensor_get_celsius(tsensNew, &temperature); success = (result == ESP_OK); if (!success) { @@ -81,9 +83,9 @@ void InternalTemperatureSensor::update() { void InternalTemperatureSensor::setup() { #ifdef USE_ESP32 -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ - defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2) || \ - defined(USE_ESP32_VARIANT_ESP32P4) +#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ + defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ + defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); esp_err_t result = temperature_sensor_install(&tsens_config, &tsensNew); diff --git a/esphome/components/jsn_sr04t/jsn_sr04t.cpp b/esphome/components/jsn_sr04t/jsn_sr04t.cpp index 077d4e58ea..6fd8b1bd65 100644 --- a/esphome/components/jsn_sr04t/jsn_sr04t.cpp +++ b/esphome/components/jsn_sr04t/jsn_sr04t.cpp @@ -10,7 +10,7 @@ namespace jsn_sr04t { static const char *const TAG = "jsn_sr04t.sensor"; void Jsnsr04tComponent::update() { - this->write_byte(0x55); + this->write_byte((this->model_ == AJ_SR04M) ? 0x01 : 0x55); ESP_LOGV(TAG, "Request read out from sensor"); } @@ -31,24 +31,17 @@ void Jsnsr04tComponent::loop() { } void Jsnsr04tComponent::check_buffer_() { - uint8_t checksum = 0; - switch (this->model_) { - case JSN_SR04T: - checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; - break; - case AJ_SR04M: - checksum = this->buffer_[1] + this->buffer_[2]; - break; - } - + uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; if (this->buffer_[3] == checksum) { uint16_t distance = encode_uint16(this->buffer_[1], this->buffer_[2]); - if (distance > 250) { + if (distance > ((this->model_ == AJ_SR04M) ? 200 : 250)) { float meters = distance / 1000.0f; ESP_LOGV(TAG, "Distance from sensor: %umm, %.3fm", distance, meters); this->publish_state(meters); } else { - ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); + char hex_buf[format_hex_pretty_size(4)]; + ESP_LOGW(TAG, "Invalid data read from sensor: %s", + format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_.size())); } } else { ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); diff --git a/esphome/components/kalman_combinator/sensor.py b/esphome/components/kalman_combinator/sensor.py index c19a17462d..d30a41d6bf 100644 --- a/esphome/components/kalman_combinator/sensor.py +++ b/esphome/components/kalman_combinator/sensor.py @@ -2,5 +2,5 @@ import esphome.config_validation as cv CONFIG_SCHEMA = cv.invalid( "The kalman_combinator sensor has moved.\nPlease use the combination platform instead with type: kalman.\n" - "See https://esphome.io/components/sensor/combination.html" + "See https://esphome.io/components/sensor/combination/" ) diff --git a/esphome/components/kuntze/kuntze.cpp b/esphome/components/kuntze/kuntze.cpp index 30f98aaa99..1b772d062c 100644 --- a/esphome/components/kuntze/kuntze.cpp +++ b/esphome/components/kuntze/kuntze.cpp @@ -1,4 +1,5 @@ #include "kuntze.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/application.h" @@ -10,11 +11,17 @@ static const char *const TAG = "kuntze"; static const uint8_t CMD_READ_REG = 0x03; static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832}; +// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5) +static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8; + void Kuntze::on_modbus_data(const std::vector &data) { auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); }; this->waiting_ = false; - ESP_LOGV(TAG, "Data: %s", format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(KUNTZE_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Data: %s", format_hex_pretty_to(hex_buf, data.data(), data.size())); float value = (float) get_16bit(0); for (int i = 0; i < data[3]; i++) diff --git a/esphome/components/lc709203f/lc709203f.cpp b/esphome/components/lc709203f/lc709203f.cpp index 7e6ac878f8..ad9d6b3098 100644 --- a/esphome/components/lc709203f/lc709203f.cpp +++ b/esphome/components/lc709203f/lc709203f.cpp @@ -146,19 +146,14 @@ void Lc709203f::update() { } void Lc709203f::dump_config() { - ESP_LOGCONFIG(TAG, "LC709203F:"); - LOG_I2C_DEVICE(this); - - LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, + "LC709203F:\n" " Pack Size: %d mAH\n" - " Pack APA: 0x%02X", - this->pack_size_, this->apa_); - - // This is only true if the pack_voltage_ is either 0x0000 or 0x0001. The config validator - // should have already verified this. - ESP_LOGCONFIG(TAG, " Pack Rated Voltage: 3.%sV", this->pack_voltage_ == 0x0000 ? "8" : "7"); - + " Pack APA: 0x%02X\n" + " Pack Rated Voltage: 3.%sV", + this->pack_size_, this->apa_, this->pack_voltage_ == 0x0000 ? "8" : "7"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); LOG_SENSOR(" ", "Battery Remaining", this->battery_remaining_sensor_); diff --git a/esphome/components/ld2410/automation.h b/esphome/components/ld2410/automation.h index f4f1c197b2..614453b575 100644 --- a/esphome/components/ld2410/automation.h +++ b/esphome/components/ld2410/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { template class BluetoothPasswordSetAction : public Action { public: @@ -18,5 +17,4 @@ template class BluetoothPasswordSetAction : public Action LD2410Component *ld2410_comp_; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/factory_reset_button.cpp b/esphome/components/ld2410/button/factory_reset_button.cpp index a848b02a9d..0223df7086 100644 --- a/esphome/components/ld2410/button/factory_reset_button.cpp +++ b/esphome/components/ld2410/button/factory_reset_button.cpp @@ -1,9 +1,7 @@ #include "factory_reset_button.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void FactoryResetButton::press_action() { this->parent_->factory_reset(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/factory_reset_button.h b/esphome/components/ld2410/button/factory_reset_button.h index 45bf979033..715a8c4056 100644 --- a/esphome/components/ld2410/button/factory_reset_button.h +++ b/esphome/components/ld2410/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->read_all_info(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/query_button.h b/esphome/components/ld2410/button/query_button.h index c7a47e32d8..7a786901ae 100644 --- a/esphome/components/ld2410/button/query_button.h +++ b/esphome/components/ld2410/button/query_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class QueryButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class QueryButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/restart_button.cpp b/esphome/components/ld2410/button/restart_button.cpp index de0d36c1ef..0d5002d3c6 100644 --- a/esphome/components/ld2410/button/restart_button.cpp +++ b/esphome/components/ld2410/button/restart_button.cpp @@ -1,9 +1,7 @@ #include "restart_button.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/button/restart_button.h b/esphome/components/ld2410/button/restart_button.h index d00dc05a53..9bf8639a8c 100644 --- a/esphome/components/ld2410/button/restart_button.h +++ b/esphome/components/ld2410/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index 608882565f..c9b4333f7e 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -9,12 +9,9 @@ #include "esphome/core/application.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { static const char *const TAG = "ld2410"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -120,6 +117,8 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { {OUT_PIN_LEVEL_HIGH, "high"}, }; +constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800}; + // Helper functions for lookups template uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) { for (const auto &entry : arr) { @@ -181,15 +180,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui } void LD2410Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2410:\n" " Firmware version: %s\n" " MAC address: %s", - version.c_str(), mac_str.c_str()); + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); @@ -261,9 +260,10 @@ void LD2410Component::read_all_info() { this->query_parameters_(); this->set_config_mode_(false); #ifdef USE_SELECT - const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); if (this->baud_rate_select_ != nullptr) { - this->baud_rate_select_->publish_state(baud_rate); + if (auto index = ld24xx::find_index(BAUD_RATES, this->parent_->get_baud_rate())) { + this->baud_rate_select_->publish_state(*index); + } } #endif } @@ -416,7 +416,8 @@ bool LD2410Component::handle_ack_data_() { return true; } if (!ld2410::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { - ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); + char hex_buf[format_hex_pretty_size(HEADER_FOOTER_SIZE)]; + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, HEADER_FOOTER_SIZE)); return true; } if (this->buffer_data_[COMMAND_STATUS] != 0x01) { @@ -448,12 +449,12 @@ bool LD2410Component::handle_ack_data_() { case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -506,9 +507,9 @@ bool LD2410Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); @@ -600,11 +601,17 @@ void LD2410Component::readline_(int readch) { return; // Not enough data to process yet } if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { - ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)]; + ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_)); +#endif this->handle_periodic_data_(); this->buffer_pos_ = 0; // Reset position index for next message } else if (ld2410::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { - ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)]; + ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_)); +#endif if (this->handle_ack_data_()) { this->buffer_pos_ = 0; // Reset position index for next message } else { @@ -784,5 +791,4 @@ void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { } #endif -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/ld2410.h b/esphome/components/ld2410/ld2410.h index 52cf76b5b6..efe585fb76 100644 --- a/esphome/components/ld2410/ld2410.h +++ b/esphome/components/ld2410/ld2410.h @@ -29,8 +29,7 @@ #include -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { using namespace ld24xx; @@ -133,5 +132,4 @@ class LD2410Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/gate_threshold_number.cpp b/esphome/components/ld2410/number/gate_threshold_number.cpp index 5d040554d7..65e864a4d7 100644 --- a/esphome/components/ld2410/number/gate_threshold_number.cpp +++ b/esphome/components/ld2410/number/gate_threshold_number.cpp @@ -1,7 +1,6 @@ #include "gate_threshold_number.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} @@ -10,5 +9,4 @@ void GateThresholdNumber::control(float value) { this->parent_->set_gate_threshold(this->gate_); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/gate_threshold_number.h b/esphome/components/ld2410/number/gate_threshold_number.h index 2806ecce63..63491f18d3 100644 --- a/esphome/components/ld2410/number/gate_threshold_number.h +++ b/esphome/components/ld2410/number/gate_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class GateThresholdNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class GateThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_light_out_control(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/light_threshold_number.h b/esphome/components/ld2410/number/light_threshold_number.h index 8f014373c0..3c5e433416 100644 --- a/esphome/components/ld2410/number/light_threshold_number.h +++ b/esphome/components/ld2410/number/light_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class LightThresholdNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class LightThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_max_distances_timeout(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/number/max_distance_timeout_number.h b/esphome/components/ld2410/number/max_distance_timeout_number.h index 7d91b4b5fe..35f4cbbfae 100644 --- a/esphome/components/ld2410/number/max_distance_timeout_number.h +++ b/esphome/components/ld2410/number/max_distance_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class MaxDistanceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class MaxDistanceTimeoutNumber : public number::Number, public Parentedpublish_state(index); this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/baud_rate_select.h b/esphome/components/ld2410/select/baud_rate_select.h index 9385c8cf7e..fb1d016b1f 100644 --- a/esphome/components/ld2410/select/baud_rate_select.h +++ b/esphome/components/ld2410/select/baud_rate_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class BaudRateSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class BaudRateSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.cpp b/esphome/components/ld2410/select/distance_resolution_select.cpp index 4fc4c5af02..635bf206d3 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.cpp +++ b/esphome/components/ld2410/select/distance_resolution_select.cpp @@ -1,12 +1,10 @@ #include "distance_resolution_select.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void DistanceResolutionSelect::control(size_t index) { this->publish_state(index); this->parent_->set_distance_resolution(this->option_at(index)); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/distance_resolution_select.h b/esphome/components/ld2410/select/distance_resolution_select.h index 1a04f843a6..be2389d36e 100644 --- a/esphome/components/ld2410/select/distance_resolution_select.h +++ b/esphome/components/ld2410/select/distance_resolution_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class DistanceResolutionSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class DistanceResolutionSelect : public select::Select, public Parentedpublish_state(index); this->parent_->set_light_out_control(); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/select/light_out_control_select.h b/esphome/components/ld2410/select/light_out_control_select.h index e8cd8f1d6a..608c311af4 100644 --- a/esphome/components/ld2410/select/light_out_control_select.h +++ b/esphome/components/ld2410/select/light_out_control_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class LightOutControlSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class LightOutControlSelect : public select::Select, public Parentedpublish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/bluetooth_switch.h b/esphome/components/ld2410/switch/bluetooth_switch.h index 35ae1ec0c9..07804e2292 100644 --- a/esphome/components/ld2410/switch/bluetooth_switch.h +++ b/esphome/components/ld2410/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.cpp b/esphome/components/ld2410/switch/engineering_mode_switch.cpp index 967c87c887..4f2f08b03e 100644 --- a/esphome/components/ld2410/switch/engineering_mode_switch.cpp +++ b/esphome/components/ld2410/switch/engineering_mode_switch.cpp @@ -1,12 +1,10 @@ #include "engineering_mode_switch.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { void EngineeringModeSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_engineering_mode(state); } -} // namespace ld2410 -} // namespace esphome +} // namespace esphome::ld2410 diff --git a/esphome/components/ld2410/switch/engineering_mode_switch.h b/esphome/components/ld2410/switch/engineering_mode_switch.h index e521200cd6..4dd8e16653 100644 --- a/esphome/components/ld2410/switch/engineering_mode_switch.h +++ b/esphome/components/ld2410/switch/engineering_mode_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2410.h" -namespace esphome { -namespace ld2410 { +namespace esphome::ld2410 { class EngineeringModeSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class EngineeringModeSwitch : public switch_::Switch, public Parentedparent_->factory_reset(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/factory_reset_button.h b/esphome/components/ld2412/button/factory_reset_button.h index 36a3fffcd5..1ef6b23b80 100644 --- a/esphome/components/ld2412/button/factory_reset_button.h +++ b/esphome/components/ld2412/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->read_all_info(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/query_button.h b/esphome/components/ld2412/button/query_button.h index 595ef6d1e9..373e135802 100644 --- a/esphome/components/ld2412/button/query_button.h +++ b/esphome/components/ld2412/button/query_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class QueryButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class QueryButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/restart_button.cpp b/esphome/components/ld2412/button/restart_button.cpp index aca0d17841..430f6c998f 100644 --- a/esphome/components/ld2412/button/restart_button.cpp +++ b/esphome/components/ld2412/button/restart_button.cpp @@ -1,9 +1,7 @@ #include "restart_button.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/restart_button.h b/esphome/components/ld2412/button/restart_button.h index 5cd582e2a3..80c79f5e7d 100644 --- a/esphome/components/ld2412/button/restart_button.h +++ b/esphome/components/ld2412/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp index 9b37243b82..8ba41a03fb 100644 --- a/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.cpp @@ -2,10 +2,8 @@ #include "restart_button.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void StartDynamicBackgroundCorrectionButton::press_action() { this->parent_->start_dynamic_background_correction(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/button/start_dynamic_background_correction_button.h b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h index 3af0a8a149..b1f2127896 100644 --- a/esphome/components/ld2412/button/start_dynamic_background_correction_button.h +++ b/esphome/components/ld2412/button/start_dynamic_background_correction_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class StartDynamicBackgroundCorrectionButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class StartDynamicBackgroundCorrectionButton : public button::Button, public Par void press_action() override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index 5323a9a658..620ac9886b 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -10,12 +10,9 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { static const char *const TAG = "ld2412"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -131,6 +128,8 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { {OUT_PIN_LEVEL_HIGH, "high"}, }; +constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800}; + // Helper functions for lookups template uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) { for (const auto &entry : arr) { @@ -200,15 +199,15 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui } void LD2412Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2412:\n" " Firmware version: %s\n" " MAC address: %s", - version.c_str(), mac_str.c_str()); + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "DynamicBackgroundCorrectionStatus", @@ -296,9 +295,10 @@ void LD2412Component::read_all_info() { #endif this->set_config_mode_(false); #ifdef USE_SELECT - const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); if (this->baud_rate_select_ != nullptr) { - this->baud_rate_select_->publish_state(baud_rate); + if (auto index = ld24xx::find_index(BAUD_RATES, this->parent_->get_baud_rate())) { + this->baud_rate_select_->publish_state(*index); + } } #endif } @@ -460,7 +460,8 @@ bool LD2412Component::handle_ack_data_() { return true; } if (!ld2412::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { - ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); + char hex_buf[format_hex_pretty_size(HEADER_FOOTER_SIZE)]; + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, HEADER_FOOTER_SIZE)); return true; } if (this->buffer_data_[COMMAND_STATUS] != 0x01) { @@ -492,12 +493,12 @@ bool LD2412Component::handle_ack_data_() { case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -544,9 +545,9 @@ bool LD2412Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); @@ -673,11 +674,17 @@ void LD2412Component::readline_(int readch) { return; // Not enough data to process yet } if (ld2412::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { - ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)]; + ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_)); +#endif this->handle_periodic_data_(); this->buffer_pos_ = 0; // Reset position index for next message } else if (ld2412::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { - ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)]; + ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_)); +#endif if (this->handle_ack_data_()) { this->buffer_pos_ = 0; // Reset position index for next message } else { @@ -857,5 +864,4 @@ void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { } #endif -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/ld2412.h b/esphome/components/ld2412/ld2412.h index 2bed34bdd8..5dd5e7bcde 100644 --- a/esphome/components/ld2412/ld2412.h +++ b/esphome/components/ld2412/ld2412.h @@ -29,8 +29,7 @@ #include -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { using namespace ld24xx; @@ -137,5 +136,4 @@ class LD2412Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/gate_threshold_number.cpp b/esphome/components/ld2412/number/gate_threshold_number.cpp index 47f8cd9107..8d12bad115 100644 --- a/esphome/components/ld2412/number/gate_threshold_number.cpp +++ b/esphome/components/ld2412/number/gate_threshold_number.cpp @@ -1,7 +1,6 @@ #include "gate_threshold_number.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} @@ -10,5 +9,4 @@ void GateThresholdNumber::control(float value) { this->parent_->set_gate_threshold(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/gate_threshold_number.h b/esphome/components/ld2412/number/gate_threshold_number.h index 61d9945a0a..78c2e54d82 100644 --- a/esphome/components/ld2412/number/gate_threshold_number.h +++ b/esphome/components/ld2412/number/gate_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class GateThresholdNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class GateThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_light_out_control(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/light_threshold_number.h b/esphome/components/ld2412/number/light_threshold_number.h index d8727d3c98..81fd73111c 100644 --- a/esphome/components/ld2412/number/light_threshold_number.h +++ b/esphome/components/ld2412/number/light_threshold_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class LightThresholdNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class LightThresholdNumber : public number::Number, public Parentedpublish_state(value); this->parent_->set_basic_config(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/number/max_distance_timeout_number.h b/esphome/components/ld2412/number/max_distance_timeout_number.h index af0dcf68c5..c1e947fa19 100644 --- a/esphome/components/ld2412/number/max_distance_timeout_number.h +++ b/esphome/components/ld2412/number/max_distance_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class MaxDistanceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class MaxDistanceTimeoutNumber : public number::Number, public Parentedpublish_state(index); this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/baud_rate_select.h b/esphome/components/ld2412/select/baud_rate_select.h index ffe0329341..4666dd2fa0 100644 --- a/esphome/components/ld2412/select/baud_rate_select.h +++ b/esphome/components/ld2412/select/baud_rate_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class BaudRateSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class BaudRateSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.cpp b/esphome/components/ld2412/select/distance_resolution_select.cpp index 5a6f46a071..95b80f87fb 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.cpp +++ b/esphome/components/ld2412/select/distance_resolution_select.cpp @@ -1,12 +1,10 @@ #include "distance_resolution_select.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void DistanceResolutionSelect::control(size_t index) { this->publish_state(index); this->parent_->set_distance_resolution(this->option_at(index)); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/distance_resolution_select.h b/esphome/components/ld2412/select/distance_resolution_select.h index 842f63b7b1..d3b7fad2f9 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.h +++ b/esphome/components/ld2412/select/distance_resolution_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class DistanceResolutionSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class DistanceResolutionSelect : public select::Select, public Parentedpublish_state(index); this->parent_->set_light_out_control(); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/select/light_out_control_select.h b/esphome/components/ld2412/select/light_out_control_select.h index 7a50970d0d..9f86189878 100644 --- a/esphome/components/ld2412/select/light_out_control_select.h +++ b/esphome/components/ld2412/select/light_out_control_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class LightOutControlSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class LightOutControlSelect : public select::Select, public Parentedpublish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/bluetooth_switch.h b/esphome/components/ld2412/switch/bluetooth_switch.h index 730d338d87..0c0d1fa550 100644 --- a/esphome/components/ld2412/switch/bluetooth_switch.h +++ b/esphome/components/ld2412/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.cpp b/esphome/components/ld2412/switch/engineering_mode_switch.cpp index 29ca0c22a8..28b4e5d9e6 100644 --- a/esphome/components/ld2412/switch/engineering_mode_switch.cpp +++ b/esphome/components/ld2412/switch/engineering_mode_switch.cpp @@ -1,12 +1,10 @@ #include "engineering_mode_switch.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { void EngineeringModeSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_engineering_mode(state); } -} // namespace ld2412 -} // namespace esphome +} // namespace esphome::ld2412 diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.h b/esphome/components/ld2412/switch/engineering_mode_switch.h index aaa404c673..4e75a8a185 100644 --- a/esphome/components/ld2412/switch/engineering_mode_switch.h +++ b/esphome/components/ld2412/switch/engineering_mode_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2412.h" -namespace esphome { -namespace ld2412 { +namespace esphome::ld2412 { class EngineeringModeSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class EngineeringModeSwitch : public switch_::Switch, public Parentedpresence_bsensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h index ee06439090..ec52312f92 100644 --- a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420BinarySensor : public LD2420Listener, public Component, binary_sensor::BinarySensor { public: @@ -21,5 +20,4 @@ class LD2420BinarySensor : public LD2420Listener, public Component, binary_senso binary_sensor::BinarySensor *presence_bsensor_{nullptr}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/button/reconfig_buttons.cpp b/esphome/components/ld2420/button/reconfig_buttons.cpp index fb8ec2b5a6..1e748e59b8 100644 --- a/esphome/components/ld2420/button/reconfig_buttons.cpp +++ b/esphome/components/ld2420/button/reconfig_buttons.cpp @@ -4,13 +4,11 @@ static const char *const TAG = "ld2420.button"; -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { void LD2420ApplyConfigButton::press_action() { this->parent_->apply_config_action(); } void LD2420RevertConfigButton::press_action() { this->parent_->revert_config_action(); } void LD2420RestartModuleButton::press_action() { this->parent_->restart_module_action(); } void LD2420FactoryResetButton::press_action() { this->parent_->factory_reset_action(); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/button/reconfig_buttons.h b/esphome/components/ld2420/button/reconfig_buttons.h index 4e9e7a3692..72171ef386 100644 --- a/esphome/components/ld2420/button/reconfig_buttons.h +++ b/esphome/components/ld2420/button/reconfig_buttons.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2420.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420ApplyConfigButton : public button::Button, public Parented { public: @@ -38,5 +37,4 @@ class LD2420FactoryResetButton : public button::Button, public Parentedfactory_reset_button_); LOG_BUTTON(" ", "Restart Module:", this->restart_module_button_); #endif +#ifdef USE_SELECT ESP_LOGCONFIG(TAG, "Select:"); LOG_SELECT(" ", "Operating Mode", this->operating_selector_); +#endif if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) { ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_); } @@ -238,12 +239,20 @@ void LD2420Component::setup() { memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) { this->set_operating_mode(OP_SIMPLE_MODE_STRING); - this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING); +#ifdef USE_SELECT + if (this->operating_selector_ != nullptr) { + this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING); + } +#endif this->set_mode_(CMD_SYSTEM_MODE_SIMPLE); ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_); } else { this->set_mode_(CMD_SYSTEM_MODE_ENERGY); - this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING); +#ifdef USE_SELECT + if (this->operating_selector_ != nullptr) { + this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING); + } +#endif } #ifdef USE_NUMBER this->init_gate_config_numbers(); @@ -383,8 +392,12 @@ void LD2420Component::set_operating_mode(const char *state) { // If unsupported firmware ignore mode select if (ld2420::get_firmware_int(firmware_ver_) >= CALIBRATE_VERSION_MIN) { this->current_operating_mode = find_uint8(OP_MODE_BY_STR, state); - // Entering Auto Calibrate we need to clear the privoiuos data collection - this->operating_selector_->publish_state(state); + // Entering Auto Calibrate we need to clear the previous data collection +#ifdef USE_SELECT + if (this->operating_selector_ != nullptr) { + this->operating_selector_->publish_state(state); + } +#endif if (current_operating_mode == OP_CALIBRATE_MODE) { this->set_calibration_(true); for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { @@ -404,7 +417,11 @@ void LD2420Component::set_operating_mode(const char *state) { } } else { this->current_operating_mode = OP_SIMPLE_MODE; - this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING); +#ifdef USE_SELECT + if (this->operating_selector_ != nullptr) { + this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING); + } +#endif } } @@ -880,5 +897,4 @@ void LD2420Component::refresh_gate_config_numbers() { #endif -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 128baab604..50ddf45264 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -17,8 +17,7 @@ #include "esphome/components/button/button.h" #endif -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { static const uint8_t CALIBRATE_SAMPLES = 64; static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer @@ -193,5 +192,4 @@ class LD2420Component : public Component, public uart::UARTDevice { std::vector listeners_{}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/number/gate_config_number.cpp b/esphome/components/ld2420/number/gate_config_number.cpp index a373753770..998eed2188 100644 --- a/esphome/components/ld2420/number/gate_config_number.cpp +++ b/esphome/components/ld2420/number/gate_config_number.cpp @@ -4,8 +4,7 @@ static const char *const TAG = "ld2420.number"; -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { void LD2420TimeoutNumber::control(float timeout) { this->publish_state(timeout); @@ -69,5 +68,4 @@ void LD2420StillThresholdNumbers::control(float still_threshold) { } } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/number/gate_config_number.h b/esphome/components/ld2420/number/gate_config_number.h index 459a8026e3..8a8b9c61b1 100644 --- a/esphome/components/ld2420/number/gate_config_number.h +++ b/esphome/components/ld2420/number/gate_config_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2420.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420TimeoutNumber : public number::Number, public Parented { public: @@ -74,5 +73,4 @@ class LD2420MoveThresholdNumbers : public number::Number, public Parentedparent_->set_operating_mode(this->option_at(index)); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/select/operating_mode_select.h b/esphome/components/ld2420/select/operating_mode_select.h index f59eb33432..c1b8e0b11b 100644 --- a/esphome/components/ld2420/select/operating_mode_select.h +++ b/esphome/components/ld2420/select/operating_mode_select.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/select/select.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420Select : public Component, public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class LD2420Select : public Component, public select::Select, public Parenteddistance_sensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.h b/esphome/components/ld2420/sensor/ld2420_sensor.h index 82730d60e3..4849cfa047 100644 --- a/esphome/components/ld2420/sensor/ld2420_sensor.h +++ b/esphome/components/ld2420/sensor/ld2420_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { public: @@ -30,5 +29,4 @@ class LD2420Sensor : public LD2420Listener, public Component, sensor::Sensor { std::vector energy_sensors_ = std::vector(TOTAL_GATES); }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp index f647a36936..f7b016c9d9 100644 --- a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp +++ b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { static const char *const TAG = "ld2420.text_sensor"; @@ -12,5 +11,4 @@ void LD2420TextSensor::dump_config() { LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_); } -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h index 073ddd5d0f..1932eaaf69 100644 --- a/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h +++ b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h @@ -3,8 +3,7 @@ #include "../ld2420.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace ld2420 { +namespace esphome::ld2420 { class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::TextSensor { public: @@ -20,5 +19,4 @@ class LD2420TextSensor : public LD2420Listener, public Component, text_sensor::T text_sensor::TextSensor *fw_version_text_sensor_{nullptr}; }; -} // namespace ld2420 -} // namespace esphome +} // namespace esphome::ld2420 diff --git a/esphome/components/ld2450/button/factory_reset_button.cpp b/esphome/components/ld2450/button/factory_reset_button.cpp index bcac7ada2f..7a8eb5b0dd 100644 --- a/esphome/components/ld2450/button/factory_reset_button.cpp +++ b/esphome/components/ld2450/button/factory_reset_button.cpp @@ -1,9 +1,7 @@ #include "factory_reset_button.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void FactoryResetButton::press_action() { this->parent_->factory_reset(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/button/factory_reset_button.h b/esphome/components/ld2450/button/factory_reset_button.h index 8e80347119..392fc67ffd 100644 --- a/esphome/components/ld2450/button/factory_reset_button.h +++ b/esphome/components/ld2450/button/factory_reset_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class FactoryResetButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class FactoryResetButton : public button::Button, public Parentedparent_->restart_and_read_all_info(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/button/restart_button.h b/esphome/components/ld2450/button/restart_button.h index a44ae5a4d2..9219011f8b 100644 --- a/esphome/components/ld2450/button/restart_button.h +++ b/esphome/components/ld2450/button/restart_button.h @@ -3,8 +3,7 @@ #include "esphome/components/button/button.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class RestartButton : public button::Button, public Parented { public: @@ -14,5 +13,4 @@ class RestartButton : public button::Button, public Parented { void press_action() override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index c9d4da47a4..3b85694bc0 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -13,12 +13,9 @@ #include #include -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { static const char *const TAG = "ld2450"; -static const char *const UNKNOWN_MAC = "unknown"; -static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; enum BaudRate : uint8_t { BAUD_RATE_9600 = 1, @@ -91,6 +88,9 @@ constexpr StringToUint8 ZONE_TYPE_BY_STR[] = { {"Filter", ZONE_FILTER}, }; +// Baud rates in the same order as BAUD_RATES_BY_STR for index-based lookup +constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800}; + // Helper functions for lookups template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { for (const auto &entry : arr) { @@ -192,15 +192,15 @@ void LD2450Component::setup() { } void LD2450Component::dump_config() { - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); + char mac_s[18]; + char version_s[20]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ld24xx::format_version_str(this->version_, version_s); ESP_LOGCONFIG(TAG, "LD2450:\n" " Firmware version: %s\n" " MAC address: %s", - version.c_str(), mac_str.c_str()); + version_s, mac_str); #ifdef USE_BINARY_SENSOR ESP_LOGCONFIG(TAG, "Binary Sensors:"); LOG_BINARY_SENSOR(" ", "MovingTarget", this->moving_target_binary_sensor_); @@ -379,9 +379,10 @@ void LD2450Component::read_all_info() { this->query_zone_(); this->set_config_mode_(false); #ifdef USE_SELECT - const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); - if (this->baud_rate_select_ != nullptr && strcmp(this->baud_rate_select_->current_option(), baud_rate.c_str()) != 0) { - this->baud_rate_select_->publish_state(baud_rate); + if (this->baud_rate_select_ != nullptr) { + if (auto index = ld24xx::find_index(BAUD_RATES, this->parent_->get_baud_rate())) { + this->baud_rate_select_->publish_state(*index); + } } this->publish_zone_type(); #endif @@ -610,7 +611,8 @@ bool LD2450Component::handle_ack_data_() { return true; } if (!ld2450::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { - ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); + char hex_buf[format_hex_pretty_size(HEADER_FOOTER_SIZE)]; + ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, HEADER_FOOTER_SIZE)); return true; } if (this->buffer_data_[COMMAND_STATUS] != 0x01) { @@ -642,12 +644,12 @@ bool LD2450Component::handle_ack_data_() { case CMD_QUERY_VERSION: { std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); - std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], - this->version_[4], this->version_[3], this->version_[2]); - ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); + char version_s[20]; + ld24xx::format_version_str(this->version_, version_s); + ESP_LOGV(TAG, "Firmware version: %s", version_s); #ifdef USE_TEXT_SENSOR if (this->version_text_sensor_ != nullptr) { - this->version_text_sensor_->publish_state(version); + this->version_text_sensor_->publish_state(version_s); } #endif break; @@ -663,9 +665,9 @@ bool LD2450Component::handle_ack_data_() { std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); } - std::string mac_str = - mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; - ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); + char mac_s[18]; + const char *mac_str = ld24xx::format_mac_str(this->mac_address_, mac_s); + ESP_LOGV(TAG, "MAC address: %s", mac_str); #ifdef USE_TEXT_SENSOR if (this->mac_text_sensor_ != nullptr) { this->mac_text_sensor_->publish_state(mac_str); @@ -712,7 +714,7 @@ bool LD2450Component::handle_ack_data_() { case CMD_QUERY_ZONE: ESP_LOGV(TAG, "Query zone conf"); - this->zone_type_ = std::stoi(std::to_string(this->buffer_data_[10]), nullptr, 16); + this->zone_type_ = this->buffer_data_[10]; this->publish_zone_type(); #ifdef USE_SELECT if (this->zone_type_select_ != nullptr) { @@ -761,11 +763,17 @@ void LD2450Component::readline_(int readch) { } if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] && this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[1]) { - ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)]; + ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_)); +#endif this->handle_periodic_data_(); this->buffer_pos_ = 0; // Reset position index for next frame } else if (ld2450::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) { - ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_LINE_LENGTH)]; + ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty_to(hex_buf, this->buffer_data_, this->buffer_pos_)); +#endif if (this->handle_ack_data_()) { this->buffer_pos_ = 0; // Reset position index for next message } else { @@ -808,9 +816,8 @@ void LD2450Component::set_zone_type(const char *state) { // Publish Zone Type to Select component void LD2450Component::publish_zone_type() { #ifdef USE_SELECT - std::string zone_type = find_str(ZONE_TYPE_BY_UINT, this->zone_type_); if (this->zone_type_select_ != nullptr) { - this->zone_type_select_->publish_state(zone_type); + this->zone_type_select_->publish_state(find_str(ZONE_TYPE_BY_UINT, this->zone_type_)); } #endif } @@ -941,5 +948,4 @@ float LD2450Component::restore_from_flash_() { } #endif -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index 44b63be444..b94c3cac37 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -31,8 +31,7 @@ #include -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { using namespace ld24xx; @@ -193,5 +192,4 @@ class LD2450Component : public Component, public uart::UARTDevice { #endif }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/presence_timeout_number.cpp b/esphome/components/ld2450/number/presence_timeout_number.cpp index ecfe71f484..19a1ada0d7 100644 --- a/esphome/components/ld2450/number/presence_timeout_number.cpp +++ b/esphome/components/ld2450/number/presence_timeout_number.cpp @@ -1,12 +1,10 @@ #include "presence_timeout_number.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void PresenceTimeoutNumber::control(float value) { this->publish_state(value); this->parent_->set_presence_timeout(); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/presence_timeout_number.h b/esphome/components/ld2450/number/presence_timeout_number.h index b18699792f..09c8afca55 100644 --- a/esphome/components/ld2450/number/presence_timeout_number.h +++ b/esphome/components/ld2450/number/presence_timeout_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class PresenceTimeoutNumber : public number::Number, public Parented { public: @@ -14,5 +13,4 @@ class PresenceTimeoutNumber : public number::Number, public Parentedparent_->set_zone_coordinate(this->zone_); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/number/zone_coordinate_number.h b/esphome/components/ld2450/number/zone_coordinate_number.h index 72b83889c4..f5a389d712 100644 --- a/esphome/components/ld2450/number/zone_coordinate_number.h +++ b/esphome/components/ld2450/number/zone_coordinate_number.h @@ -3,8 +3,7 @@ #include "esphome/components/number/number.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class ZoneCoordinateNumber : public number::Number, public Parented { public: @@ -15,5 +14,4 @@ class ZoneCoordinateNumber : public number::Number, public Parentedpublish_state(index); this->parent_->set_baud_rate(this->option_at(index)); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/baud_rate_select.h b/esphome/components/ld2450/select/baud_rate_select.h index 22810d5f13..cb53118170 100644 --- a/esphome/components/ld2450/select/baud_rate_select.h +++ b/esphome/components/ld2450/select/baud_rate_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class BaudRateSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class BaudRateSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.cpp b/esphome/components/ld2450/select/zone_type_select.cpp index 1111428c7c..39642b99ad 100644 --- a/esphome/components/ld2450/select/zone_type_select.cpp +++ b/esphome/components/ld2450/select/zone_type_select.cpp @@ -1,12 +1,10 @@ #include "zone_type_select.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void ZoneTypeSelect::control(size_t index) { this->publish_state(index); this->parent_->set_zone_type(this->option_at(index)); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/select/zone_type_select.h b/esphome/components/ld2450/select/zone_type_select.h index fc95ec1021..566346eb48 100644 --- a/esphome/components/ld2450/select/zone_type_select.h +++ b/esphome/components/ld2450/select/zone_type_select.h @@ -3,8 +3,7 @@ #include "esphome/components/select/select.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class ZoneTypeSelect : public select::Select, public Parented { public: @@ -14,5 +13,4 @@ class ZoneTypeSelect : public select::Select, public Parented { void control(size_t index) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/bluetooth_switch.cpp b/esphome/components/ld2450/switch/bluetooth_switch.cpp index fa0d4fb06a..0e19a3e6c6 100644 --- a/esphome/components/ld2450/switch/bluetooth_switch.cpp +++ b/esphome/components/ld2450/switch/bluetooth_switch.cpp @@ -1,12 +1,10 @@ #include "bluetooth_switch.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void BluetoothSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_bluetooth(state); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/bluetooth_switch.h b/esphome/components/ld2450/switch/bluetooth_switch.h index 3c1c4f755c..3d48a89b57 100644 --- a/esphome/components/ld2450/switch/bluetooth_switch.h +++ b/esphome/components/ld2450/switch/bluetooth_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class BluetoothSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class BluetoothSwitch : public switch_::Switch, public Parented void write_state(bool state) override; }; -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/multi_target_switch.cpp b/esphome/components/ld2450/switch/multi_target_switch.cpp index a163e29fc5..0b1cb04a68 100644 --- a/esphome/components/ld2450/switch/multi_target_switch.cpp +++ b/esphome/components/ld2450/switch/multi_target_switch.cpp @@ -1,12 +1,10 @@ #include "multi_target_switch.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { void MultiTargetSwitch::write_state(bool state) { this->publish_state(state); this->parent_->set_multi_target(state); } -} // namespace ld2450 -} // namespace esphome +} // namespace esphome::ld2450 diff --git a/esphome/components/ld2450/switch/multi_target_switch.h b/esphome/components/ld2450/switch/multi_target_switch.h index ca6253588d..739f308cce 100644 --- a/esphome/components/ld2450/switch/multi_target_switch.h +++ b/esphome/components/ld2450/switch/multi_target_switch.h @@ -3,8 +3,7 @@ #include "esphome/components/switch/switch.h" #include "../ld2450.h" -namespace esphome { -namespace ld2450 { +namespace esphome::ld2450 { class MultiTargetSwitch : public switch_::Switch, public Parented { public: @@ -14,5 +13,4 @@ class MultiTargetSwitch : public switch_::Switch, public Parented +#include #ifdef USE_SENSOR -#include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" #define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \ @@ -36,8 +37,37 @@ #define highbyte(val) (uint8_t)((val) >> 8) #define lowbyte(val) (uint8_t)((val) &0xff) -namespace esphome { -namespace ld24xx { +namespace esphome::ld24xx { + +// Helper to find index of value in constexpr array +template optional find_index(const uint32_t (&arr)[N], uint32_t value) { + for (size_t i = 0; i < N; i++) { + if (arr[i] == value) + return i; + } + return {}; +} + +static const char *const UNKNOWN_MAC = "unknown"; +static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; + +// Helper function to format MAC address with stack allocation +// Returns pointer to UNKNOWN_MAC constant or formatted buffer +// Buffer must be exactly 18 bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator) +inline const char *format_mac_str(const uint8_t *mac_address, std::span buffer) { + if (mac_address_is_valid(mac_address)) { + format_mac_addr_upper(mac_address, buffer.data()); + return buffer.data(); + } + return UNKNOWN_MAC; +} + +// Helper function to format firmware version with stack allocation +// Buffer must be exactly 20 bytes (format: "x.xxXXXXXX" fits in 11 + null terminator, 20 for safety) +inline void format_version_str(const uint8_t *version, std::span buffer) { + snprintf(buffer.data(), buffer.size(), VERSION_FMT, version[1], version[0], version[5], version[4], version[3], + version[2]); +} #ifdef USE_SENSOR // Helper class to store a sensor with a deduplicator & publish state only when the value changes @@ -61,5 +91,4 @@ template class SensorWithDedup { Deduplicator publish_dedup; }; #endif -} // namespace ld24xx -} // namespace esphome +} // namespace esphome::ld24xx diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index aaa4794586..a203dde115 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -130,8 +130,10 @@ void LEDCOutput::setup() { } int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); - ESP_LOGV(TAG, "Configured frequency %f with a bit depth of %u bits", this->frequency_, this->bit_depth_); - ESP_LOGV(TAG, "Angle of %.1f° results in hpoint %u", this->phase_angle_, hpoint); + ESP_LOGV(TAG, + "Configured frequency %f with a bit depth of %u bits\n" + "Angle of %.1f° results in hpoint %u", + this->frequency_, this->bit_depth_, this->phase_angle_, hpoint); ledc_channel_config_t chan_conf{}; chan_conf.gpio_num = this->pin_->get_pin(); @@ -147,25 +149,30 @@ void LEDCOutput::setup() { } void LEDCOutput::dump_config() { - ESP_LOGCONFIG(TAG, "Output:"); - LOG_PIN(" Pin ", this->pin_); ESP_LOGCONFIG(TAG, + "Output:\n" " Channel: %u\n" " PWM Frequency: %.1f Hz\n" " Phase angle: %.1f°\n" " Bit depth: %u", this->channel_, this->frequency_, this->phase_angle_, this->bit_depth_); - ESP_LOGV(TAG, " Max frequency for bit depth: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_)); - ESP_LOGV(TAG, " Min frequency for bit depth: %f", - ledc_min_frequency_for_bit_depth(this->bit_depth_, (this->frequency_ < 100))); - ESP_LOGV(TAG, " Max frequency for bit depth-1: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_ - 1)); - ESP_LOGV(TAG, " Min frequency for bit depth-1: %f", - ledc_min_frequency_for_bit_depth(this->bit_depth_ - 1, (this->frequency_ < 100))); - ESP_LOGV(TAG, " Max frequency for bit depth+1: %f", ledc_max_frequency_for_bit_depth(this->bit_depth_ + 1)); - ESP_LOGV(TAG, " Min frequency for bit depth+1: %f", - ledc_min_frequency_for_bit_depth(this->bit_depth_ + 1, (this->frequency_ < 100))); - ESP_LOGV(TAG, " Max res bits: %d", MAX_RES_BITS); - ESP_LOGV(TAG, " Clock frequency: %f", CLOCK_FREQUENCY); + LOG_PIN(" Pin ", this->pin_); + ESP_LOGV(TAG, + " Max frequency for bit depth: %f\n" + " Min frequency for bit depth: %f\n" + " Max frequency for bit depth-1: %f\n" + " Min frequency for bit depth-1: %f\n" + " Max frequency for bit depth+1: %f\n" + " Min frequency for bit depth+1: %f\n" + " Max res bits: %d\n" + " Clock frequency: %f", + ledc_max_frequency_for_bit_depth(this->bit_depth_), + ledc_min_frequency_for_bit_depth(this->bit_depth_, (this->frequency_ < 100)), + ledc_max_frequency_for_bit_depth(this->bit_depth_ - 1), + ledc_min_frequency_for_bit_depth(this->bit_depth_ - 1, (this->frequency_ < 100)), + ledc_max_frequency_for_bit_depth(this->bit_depth_ + 1), + ledc_min_frequency_for_bit_depth(this->bit_depth_ + 1, (this->frequency_ < 100)), MAX_RES_BITS, + CLOCK_FREQUENCY); } void LEDCOutput::update_frequency(float frequency) { diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index 2133c4daf9..7a45b9dc3f 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -45,10 +45,12 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.Required(CONF_ID): cv.declare_id(LEDCOutput), cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, - cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency, + cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.All( + cv.frequency, cv.float_range(min=0, min_included=False) + ), cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15), cv.Optional(CONF_PHASE_ANGLE): cv.All( - cv.only_with_esp_idf, cv.angle, cv.float_range(min=0.0, max=360.0) + cv.angle, cv.float_range(min=0.0, max=360.0) ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index c63d6d7faa..93b66888da 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -261,6 +261,10 @@ async def component_to_code(config): cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}") cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]]) + # LibreTiny uses MULTI_NO_ATOMICS because platforms like BK7231N (ARM968E-S) lack + # exclusive load/store (no LDREX/STREX). std::atomic RMW operations require libatomic, + # which is not linked to save flash (4-8KB). Even if linked, libatomic would use locks + # (ATOMIC_INT_LOCK_FREE=1), so explicit FreeRTOS mutexes are simpler and equivalent. cg.add_define(ThreadModel.MULTI_NO_ATOMICS) # force using arduino framework diff --git a/esphome/components/libretiny/gpio_arduino.cpp b/esphome/components/libretiny/gpio_arduino.cpp index 7a1e014ea4..0b14c77cf2 100644 --- a/esphome/components/libretiny/gpio_arduino.cpp +++ b/esphome/components/libretiny/gpio_arduino.cpp @@ -63,10 +63,8 @@ void ArduinoInternalGPIOPin::pin_mode(gpio::Flags flags) { pinMode(pin_, flags_to_mode(flags)); // NOLINT } -std::string ArduinoInternalGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u", pin_); - return buffer; +size_t ArduinoInternalGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u", this->pin_); } bool ArduinoInternalGPIOPin::digital_read() { diff --git a/esphome/components/libretiny/gpio_arduino.h b/esphome/components/libretiny/gpio_arduino.h index 3674748c18..30c7c33869 100644 --- a/esphome/components/libretiny/gpio_arduino.h +++ b/esphome/components/libretiny/gpio_arduino.h @@ -16,7 +16,7 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return pin_; } diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 871b186d8e..68bc279767 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -4,24 +4,29 @@ #include "esphome/core/log.h" #include "esphome/core/preferences.h" #include +#include #include #include -#include namespace esphome { namespace libretiny { static const char *const TAG = "lt.preferences"; +// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding +static constexpr size_t KEY_BUFFER_SIZE = 12; + struct NVSData { - std::string key; + uint32_t key; std::unique_ptr data; size_t len; void set_data(const uint8_t *src, size_t size) { - data = std::make_unique(size); - memcpy(data.get(), src, size); - len = size; + if (!this->data || this->len != size) { + this->data = std::make_unique(size); + this->len = size; + } + memcpy(this->data.get(), src, size); } }; @@ -29,30 +34,30 @@ static std::vector s_pending_save; // NOLINT(cppcoreguidelines-avoid-n class LibreTinyPreferenceBackend : public ESPPreferenceBackend { public: - std::string key; + uint32_t key; fdb_kvdb_t db; fdb_blob_t blob; bool save(const uint8_t *data, size_t len) override { // try find in pending saves and update that for (auto &obj : s_pending_save) { - if (obj.key == key) { + if (obj.key == this->key) { obj.set_data(data, len); return true; } } NVSData save{}; - save.key = key; + save.key = this->key; save.set_data(data, len); s_pending_save.emplace_back(std::move(save)); - ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %zu", key.c_str(), len); + ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len); return true; } bool load(uint8_t *data, size_t len) override { // try find in pending saves and load from that for (auto &obj : s_pending_save) { - if (obj.key == key) { + if (obj.key == this->key) { if (obj.len != len) { // size mismatch return false; @@ -62,13 +67,15 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend { } } - fdb_blob_make(blob, data, len); - size_t actual_len = fdb_kv_get_blob(db, key.c_str(), blob); + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key); + fdb_blob_make(this->blob, data, len); + size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob); if (actual_len != len) { ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); return false; } else { - ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %zu", key.c_str(), len); + ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %zu", key_str, len); } return true; } @@ -90,16 +97,14 @@ class LibreTinyPreferences : public ESPPreferences { } ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - return make_preference(length, type); + return this->make_preference(length, type); } ESPPreferenceObject make_preference(size_t length, uint32_t type) override { auto *pref = new LibreTinyPreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->db = &db; - pref->blob = &blob; - - uint32_t keyval = type; - pref->key = str_sprintf("%u", keyval); + pref->db = &this->db; + pref->blob = &this->blob; + pref->key = type; return ESPPreferenceObject(pref); } @@ -112,18 +117,20 @@ class LibreTinyPreferences : public ESPPreferences { // goal try write all pending saves even if one fails int cached = 0, written = 0, failed = 0; fdb_err_t last_err = FDB_NO_ERR; - std::string last_key{}; + uint32_t last_key = 0; // go through vector from back to front (makes erase easier/more efficient) for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { const auto &save = s_pending_save[i]; - ESP_LOGVV(TAG, "Checking if FDB data %s has changed", save.key.c_str()); - if (is_changed(&db, save)) { - ESP_LOGV(TAG, "sync: key: %s, len: %zu", save.key.c_str(), save.len); - fdb_blob_make(&blob, save.data.get(), save.len); - fdb_err_t err = fdb_kv_set_blob(&db, save.key.c_str(), &blob); + char key_str[KEY_BUFFER_SIZE]; + snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); + ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str); + if (this->is_changed_(&this->db, save, key_str)) { + ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len); + fdb_blob_make(&this->blob, save.data.get(), save.len); + fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob); if (err != FDB_NO_ERR) { - ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", save.key.c_str(), save.len, err); + ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err); failed++; last_err = err; last_key = save.key; @@ -131,7 +138,7 @@ class LibreTinyPreferences : public ESPPreferences { } written++; } else { - ESP_LOGD(TAG, "FDB data not changed; skipping %s len=%zu", save.key.c_str(), save.len); + ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len); cached++; } s_pending_save.erase(s_pending_save.begin() + i); @@ -139,17 +146,18 @@ class LibreTinyPreferences : public ESPPreferences { ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, failed); if (failed > 0) { - ESP_LOGE(TAG, "Writing %d items failed. Last error=%d for key=%s", failed, last_err, last_key.c_str()); + ESP_LOGE(TAG, "Writing %d items failed. Last error=%d for key=%" PRIu32, failed, last_err, last_key); } return failed == 0; } - bool is_changed(const fdb_kvdb_t db, const NVSData &to_save) { + protected: + bool is_changed_(fdb_kvdb_t db, const NVSData &to_save, const char *key_str) { struct fdb_kv kv; - fdb_kv_t kvp = fdb_kv_get_obj(db, to_save.key.c_str(), &kv); + fdb_kv_t kvp = fdb_kv_get_obj(db, key_str, &kv); if (kvp == nullptr) { - ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str()); + ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", key_str); return true; } @@ -160,10 +168,10 @@ class LibreTinyPreferences : public ESPPreferences { // Allocate buffer on heap to avoid stack allocation for large data auto stored_data = std::make_unique(kv.value_len); - fdb_blob_make(&blob, stored_data.get(), kv.value_len); - size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob); + fdb_blob_make(&this->blob, stored_data.get(), kv.value_len); + size_t actual_len = fdb_kv_get_blob(db, key_str, &this->blob); if (actual_len != kv.value_len) { - ESP_LOGV(TAG, "fdb_kv_get_blob('%s') len mismatch: %u != %u", to_save.key.c_str(), actual_len, kv.value_len); + ESP_LOGV(TAG, "fdb_kv_get_blob('%s') len mismatch: %u != %u", key_str, actual_len, kv.value_len); return true; } diff --git a/esphome/components/libretiny_pwm/libretiny_pwm.cpp b/esphome/components/libretiny_pwm/libretiny_pwm.cpp index 92e4097c0e..4e4a16d761 100644 --- a/esphome/components/libretiny_pwm/libretiny_pwm.cpp +++ b/esphome/components/libretiny_pwm/libretiny_pwm.cpp @@ -31,9 +31,11 @@ void LibreTinyPWM::setup() { } void LibreTinyPWM::dump_config() { - ESP_LOGCONFIG(TAG, "PWM Output:"); + ESP_LOGCONFIG(TAG, + "PWM Output:\n" + " Frequency: %.1f Hz", + this->frequency_); LOG_PIN(" Pin ", this->pin_); - ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); } void LibreTinyPWM::update_frequency(float frequency) { diff --git a/esphome/components/libretiny_pwm/output.py b/esphome/components/libretiny_pwm/output.py index 1eb4869da3..28556514d8 100644 --- a/esphome/components/libretiny_pwm/output.py +++ b/esphome/components/libretiny_pwm/output.py @@ -14,7 +14,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.Required(CONF_ID): cv.declare_id(LibreTinyPWM), cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, - cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency, + cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.All( + cv.frequency, cv.float_range(min=0, min_included=False) + ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 5cbdcb0e86..2f6ffc9a38 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -1,8 +1,7 @@ #include "addressable_light.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light.addressable"; @@ -112,5 +111,4 @@ optional AddressableLightTransformer::apply() { return {}; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 393cc679bc..fcaf07f578 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -14,8 +14,7 @@ #include "esphome/components/power_supply/power_supply.h" #endif -namespace esphome { -namespace light { +namespace esphome::light { /// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness). Color color_from_light_color_values(LightColorValues val); @@ -71,7 +70,7 @@ class AddressableLight : public LightOutput, public Component { this->state_parent_ = state; } void update_state(LightState *state) override; - void schedule_show() { this->state_parent_->next_write_ = true; } + void schedule_show() { this->state_parent_->schedule_write_(); } #ifdef USE_POWER_SUPPLY void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); } @@ -116,5 +115,4 @@ class AddressableLightTransformer : public LightTransformer { Color target_color_{}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 0847db3770..a85ea4661d 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -7,8 +7,7 @@ #include "esphome/components/light/light_state.h" #include "esphome/components/light/addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { inline static int16_t sin16_c(uint16_t theta) { static const uint16_t BASE[] = {0, 6393, 12539, 18204, 23170, 27245, 30273, 32137}; @@ -371,5 +370,4 @@ class AddressableFlickerEffect : public AddressableLightEffect { uint8_t intensity_{13}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/addressable_light_wrapper.h b/esphome/components/light/addressable_light_wrapper.h index d358502430..8665e62a79 100644 --- a/esphome/components/light/addressable_light_wrapper.h +++ b/esphome/components/light/addressable_light_wrapper.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { class AddressableLightWrapper : public light::AddressableLight { public: @@ -123,5 +122,4 @@ class AddressableLightWrapper : public light::AddressableLight { ColorMode color_mode_{ColorMode::UNKNOWN}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/automation.cpp b/esphome/components/light/automation.cpp index 8c1785f061..ddac2f9341 100644 --- a/esphome/components/light/automation.cpp +++ b/esphome/components/light/automation.cpp @@ -1,8 +1,7 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light.automation"; @@ -11,5 +10,4 @@ void addressableset_warn_about_scale(const char *field) { field); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 8899db8bba..c90d71c5df 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -4,8 +4,7 @@ #include "light_state.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { enum class LimitMode { CLAMP, DO_NOTHING }; @@ -121,46 +120,54 @@ template class LightIsOffCondition : public Condition { LightState *state_; }; -class LightTurnOnTrigger : public Trigger<> { +class LightTurnOnTrigger : public Trigger<>, public LightRemoteValuesListener { public: - LightTurnOnTrigger(LightState *a_light) { - a_light->add_new_remote_values_callback([this, a_light]() { - // using the remote value because of transitions we need to trigger as early as possible - auto is_on = a_light->remote_values.is_on(); - // only trigger when going from off to on - auto should_trigger = is_on && !this->last_on_; - // Set new state immediately so that trigger() doesn't devolve - // into infinite loop - this->last_on_ = is_on; - if (should_trigger) { - this->trigger(); - } - }); + explicit LightTurnOnTrigger(LightState *a_light) : light_(a_light) { + a_light->add_remote_values_listener(this); this->last_on_ = a_light->current_values.is_on(); } + void on_light_remote_values_update() override { + // using the remote value because of transitions we need to trigger as early as possible + auto is_on = this->light_->remote_values.is_on(); + // only trigger when going from off to on + auto should_trigger = is_on && !this->last_on_; + // Set new state immediately so that trigger() doesn't devolve + // into infinite loop + this->last_on_ = is_on; + if (should_trigger) { + this->trigger(); + } + } + protected: + LightState *light_; bool last_on_; }; -class LightTurnOffTrigger : public Trigger<> { +class LightTurnOffTrigger : public Trigger<>, public LightTargetStateReachedListener { public: - LightTurnOffTrigger(LightState *a_light) { - a_light->add_new_target_state_reached_callback([this, a_light]() { - auto is_on = a_light->current_values.is_on(); - // only trigger when going from on to off - if (!is_on) { - this->trigger(); - } - }); + explicit LightTurnOffTrigger(LightState *a_light) : light_(a_light) { + a_light->add_target_state_reached_listener(this); } + + void on_light_target_state_reached() override { + auto is_on = this->light_->current_values.is_on(); + // only trigger when going from on to off + if (!is_on) { + this->trigger(); + } + } + + protected: + LightState *light_; }; -class LightStateTrigger : public Trigger<> { +class LightStateTrigger : public Trigger<>, public LightRemoteValuesListener { public: - LightStateTrigger(LightState *a_light) { - a_light->add_new_remote_values_callback([this]() { this->trigger(); }); - } + explicit LightStateTrigger(LightState *a_light) { a_light->add_remote_values_listener(this); } + + void on_light_remote_values_update() override { this->trigger(); } }; // This is slightly ugly, but we can't log in headers, and can't make this a static method on AddressableSet @@ -216,5 +223,4 @@ template class AddressableSet : public Action { } }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 515afc5c59..2eeae574e7 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "light_effect.h" -namespace esphome { -namespace light { +namespace esphome::light { inline static float random_cubic_float() { const float r = random_float() * 2.0f - 1.0f; @@ -235,5 +234,4 @@ class FlickerLightEffect : public LightEffect { float alpha_{}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h index aa3448c145..0750ae250d 100644 --- a/esphome/components/light/color_mode.h +++ b/esphome/components/light/color_mode.h @@ -3,8 +3,7 @@ #include #include "esphome/core/finite_set_mask.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Color capabilities are the various outputs that a light has and that can be independently controlled by the user. enum class ColorCapability : uint8_t { @@ -210,5 +209,4 @@ inline bool has_capability(const ColorModeMask &mask, ColorCapability capability return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp index e5e68264cc..1b511a94b2 100644 --- a/esphome/components/light/esp_color_correction.cpp +++ b/esphome/components/light/esp_color_correction.cpp @@ -2,8 +2,7 @@ #include "light_color_values.h" #include "esphome/core/log.h" -namespace esphome { -namespace light { +namespace esphome::light { void ESPColorCorrection::calculate_gamma_table(float gamma) { for (uint16_t i = 0; i < 256; i++) { @@ -23,5 +22,4 @@ void ESPColorCorrection::calculate_gamma_table(float gamma) { } } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 14c065058c..d275e045b7 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -2,8 +2,7 @@ #include "esphome/core/color.h" -namespace esphome { -namespace light { +namespace esphome::light { class ESPColorCorrection { public: @@ -73,5 +72,4 @@ class ESPColorCorrection { uint8_t local_brightness_{255}; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_color_view.h b/esphome/components/light/esp_color_view.h index 35117e7dd8..440a23e9c9 100644 --- a/esphome/components/light/esp_color_view.h +++ b/esphome/components/light/esp_color_view.h @@ -4,8 +4,7 @@ #include "esp_hsv_color.h" #include "esp_color_correction.h" -namespace esphome { -namespace light { +namespace esphome::light { class ESPColorSettable { public: @@ -106,5 +105,4 @@ class ESPColorView : public ESPColorSettable { const ESPColorCorrection *color_correction_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_hsv_color.cpp b/esphome/components/light/esp_hsv_color.cpp index 450c2e11ce..07205ea6d0 100644 --- a/esphome/components/light/esp_hsv_color.cpp +++ b/esphome/components/light/esp_hsv_color.cpp @@ -1,7 +1,6 @@ #include "esp_hsv_color.h" -namespace esphome { -namespace light { +namespace esphome::light { Color ESPHSVColor::to_rgb() const { // based on FastLED's hsv rainbow to rgb @@ -70,5 +69,4 @@ Color ESPHSVColor::to_rgb() const { return rgb; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_hsv_color.h b/esphome/components/light/esp_hsv_color.h index cdde91c71c..4b54039258 100644 --- a/esphome/components/light/esp_hsv_color.h +++ b/esphome/components/light/esp_hsv_color.h @@ -3,8 +3,7 @@ #include "esphome/core/color.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace light { +namespace esphome::light { struct ESPHSVColor { union { @@ -32,5 +31,4 @@ struct ESPHSVColor { Color to_rgb() const; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_range_view.cpp b/esphome/components/light/esp_range_view.cpp index e1f0a507bd..58d552031a 100644 --- a/esphome/components/light/esp_range_view.cpp +++ b/esphome/components/light/esp_range_view.cpp @@ -1,8 +1,7 @@ #include "esp_range_view.h" #include "addressable_light.h" -namespace esphome { -namespace light { +namespace esphome::light { int32_t HOT interpret_index(int32_t index, int32_t size) { if (index < 0) @@ -92,5 +91,4 @@ ESPRangeView &ESPRangeView::operator=(const ESPRangeView &rhs) { // NOLINT ESPColorView ESPRangeIterator::operator*() const { return this->range_.parent_->get(this->i_); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/esp_range_view.h b/esphome/components/light/esp_range_view.h index 07d18af79f..f5e4ebb83f 100644 --- a/esphome/components/light/esp_range_view.h +++ b/esphome/components/light/esp_range_view.h @@ -3,8 +3,7 @@ #include "esp_color_view.h" #include "esp_hsv_color.h" -namespace esphome { -namespace light { +namespace esphome::light { int32_t interpret_index(int32_t index, int32_t size); @@ -76,5 +75,4 @@ class ESPRangeIterator { int32_t i_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index b15ff84b97..8161e8b814 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include "esphome/core/optional.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light"; @@ -75,11 +74,11 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { // Helper to log percentage values #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG -static void log_percent(const char *name, const char *param, float value) { - ESP_LOGD(TAG, " %s: %.0f%%", param, value * 100.0f); +static void log_percent(const LogString *param, float value) { + ESP_LOGD(TAG, " %s: %.0f%%", LOG_STR_ARG(param), value * 100.0f); } #else -#define log_percent(name, param, value) +#define log_percent(param, value) #endif void LightCall::perform() { @@ -105,11 +104,11 @@ void LightCall::perform() { } if (this->has_brightness()) { - log_percent(name, "Brightness", v.get_brightness()); + log_percent(LOG_STR("Brightness"), v.get_brightness()); } if (this->has_color_brightness()) { - log_percent(name, "Color brightness", v.get_color_brightness()); + log_percent(LOG_STR("Color brightness"), v.get_color_brightness()); } if (this->has_red() || this->has_green() || this->has_blue()) { ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, @@ -117,7 +116,7 @@ void LightCall::perform() { } if (this->has_white()) { - log_percent(name, "White", v.get_white()); + log_percent(LOG_STR("White"), v.get_white()); } if (this->has_color_temperature()) { ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); @@ -175,8 +174,10 @@ void LightCall::perform() { this->parent_->set_immediately_(v, publish); } - if (!this->has_transition_()) { - this->parent_->target_state_reached_callback_.call(); + if (!this->has_transition_() && this->parent_->target_state_reached_listeners_) { + for (auto *listener : *this->parent_->target_state_reached_listeners_) { + listener->on_light_target_state_reached(); + } } if (publish) { this->parent_->publish_state(); @@ -503,8 +504,8 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() { #undef KEY } -LightCall &LightCall::set_effect(const std::string &effect) { - if (strcasecmp(effect.c_str(), "none") == 0) { +LightCall &LightCall::set_effect(const char *effect, size_t len) { + if (len == 4 && strncasecmp(effect, "none", 4) == 0) { this->set_effect(0); return *this; } @@ -512,15 +513,16 @@ LightCall &LightCall::set_effect(const std::string &effect) { bool found = false; for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) { LightEffect *e = this->parent_->effects_[i]; + const char *name = e->get_name(); - if (strcasecmp(effect.c_str(), e->get_name()) == 0) { + if (strncasecmp(effect, name, len) == 0 && name[len] == '\0') { this->set_effect(i + 1); found = true; break; } } if (!found) { - ESP_LOGW(TAG, "'%s': no such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); + ESP_LOGW(TAG, "'%s': no such effect '%.*s'", this->parent_->get_name().c_str(), (int) len, effect); } return *this; } @@ -647,5 +649,4 @@ LightCall &LightCall::set_rgbw(float red, float green, float blue, float white) return *this; } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index 6931b58b9d..0926ab6108 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -129,7 +129,9 @@ class LightCall { /// Set the effect of the light by its name. LightCall &set_effect(optional effect); /// Set the effect of the light by its name. - LightCall &set_effect(const std::string &effect); + LightCall &set_effect(const std::string &effect) { return this->set_effect(effect.data(), effect.size()); } + /// Set the effect of the light by its name and length (zero-copy from API). + LightCall &set_effect(const char *effect, size_t len); /// Set the effect of the light by its internal index number (only for internal use). LightCall &set_effect(uint32_t effect_number); LightCall &set_effect(optional effect_number); diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 04d7d1e7d8..bedfad2c35 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -4,8 +4,7 @@ #include "color_mode.h" #include -namespace esphome { -namespace light { +namespace esphome::light { inline static uint8_t to_uint8_scale(float x) { return static_cast(roundf(x * 255.0f)); } @@ -310,5 +309,4 @@ class LightColorValues { ColorMode color_mode_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_effect.cpp b/esphome/components/light/light_effect.cpp index a210b48e5b..81b923f7f9 100644 --- a/esphome/components/light/light_effect.cpp +++ b/esphome/components/light/light_effect.cpp @@ -1,8 +1,7 @@ #include "light_effect.h" #include "light_state.h" -namespace esphome { -namespace light { +namespace esphome::light { uint32_t LightEffect::get_index() const { if (this->state_ == nullptr) { @@ -32,5 +31,4 @@ uint32_t LightEffect::get_index_in_parent_() const { return 0; // Not found } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_effect.h b/esphome/components/light/light_effect.h index d4c2dc3582..aa1f6f7899 100644 --- a/esphome/components/light/light_effect.h +++ b/esphome/components/light/light_effect.h @@ -2,8 +2,7 @@ #include "esphome/core/component.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightState; @@ -55,5 +54,4 @@ class LightEffect { uint32_t get_index_in_parent_() const; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index e754c453b5..98b03f9458 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -1,45 +1,44 @@ #include "light_json_schema.h" #include "light_output.h" +#include "esphome/core/progmem.h" #ifdef USE_JSON -namespace esphome { -namespace light { +namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema -// Lookup table for color mode strings -static constexpr const char *get_color_mode_json_str(ColorMode mode) { - switch (mode) { - case ColorMode::ON_OFF: - return "onoff"; - case ColorMode::BRIGHTNESS: - return "brightness"; - case ColorMode::WHITE: - return "white"; // not supported by HA in MQTT - case ColorMode::COLOR_TEMPERATURE: - return "color_temp"; - case ColorMode::COLD_WARM_WHITE: - return "cwww"; // not supported by HA - case ColorMode::RGB: - return "rgb"; - case ColorMode::RGB_WHITE: - return "rgbw"; - case ColorMode::RGB_COLOR_TEMPERATURE: - return "rgbct"; // not supported by HA - case ColorMode::RGB_COLD_WARM_WHITE: - return "rgbww"; - default: - return nullptr; +// Get JSON string for color mode using linear search (avoids large switch jump table) +static const char *get_color_mode_json_str(ColorMode mode) { + // Parallel arrays: mode values and their corresponding strings + // Uses less RAM than a switch jump table on sparse enum values + static constexpr ColorMode MODES[] = { + ColorMode::ON_OFF, + ColorMode::BRIGHTNESS, + ColorMode::WHITE, + ColorMode::COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE, + ColorMode::RGB, + ColorMode::RGB_WHITE, + ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE, + }; + static constexpr const char *STRINGS[] = { + "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", "rgbww", + }; + for (size_t i = 0; i < sizeof(MODES) / sizeof(MODES[0]); i++) { + if (MODES[i] == mode) + return STRINGS[i]; } + return nullptr; } void LightJSONSchema::dump_json(LightState &state, JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson if (state.supports_effects()) { - root["effect"] = state.get_effect_name(); - root["effect_index"] = state.get_current_effect_index(); - root["effect_count"] = state.get_effect_count(); + root[ESPHOME_F("effect")] = state.get_effect_name_ref(); + root[ESPHOME_F("effect_index")] = state.get_current_effect_index(); + root[ESPHOME_F("effect_count")] = state.get_effect_count(); } auto values = state.remote_values; @@ -47,39 +46,39 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) { const auto color_mode = values.get_color_mode(); const char *mode_str = get_color_mode_json_str(color_mode); if (mode_str != nullptr) { - root["color_mode"] = mode_str; + root[ESPHOME_F("color_mode")] = mode_str; } if (color_mode & ColorCapability::ON_OFF) - root["state"] = (values.get_state() != 0.0f) ? "ON" : "OFF"; + root[ESPHOME_F("state")] = (values.get_state() != 0.0f) ? "ON" : "OFF"; if (color_mode & ColorCapability::BRIGHTNESS) - root["brightness"] = to_uint8_scale(values.get_brightness()); + root[ESPHOME_F("brightness")] = to_uint8_scale(values.get_brightness()); - JsonObject color = root["color"].to(); + JsonObject color = root[ESPHOME_F("color")].to(); if (color_mode & ColorCapability::RGB) { float color_brightness = values.get_color_brightness(); - color["r"] = to_uint8_scale(color_brightness * values.get_red()); - color["g"] = to_uint8_scale(color_brightness * values.get_green()); - color["b"] = to_uint8_scale(color_brightness * values.get_blue()); + color[ESPHOME_F("r")] = to_uint8_scale(color_brightness * values.get_red()); + color[ESPHOME_F("g")] = to_uint8_scale(color_brightness * values.get_green()); + color[ESPHOME_F("b")] = to_uint8_scale(color_brightness * values.get_blue()); } if (color_mode & ColorCapability::WHITE) { uint8_t white_val = to_uint8_scale(values.get_white()); - color["w"] = white_val; - root["white_value"] = white_val; // legacy API + color[ESPHOME_F("w")] = white_val; + root[ESPHOME_F("white_value")] = white_val; // legacy API } if (color_mode & ColorCapability::COLOR_TEMPERATURE) { // this one isn't under the color subkey for some reason - root["color_temp"] = uint32_t(values.get_color_temperature()); + root[ESPHOME_F("color_temp")] = uint32_t(values.get_color_temperature()); } if (color_mode & ColorCapability::COLD_WARM_WHITE) { - color["c"] = to_uint8_scale(values.get_cold_white()); - color["w"] = to_uint8_scale(values.get_warm_white()); + color[ESPHOME_F("c")] = to_uint8_scale(values.get_cold_white()); + color[ESPHOME_F("w")] = to_uint8_scale(values.get_warm_white()); } } void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) { - if (root["state"].is()) { - auto val = parse_on_off(root["state"]); + if (root[ESPHOME_F("state")].is()) { + auto val = parse_on_off(root[ESPHOME_F("state")]); switch (val) { case PARSE_ON: call.set_state(true); @@ -95,81 +94,81 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO } } - if (root["brightness"].is()) { - call.set_brightness(float(root["brightness"]) / 255.0f); + if (root[ESPHOME_F("brightness")].is()) { + call.set_brightness(float(root[ESPHOME_F("brightness")]) / 255.0f); } - if (root["color"].is()) { - JsonObject color = root["color"]; + if (root[ESPHOME_F("color")].is()) { + JsonObject color = root[ESPHOME_F("color")]; // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. float max_rgb = 0.0f; - if (color["r"].is()) { - float r = float(color["r"]) / 255.0f; + if (color[ESPHOME_F("r")].is()) { + float r = float(color[ESPHOME_F("r")]) / 255.0f; max_rgb = fmaxf(max_rgb, r); call.set_red(r); } - if (color["g"].is()) { - float g = float(color["g"]) / 255.0f; + if (color[ESPHOME_F("g")].is()) { + float g = float(color[ESPHOME_F("g")]) / 255.0f; max_rgb = fmaxf(max_rgb, g); call.set_green(g); } - if (color["b"].is()) { - float b = float(color["b"]) / 255.0f; + if (color[ESPHOME_F("b")].is()) { + float b = float(color[ESPHOME_F("b")]) / 255.0f; max_rgb = fmaxf(max_rgb, b); call.set_blue(b); } - if (color["r"].is() || color["g"].is() || color["b"].is()) { + if (color[ESPHOME_F("r")].is() || color[ESPHOME_F("g")].is() || + color[ESPHOME_F("b")].is()) { call.set_color_brightness(max_rgb); } - if (color["c"].is()) { - call.set_cold_white(float(color["c"]) / 255.0f); + if (color[ESPHOME_F("c")].is()) { + call.set_cold_white(float(color[ESPHOME_F("c")]) / 255.0f); } - if (color["w"].is()) { + if (color[ESPHOME_F("w")].is()) { // the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm // white channel in RGBWW. - if (color["c"].is()) { - call.set_warm_white(float(color["w"]) / 255.0f); + if (color[ESPHOME_F("c")].is()) { + call.set_warm_white(float(color[ESPHOME_F("w")]) / 255.0f); } else { - call.set_white(float(color["w"]) / 255.0f); + call.set_white(float(color[ESPHOME_F("w")]) / 255.0f); } } } - if (root["white_value"].is()) { // legacy API - call.set_white(float(root["white_value"]) / 255.0f); + if (root[ESPHOME_F("white_value")].is()) { // legacy API + call.set_white(float(root[ESPHOME_F("white_value")]) / 255.0f); } - if (root["color_temp"].is()) { - call.set_color_temperature(float(root["color_temp"])); + if (root[ESPHOME_F("color_temp")].is()) { + call.set_color_temperature(float(root[ESPHOME_F("color_temp")])); } } void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) { LightJSONSchema::parse_color_json(state, call, root); - if (root["flash"].is()) { - auto length = uint32_t(float(root["flash"]) * 1000); + if (root[ESPHOME_F("flash")].is()) { + auto length = uint32_t(float(root[ESPHOME_F("flash")]) * 1000); call.set_flash_length(length); } - if (root["transition"].is()) { - auto length = uint32_t(float(root["transition"]) * 1000); + if (root[ESPHOME_F("transition")].is()) { + auto length = uint32_t(float(root[ESPHOME_F("transition")]) * 1000); call.set_transition_length(length); } - if (root["effect"].is()) { - const char *effect = root["effect"]; - call.set_effect(effect); + if (root[ESPHOME_F("effect")].is()) { + const char *effect = root[ESPHOME_F("effect")]; + call.set_effect(effect, strlen(effect)); } - if (root["effect_index"].is()) { - uint32_t effect_index = root["effect_index"]; + if (root[ESPHOME_F("effect_index")].is()) { + uint32_t effect_index = root[ESPHOME_F("effect_index")]; call.set_effect(effect_index); } } -} // namespace light -} // namespace esphome +} // namespace esphome::light #endif diff --git a/esphome/components/light/light_json_schema.h b/esphome/components/light/light_json_schema.h index c92dd7b655..dac81e32e3 100644 --- a/esphome/components/light/light_json_schema.h +++ b/esphome/components/light/light_json_schema.h @@ -8,8 +8,7 @@ #include "light_call.h" #include "light_state.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightJSONSchema { public: @@ -22,7 +21,6 @@ class LightJSONSchema { static void parse_color_json(LightState &state, LightCall &call, JsonObject root); }; -} // namespace light -} // namespace esphome +} // namespace esphome::light #endif diff --git a/esphome/components/light/light_output.cpp b/esphome/components/light/light_output.cpp index e805a0b694..a86e8e5bf1 100644 --- a/esphome/components/light/light_output.cpp +++ b/esphome/components/light/light_output.cpp @@ -1,12 +1,10 @@ #include "light_output.h" #include "transformers.h" -namespace esphome { -namespace light { +namespace esphome::light { std::unique_ptr LightOutput::create_default_transition() { return make_unique(); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_output.h b/esphome/components/light/light_output.h index 73ba0371cd..c82d270be8 100644 --- a/esphome/components/light/light_output.h +++ b/esphome/components/light/light_output.h @@ -5,8 +5,7 @@ #include "light_state.h" #include "light_transformer.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Interface to write LightStates to hardware. class LightOutput { @@ -29,5 +28,4 @@ class LightOutput { virtual void write_state(LightState *state) = 0; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 4c253ec5a8..5a50bae50b 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -5,8 +5,7 @@ #include "light_output.h" #include "transformers.h" -namespace esphome { -namespace light { +namespace esphome::light { static const char *const TAG = "light"; @@ -24,6 +23,9 @@ void LightState::setup() { effect->init_internal(this); } + // Start with loop disabled if idle - respects any effects/transitions set up during initialization + this->disable_loop_if_idle_(); + // When supported color temperature range is known, initialize color temperature setting within bounds. auto traits = this->get_traits(); float min_mireds = traits.get_min_mireds(); @@ -125,7 +127,14 @@ void LightState::loop() { this->transformer_->stop(); this->is_transformer_active_ = false; this->transformer_ = nullptr; - this->target_state_reached_callback_.call(); + if (this->target_state_reached_listeners_) { + for (auto *listener : *this->target_state_reached_listeners_) { + listener->on_light_target_state_reached(); + } + } + + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); } } @@ -133,13 +142,19 @@ void LightState::loop() { if (this->next_write_) { this->next_write_ = false; this->output_->write_state(this); + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); } } float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } void LightState::publish_state() { - this->remote_values_callback_.call(); + if (this->remote_values_listeners_) { + for (auto *listener : *this->remote_values_listeners_) { + listener->on_light_remote_values_update(); + } + } #if defined(USE_LIGHT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_light_update(this); #endif @@ -164,11 +179,17 @@ StringRef LightState::get_effect_name_ref() { return EFFECT_NONE_REF; } -void LightState::add_new_remote_values_callback(std::function &&send_callback) { - this->remote_values_callback_.add(std::move(send_callback)); +void LightState::add_remote_values_listener(LightRemoteValuesListener *listener) { + if (!this->remote_values_listeners_) { + this->remote_values_listeners_ = make_unique>(); + } + this->remote_values_listeners_->push_back(listener); } -void LightState::add_new_target_state_reached_callback(std::function &&send_callback) { - this->target_state_reached_callback_.add(std::move(send_callback)); +void LightState::add_target_state_reached_listener(LightTargetStateReachedListener *listener) { + if (!this->target_state_reached_listeners_) { + this->target_state_reached_listeners_ = make_unique>(); + } + this->target_state_reached_listeners_->push_back(listener); } void LightState::set_default_transition_length(uint32_t default_transition_length) { @@ -228,6 +249,8 @@ void LightState::start_effect_(uint32_t effect_index) { this->active_effect_index_ = effect_index; auto *effect = this->get_active_effect_(); effect->start_internal(); + // Enable loop while effect is active + this->enable_loop(); } LightEffect *LightState::get_active_effect_() { if (this->active_effect_index_ == 0) { @@ -242,6 +265,8 @@ void LightState::stop_effect_() { effect->stop(); } this->active_effect_index_ = 0; + // Disable loop if idle (no effect and no transformer) + this->disable_loop_if_idle_(); } void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) { @@ -251,6 +276,8 @@ void LightState::start_transition_(const LightColorValues &target, uint32_t leng if (set_remote_values) { this->remote_values = target; } + // Enable loop while transition is active + this->enable_loop(); } void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) { @@ -266,6 +293,8 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b if (set_remote_values) { this->remote_values = target; }; + // Enable loop while flash is active + this->enable_loop(); } void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { @@ -276,7 +305,14 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot this->remote_values = target; } this->output_->update_state(this); - this->next_write_ = true; + this->schedule_write_(); +} + +void LightState::disable_loop_if_idle_() { + // Only disable loop if both transformer and effect are inactive, and no pending writes + if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr && !this->next_write_) { + this->disable_loop(); + } } void LightState::save_remote_values_() { @@ -304,5 +340,4 @@ void LightState::save_remote_values_() { this->rtc_.save(&saved); } -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index bf63c0ec27..a21c2c7693 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -15,10 +15,32 @@ #include #include -namespace esphome { -namespace light { +namespace esphome::light { class LightOutput; +class LightState; + +/** Listener interface for light remote value changes. + * + * Components can implement this interface to receive notifications + * when the light's remote values change (state, brightness, color, etc.) + * without the overhead of std::function callbacks. + */ +class LightRemoteValuesListener { + public: + virtual void on_light_remote_values_update() = 0; +}; + +/** Listener interface for light target state reached. + * + * Components can implement this interface to receive notifications + * when the light finishes a transition and reaches its target state + * without the overhead of std::function callbacks. + */ +class LightTargetStateReachedListener { + public: + virtual void on_light_target_state_reached() = 0; +}; enum LightRestoreMode : uint8_t { LIGHT_RESTORE_DEFAULT_OFF, @@ -122,21 +144,17 @@ class LightState : public EntityBase, public Component { /// Return the name of the current effect as StringRef (for API usage) StringRef get_effect_name_ref(); - /** - * This lets front-end components subscribe to light change events. This callback is called once - * when the remote color values are changed. - * - * @param send_callback The callback. + /** Add a listener for remote values changes. + * Listener is notified when the light's remote values change (state, brightness, color, etc.) + * Lazily allocates the listener vector on first registration. */ - void add_new_remote_values_callback(std::function &&send_callback); + void add_remote_values_listener(LightRemoteValuesListener *listener); - /** - * The callback is called once the state of current_values and remote_values are equal (when the - * transition is finished). - * - * @param send_callback + /** Add a listener for target state reached. + * Listener is notified when the light finishes a transition and reaches its target state. + * Lazily allocates the listener vector on first registration. */ - void add_new_target_state_reached_callback(std::function &&send_callback); + void add_target_state_reached_listener(LightTargetStateReachedListener *listener); /// Set the default transition length, i.e. the transition length when no transition is provided. void set_default_transition_length(uint32_t default_transition_length); @@ -256,6 +274,15 @@ class LightState : public EntityBase, public Component { /// Internal method to save the current remote_values to the preferences void save_remote_values_(); + /// Disable loop if neither transformer nor effect is active + void disable_loop_if_idle_(); + + /// Schedule a write to the light output and enable the loop to process it + void schedule_write_() { + this->next_write_ = true; + this->enable_loop(); + } + /// Store the output to allow effects to have more access. LightOutput *output_; /// The currently active transformer for this light (transition/flash). @@ -277,19 +304,24 @@ class LightState : public EntityBase, public Component { // for effects, true if a transformer (transition) is active. bool is_transformer_active_ = false; - /** Callback to call when new values for the frontend are available. + /** Listeners for remote values changes. * * "Remote values" are light color values that are reported to the frontend and have a lower * publish frequency than the "real" color values. For example, during transitions the current * color value may change continuously, but the remote values will be reported as the target values * starting with the beginning of the transition. + * + * Lazily allocated - only created when a listener is actually registered. */ - CallbackManager remote_values_callback_{}; + std::unique_ptr> remote_values_listeners_; - /** Callback to call when the state of current_values and remote_values are equal - * This should be called once the state of current_values changed and equals the state of remote_values + /** Listeners for target state reached. + * Notified when the state of current_values and remote_values are equal + * (when the transition is finished). + * + * Lazily allocated - only created when a listener is actually registered. */ - CallbackManager target_state_reached_callback_{}; + std::unique_ptr> target_state_reached_listeners_; /// Initial state of the light. optional initial_state_{}; @@ -298,5 +330,4 @@ class LightState : public EntityBase, public Component { LightRestoreMode restore_mode_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index a84183c03c..079c2d2ae0 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -4,8 +4,7 @@ #include "esphome/core/helpers.h" #include "light_color_values.h" -namespace esphome { -namespace light { +namespace esphome::light { /// Base class for all light color transformers, such as transitions or flashes. class LightTransformer { @@ -59,5 +58,4 @@ class LightTransformer { LightColorValues target_values_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index 71d41a66d3..a26713b723 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -6,8 +6,7 @@ #include "light_state.h" #include "light_transformer.h" -namespace esphome { -namespace light { +namespace esphome::light { class LightTransitionTransformer : public LightTransformer { public: @@ -118,5 +117,4 @@ class LightFlashTransformer : public LightTransformer { bool begun_lightstate_restore_; }; -} // namespace light -} // namespace esphome +} // namespace esphome::light diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 0f596ef5e6..011c6cc6af 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace lock { +namespace esphome::lock { template class LockAction : public Action { public: @@ -50,27 +49,18 @@ template class LockCondition : public Condition { bool state_; }; -class LockLockTrigger : public Trigger<> { +template class LockStateTrigger : public Trigger<> { public: - LockLockTrigger(Lock *a_lock) { + explicit LockStateTrigger(Lock *a_lock) { a_lock->add_on_state_callback([this, a_lock]() { - if (a_lock->state == LockState::LOCK_STATE_LOCKED) { + if (a_lock->state == State) { this->trigger(); } }); } }; -class LockUnlockTrigger : public Trigger<> { - public: - LockUnlockTrigger(Lock *a_lock) { - a_lock->add_on_state_callback([this, a_lock]() { - if (a_lock->state == LockState::LOCK_STATE_UNLOCKED) { - this->trigger(); - } - }); - } -}; +using LockLockTrigger = LockStateTrigger; +using LockUnlockTrigger = LockStateTrigger; -} // namespace lock -} // namespace esphome +} // namespace esphome::lock diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 54fefe8745..018f5113e3 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -3,26 +3,25 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace lock { +namespace esphome::lock { static const char *const TAG = "lock"; -const char *lock_state_to_string(LockState state) { +const LogString *lock_state_to_string(LockState state) { switch (state) { case LOCK_STATE_LOCKED: - return "LOCKED"; + return LOG_STR("LOCKED"); case LOCK_STATE_UNLOCKED: - return "UNLOCKED"; + return LOG_STR("UNLOCKED"); case LOCK_STATE_JAMMED: - return "JAMMED"; + return LOG_STR("JAMMED"); case LOCK_STATE_LOCKING: - return "LOCKING"; + return LOG_STR("LOCKING"); case LOCK_STATE_UNLOCKING: - return "UNLOCKING"; + return LOG_STR("UNLOCKING"); case LOCK_STATE_NONE: default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } @@ -53,7 +52,7 @@ void Lock::publish_state(LockState state) { this->state = state; this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), lock_state_to_string(state)); + ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); this->state_callback_.call(); #if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_lock_update(this); @@ -66,8 +65,7 @@ void LockCall::perform() { ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->state_.has_value()) { - const char *state_s = lock_state_to_string(*this->state_); - ESP_LOGD(TAG, " State: %s", state_s); + ESP_LOGD(TAG, " State: %s", LOG_STR_ARG(lock_state_to_string(*this->state_))); } this->parent_->control(*this); } @@ -75,7 +73,7 @@ void LockCall::validate_() { if (this->state_.has_value()) { auto state = *this->state_; if (!this->parent_->traits.supports_state(state)) { - ESP_LOGW(TAG, " State %s is not supported by this device!", lock_state_to_string(*this->state_)); + ESP_LOGW(TAG, " State %s is not supported by this device!", LOG_STR_ARG(lock_state_to_string(*this->state_))); this->state_.reset(); } } @@ -108,5 +106,4 @@ LockCall &LockCall::set_state(const std::string &state) { } const optional &LockCall::get_state() const { return this->state_; } -} // namespace lock -} // namespace esphome +} // namespace esphome::lock diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 9737569921..f77b11b145 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -7,8 +7,7 @@ #include "esphome/core/preferences.h" #include -namespace esphome { -namespace lock { +namespace esphome::lock { class Lock; @@ -31,7 +30,10 @@ enum LockState : uint8_t { LOCK_STATE_LOCKING = 4, LOCK_STATE_UNLOCKING = 5 }; -const char *lock_state_to_string(LockState state); +const LogString *lock_state_to_string(LockState state); + +/// Maximum length of lock state string (including null terminator): "UNLOCKING" = 10 +static constexpr size_t LOCK_STATE_STR_SIZE = 10; class LockTraits { public: @@ -172,10 +174,9 @@ class Lock : public EntityBase { */ virtual void control(const LockCall &call) = 0; - CallbackManager state_callback_{}; + LazyCallbackManager state_callback_{}; Deduplicator publish_dedup_; ESPPreferenceObject rtc_; }; -} // namespace lock -} // namespace esphome +} // namespace esphome::lock diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index cf78e6ae63..79a9a4208c 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -3,17 +3,19 @@ import re from esphome import automation from esphome.automation import LambdaAction, StatelessLambdaAction import esphome.codegen as cg -from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant -from esphome.components.esp32.const import ( +from esphome.components.esp32 import ( VARIANT_ESP32, VARIANT_ESP32C2, VARIANT_ESP32C3, VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + add_idf_sdkconfig_option, + get_esp32_variant, ) from esphome.components.libretiny import get_libretiny_component, get_libretiny_family from esphome.components.libretiny.const import ( @@ -100,14 +102,15 @@ CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" UART_SELECTION_ESP32 = { VARIANT_ESP32: [UART0, UART1, UART2], - VARIANT_ESP32S2: [UART0, UART1, USB_CDC], - VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], - VARIANT_ESP32C3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32C2: [UART0, UART1], + VARIANT_ESP32C3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32C5: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32C6: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32C61: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32H2: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32P4: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32S2: [UART0, UART1, USB_CDC], + VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], } UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] @@ -238,12 +241,15 @@ CONFIG_SCHEMA = cv.All( CONF_HARDWARE_UART, esp8266=UART0, esp32=UART0, - esp32_s2=USB_CDC, - esp32_s3=USB_SERIAL_JTAG, + esp32_c2=UART0, esp32_c3=USB_SERIAL_JTAG, esp32_c5=USB_SERIAL_JTAG, esp32_c6=USB_SERIAL_JTAG, + esp32_c61=USB_SERIAL_JTAG, + esp32_h2=USB_SERIAL_JTAG, esp32_p4=USB_SERIAL_JTAG, + esp32_s2=USB_CDC, + esp32_s3=USB_SERIAL_JTAG, rp2040=USB_CDC, bk72xx=DEFAULT, ln882x=DEFAULT, @@ -304,6 +310,10 @@ async def to_code(config): if task_log_buffer_size > 0: cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") cg.add(log.init_log_buffer(task_log_buffer_size)) + elif CORE.is_host: + cg.add(log.create_pthread_key()) + cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") + cg.add(log.init_log_buffer(64)) # Fixed 64 slots for host cg.add(log.set_log_level(initial_level)) if CONF_HARDWARE_UART in config: @@ -331,6 +341,18 @@ async def to_code(config): is_at_least_very_verbose = this_severity >= very_verbose_severity has_serial_logging = baud_rate != 0 + # Add defines for which Serial object is needed (allows linker to exclude unused) + if CORE.is_esp8266: + from esphome.components.esp8266.const import enable_serial, enable_serial1 + + hw_uart = config.get(CONF_HARDWARE_UART, UART0) + if has_serial_logging and hw_uart in (UART0, UART0_SWAP): + cg.add_define("USE_ESP8266_LOGGER_SERIAL") + enable_serial() + elif has_serial_logging and hw_uart == UART1: + cg.add_define("USE_ESP8266_LOGGER_SERIAL1") + enable_serial1() + if ( (CORE.is_esp8266 or CORE.is_rp2040) and has_serial_logging @@ -365,8 +387,10 @@ async def to_code(config): if CORE.is_esp32: if config[CONF_HARDWARE_UART] == USB_CDC: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) + cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC") elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) + cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG") try: uart_selection(USB_SERIAL_JTAG) cg.add_define("USE_LOGGER_USB_SERIAL_JTAG") @@ -378,7 +402,7 @@ async def to_code(config): except cv.Invalid: pass - if CORE.using_zephyr: + if CORE.is_nrf52: if config[CONF_HARDWARE_UART] == UART0: zephyr_add_overlay("""&uart0 { status = "okay";};""") if config[CONF_HARDWARE_UART] == UART1: @@ -404,6 +428,8 @@ async def to_code(config): conf, ) + CORE.add_job(final_step) + def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python @@ -498,9 +524,31 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.LN882X_ARDUINO, }, "logger_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, - "task_log_buffer.cpp": { + "task_log_buffer_esp32.cpp": { PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP32_IDF, }, + "task_log_buffer_host.cpp": {PlatformFramework.HOST_NATIVE}, } ) + +# Keys for CORE.data storage +DOMAIN = "logger" +KEY_LEVEL_LISTENERS = "level_listeners" + + +def request_logger_level_listeners() -> None: + """Request that logger level listeners be compiled in. + + Components that need to be notified about log level changes should call this + function during their code generation. This enables the add_level_listener() + method and compiles in the listener vector. + """ + CORE.data.setdefault(DOMAIN, {})[KEY_LEVEL_LISTENERS] = True + + +@coroutine_with_priority(CoroPriority.FINAL) +async def final_step(): + """Final code generation step to configure optional logger features.""" + if CORE.data.get(DOMAIN, {}).get(KEY_LEVEL_LISTENERS, False): + cg.add_define("USE_LOGGER_LEVEL_LISTENERS") diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 9a9bf89fe3..e633f9fd7d 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -12,14 +12,14 @@ namespace esphome::logger { static const char *const TAG = "logger"; -#ifdef USE_ESP32 -// Implementation for ESP32 (multi-task platform with task-specific tracking) -// Main task always uses direct buffer access for console output and callbacks +#if defined(USE_ESP32) || defined(USE_HOST) +// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads) +// Main thread/task always uses direct buffer access for console output and callbacks // -// For non-main tasks: +// For non-main threads/tasks: // - WITH task log buffer: Prefer sending to ring buffer for async processing // - Avoids allocating stack memory for console output in normal operation -// - Prevents console corruption from concurrent writes by multiple tasks +// - Prevents console corruption from concurrent writes by multiple threads // - Messages are serialized through main loop for proper console output // - Fallback to emergency console logging only if ring buffer is full // - WITHOUT task log buffer: Only emergency console output, no callbacks @@ -27,15 +27,20 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch if (level > this->level_for(tag)) return; +#ifdef USE_ESP32 TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); bool is_main_task = (current_task == main_task_); +#else // USE_HOST + pthread_t current_thread = pthread_self(); + bool is_main_task = pthread_equal(current_thread, main_thread_); +#endif - // Check and set recursion guard - uses pthread TLS for per-task state + // Check and set recursion guard - uses pthread TLS for per-thread/task state if (this->check_and_set_task_log_recursion_(is_main_task)) { return; // Recursion detected } - // Main task uses the shared buffer for efficiency + // Main thread/task uses the shared buffer for efficiency if (is_main_task) { this->log_message_to_buffer_and_send_(level, tag, line, format, args); this->reset_task_log_recursion_(is_main_task); @@ -44,9 +49,13 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch bool message_sent = false; #ifdef USE_ESPHOME_TASK_LOG_BUFFER - // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered + // For non-main threads/tasks, queue the message for callbacks +#ifdef USE_ESP32 message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), current_task, format, args); +#else // USE_HOST + message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), format, args); +#endif if (message_sent) { // Enable logger loop to process the buffered message // This is safe to call from any context including ISRs @@ -54,21 +63,29 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch } #endif // USE_ESPHOME_TASK_LOG_BUFFER - // Emergency console logging for non-main tasks when ring buffer is full or disabled + // Emergency console logging for non-main threads when ring buffer is full or disabled // This is a fallback mechanism to ensure critical log messages are visible - // Note: This may cause interleaved/corrupted console output if multiple tasks + // Note: This may cause interleaved/corrupted console output if multiple threads // log simultaneously, but it's better than losing important messages entirely +#ifdef USE_HOST + if (!message_sent) { + // Host always has console output - no baud_rate check needed + static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512; +#else if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console // Maximum size for console log messages (includes null terminator) static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; +#endif char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety uint16_t buffer_at = 0; // Initialize buffer position this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); - this->write_msg_(console_buffer); + // Add newline before writing to console + this->add_newline_to_buffer_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); + this->write_msg_(console_buffer, buffer_at); } - // Reset the recursion guard for this task + // Reset the recursion guard for this thread/task this->reset_task_log_recursion_(is_main_task); } #else @@ -84,7 +101,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch global_recursion_guard_ = false; } -#endif // !USE_ESP32 +#endif // USE_ESP32 / USE_HOST #ifdef USE_STORE_LOG_STR_IN_FLASH // Implementation for ESP8266 with flash string support. @@ -131,17 +148,19 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas // Save the offset before calling format_log_to_buffer_with_terminator_ // since it will increment tx_buffer_at_ to the end of the formatted string - uint32_t msg_start = this->tx_buffer_at_; + uint16_t msg_start = this->tx_buffer_at_; this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - // Write to console and send callback starting at the msg_start - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_ + msg_start); - } - size_t msg_length = + uint16_t msg_length = this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position - this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length); + + // Listeners get message first (before console write) + for (auto *listener : this->log_listeners_) + listener->on_log(level, tag, this->tx_buffer_ + msg_start, msg_length); + + // Write to console starting at the msg_start + this->write_tx_buffer_to_console_(msg_start, &msg_length); global_recursion_guard_ = false; } @@ -163,15 +182,24 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate this->main_task_ = xTaskGetCurrentTaskHandle(); #elif defined(USE_ZEPHYR) this->main_task_ = k_current_get(); +#elif defined(USE_HOST) + this->main_thread_ = pthread_self(); #endif } #ifdef USE_ESPHOME_TASK_LOG_BUFFER void Logger::init_log_buffer(size_t total_buffer_size) { +#ifdef USE_HOST + // Host uses slot count instead of byte size + this->log_buffer_ = esphome::make_unique(total_buffer_size); +#else this->log_buffer_ = esphome::make_unique(total_buffer_size); +#endif +#ifdef USE_ESP32 // Start with loop disabled when using task buffer (unless using USB CDC) // The loop will be enabled automatically when messages arrive this->disable_loop_when_buffer_empty_(); +#endif } #endif @@ -183,42 +211,37 @@ void Logger::process_messages_() { #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Process any buffered messages when available if (this->log_buffer_->has_messages()) { +#ifdef USE_HOST + logger::TaskLogBufferHost::LogMessage *message; + while (this->log_buffer_->get_message_main_loop(&message)) { + const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; + this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text, + message->text_length); + this->log_buffer_->release_message_main_loop(); + this->write_tx_buffer_to_console_(); + } +#else // USE_ESP32 logger::TaskLogBuffer::LogMessage *message; const char *text; void *received_token; - - // Process messages from the buffer while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) { - this->tx_buffer_at_ = 0; - // Use the thread name that was stored when the message was created - // This avoids potential crashes if the task no longer exists const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; - this->write_header_to_buffer_(message->level, message->tag, message->line, thread_name, this->tx_buffer_, - &this->tx_buffer_at_, this->tx_buffer_size_); - this->write_body_to_buffer_(text, message->text_length, this->tx_buffer_, &this->tx_buffer_at_, - this->tx_buffer_size_); - this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - this->tx_buffer_[this->tx_buffer_at_] = '\0'; - size_t msg_len = this->tx_buffer_at_; // We already know the length from tx_buffer_at_ - this->log_callback_.call(message->level, message->tag, this->tx_buffer_, msg_len); - // At this point all the data we need from message has been transferred to the tx_buffer - // so we can release the message to allow other tasks to use it as soon as possible. + this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, + message->text_length); + // Release the message to allow other tasks to use it as soon as possible this->log_buffer_->release_message_main_loop(received_token); - - // Write to console from the main loop to prevent corruption from concurrent writes - // This ensures all log messages appear on the console in a clean, serialized manner - // Note: Messages may appear slightly out of order due to async processing, but - // this is preferred over corrupted/interleaved console output - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_); - } + this->write_tx_buffer_to_console_(); } - } else { +#endif + } +#ifdef USE_ESP32 + else { // No messages to process, disable loop if appropriate // This reduces overhead when there's no async logging activity this->disable_loop_when_buffer_empty_(); } #endif +#endif // USE_ESPHOME_TASK_LOG_BUFFER } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } @@ -230,9 +253,6 @@ void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_level UARTSelection Logger::get_uart() const { return this->uart_; } #endif -void Logger::add_on_log_callback(std::function &&callback) { - this->log_callback_.add(std::move(callback)); -} float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } #ifdef USE_STORE_LOG_STR_IN_FLASH @@ -271,7 +291,11 @@ void Logger::dump_config() { #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER if (this->log_buffer_) { - ESP_LOGCONFIG(TAG, " Task Log Buffer Size: %u", this->log_buffer_->size()); +#ifdef USE_HOST + ESP_LOGCONFIG(TAG, " Task Log Buffer Slots: %u", static_cast(this->log_buffer_->size())); +#else + ESP_LOGCONFIG(TAG, " Task Log Buffer Size: %u bytes", static_cast(this->log_buffer_->size())); +#endif } #endif @@ -288,7 +312,10 @@ void Logger::set_log_level(uint8_t level) { ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL])); } this->current_level_ = level; - this->level_callback_.call(level); +#ifdef USE_LOGGER_LEVEL_LISTENERS + for (auto *listener : this->level_listeners_) + listener->on_log_level_change(level); +#endif } Logger *global_logger = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index dc8e06e0c9..86d2943135 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -2,7 +2,7 @@ #include #include -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_HOST) #include #endif #include "esphome/core/automation.h" @@ -12,7 +12,11 @@ #include "esphome/core/log.h" #ifdef USE_ESPHOME_TASK_LOG_BUFFER -#include "task_log_buffer.h" +#ifdef USE_HOST +#include "task_log_buffer_host.h" +#elif defined(USE_ESP32) +#include "task_log_buffer_esp32.h" +#endif #endif #ifdef USE_ARDUINO @@ -36,6 +40,52 @@ struct device; namespace esphome::logger { +/** Interface for receiving log messages without std::function overhead. + * + * Components can implement this interface instead of using lambdas with std::function + * to reduce flash usage from std::function type erasure machinery. + * + * Usage: + * class MyComponent : public Component, public LogListener { + * public: + * void setup() override { + * if (logger::global_logger != nullptr) + * logger::global_logger->add_log_listener(this); + * } + * void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { + * // Handle log message + * } + * }; + */ +class LogListener { + public: + virtual void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) = 0; +}; + +#ifdef USE_LOGGER_LEVEL_LISTENERS +/** Interface for receiving log level changes without std::function overhead. + * + * Components can implement this interface instead of using lambdas with std::function + * to reduce flash usage from std::function type erasure machinery. + * + * Usage: + * class MyComponent : public Component, public LoggerLevelListener { + * public: + * void setup() override { + * if (logger::global_logger != nullptr) + * logger::global_logger->add_logger_level_listener(this); + * } + * void on_log_level_change(uint8_t level) override { + * // Handle log level change + * } + * }; + */ +class LoggerLevelListener { + public: + virtual void on_log_level_change(uint8_t level) = 0; +}; +#endif + #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS // Comparison function for const char* keys in log_levels_ map struct CStrCompare { @@ -135,6 +185,9 @@ class Logger : public Component { uart_port_t get_uart_num() const { return uart_num_; } void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } #endif +#ifdef USE_HOST + void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); } +#endif #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. @@ -157,11 +210,13 @@ class Logger : public Component { inline uint8_t level_for(const char *tag); - /// Register a callback that will be called for every log message sent - void add_on_log_callback(std::function &&callback); + /// Register a log listener to receive log messages + void add_log_listener(LogListener *listener) { this->log_listeners_.push_back(listener); } - // add a listener for log level changes - void add_listener(std::function &&callback) { this->level_callback_.add(std::move(callback)); } +#ifdef USE_LOGGER_LEVEL_LISTENERS + /// Register a listener for log level changes + void add_level_listener(LoggerLevelListener *listener) { this->level_listeners_.push_back(listener); } +#endif float get_setup_priority() const override; @@ -173,14 +228,14 @@ class Logger : public Component { protected: void process_messages_(); - void write_msg_(const char *msg); + void write_msg_(const char *msg, size_t len); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // It's the caller's responsibility to initialize buffer_at (typically to 0) inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, va_list args, char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST) this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); #elif defined(USE_ZEPHYR) char buff[MAX_POINTER_REPRESENTATION]; @@ -200,7 +255,34 @@ class Logger : public Component { } } - // Helper to format and send a log message to both console and callbacks + // Helper to add newline to buffer before writing to console + // Modifies buffer_at to include the newline + inline void HOT add_newline_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { + // Add newline - don't need to maintain null termination + // write_msg_ receives explicit length, so we can safely overwrite the null terminator + // This is safe because: + // 1. Callbacks already received the message (before we add newline) + // 2. write_msg_ receives the length explicitly (doesn't need null terminator) + if (*buffer_at < buffer_size) { + buffer[(*buffer_at)++] = '\n'; + } else if (buffer_size > 0) { + // Buffer was full - replace last char with newline to ensure it's visible + buffer[buffer_size - 1] = '\n'; + *buffer_at = buffer_size; + } + } + + // Helper to write tx_buffer_ to console if logging is enabled + // INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null + inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) { + if (this->baud_rate_ > 0) { + uint16_t *len_ptr = length ? length : &this->tx_buffer_at_; + this->add_newline_to_buffer_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset); + this->write_msg_(this->tx_buffer_ + offset, *len_ptr); + } + } + + // Helper to format and send a log message to both console and listeners inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // Format to tx_buffer and prepare for output @@ -208,12 +290,30 @@ class Logger : public Component { this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - if (this->baud_rate_ > 0) { - this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console - } - this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_); + // Listeners get message WITHOUT newline (for API/MQTT/syslog) + for (auto *listener : this->log_listeners_) + listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); + + // Console gets message WITH newline (if platform needs it) + this->write_tx_buffer_to_console_(); } +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + // Helper to format a pre-formatted message from the task log buffer and notify listeners + // Used by process_messages_ to avoid code duplication between ESP32 and host platforms + inline void HOT format_buffered_message_and_notify_(uint8_t level, const char *tag, uint16_t line, + const char *thread_name, const char *text, size_t text_length) { + this->tx_buffer_at_ = 0; + this->write_header_to_buffer_(level, tag, line, thread_name, this->tx_buffer_, &this->tx_buffer_at_, + this->tx_buffer_size_); + this->write_body_to_buffer_(text, text_length, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); + this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); + this->tx_buffer_[this->tx_buffer_at_] = '\0'; + for (auto *listener : this->log_listeners_) + listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); + } +#endif + // Write the body of the log message to the buffer inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { @@ -248,6 +348,9 @@ class Logger : public Component { #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) void *main_task_ = nullptr; // Only used for thread name identification #endif +#ifdef USE_HOST + pthread_t main_thread_{}; // Main thread for identification +#endif #ifdef USE_ESP32 // Task-specific recursion guards: // - Main task uses a dedicated member variable for efficiency @@ -255,15 +358,25 @@ class Logger : public Component { pthread_key_t log_recursion_key_; // 4 bytes uart_port_t uart_num_; // 4 bytes (enum defaults to int size) #endif +#ifdef USE_HOST + // Thread-specific recursion guards using pthread TLS + pthread_key_t log_recursion_key_; +#endif // Large objects (internally aligned) #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS std::map log_levels_{}; #endif - CallbackManager log_callback_{}; - CallbackManager level_callback_{}; + std::vector log_listeners_; // Log message listeners (API, MQTT, syslog, etc.) +#ifdef USE_LOGGER_LEVEL_LISTENERS + std::vector level_listeners_; // Log level change listeners +#endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER +#ifdef USE_HOST + std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer +#elif defined(USE_ESP32) std::unique_ptr log_buffer_; // Will be initialized with init_log_buffer +#endif #endif // Group smaller types together at the end @@ -276,7 +389,7 @@ class Logger : public Component { #ifdef USE_LIBRETINY UARTSelection uart_{UART_SELECTION_DEFAULT}; #endif -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_HOST) bool main_task_recursion_guard_{false}; #else bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms @@ -313,7 +426,7 @@ class Logger : public Component { } #endif -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_HOST) inline bool HOT check_and_set_task_log_recursion_(bool is_main_task) { if (is_main_task) { const bool was_recursive = main_task_recursion_guard_; @@ -339,6 +452,22 @@ class Logger : public Component { } #endif +#ifdef USE_HOST + const char *HOT get_thread_name_() { + pthread_t current_thread = pthread_self(); + if (pthread_equal(current_thread, main_thread_)) { + return nullptr; // Main thread + } + // For non-main threads, return the thread name + // We store it in thread-local storage to avoid allocation + static thread_local char thread_name_buf[32]; + if (pthread_getname_np(current_thread, thread_name_buf, sizeof(thread_name_buf)) == 0) { + return thread_name_buf; + } + return nullptr; + } +#endif + static inline void copy_string(char *buffer, uint16_t &pos, const char *str) { const size_t len = strlen(str); // Intentionally no null terminator, building larger string @@ -396,7 +525,7 @@ class Logger : public Component { buffer[pos++] = '0' + (remainder - tens * 10); buffer[pos++] = ']'; -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) if (thread_name != nullptr) { write_ansi_color_for_level(buffer, pos, 1); // Always use bold red for thread name buffer[pos++] = '['; @@ -425,7 +554,9 @@ class Logger : public Component { } // Update buffer_at with the formatted length (handle truncation) - uint16_t formatted_len = (ret >= remaining) ? remaining : ret; + // When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator + // When it doesn't truncate (ret < remaining), it writes ret chars + null terminator + uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret; *buffer_at += formatted_len; // Remove all trailing newlines right after formatting @@ -453,15 +584,15 @@ class Logger : public Component { }; extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -class LoggerMessageTrigger : public Trigger { +class LoggerMessageTrigger : public Trigger, public LogListener { public: - explicit LoggerMessageTrigger(Logger *parent, uint8_t level) { - this->level_ = level; - parent->add_on_log_callback([this](uint8_t level, const char *tag, const char *message, size_t message_len) { - if (level <= this->level_) { - this->trigger(level, tag, message); - } - }); + explicit LoggerMessageTrigger(Logger *parent, uint8_t level) : level_(level) { parent->add_log_listener(this); } + + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override { + (void) message_len; + if (level <= this->level_) { + this->trigger(level, tag, message); + } } protected: diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 7fc79e6f54..32ef752462 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -121,25 +121,23 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { - if ( -#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG) - this->uart_ == UART_SELECTION_USB_CDC -#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC) - this->uart_ == UART_SELECTION_USB_SERIAL_JTAG -#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG) - this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Length is now always passed explicitly - no strlen() fallback needed + +#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) + // USB CDC/JTAG - single write including newline (already in buffer) + // Use fwrite to stdout which goes through VFS to USB console + // + // Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG). + // They are ONLY defined when the user explicitly selects USB as the logger output in their config. + // This is compile-time selection, not runtime detection - if USB is configured, it's always used. + // There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility + // to configure correctly for their hardware. This approach eliminates runtime overhead. + fwrite(msg, 1, len, stdout); #else - /* DISABLES CODE */ (false) // NOLINT + // Regular UART - single write including newline (already in buffer) + uart_write_bytes(this->uart_num_, msg, len); #endif - ) { - puts(msg); - } else { - // Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen - size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg); - uart_write_bytes(this->uart_num_, msg, len); - uart_write_bytes(this->uart_num_, "\n", 1); - } } const LogString *Logger::get_uart_selection_() { diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index 5063d88b92..6cee1baca5 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -7,44 +7,43 @@ namespace esphome::logger { static const char *const TAG = "logger"; void Logger::pre_setup() { - if (this->baud_rate_ > 0) { - switch (this->uart_) { - case UART_SELECTION_UART0: - case UART_SELECTION_UART0_SWAP: - this->hw_serial_ = &Serial; - Serial.begin(this->baud_rate_); - if (this->uart_ == UART_SELECTION_UART0_SWAP) { - Serial.swap(); - } - Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); - break; - case UART_SELECTION_UART1: - this->hw_serial_ = &Serial1; - Serial1.begin(this->baud_rate_); - Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); - break; - } - } else { - uart_set_debug(UART_NO); +#if defined(USE_ESP8266_LOGGER_SERIAL) + this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); + if (this->uart_ == UART_SELECTION_UART0_SWAP) { + Serial.swap(); } + Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); +#elif defined(USE_ESP8266_LOGGER_SERIAL1) + this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); + Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); +#else + // No serial logging - disable debug output + uart_set_debug(UART_NO); +#endif global_logger = this; ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Single write with newline already in buffer (added by caller) + this->hw_serial_->write(msg, len); +} const LogString *Logger::get_uart_selection_() { - switch (this->uart_) { - case UART_SELECTION_UART0: - return LOG_STR("UART0"); - case UART_SELECTION_UART1: - return LOG_STR("UART1"); - case UART_SELECTION_UART0_SWAP: - default: - return LOG_STR("UART0_SWAP"); +#if defined(USE_ESP8266_LOGGER_SERIAL) + if (this->uart_ == UART_SELECTION_UART0_SWAP) { + return LOG_STR("UART0_SWAP"); } + return LOG_STR("UART0"); +#elif defined(USE_ESP8266_LOGGER_SERIAL1) + return LOG_STR("UART1"); +#else + return LOG_STR("NONE"); +#endif } } // namespace esphome::logger diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index 4abe92286a..874cdabd22 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -3,16 +3,24 @@ namespace esphome::logger { -void HOT Logger::write_msg_(const char *msg) { - time_t rawtime; - struct tm *timeinfo; - char buffer[80]; +void HOT Logger::write_msg_(const char *msg, size_t len) { + static constexpr size_t TIMESTAMP_LEN = 10; // "[HH:MM:SS]" + // tx_buffer_size_ defaults to 512, so 768 covers default + headroom + char buffer[TIMESTAMP_LEN + 768]; + time_t rawtime; time(&rawtime); - timeinfo = localtime(&rawtime); - strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo); - fputs(buffer, stdout); - puts(msg); + struct tm timeinfo; + localtime_r(&rawtime, &timeinfo); // Thread-safe version + size_t pos = strftime(buffer, TIMESTAMP_LEN + 1, "[%H:%M:%S]", &timeinfo); + + // Copy message (with newline already included by caller) + size_t copy_len = std::min(len, sizeof(buffer) - pos); + memcpy(buffer + pos, msg, copy_len); + pos += copy_len; + + // Single write for everything + fwrite(buffer, 1, pos, stdout); } void Logger::pre_setup() { global_logger = this; } diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index 3edfa74480..cdf55e710c 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,7 +49,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index 63727c2cda..be8252f56a 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -27,7 +27,10 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Single write with newline already in buffer (added by caller) + this->hw_serial_->write(msg, len); +} const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index fb0c7dcca3..41f53beec0 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace esphome::logger { @@ -14,7 +15,7 @@ static const char *const TAG = "logger"; #ifdef USE_LOGGER_USB_CDC void Logger::loop() { - if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) { + if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) { return; } static bool opened = false; @@ -62,18 +63,19 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg) { +void HOT Logger::write_msg_(const char *msg, size_t len) { + // Single write with newline already in buffer (added by caller) #ifdef CONFIG_PRINTK - printk("%s\n", msg); + // Requires the debug component and an active SWD connection. + // It is used for pyocd rtt -t nrf52840 + k_str_out(const_cast(msg), len); #endif - if (nullptr == this->uart_dev_) { + if (this->uart_dev_ == nullptr) { return; } - while (*msg) { - uart_poll_out(this->uart_dev_, *msg); - ++msg; + for (size_t i = 0; i < len; ++i) { + uart_poll_out(this->uart_dev_, msg[i]); } - uart_poll_out(this->uart_dev_, '\n'); } const LogString *Logger::get_uart_selection_() { diff --git a/esphome/components/logger/select/__init__.py b/esphome/components/logger/select/__init__.py index 2e83599eb4..6ce663978e 100644 --- a/esphome/components/logger/select/__init__.py +++ b/esphome/components/logger/select/__init__.py @@ -5,7 +5,13 @@ from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_ from esphome.core import CORE from esphome.cpp_helpers import register_component, register_parented -from .. import CONF_LOGGER_ID, LOG_LEVELS, Logger, logger_ns +from .. import ( + CONF_LOGGER_ID, + LOG_LEVELS, + Logger, + logger_ns, + request_logger_level_listeners, +) CODEOWNERS = ["@clydebarrow"] @@ -21,6 +27,7 @@ CONFIG_SCHEMA = select.select_schema( async def to_code(config): + request_logger_level_listeners() parent = await cg.get_variable(config[CONF_LOGGER_ID]) levels = list(LOG_LEVELS) index = levels.index(CORE.data[CONF_LOGGER][CONF_LEVEL]) diff --git a/esphome/components/logger/select/logger_level_select.cpp b/esphome/components/logger/select/logger_level_select.cpp index e2ec28a390..3091ca1851 100644 --- a/esphome/components/logger/select/logger_level_select.cpp +++ b/esphome/components/logger/select/logger_level_select.cpp @@ -2,7 +2,7 @@ namespace esphome::logger { -void LoggerLevelSelect::publish_state(int level) { +void LoggerLevelSelect::on_log_level_change(uint8_t level) { auto index = level_to_index(level); if (!this->has_index(index)) return; @@ -10,8 +10,8 @@ void LoggerLevelSelect::publish_state(int level) { } void LoggerLevelSelect::setup() { - this->parent_->add_listener([this](int level) { this->publish_state(level); }); - this->publish_state(this->parent_->get_log_level()); + this->parent_->add_level_listener(this); + this->on_log_level_change(this->parent_->get_log_level()); } void LoggerLevelSelect::control(size_t index) { this->parent_->set_log_level(index_to_level(index)); } diff --git a/esphome/components/logger/select/logger_level_select.h b/esphome/components/logger/select/logger_level_select.h index 950edd29ac..6482114943 100644 --- a/esphome/components/logger/select/logger_level_select.h +++ b/esphome/components/logger/select/logger_level_select.h @@ -5,12 +5,17 @@ #include "esphome/components/logger/logger.h" namespace esphome::logger { -class LoggerLevelSelect : public Component, public select::Select, public Parented { +class LoggerLevelSelect final : public Component, + public select::Select, + public Parented, + public LoggerLevelListener { public: - void publish_state(int level); void setup() override; void control(size_t index) override; + // LoggerLevelListener interface + void on_log_level_change(uint8_t level) override; + protected: // Convert log level to option index (skip CONFIG at level 4) static uint8_t level_to_index(uint8_t level) { return (level > ESPHOME_LOG_LEVEL_CONFIG) ? level - 1 : level; } diff --git a/esphome/components/logger/task_log_buffer.cpp b/esphome/components/logger/task_log_buffer_esp32.cpp similarity index 98% rename from esphome/components/logger/task_log_buffer.cpp rename to esphome/components/logger/task_log_buffer_esp32.cpp index b5dd9f0239..b9dfe45b7f 100644 --- a/esphome/components/logger/task_log_buffer.cpp +++ b/esphome/components/logger/task_log_buffer_esp32.cpp @@ -1,5 +1,6 @@ +#ifdef USE_ESP32 -#include "task_log_buffer.h" +#include "task_log_buffer_esp32.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -134,3 +135,4 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin } // namespace esphome::logger #endif // USE_ESPHOME_TASK_LOG_BUFFER +#endif // USE_ESP32 diff --git a/esphome/components/logger/task_log_buffer.h b/esphome/components/logger/task_log_buffer_esp32.h similarity index 77% rename from esphome/components/logger/task_log_buffer.h rename to esphome/components/logger/task_log_buffer_esp32.h index fdda07190d..fde9bd60d5 100644 --- a/esphome/components/logger/task_log_buffer.h +++ b/esphome/components/logger/task_log_buffer_esp32.h @@ -1,5 +1,7 @@ #pragma once +#ifdef USE_ESP32 + #include "esphome/core/defines.h" #include "esphome/core/helpers.h" @@ -13,6 +15,22 @@ namespace esphome::logger { +/** + * @brief Task log buffer for ESP32 platform using FreeRTOS ring buffer. + * + * Threading Model: Multi-Producer Single-Consumer (MPSC) + * - Multiple FreeRTOS tasks can safely call send_message_thread_safe() concurrently + * - Only the main loop task calls borrow_message_main_loop() and release_message_main_loop() + * + * This uses the FreeRTOS ring buffer (RINGBUF_TYPE_NOSPLIT) which provides + * built-in thread-safety for the MPSC pattern. The ring buffer ensures + * message integrity - each message is stored contiguously. + * + * Design: + * - Variable-size messages with header + text stored contiguously + * - FreeRTOS ring buffer handles synchronization internally + * - Atomic counter for fast has_messages() check without ring buffer lock + */ class TaskLogBuffer { public: // Structure for a log message header (text data follows immediately after) @@ -65,3 +83,4 @@ class TaskLogBuffer { } // namespace esphome::logger #endif // USE_ESPHOME_TASK_LOG_BUFFER +#endif // USE_ESP32 diff --git a/esphome/components/logger/task_log_buffer_host.cpp b/esphome/components/logger/task_log_buffer_host.cpp new file mode 100644 index 0000000000..0660aeb061 --- /dev/null +++ b/esphome/components/logger/task_log_buffer_host.cpp @@ -0,0 +1,157 @@ +#ifdef USE_HOST + +#include "task_log_buffer_host.h" + +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + +#include "esphome/core/log.h" +#include +#include + +namespace esphome::logger { + +TaskLogBufferHost::TaskLogBufferHost(size_t slot_count) : slot_count_(slot_count) { + // Allocate message slots + this->slots_ = std::make_unique(slot_count); +} + +TaskLogBufferHost::~TaskLogBufferHost() { + // unique_ptr handles cleanup automatically +} + +int TaskLogBufferHost::acquire_write_slot_() { + // Try to reserve a slot using compare-and-swap + size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed); + + while (true) { + // Calculate next index (with wrap-around) + size_t next_reserve = (current_reserve + 1) % this->slot_count_; + + // Check if buffer would be full + // Buffer is full when next write position equals read position + size_t current_read = this->read_index_.load(std::memory_order_acquire); + if (next_reserve == current_read) { + return -1; // Buffer full + } + + // Try to claim this slot + if (this->reserve_index_.compare_exchange_weak(current_reserve, next_reserve, std::memory_order_acq_rel, + std::memory_order_relaxed)) { + return static_cast(current_reserve); + } + // If CAS failed, current_reserve was updated, retry with new value + } +} + +void TaskLogBufferHost::commit_write_slot_(int slot_index) { + // Mark the slot as ready for reading + this->slots_[slot_index].ready.store(true, std::memory_order_release); + + // Try to advance the write_index if we're the next expected commit + // This ensures messages are read in order + size_t expected = slot_index; + size_t next = (slot_index + 1) % this->slot_count_; + + // We only advance write_index if this slot is the next one expected + // This handles out-of-order commits correctly + while (true) { + if (!this->write_index_.compare_exchange_weak(expected, next, std::memory_order_release, + std::memory_order_relaxed)) { + // Someone else advanced it or we're not next in line, that's fine + break; + } + + // Successfully advanced, check if next slot is also ready + expected = next; + next = (next + 1) % this->slot_count_; + if (!this->slots_[expected].ready.load(std::memory_order_acquire)) { + break; + } + } +} + +bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, + va_list args) { + // Acquire a slot + int slot_index = this->acquire_write_slot_(); + if (slot_index < 0) { + return false; // Buffer full + } + + LogMessage &msg = this->slots_[slot_index]; + + // Fill in the message header + msg.level = level; + msg.tag = tag; + msg.line = line; + + // Get thread name using pthread + char thread_name_buf[LogMessage::MAX_THREAD_NAME_SIZE]; + // pthread_getname_np works the same on Linux and macOS + if (pthread_getname_np(pthread_self(), thread_name_buf, sizeof(thread_name_buf)) == 0) { + strncpy(msg.thread_name, thread_name_buf, sizeof(msg.thread_name) - 1); + msg.thread_name[sizeof(msg.thread_name) - 1] = '\0'; + } else { + msg.thread_name[0] = '\0'; + } + + // Format the message text + int ret = vsnprintf(msg.text, sizeof(msg.text), format, args); + if (ret < 0) { + // Formatting error - still commit the slot but with empty text + msg.text[0] = '\0'; + msg.text_length = 0; + } else { + msg.text_length = static_cast(std::min(static_cast(ret), sizeof(msg.text) - 1)); + } + + // Remove trailing newlines + while (msg.text_length > 0 && msg.text[msg.text_length - 1] == '\n') { + msg.text_length--; + } + msg.text[msg.text_length] = '\0'; + + // Commit the slot + this->commit_write_slot_(slot_index); + + return true; +} + +bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) { + if (message == nullptr) { + return false; + } + + size_t current_read = this->read_index_.load(std::memory_order_relaxed); + size_t current_write = this->write_index_.load(std::memory_order_acquire); + + // Check if buffer is empty + if (current_read == current_write) { + return false; + } + + // Check if the slot is ready (should always be true if write_index advanced) + LogMessage &msg = this->slots_[current_read]; + if (!msg.ready.load(std::memory_order_acquire)) { + return false; + } + + *message = &msg; + return true; +} + +void TaskLogBufferHost::release_message_main_loop() { + size_t current_read = this->read_index_.load(std::memory_order_relaxed); + + // Clear the ready flag + this->slots_[current_read].ready.store(false, std::memory_order_release); + + // Advance read index + size_t next_read = (current_read + 1) % this->slot_count_; + this->read_index_.store(next_read, std::memory_order_release); +} + +} // namespace esphome::logger + +#endif // USE_ESPHOME_TASK_LOG_BUFFER +#endif // USE_HOST diff --git a/esphome/components/logger/task_log_buffer_host.h b/esphome/components/logger/task_log_buffer_host.h new file mode 100644 index 0000000000..d421d50ec6 --- /dev/null +++ b/esphome/components/logger/task_log_buffer_host.h @@ -0,0 +1,122 @@ +#pragma once + +#ifdef USE_HOST + +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#ifdef USE_ESPHOME_TASK_LOG_BUFFER + +#include +#include +#include +#include +#include +#include + +namespace esphome::logger { + +/** + * @brief Lock-free task log buffer for host platform. + * + * Threading Model: Multi-Producer Single-Consumer (MPSC) + * - Multiple threads can safely call send_message_thread_safe() concurrently + * - Only the main loop thread calls get_message_main_loop() and release_message_main_loop() + * + * Producers (multiple threads) Consumer (main loop only) + * │ │ + * ▼ ▼ + * acquire_write_slot_() get_message_main_loop() + * CAS on reserve_index_ read write_index_ + * │ check ready flag + * ▼ │ + * write to slot (exclusive) ▼ + * │ read slot data + * ▼ │ + * commit_write_slot_() ▼ + * set ready=true release_message_main_loop() + * advance write_index_ set ready=false + * advance read_index_ + * + * This implements a lock-free ring buffer for log messages on the host platform. + * It uses atomic compare-and-swap (CAS) operations for thread-safe slot reservation + * without requiring mutexes in the hot path. + * + * Design: + * - Fixed number of pre-allocated message slots to avoid dynamic allocation + * - Each slot contains a header and fixed-size text buffer + * - Atomic CAS for slot reservation allows multiple producers without locks + * - Single consumer (main loop) processes messages in order + */ +class TaskLogBufferHost { + public: + // Default number of message slots - host has plenty of memory + static constexpr size_t DEFAULT_SLOT_COUNT = 64; + + // Structure for a log message (fixed size for lock-free operation) + struct LogMessage { + // Size constants + static constexpr size_t MAX_THREAD_NAME_SIZE = 32; + static constexpr size_t MAX_TEXT_SIZE = 512; + + const char *tag; // Pointer to static tag string + char thread_name[MAX_THREAD_NAME_SIZE]; // Thread name (copied) + char text[MAX_TEXT_SIZE + 1]; // Message text with null terminator + uint16_t text_length; // Actual length of text + uint16_t line; // Source line number + uint8_t level; // Log level + std::atomic ready; // Message is ready to be consumed + + LogMessage() : tag(nullptr), text_length(0), line(0), level(0), ready(false) { + thread_name[0] = '\0'; + text[0] = '\0'; + } + }; + + /// Constructor that takes the number of message slots + explicit TaskLogBufferHost(size_t slot_count); + ~TaskLogBufferHost(); + + // NOT thread-safe - get next message from buffer, only call from main loop + // Returns true if a message was retrieved, false if buffer is empty + bool get_message_main_loop(LogMessage **message); + + // NOT thread-safe - release the message after processing, only call from main loop + void release_message_main_loop(); + + // Thread-safe - send a message to the buffer from any thread + // Returns true if message was queued, false if buffer is full + bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, va_list args); + + // Check if there are messages ready to be processed + inline bool HOT has_messages() const { + return read_index_.load(std::memory_order_acquire) != write_index_.load(std::memory_order_acquire); + } + + // Get the buffer size (number of slots) + inline size_t size() const { return slot_count_; } + + private: + // Acquire a slot for writing (thread-safe) + // Returns slot index or -1 if buffer is full + int acquire_write_slot_(); + + // Commit a slot after writing (thread-safe) + void commit_write_slot_(int slot_index); + + std::unique_ptr slots_; // Pre-allocated message slots + size_t slot_count_; // Number of slots + + // Lock-free indices using atomics + // - reserve_index_: Next slot to reserve (producers CAS this to claim slots) + // - write_index_: Boundary of committed/ready slots (consumer reads up to this) + // - read_index_: Next slot to read (only consumer modifies this) + std::atomic reserve_index_{0}; // Next slot to reserve for writing + std::atomic write_index_{0}; // Last committed slot boundary + std::atomic read_index_{0}; // Next slot to read from +}; + +} // namespace esphome::logger + +#endif // USE_ESPHOME_TASK_LOG_BUFFER +#endif // USE_HOST diff --git a/esphome/components/ltr390/ltr390.cpp b/esphome/components/ltr390/ltr390.cpp index c1885dcb6f..ba4a7ea5cb 100644 --- a/esphome/components/ltr390/ltr390.cpp +++ b/esphome/components/ltr390/ltr390.cpp @@ -104,12 +104,17 @@ void LTR390Component::read_uvs_() { } } -void LTR390Component::read_mode_(int mode_index) { - // Set mode - LTR390MODE mode = std::get<0>(this->mode_funcs_[mode_index]); - +void LTR390Component::standby_() { std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); - ctrl[LTR390_CTRL_MODE] = mode; + ctrl[LTR390_CTRL_EN] = false; + this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); + this->reading_ = false; +} + +void LTR390Component::read_mode_(LTR390MODE mode) { + // Set mode + std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); + ctrl[LTR390_CTRL_MODE] = (mode == LTR390_MODE_UVS); ctrl[LTR390_CTRL_EN] = true; this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); @@ -129,21 +134,18 @@ void LTR390Component::read_mode_(int mode_index) { } // After the sensor integration time do the following - this->set_timeout(int_time + LTR390_WAKEUP_TIME + LTR390_SETTLE_TIME, [this, mode_index]() { - // Read from the sensor - std::get<1>(this->mode_funcs_[mode_index])(); - - // If there are more modes to read then begin the next - // otherwise stop - if (mode_index + 1 < (int) this->mode_funcs_.size()) { - this->read_mode_(mode_index + 1); + this->set_timeout(int_time + LTR390_WAKEUP_TIME + LTR390_SETTLE_TIME, [this, mode]() { + // Read from the sensor and continue to next mode or standby + if (mode == LTR390_MODE_ALS) { + this->read_als_(); + if (this->enabled_modes_ & ENABLED_MODE_UVS) { + this->read_mode_(LTR390_MODE_UVS); + return; + } } else { - // put sensor in standby - std::bitset<8> ctrl = this->reg(LTR390_MAIN_CTRL).get(); - ctrl[LTR390_CTRL_EN] = false; - this->reg(LTR390_MAIN_CTRL) = ctrl.to_ulong(); - this->reading_ = false; + this->read_uvs_(); } + this->standby_(); }); } @@ -172,14 +174,12 @@ void LTR390Component::setup() { // Set sensor read state this->reading_ = false; - // If we need the light sensor then add to the list + // Determine which modes are enabled based on configured sensors if (this->light_sensor_ != nullptr || this->als_sensor_ != nullptr) { - this->mode_funcs_.emplace_back(LTR390_MODE_ALS, std::bind(<R390Component::read_als_, this)); + this->enabled_modes_ |= ENABLED_MODE_ALS; } - - // If we need the UV sensor then add to the list if (this->uvi_sensor_ != nullptr || this->uv_sensor_ != nullptr) { - this->mode_funcs_.emplace_back(LTR390_MODE_UVS, std::bind(<R390Component::read_uvs_, this)); + this->enabled_modes_ |= ENABLED_MODE_UVS; } } @@ -195,10 +195,11 @@ void LTR390Component::dump_config() { } void LTR390Component::update() { - if (!this->reading_ && !mode_funcs_.empty()) { - this->reading_ = true; - this->read_mode_(0); - } + if (this->reading_ || this->enabled_modes_ == 0) + return; + + this->reading_ = true; + this->read_mode_((this->enabled_modes_ & ENABLED_MODE_ALS) ? LTR390_MODE_ALS : LTR390_MODE_UVS); } } // namespace ltr390 diff --git a/esphome/components/ltr390/ltr390.h b/esphome/components/ltr390/ltr390.h index 7db73d68ff..47884b9166 100644 --- a/esphome/components/ltr390/ltr390.h +++ b/esphome/components/ltr390/ltr390.h @@ -1,7 +1,5 @@ #pragma once -#include -#include #include "esphome/components/i2c/i2c.h" #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" @@ -60,17 +58,19 @@ class LTR390Component : public PollingComponent, public i2c::I2CDevice { void set_uv_sensor(sensor::Sensor *uv_sensor) { this->uv_sensor_ = uv_sensor; } protected: + static constexpr uint8_t ENABLED_MODE_ALS = 1 << 0; + static constexpr uint8_t ENABLED_MODE_UVS = 1 << 1; + optional read_sensor_data_(LTR390MODE mode); void read_als_(); void read_uvs_(); - void read_mode_(int mode_index); + void read_mode_(LTR390MODE mode); + void standby_(); - bool reading_; - - // a list of modes and corresponding read functions - std::vector>> mode_funcs_; + bool reading_{false}; + uint8_t enabled_modes_{0}; LTR390GAIN gain_als_; LTR390GAIN gain_uv_; diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index be5a4ddccf..04de91e362 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -174,7 +174,7 @@ void LTRAlsPs501Component::loop() { break; case State::WAITING_FOR_DATA: - if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) { + if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { tries = 0; ESP_LOGV(TAG, "Reading sensor data assuming gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); @@ -379,18 +379,18 @@ void LTRAlsPs501Component::configure_integration_time_(IntegrationTime501 time) } } -DataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) { +LtrDataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) { AlsPsStatusRegister als_status{0}; als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); if (!als_status.als_new_data) - return DataAvail::NO_DATA; + return LtrDataAvail::LTR_NO_DATA; ESP_LOGV(TAG, "Data ready, reported gain is %.0fx", get_gain_coeff(als_status.gain)); if (data.gain != als_status.gain) { ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain)); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } data.gain = als_status.gain; - return DataAvail::DATA_OK; + return LtrDataAvail::LTR_DATA_OK; } void LTRAlsPs501Component::read_sensor_data_(AlsReadings &data) { diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 849ff6bc23..02c025da30 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -11,7 +11,7 @@ namespace esphome { namespace ltr501 { -enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; +enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK }; enum LtrType : uint8_t { LTR_TYPE_UNKNOWN = 0, @@ -106,7 +106,7 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { void configure_als_(); void configure_integration_time_(IntegrationTime501 time); void configure_gain_(AlsGain501 gain); - DataAvail is_als_data_ready_(AlsReadings &data); + LtrDataAvail is_als_data_ready_(AlsReadings &data); void read_sensor_data_(AlsReadings &data); bool are_adjustments_required_(AlsReadings &data); void apply_lux_calculation_(AlsReadings &data); diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.cpp b/esphome/components/ltr_als_ps/ltr_als_ps.cpp index c3ea5848c8..f9c1474c85 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.cpp +++ b/esphome/components/ltr_als_ps/ltr_als_ps.cpp @@ -165,7 +165,7 @@ void LTRAlsPsComponent::loop() { break; case State::WAITING_FOR_DATA: - if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) { + if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { tries = 0; ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); @@ -376,23 +376,23 @@ void LTRAlsPsComponent::configure_integration_time_(IntegrationTime time) { } } -DataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) { +LtrDataAvail LTRAlsPsComponent::is_als_data_ready_(AlsReadings &data) { AlsPsStatusRegister als_status{0}; als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); if (!als_status.als_new_data) - return DataAvail::NO_DATA; + return LtrDataAvail::LTR_NO_DATA; if (als_status.data_invalid) { ESP_LOGW(TAG, "Data available but not valid"); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } ESP_LOGV(TAG, "Data ready, reported gain is %.0f", get_gain_coeff(als_status.gain)); if (data.gain != als_status.gain) { ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain)); - return DataAvail::BAD_DATA; + return LtrDataAvail::LTR_BAD_DATA; } - return DataAvail::DATA_OK; + return LtrDataAvail::LTR_DATA_OK; } void LTRAlsPsComponent::read_sensor_data_(AlsReadings &data) { diff --git a/esphome/components/ltr_als_ps/ltr_als_ps.h b/esphome/components/ltr_als_ps/ltr_als_ps.h index 2c768009ab..c6052300de 100644 --- a/esphome/components/ltr_als_ps/ltr_als_ps.h +++ b/esphome/components/ltr_als_ps/ltr_als_ps.h @@ -11,7 +11,7 @@ namespace esphome { namespace ltr_als_ps { -enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; +enum LtrDataAvail : uint8_t { LTR_NO_DATA, LTR_BAD_DATA, LTR_DATA_OK }; enum LtrType : uint8_t { LTR_TYPE_UNKNOWN = 0, @@ -106,7 +106,7 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice { void configure_als_(); void configure_integration_time_(IntegrationTime time); void configure_gain_(AlsGain gain); - DataAvail is_als_data_ready_(AlsReadings &data); + LtrDataAvail is_als_data_ready_(AlsReadings &data); void read_sensor_data_(AlsReadings &data); bool are_adjustments_required_(AlsReadings &data); void apply_lux_calculation_(AlsReadings &data); diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index eaa37b54dd..c9cad1ac90 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -1,5 +1,6 @@ import importlib import logging +from pathlib import Path import pkgutil from esphome.automation import build_automation, validate_automation @@ -26,6 +27,7 @@ from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import MockObj from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed +from esphome.yaml_util import load_yaml from . import defines as df, helpers, lv_validation as lvalid, widgets from .automation import disp_update, focused_widgets, refreshed_widgets @@ -37,7 +39,6 @@ from .encoders import ( initial_focus_to_code, ) from .gradient import GRADIENT_SCHEMA, gradients_to_code -from .hello_world import get_hello_world from .keypads import KEYPADS_CONFIG, keypads_to_code from .lv_validation import lv_bool, lv_images_used from .lvcode import LvContext, LvglComponent, lvgl_static @@ -84,6 +85,7 @@ DEPENDENCIES = ["display"] AUTO_LOAD = ["key_provider"] CODEOWNERS = ["@clydebarrow"] LOGGER = logging.getLogger(__name__) +HELLO_WORLD_FILE = "hello_world.yaml" SIMPLE_TRIGGERS = ( @@ -108,7 +110,7 @@ LV_CONF_H_FORMAT = """\ def generate_lv_conf_h(): - definitions = [as_macro(m, v) for m, v in df.lv_defines.items()] + definitions = [as_macro(m, v) for m, v in df.get_data(df.KEY_LV_DEFINES).items()] definitions.sort() return LV_CONF_H_FORMAT.format("\n".join(definitions)) @@ -140,11 +142,11 @@ def multi_conf_validate(configs: list[dict]): ) -def final_validation(configs): - if len(configs) != 1: - multi_conf_validate(configs) +def final_validation(config_list): + if len(config_list) != 1: + multi_conf_validate(config_list) global_config = full_config.get() - for config in configs: + for config in config_list: if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages): raise cv.Invalid("At least one page must not be skipped") for display_id in config[df.CONF_DISPLAYS]: @@ -190,6 +192,14 @@ def final_validation(configs): raise cv.Invalid( f"Widget '{w}' does not have any dynamic properties to refresh", ) + # Do per-widget type final validation for update actions + for widget_type, update_configs in df.get_data(df.KEY_UPDATED_WIDGETS).items(): + for conf in update_configs: + for id_conf in conf.get(CONF_ID, ()): + name = id_conf[CONF_ID] + path = global_config.get_path_for_id(name) + widget_conf = global_config.get_config_for_path(path[:-1]) + widget_type.final_validate(name, conf, widget_conf, path[1:]) async def to_code(configs): @@ -246,9 +256,11 @@ async def to_code(configs): True, type=lv_font_t.operator("ptr").operator("const"), ) + # static=False because LV_FONT_CUSTOM_DECLARE creates an extern declaration cg.new_variable( globfont_id, MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(), + static=False, ) add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) else: @@ -276,6 +288,7 @@ async def to_code(configs): config[df.CONF_FULL_REFRESH], config[CONF_DRAW_ROUNDING], config[df.CONF_RESUME_ON_INPUT], + config[df.CONF_UPDATE_WHEN_DISPLAY_IDLE], ) await cg.register_component(lv_component, config) Widget.create(config[CONF_ID], lv_component, LvScrActType(), config) @@ -345,7 +358,8 @@ def display_schema(config): def add_hello_world(config): if df.CONF_WIDGETS not in config and CONF_PAGES not in config: LOGGER.info("No pages or widgets configured, creating default hello_world page") - config[df.CONF_WIDGETS] = any_widget_schema()(get_hello_world()) + hello_world_path = Path(__file__).parent / HELLO_WORLD_FILE + config[df.CONF_WIDGETS] = any_widget_schema()(load_yaml(hello_world_path)) return config @@ -373,6 +387,9 @@ LVGL_SCHEMA = cv.All( df.CONF_DEFAULT_FONT, default="montserrat_14" ): lvalid.lv_font, cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean, + cv.Optional( + df.CONF_UPDATE_WHEN_DISPLAY_IDLE, default=False + ): cv.boolean, cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int, cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage, cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of( diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index f2bcb6cc06..91077a1ff4 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -5,11 +5,11 @@ Constants already defined in esphome.const are not duplicated here and must be i """ import logging -from typing import TYPE_CHECKING, Any +from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS -from esphome.core import ID, Lambda +from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import LambdaExpression, MockObj from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor @@ -20,11 +20,27 @@ from .helpers import requires_component LOGGER = logging.getLogger(__name__) lvgl_ns = cg.esphome_ns.namespace("lvgl") -lv_defines = {} # Dict of #defines to provide as build flags +DOMAIN = "lvgl" +KEY_LV_DEFINES = "lv_defines" +KEY_UPDATED_WIDGETS = "updated_widgets" + + +def get_data(key, default=None): + """ + Get a data structure from the global data store by key + :param key: A key for the data + :param default: The default data - the default is an empty dict + :return: + """ + return CORE.data.setdefault(DOMAIN, {}).setdefault( + key, default if default is not None else {} + ) def add_define(macro, value="1"): - if macro in lv_defines and lv_defines[macro] != value: + lv_defines = get_data(KEY_LV_DEFINES) + value = str(value) + if lv_defines.setdefault(macro, value) != value: LOGGER.error( "Redefinition of %s - was %s now %s", macro, lv_defines[macro], value ) @@ -80,13 +96,9 @@ class LValidator: return None if isinstance(value, Lambda): # Local import to avoid circular import - from .lvcode import CodeContext, LambdaContext + from .lvcode import get_lambda_context_args - if TYPE_CHECKING: - # CodeContext does not have get_automation_parameters - # so we need to assert the type here - assert isinstance(CodeContext.code_context, LambdaContext) - args = args or CodeContext.code_context.get_automation_parameters() + args = args or get_lambda_context_args() return cg.RawExpression( call_lambda( await cg.process_lambda(value, args, return_type=self.rtype) @@ -279,6 +291,8 @@ KEYBOARD_MODES = LvConstant( ) ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE") TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL") +SCROLL_DIRECTIONS = TILE_DIRECTIONS.extend("NONE") +SNAP_DIRECTIONS = LvConstant("LV_SCROLL_SNAP_", "NONE", "START", "END", "CENTER") CHILD_ALIGNMENTS = LvConstant( "LV_ALIGN_", "TOP_LEFT", @@ -511,6 +525,9 @@ CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" CONF_SCALE_LINES = "scale_lines" CONF_SCROLLBAR_MODE = "scrollbar_mode" +CONF_SCROLL_DIR = "scroll_dir" +CONF_SCROLL_SNAP_X = "scroll_snap_x" +CONF_SCROLL_SNAP_Y = "scroll_snap_y" CONF_SELECTED_INDEX = "selected_index" CONF_SELECTED_TEXT = "selected_text" CONF_SHOW_SNOW = "show_snow" @@ -537,6 +554,7 @@ CONF_TOUCHSCREENS = "touchscreens" CONF_TRANSPARENCY_KEY = "transparency_key" CONF_THEME = "theme" CONF_UPDATE_ON_RELEASE = "update_on_release" +CONF_UPDATE_WHEN_DISPLAY_IDLE = "update_when_display_idle" CONF_VISIBLE_ROW_COUNT = "visible_row_count" CONF_WIDGET = "widget" CONF_WIDGETS = "widgets" diff --git a/esphome/components/lvgl/hello_world.py b/esphome/components/lvgl/hello_world.py deleted file mode 100644 index f85da9d8e4..0000000000 --- a/esphome/components/lvgl/hello_world.py +++ /dev/null @@ -1,127 +0,0 @@ -from io import StringIO - -from esphome.yaml_util import parse_yaml - -CONFIG = """ -- obj: - id: hello_world_card_ - pad_all: 12 - bg_color: white - height: 100% - width: 100% - scrollable: false - widgets: - - obj: - align: top_mid - outline_width: 0 - border_width: 0 - pad_all: 4 - scrollable: false - height: size_content - width: 100% - layout: - type: flex - flex_flow: row - flex_align_cross: center - flex_align_track: start - flex_align_main: space_between - widgets: - - button: - checkable: true - radius: 4 - text_font: montserrat_20 - on_click: - lvgl.label.update: - id: hello_world_label_ - text: "Clicked!" - widgets: - - label: - text: "Button" - - label: - id: hello_world_title_ - text: ESPHome - text_font: montserrat_20 - width: 100% - text_align: center - on_boot: - lvgl.widget.refresh: hello_world_title_ - hidden: !lambda |- - return lv_obj_get_width(lv_scr_act()) < 400; - - checkbox: - text: Checkbox - id: hello_world_checkbox_ - on_boot: - lvgl.widget.refresh: hello_world_checkbox_ - hidden: !lambda |- - return lv_obj_get_width(lv_scr_act()) < 240; - on_click: - lvgl.label.update: - id: hello_world_label_ - text: "Checked!" - - obj: - id: hello_world_container_ - align: center - y: 14 - pad_all: 0 - outline_width: 0 - border_width: 0 - width: 100% - height: size_content - scrollable: false - on_click: - lvgl.spinner.update: - id: hello_world_spinner_ - arc_color: springgreen - layout: - type: flex - flex_flow: row_wrap - flex_align_cross: center - flex_align_track: center - flex_align_main: space_evenly - widgets: - - spinner: - id: hello_world_spinner_ - indicator: - arc_color: tomato - height: 100 - width: 100 - spin_time: 2s - arc_length: 60deg - widgets: - - label: - id: hello_world_label_ - text: "Hello World!" - align: center - - obj: - id: hello_world_qrcode_ - outline_width: 0 - border_width: 0 - hidden: !lambda |- - return lv_obj_get_width(lv_scr_act()) < 300 && lv_obj_get_height(lv_scr_act()) < 400; - widgets: - - label: - text_font: montserrat_14 - text: esphome.io - align: top_mid - - qrcode: - text: "https://esphome.io" - size: 80 - align: bottom_mid - on_boot: - lvgl.widget.refresh: hello_world_qrcode_ - - - slider: - width: 80% - align: bottom_mid - on_value: - lvgl.label.update: - id: hello_world_label_ - text: - format: "%.0f%%" - args: [x] -""" - - -def get_hello_world(): - with StringIO(CONFIG) as fp: - return parse_yaml("hello_world", fp) diff --git a/esphome/components/lvgl/hello_world.yaml b/esphome/components/lvgl/hello_world.yaml new file mode 100644 index 0000000000..359e73cd52 --- /dev/null +++ b/esphome/components/lvgl/hello_world.yaml @@ -0,0 +1,118 @@ +# This file defines a placeholder LVGL "Hello World" that is shown when no +# widgets are configured. +- obj: + id: hello_world_card_ + pad_all: 12 + bg_color: white + height: 100% + width: 100% + scrollable: false + widgets: + - obj: + align: top_mid + outline_width: 0 + border_width: 0 + pad_all: 4 + scrollable: false + height: size_content + width: 100% + layout: + type: flex + flex_flow: row + flex_align_cross: center + flex_align_track: start + flex_align_main: space_between + widgets: + - button: + checkable: true + radius: 4 + text_font: montserrat_20 + on_click: + lvgl.label.update: + id: hello_world_label_ + text: "Clicked!" + widgets: + - label: + text: "Button" + - label: + id: hello_world_title_ + text: ESPHome + text_font: montserrat_20 + width: 100% + text_align: center + on_boot: + lvgl.widget.refresh: hello_world_title_ + hidden: !lambda |- + return lv_obj_get_width(lv_scr_act()) < 400; + - checkbox: + text: Checkbox + id: hello_world_checkbox_ + on_boot: + lvgl.widget.refresh: hello_world_checkbox_ + hidden: !lambda |- + return lv_obj_get_width(lv_scr_act()) < 240; + on_click: + lvgl.label.update: + id: hello_world_label_ + text: "Checked!" + - obj: + id: hello_world_container_ + align: center + y: 14 + pad_all: 0 + outline_width: 0 + border_width: 0 + width: 100% + height: size_content + scrollable: false + on_click: + lvgl.spinner.update: + id: hello_world_spinner_ + arc_color: springgreen + layout: + type: flex + flex_flow: row_wrap + flex_align_cross: center + flex_align_track: center + flex_align_main: space_evenly + widgets: + - spinner: + id: hello_world_spinner_ + indicator: + arc_color: tomato + height: 100 + width: 100 + spin_time: 2s + arc_length: 60deg + widgets: + - label: + id: hello_world_label_ + text: "Hello World!" + align: center + - obj: + id: hello_world_qrcode_ + outline_width: 0 + border_width: 0 + hidden: !lambda |- + return lv_obj_get_width(lv_scr_act()) < 300 && lv_obj_get_height(lv_scr_act()) < 400; + widgets: + - label: + text_font: montserrat_14 + text: esphome.io + align: top_mid + - qrcode: + text: "https://esphome.io" + size: 80 + align: bottom_mid + on_boot: + lvgl.widget.refresh: hello_world_qrcode_ + + - slider: + width: 80% + align: bottom_mid + on_value: + lvgl.label.update: + id: hello_world_label_ + text: + format: "%.0f%%" + args: [x] diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py index a6aa816fda..b27a0b54a2 100644 --- a/esphome/components/lvgl/layout.py +++ b/esphome/components/lvgl/layout.py @@ -36,6 +36,8 @@ from .defines import ( ) from .lv_validation import padding, size +CONF_MULTIPLE_WIDGETS_PER_CELL = "multiple_widgets_per_cell" + cell_alignments = LV_CELL_ALIGNMENTS.one_of grid_alignments = LV_GRID_ALIGNMENTS.one_of flex_alignments = LV_FLEX_ALIGNMENTS.one_of @@ -170,10 +172,14 @@ class DirectionalLayout(FlexLayout): def validate(self, config): assert config[CONF_LAYOUT].lower() == self.direction - config[CONF_LAYOUT] = { + layout = { **FLEX_HV_STYLE, CONF_FLEX_FLOW: "LV_FLEX_FLOW_" + self.flow.upper(), } + if pad_all := config.get("pad_all"): + layout[CONF_PAD_ROW] = pad_all + layout[CONF_PAD_COLUMN] = pad_all + config[CONF_LAYOUT] = layout return config @@ -220,6 +226,7 @@ class GridLayout(Layout): cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments, cv.Optional(CONF_PAD_ROW): padding, cv.Optional(CONF_PAD_COLUMN): padding, + cv.Optional(CONF_MULTIPLE_WIDGETS_PER_CELL, default=False): cv.boolean, }, { cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, @@ -263,6 +270,7 @@ class GridLayout(Layout): # should be guaranteed to be a dict at this point assert isinstance(layout, dict) assert layout.get(CONF_TYPE).lower() == TYPE_GRID + allow_multiple = layout.get(CONF_MULTIPLE_WIDGETS_PER_CELL, False) rows = len(layout[CONF_GRID_ROWS]) columns = len(layout[CONF_GRID_COLUMNS]) used_cells = [[None] * columns for _ in range(rows)] @@ -299,7 +307,10 @@ class GridLayout(Layout): f"exceeds grid size {rows}x{columns}", [CONF_WIDGETS, index], ) - if used_cells[row + i][column + j] is not None: + if ( + not allow_multiple + and used_cells[row + i][column + j] is not None + ): raise cv.Invalid( f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", [CONF_WIDGETS, index], diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 23c322c31f..947e44b131 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,5 +1,5 @@ import re -from typing import TYPE_CHECKING, Any +from typing import Any import esphome.codegen as cg from esphome.components import image @@ -40,7 +40,7 @@ from .helpers import ( lv_fonts_used, requires_component, ) -from .types import lv_font_t, lv_gradient_t +from .types import lv_gradient_t opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") @@ -404,14 +404,9 @@ class TextValidator(LValidator): self, value: Any, args: list[tuple[SafeExpType, str]] | None = None ) -> Expression: # Local import to avoid circular import at module level + from .lvcode import get_lambda_context_args - from .lvcode import CodeContext, LambdaContext - - if TYPE_CHECKING: - # CodeContext does not have get_automation_parameters - # so we need to assert the type here - assert isinstance(CodeContext.code_context, LambdaContext) - args = args or CodeContext.code_context.get_automation_parameters() + args = args or get_lambda_context_args() if isinstance(value, dict): if format_str := value.get(CONF_FORMAT): @@ -498,7 +493,9 @@ class LvFont(LValidator): esphome_fonts_used.add(fontval) return requires_component("font")(fontval) - super().__init__(validator, lv_font_t) + # Use font::Font* as return type for lambdas returning ESPHome fonts + # The inline overloads in lvgl_esphome.h handle conversion to lv_font_t* + super().__init__(validator, Font.operator("ptr")) async def process(self, value, args=()): if is_lv_font(value): diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index c11597131f..b79d1e88dd 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -1,4 +1,5 @@ import abc +from typing import TYPE_CHECKING from esphome import codegen as cg from esphome.config import Config @@ -200,6 +201,21 @@ class LvContext(LambdaContext): return self.add(*args) +def get_lambda_context_args() -> list[tuple[SafeExpType, str]]: + """Get automation parameters from the current lambda context if available. + + When called from outside LVGL's context (e.g., from interval), + CodeContext.code_context will be None, so return empty args. + """ + if CodeContext.code_context is None: + return [] + if TYPE_CHECKING: + # CodeContext base class doesn't define get_automation_parameters(), + # but LambdaContext and LvContext (the concrete implementations) do. + assert isinstance(CodeContext.code_context, LambdaContext) + return CodeContext.code_context.get_automation_parameters() + + class LocalVariable(MockObj): """ Create a local variable and enclose the code using it within a block. @@ -337,7 +353,7 @@ def lv_Pvariable(type, name) -> MockObj: """ if isinstance(name, str): name = ID(name, True, type) - decl = VariableDeclarationExpression(type, "*", name) + decl = VariableDeclarationExpression(type, "*", name, static=True) CORE.add_global(decl) var = MockObj(name, "->") CORE.register_variable(name, var) @@ -353,7 +369,7 @@ def lv_variable(type, name) -> MockObj: """ if isinstance(name, str): name = ID(name, True, type) - decl = VariableDeclarationExpression(type, "", name) + decl = VariableDeclarationExpression(type, "", name, static=True) CORE.add_global(decl) var = MockObj(name, ".") CORE.register_variable(name, var) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 05005b0217..50dba94a2b 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -106,6 +106,7 @@ void LvglComponent::dump_config() { this->disp_drv_.hor_res, this->disp_drv_.ver_res, 100 / this->buffer_frac_, this->rotation, (int) this->draw_rounding); } + void LvglComponent::set_paused(bool paused, bool show_snow) { this->paused_ = paused; this->show_snow_ = show_snow; @@ -124,32 +125,38 @@ void LvglComponent::esphome_lvgl_init() { lv_update_event = static_cast(lv_event_register_id()); lv_api_event = static_cast(lv_event_register_id()); } + void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { lv_obj_add_event_cb(obj, callback, event, nullptr); } + void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2) { add_event_cb(obj, callback, event1); add_event_cb(obj, callback, event2); } + void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, lv_event_code_t event3) { add_event_cb(obj, callback, event1); add_event_cb(obj, callback, event2); add_event_cb(obj, callback, event3); } + void LvglComponent::add_page(LvPageType *page) { this->pages_.push_back(page); page->set_parent(this); lv_disp_set_default(this->disp_); page->setup(this->pages_.size() - 1); } + void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { if (index >= this->pages_.size()) return; this->current_page_ = index; lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); } + void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_)) return; @@ -158,6 +165,7 @@ void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } + void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_)) return; @@ -166,8 +174,10 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } + size_t LvglComponent::get_current_page() const { return this->current_page_; } bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; } + void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); @@ -222,7 +232,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { } void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { - if (!this->paused_) { + if (!this->is_paused()) { auto now = millis(); this->draw_buffer_(area, color_p); ESP_LOGVV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), @@ -230,6 +240,7 @@ void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv } lv_disp_flush_ready(disp_drv); } + IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue timeout) : timeout_(std::move(timeout)) { parent->add_on_idle_callback([this](uint32_t idle_time) { if (!this->is_idle_ && idle_time > this->timeout_.value()) { @@ -377,6 +388,27 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) { } #endif // USE_LVGL_KEYBOARD +void LvglComponent::draw_end_() { + if (this->draw_end_callback_ != nullptr) + this->draw_end_callback_->trigger(); + if (this->update_when_display_idle_) { + for (auto *disp : this->displays_) + disp->update(); + } +} + +bool LvglComponent::is_paused() const { + if (this->paused_) + return true; + if (this->update_when_display_idle_) { + for (auto *disp : this->displays_) { + if (!disp->is_idle()) + return true; + } + } + return false; +} + void LvglComponent::write_random_() { int iterations = 6 - lv_disp_get_inactive_time(this->disp_) / 60000; if (iterations <= 0) @@ -426,12 +458,13 @@ void LvglComponent::write_random_() { * presses a key or clicks on the screen. */ LvglComponent::LvglComponent(std::vector displays, float buffer_frac, bool full_refresh, - int draw_rounding, bool resume_on_input) + int draw_rounding, bool resume_on_input, bool update_when_display_idle) : draw_rounding(draw_rounding), displays_(std::move(displays)), buffer_frac_(buffer_frac), full_refresh_(full_refresh), - resume_on_input_(resume_on_input) { + resume_on_input_(resume_on_input), + update_when_display_idle_(update_when_display_idle) { lv_disp_draw_buf_init(&this->draw_buf_, nullptr, nullptr, 0); lv_disp_drv_init(&this->disp_drv_); this->disp_drv_.draw_buf = &this->draw_buf_; @@ -465,12 +498,12 @@ void LvglComponent::setup() { buf_bytes /= MIN_BUFFER_FRAC; buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT } + this->buffer_frac_ = frac; if (buffer == nullptr) { - this->status_set_error("Memory allocation failure"); + this->status_set_error(LOG_STR("Memory allocation failure")); this->mark_failed(); return; } - this->buffer_frac_ = frac; lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels); this->disp_drv_.hor_res = display->get_width(); this->disp_drv_.ver_res = display->get_height(); @@ -479,7 +512,7 @@ void LvglComponent::setup() { if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { this->rotate_buf_ = static_cast(lv_custom_mem_alloc(buf_bytes)); // NOLINT if (this->rotate_buf_ == nullptr) { - this->status_set_error("Memory allocation failure"); + this->status_set_error(LOG_STR("Memory allocation failure")); this->mark_failed(); return; } @@ -487,7 +520,7 @@ void LvglComponent::setup() { if (this->draw_start_callback_ != nullptr) { this->disp_drv_.render_start_cb = render_start_cb; } - if (this->draw_end_callback_ != nullptr) { + if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) { this->disp_drv_.monitor_cb = monitor_cb; } #if LV_USE_LOG @@ -509,14 +542,15 @@ void LvglComponent::setup() { void LvglComponent::update() { // update indicators - if (this->paused_) { + if (this->is_paused()) { return; } this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); } + void LvglComponent::loop() { - if (this->paused_) { - if (this->show_snow_) + if (this->is_paused()) { + if (this->paused_ && this->show_snow_) this->write_random_(); } else { lv_timer_handler_run_in_period(5); diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index bd6f1fdb61..9c82f3646b 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -151,7 +151,7 @@ class LvglComponent : public PollingComponent { public: LvglComponent(std::vector displays, float buffer_frac, bool full_refresh, int draw_rounding, - bool resume_on_input); + bool resume_on_input, bool update_when_display_idle); static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); float get_setup_priority() const override { return setup_priority::PROCESSOR; } @@ -171,7 +171,9 @@ class LvglComponent : public PollingComponent { // @param paused If true, pause the display. If false, resume the display. // @param show_snow If true, show the snow effect when paused. void set_paused(bool paused, bool show_snow); - bool is_paused() const { return this->paused_; } + + // Returns true if the display is explicitly paused, or a blocking display update is in progress. + bool is_paused() const; // If the display is paused and we have resume_on_input_ set to true, resume the display. void maybe_wakeup() { if (this->paused_ && this->resume_on_input_) { @@ -210,10 +212,10 @@ class LvglComponent : public PollingComponent { void set_draw_end_trigger(Trigger<> *trigger) { this->draw_end_callback_ = trigger; } protected: - // these functions are never called unless the callbacks are non-null since the - // LVGL callbacks that call them are not set unless the start/end callbacks are non-null + void draw_end_(); + // Not checking for non-null callback since the + // LVGL callback that calls it is not set in that case void draw_start_() const { this->draw_start_callback_->trigger(); } - void draw_end_() const { this->draw_end_callback_->trigger(); } void write_random_(); void draw_buffer_(const lv_area_t *area, lv_color_t *ptr); @@ -222,6 +224,7 @@ class LvglComponent : public PollingComponent { size_t buffer_frac_{1}; bool full_refresh_{}; bool resume_on_input_{}; + bool update_when_display_idle_{}; lv_disp_draw_buf_t draw_buf_{}; lv_disp_drv_t disp_drv_{}; diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index 7bc44c9e20..d9885bc7fb 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -29,15 +29,18 @@ class LVGLNumber : public number::Number, public Component { this->publish_state(value); } - void on_value() { this->publish_state(this->value_lambda_()); } + void on_value() { this->publish_(this->value_lambda_()); } protected: - void control(float value) override { - this->control_lambda_(value); + void publish_(float value) { this->publish_state(value); if (this->restore_) this->pref_.save(&value); } + void control(float value) override { + this->control_lambda_(value); + this->publish_(value); + } std::function control_lambda_; std::function value_lambda_; lv_event_code_t event_; diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 6b77f66abb..45d933c00e 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,6 +1,9 @@ +from collections.abc import Callable + from esphome import config_validation as cv from esphome.automation import Trigger, validate_automation from esphome.components.time import RealTimeClock +from esphome.config_validation import prepend_path from esphome.const import ( CONF_ARGS, CONF_FORMAT, @@ -19,7 +22,14 @@ from esphome.core import TimePeriod from esphome.core.config import StartupTrigger from . import defines as df, lv_validation as lvalid -from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR +from .defines import ( + CONF_SCROLL_DIR, + CONF_SCROLL_SNAP_X, + CONF_SCROLL_SNAP_Y, + CONF_SCROLLBAR_MODE, + CONF_TIME_FORMAT, + LV_GRAD_DIR, +) from .helpers import CONF_IF_NAN, requires_component, validate_printf from .layout import ( FLEX_OBJ_SCHEMA, @@ -233,9 +243,19 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" ).one_of, + cv.Optional(CONF_SCROLL_DIR): df.SCROLL_DIRECTIONS.one_of, + cv.Optional(CONF_SCROLL_SNAP_X): df.SNAP_DIRECTIONS.one_of, + cv.Optional(CONF_SCROLL_SNAP_Y): df.SNAP_DIRECTIONS.one_of, } ) +OBJ_PROPERTIES = { + CONF_SCROLL_SNAP_X, + CONF_SCROLL_SNAP_Y, + CONF_SCROLL_DIR, + CONF_SCROLLBAR_MODE, +} + # Also allow widget specific properties for use in style definitions FULL_STYLE_SCHEMA = STYLE_SCHEMA.extend( { @@ -293,19 +313,36 @@ def automation_schema(typ: LvType): } -def base_update_schema(widget_type, parts): +def _update_widget(widget_type: WidgetType) -> Callable[[dict], dict]: """ - Create a schema for updating a widgets style properties, states and flags + During validation of update actions, create a map of action types to affected widgets + for use in final validation. + :param widget_type: + :return: + """ + + def validator(value: dict) -> dict: + df.get_data(df.KEY_UPDATED_WIDGETS).setdefault(widget_type, []).append(value) + return value + + return validator + + +def base_update_schema(widget_type: WidgetType | LvType, parts): + """ + Create a schema for updating a widget's style properties, states and flags. :param widget_type: The type of the ID :param parts: The allowable parts to specify :return: """ - return part_schema(parts).extend( + + w_type = widget_type.w_type if isinstance(widget_type, WidgetType) else widget_type + schema = part_schema(parts).extend( { cv.Required(CONF_ID): cv.ensure_list( cv.maybe_simple_value( { - cv.Required(CONF_ID): cv.use_id(widget_type), + cv.Required(CONF_ID): cv.use_id(w_type), }, key=CONF_ID, ) @@ -314,11 +351,9 @@ def base_update_schema(widget_type, parts): } ) - -def create_modify_schema(widget_type): - return base_update_schema(widget_type.w_type, widget_type.parts).extend( - widget_type.modify_schema - ) + if isinstance(widget_type, WidgetType): + schema.add_extra(_update_widget(widget_type)) + return schema def obj_schema(widget_type: WidgetType): @@ -422,7 +457,10 @@ def any_widget_schema(extras=None): def validator(value): if isinstance(value, dict): # Convert to list + is_dict = True value = [{k: v} for k, v in value.items()] + else: + is_dict = False if not isinstance(value, list): raise cv.Invalid("Expected a list of widgets") result = [] @@ -443,7 +481,9 @@ def any_widget_schema(extras=None): ) # Apply custom validation value = widget_type.validate(value or {}) - result.append({key: container_validator(value)}) + path = [key] if is_dict else [index, key] + with prepend_path(path): + result.append({key: container_validator(value)}) return result return validator diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index b99c0ad5a3..9c92ca7e98 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -152,18 +152,18 @@ class WidgetType: # Local import to avoid circular import from .automation import update_to_code - from .schemas import WIDGET_TYPES, create_modify_schema + from .schemas import WIDGET_TYPES, base_update_schema if not is_mock: if self.name in WIDGET_TYPES: raise EsphomeError(f"Duplicate definition of widget type '{self.name}'") WIDGET_TYPES[self.name] = self - # Register the update action automatically + # Register the update action automatically, adding widget-specific properties register_action( f"lvgl.{self.name}.update", ObjUpdateAction, - create_modify_schema(self), + base_update_schema(self, self.parts).extend(self.modify_schema), )(update_to_code) @property @@ -182,7 +182,6 @@ class WidgetType: Generate code for a given widget :param w: The widget :param config: Its configuration - :return: Generated code as a list of text lines """ async def obj_creator(self, parent: MockObjClass, config: dict): @@ -228,6 +227,15 @@ class WidgetType: """ return value + def final_validate(self, widget, update_config, widget_config, path): + """ + Allow final validation for a given widget type update action + :param widget: A widget + :param update_config: The configuration for the update action + :param widget_config: The configuration for the widget itself + :param path: The path to the widget, for error reporting + """ + class NumberType(WidgetType): def get_max(self, config: dict): diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 187b5828c2..b1d157325b 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -21,7 +21,6 @@ from ..defines import ( CONF_MAIN, CONF_PAD_COLUMN, CONF_PAD_ROW, - CONF_SCROLLBAR_MODE, CONF_STYLES, CONF_WIDGETS, OBJ_FLAGS, @@ -45,7 +44,7 @@ from ..lvcode import ( lv_obj, lv_Pvariable, ) -from ..schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES +from ..schemas import ALL_STYLES, OBJ_PROPERTIES, STYLE_REMAP, WIDGET_TYPES from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr EVENT_LAMB = "event_lamb__" @@ -383,7 +382,7 @@ async def set_obj_properties(w: Widget, config): clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") w.clear_flag(clrs) for key, value in lambs.items(): - lamb = await cg.process_lambda(value, [], return_type=cg.bool_) + lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_) flag = f"LV_OBJ_FLAG_{key.upper()}" with LvConditional(call_lambda(lamb)) as cond: w.add_flag(flag) @@ -408,13 +407,14 @@ async def set_obj_properties(w: Widget, config): clears = join_enums(clears, "LV_STATE_") w.clear_state(clears) for key, value in lambs.items(): - lamb = await cg.process_lambda(value, [], return_type=cg.bool_) + lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_) state = f"LV_STATE_{key.upper()}" with LvConditional(call_lambda(lamb)) as cond: w.add_state(state) cond.else_() w.clear_state(state) - await w.set_property(CONF_SCROLLBAR_MODE, config, lv_name="obj") + for property in OBJ_PROPERTIES: + await w.set_property(property, config, lv_name="obj") async def add_widgets(parent: Widget, config: dict): diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py index ef4da0d815..34ac9c51f7 100644 --- a/esphome/components/lvgl/widgets/arc.py +++ b/esphome/components/lvgl/widgets/arc.py @@ -20,7 +20,13 @@ from ..defines import ( CONF_START_ANGLE, literal, ) -from ..lv_validation import get_start_value, lv_angle_degrees, lv_float, lv_int +from ..lv_validation import ( + get_start_value, + lv_angle_degrees, + lv_float, + lv_int, + lv_positive_int, +) from ..lvcode import lv, lv_expr, lv_obj from ..types import LvNumber, NumberType from . import Widget @@ -36,13 +42,20 @@ ARC_SCHEMA = cv.Schema( cv.Optional(CONF_ROTATION, default=0.0): lv_angle_degrees, cv.Optional(CONF_ADJUSTABLE, default=False): bool, cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of, - cv.Optional(CONF_CHANGE_RATE, default=720): cv.uint16_t, + cv.Optional(CONF_CHANGE_RATE, default=720): lv_positive_int, } ) ARC_MODIFY_SCHEMA = cv.Schema( { cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_MIN_VALUE): lv_int, + cv.Optional(CONF_MAX_VALUE): lv_int, + cv.Optional(CONF_START_ANGLE): lv_angle_degrees, + cv.Optional(CONF_END_ANGLE): lv_angle_degrees, + cv.Optional(CONF_ROTATION): lv_angle_degrees, + cv.Optional(CONF_MODE): ARC_MODES.one_of, + cv.Optional(CONF_CHANGE_RATE): lv_positive_int, } ) @@ -58,17 +71,34 @@ class ArcType(NumberType): ) async def to_code(self, w: Widget, config): - if CONF_MIN_VALUE in config: + if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config: max_value = await lv_int.process(config[CONF_MAX_VALUE]) min_value = await lv_int.process(config[CONF_MIN_VALUE]) lv.arc_set_range(w.obj, min_value, max_value) - start = await lv_angle_degrees.process(config[CONF_START_ANGLE]) - end = await lv_angle_degrees.process(config[CONF_END_ANGLE]) - rotation = await lv_angle_degrees.process(config[CONF_ROTATION]) - lv.arc_set_bg_angles(w.obj, start, end) - lv.arc_set_rotation(w.obj, rotation) - lv.arc_set_mode(w.obj, literal(config[CONF_MODE])) - lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE]) + elif CONF_MIN_VALUE in config: + max_value = w.get_property(CONF_MAX_VALUE) + min_value = await lv_int.process(config[CONF_MIN_VALUE]) + lv.arc_set_range(w.obj, min_value, max_value) + elif CONF_MAX_VALUE in config: + max_value = await lv_int.process(config[CONF_MAX_VALUE]) + min_value = w.get_property(CONF_MIN_VALUE) + lv.arc_set_range(w.obj, min_value, max_value) + + await w.set_property( + "bg_start_angle", + await lv_angle_degrees.process(config.get(CONF_START_ANGLE)), + ) + await w.set_property( + "bg_end_angle", await lv_angle_degrees.process(config.get(CONF_END_ANGLE)) + ) + await w.set_property( + CONF_ROTATION, await lv_angle_degrees.process(config.get(CONF_ROTATION)) + ) + await w.set_property(CONF_MODE, config) + await w.set_property( + CONF_CHANGE_RATE, + await lv_positive_int.process(config.get(CONF_CHANGE_RATE)), + ) if CONF_ADJUSTABLE in config: if not config[CONF_ADJUSTABLE]: @@ -78,9 +108,7 @@ class ArcType(NumberType): # For some reason arc does not get automatically added to the default group lv.group_add_obj(lv_expr.group_get_default(), w.obj) - value = await get_start_value(config) - if value is not None: - lv.arc_set_value(w.obj, value) + await w.set_property(CONF_VALUE, await get_start_value(config)) arc_spec = ArcType() diff --git a/esphome/components/lvgl/widgets/button.py b/esphome/components/lvgl/widgets/button.py index b59884ee67..5f2910174f 100644 --- a/esphome/components/lvgl/widgets/button.py +++ b/esphome/components/lvgl/widgets/button.py @@ -1,20 +1,52 @@ -from esphome.const import CONF_BUTTON +from esphome import config_validation as cv +from esphome.const import CONF_BUTTON, CONF_TEXT +from esphome.cpp_generator import MockObj -from ..defines import CONF_MAIN +from ..defines import CONF_MAIN, CONF_WIDGETS +from ..helpers import add_lv_use +from ..lv_validation import lv_text +from ..lvcode import lv, lv_expr +from ..schemas import TEXT_SCHEMA from ..types import LvBoolean, WidgetType +from . import Widget +from .label import label_spec lv_button_t = LvBoolean("lv_btn_t") class ButtonType(WidgetType): def __init__(self): - super().__init__(CONF_BUTTON, lv_button_t, (CONF_MAIN,), lv_name="btn") + super().__init__( + CONF_BUTTON, lv_button_t, (CONF_MAIN,), schema=TEXT_SCHEMA, lv_name="btn" + ) + + def validate(self, value): + if CONF_TEXT in value: + if CONF_WIDGETS in value: + raise cv.Invalid("Cannot use both text and widgets in a button") + add_lv_use("label") + return value def get_uses(self): return ("btn",) - async def to_code(self, w, config): - return [] + def on_create(self, var: MockObj, config: dict): + if CONF_TEXT in config: + lv.label_create(var) + return var + + async def to_code(self, w: Widget, config): + if text := config.get(CONF_TEXT): + label_widget = Widget.create( + None, lv_expr.obj_get_child(w.obj, 0), label_spec + ) + await label_widget.set_property(CONF_TEXT, await lv_text.process(text)) + + def final_validate(self, widget, update_config, widget_config, path): + if CONF_TEXT in update_config and CONF_TEXT not in widget_config: + raise cv.Invalid( + "Button must have 'text:' configured to allow updating text", path + ) button_spec = ButtonType() diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py index bd90edbefc..57cb965737 100644 --- a/esphome/components/lvgl/widgets/line.py +++ b/esphome/components/lvgl/widgets/line.py @@ -6,7 +6,7 @@ from esphome.core import Lambda from ..defines import CONF_MAIN, call_lambda from ..lvcode import lv_add from ..schemas import point_schema -from ..types import LvCompound, LvType +from ..types import LvCompound, LvType, lv_coord_t from . import Widget, WidgetType CONF_LINE = "line" @@ -23,9 +23,7 @@ LINE_SCHEMA = { async def process_coord(coord): if isinstance(coord, Lambda): - coord = call_lambda( - await cg.process_lambda(coord, [], return_type="lv_coord_t") - ) + coord = call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t)) if not coord.endswith("()"): coord = f"static_cast({coord})" return cg.RawExpression(coord) diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index ac23ded723..c6f25e9587 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -1,6 +1,7 @@ from esphome import automation import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE +from esphome.cpp_generator import MockObj from ..automation import action_to_code from ..defines import ( @@ -114,7 +115,9 @@ class SpinboxType(WidgetType): w.obj, digits, digits - config[CONF_DECIMAL_PLACES] ) if (value := config.get(CONF_VALUE)) is not None: - lv.spinbox_set_value(w.obj, await lv_float.process(value)) + lv.spinbox_set_value( + w.obj, MockObj(await lv_float.process(value)) * w.get_scale() + ) def get_scale(self, config): return 10 ** config[CONF_DECIMAL_PLACES] diff --git a/esphome/components/m5stack_8angle/m5stack_8angle.cpp b/esphome/components/m5stack_8angle/m5stack_8angle.cpp index c542b4459e..5a9a5e8c9d 100644 --- a/esphome/components/m5stack_8angle/m5stack_8angle.cpp +++ b/esphome/components/m5stack_8angle/m5stack_8angle.cpp @@ -26,9 +26,11 @@ void M5Stack8AngleComponent::setup() { } void M5Stack8AngleComponent::dump_config() { - ESP_LOGCONFIG(TAG, "M5STACK_8ANGLE:"); + ESP_LOGCONFIG(TAG, + "M5STACK_8ANGLE:\n" + " Firmware version: %d", + this->fw_version_); LOG_I2C_DEVICE(this); - ESP_LOGCONFIG(TAG, " Firmware version: %d ", this->fw_version_); } float M5Stack8AngleComponent::read_knob_pos(uint8_t channel, AnalogBits bits) { diff --git a/esphome/components/mapping/__init__.py b/esphome/components/mapping/__init__.py index 94c7c10a82..a36b414fd5 100644 --- a/esphome/components/mapping/__init__.py +++ b/esphome/components/mapping/__init__.py @@ -133,7 +133,7 @@ async def to_code(config): value_type, ) var = MockObj(varid, ".") - decl = VariableDeclarationExpression(varid.type, "", varid) + decl = VariableDeclarationExpression(varid.type, "", varid, static=True) add_global(decl) CORE.register_variable(varid, var) diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index f605fb1324..e8cf4d5ab1 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -57,14 +57,14 @@ void MAX17043Component::setup() { if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) { ESP_LOGE(TAG, "Device does not appear to be a MAX17043"); - this->status_set_error("unrecognised"); + this->status_set_error(LOG_STR("unrecognised")); this->mark_failed(); return; } // need to write back to config register to reset the sleep bit if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) { - this->status_set_error("sleep reset failed"); + this->status_set_error(LOG_STR("sleep reset failed")); this->mark_failed(); return; } diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp index a377a1a192..13fe5a5323 100644 --- a/esphome/components/max6956/max6956.cpp +++ b/esphome/components/max6956/max6956.cpp @@ -161,10 +161,8 @@ void MAX6956GPIOPin::setup() { pin_mode(flags_); } void MAX6956GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool MAX6956GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MAX6956GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string MAX6956GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via Max6956", pin_); - return buffer; +size_t MAX6956GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via Max6956", this->pin_); } } // namespace max6956 diff --git a/esphome/components/max6956/max6956.h b/esphome/components/max6956/max6956.h index 0a1fd5e4b5..0c609b0b43 100644 --- a/esphome/components/max6956/max6956.h +++ b/esphome/components/max6956/max6956.h @@ -76,7 +76,7 @@ class MAX6956GPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(MAX6956 *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index be86cb2256..87c2668962 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -99,10 +99,8 @@ void MCP23016GPIOPin::setup() { pin_mode(flags_); } void MCP23016GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool MCP23016GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MCP23016GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string MCP23016GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via MCP23016", pin_); - return buffer; +size_t MCP23016GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via MCP23016", this->pin_); } } // namespace mcp23016 diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h index 781c207de0..c2bc885c95 100644 --- a/esphome/components/mcp23016/mcp23016.h +++ b/esphome/components/mcp23016/mcp23016.h @@ -60,7 +60,7 @@ class MCP23016GPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(MCP23016 *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 81324e794f..302f6b8280 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -16,8 +16,8 @@ template bool MCP23XXXGPIOPin::digital_read() { template void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -template std::string MCP23XXXGPIOPin::dump_summary() const { - return str_snprintf("%u via MCP23XXX", 15, pin_); +template size_t MCP23XXXGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via MCP23XXX", this->pin_); } template class MCP23XXXGPIOPin<8>; diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index cf0ef5d41c..fb992466d5 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -36,7 +36,7 @@ template class MCP23XXXGPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(MCP23XXXBase *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/mcp3204/mcp3204.cpp b/esphome/components/mcp3204/mcp3204.cpp index 4bb0cbed76..abefcad0eb 100644 --- a/esphome/components/mcp3204/mcp3204.cpp +++ b/esphome/components/mcp3204/mcp3204.cpp @@ -11,21 +11,28 @@ float MCP3204::get_setup_priority() const { return setup_priority::HARDWARE; } void MCP3204::setup() { this->spi_setup(); } void MCP3204::dump_config() { - ESP_LOGCONFIG(TAG, "MCP3204:"); + ESP_LOGCONFIG(TAG, + "MCP3204:\n" + " Reference Voltage: %.2fV", + this->reference_voltage_); LOG_PIN(" CS Pin:", this->cs_); - ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); } -float MCP3204::read_data(uint8_t pin) { - uint8_t adc_primary_config = 0b00000110 | (pin >> 2); - uint8_t adc_secondary_config = pin << 6; +float MCP3204::read_data(uint8_t pin, bool differential) { + uint8_t command, b0, b1; + + command = (1 << 6) | // start bit + ((differential ? 0 : 1) << 5) | // single or differential bit + ((pin & 0x07) << 2); // pin + this->enable(); - this->transfer_byte(adc_primary_config); - uint8_t adc_primary_byte = this->transfer_byte(adc_secondary_config); - uint8_t adc_secondary_byte = this->transfer_byte(0x00); + this->transfer_byte(command); + b0 = this->transfer_byte(0x00); + b1 = this->transfer_byte(0x00); this->disable(); - uint16_t digital_value = (adc_primary_byte << 8 | adc_secondary_byte) & 0b111111111111; - return float(digital_value) / 4096.000 * this->reference_voltage_; + + uint16_t digital_value = encode_uint16(b0, b1) >> 4; + return float(digital_value) / 4096.000 * this->reference_voltage_; // in V } } // namespace mcp3204 diff --git a/esphome/components/mcp3204/mcp3204.h b/esphome/components/mcp3204/mcp3204.h index 27261aa373..6287263a2a 100644 --- a/esphome/components/mcp3204/mcp3204.h +++ b/esphome/components/mcp3204/mcp3204.h @@ -18,7 +18,7 @@ class MCP3204 : public Component, void setup() override; void dump_config() override; float get_setup_priority() const override; - float read_data(uint8_t pin); + float read_data(uint8_t pin, bool differential); protected: float reference_voltage_; diff --git a/esphome/components/mcp3204/sensor/__init__.py b/esphome/components/mcp3204/sensor/__init__.py index a4b177cbcf..5f9aa9fdb6 100644 --- a/esphome/components/mcp3204/sensor/__init__.py +++ b/esphome/components/mcp3204/sensor/__init__.py @@ -13,6 +13,7 @@ MCP3204Sensor = mcp3204_ns.class_( "MCP3204Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler ) CONF_MCP3204_ID = "mcp3204_id" +CONF_DIFF_MODE = "diff_mode" CONFIG_SCHEMA = ( sensor.sensor_schema(MCP3204Sensor) @@ -20,6 +21,7 @@ CONFIG_SCHEMA = ( { cv.GenerateID(CONF_MCP3204_ID): cv.use_id(MCP3204), cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7), + cv.Optional(CONF_DIFF_MODE, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("60s")) @@ -30,6 +32,7 @@ async def to_code(config): var = cg.new_Pvariable( config[CONF_ID], config[CONF_NUMBER], + config[CONF_DIFF_MODE], ) await cg.register_parented(var, config[CONF_MCP3204_ID]) await cg.register_component(var, config) diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp index ce0fd25462..e673537be1 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp @@ -7,16 +7,17 @@ namespace mcp3204 { static const char *const TAG = "mcp3204.sensor"; -MCP3204Sensor::MCP3204Sensor(uint8_t pin) : pin_(pin) {} - float MCP3204Sensor::get_setup_priority() const { return setup_priority::DATA; } void MCP3204Sensor::dump_config() { LOG_SENSOR("", "MCP3204 Sensor", this); - ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + ESP_LOGCONFIG(TAG, + " Pin: %u\n" + " Differential Mode: %s", + this->pin_, YESNO(this->differential_mode_)); LOG_UPDATE_INTERVAL(this); } -float MCP3204Sensor::sample() { return this->parent_->read_data(this->pin_); } +float MCP3204Sensor::sample() { return this->parent_->read_data(this->pin_, this->differential_mode_); } void MCP3204Sensor::update() { this->publish_state(this->sample()); } } // namespace mcp3204 diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.h b/esphome/components/mcp3204/sensor/mcp3204_sensor.h index 21c45590ab..5665b80b98 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.h +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.h @@ -15,7 +15,7 @@ class MCP3204Sensor : public PollingComponent, public sensor::Sensor, public voltage_sampler::VoltageSampler { public: - MCP3204Sensor(uint8_t pin); + MCP3204Sensor(uint8_t pin, bool differential_mode) : pin_(pin), differential_mode_(differential_mode) {} void update() override; void dump_config() override; @@ -24,6 +24,7 @@ class MCP3204Sensor : public PollingComponent, protected: uint8_t pin_; + bool differential_mode_; }; } // namespace mcp3204 diff --git a/esphome/components/mcp9600/mcp9600.cpp b/esphome/components/mcp9600/mcp9600.cpp index e1a88988c4..ff411bef7a 100644 --- a/esphome/components/mcp9600/mcp9600.cpp +++ b/esphome/components/mcp9600/mcp9600.cpp @@ -63,12 +63,12 @@ void MCP9600Component::setup() { } void MCP9600Component::dump_config() { - ESP_LOGCONFIG(TAG, "MCP9600:"); + ESP_LOGCONFIG(TAG, + "MCP9600:\n" + " Device ID: 0x%x", + this->device_id_); LOG_I2C_DEVICE(this); LOG_UPDATE_INTERVAL(this); - - ESP_LOGCONFIG(TAG, " Device ID: 0x%x", this->device_id_); - LOG_SENSOR(" ", "Hot Junction Temperature", this->hot_junction_sensor_); LOG_SENSOR(" ", "Cold Junction Temperature", this->cold_junction_sensor_); diff --git a/esphome/components/md5/__init__.py b/esphome/components/md5/__init__.py index 1af9ee0b29..1710b00e66 100644 --- a/esphome/components/md5/__init__.py +++ b/esphome/components/md5/__init__.py @@ -1,7 +1,17 @@ import esphome.codegen as cg +from esphome.core import CORE +from esphome.helpers import IS_MACOS CODEOWNERS = ["@esphome/core"] async def to_code(config): cg.add_define("USE_MD5") + + # Add OpenSSL library for host platform + if CORE.is_host: + if IS_MACOS: + # macOS needs special handling for Homebrew OpenSSL + cg.add_build_flag("-I/opt/homebrew/opt/openssl/include") + cg.add_build_flag("-L/opt/homebrew/opt/openssl/lib") + cg.add_build_flag("-lcrypto") diff --git a/esphome/components/md5/md5.cpp b/esphome/components/md5/md5.cpp index 866f00eda4..26554e4d3c 100644 --- a/esphome/components/md5/md5.cpp +++ b/esphome/components/md5/md5.cpp @@ -39,6 +39,44 @@ void MD5Digest::add(const uint8_t *data, size_t len) { br_md5_update(&this->ctx_ void MD5Digest::calculate() { br_md5_out(&this->ctx_, this->digest_); } #endif // USE_RP2040 +#ifdef USE_HOST +MD5Digest::~MD5Digest() { + if (this->ctx_) { + EVP_MD_CTX_free(this->ctx_); + } +} + +void MD5Digest::init() { + if (this->ctx_) { + EVP_MD_CTX_free(this->ctx_); + } + this->ctx_ = EVP_MD_CTX_new(); + EVP_DigestInit_ex(this->ctx_, EVP_md5(), nullptr); + this->calculated_ = false; + memset(this->digest_, 0, 16); +} + +void MD5Digest::add(const uint8_t *data, size_t len) { + if (!this->ctx_) { + this->init(); + } + EVP_DigestUpdate(this->ctx_, data, len); +} + +void MD5Digest::calculate() { + if (!this->ctx_) { + this->init(); + } + if (!this->calculated_) { + unsigned int len = 16; + EVP_DigestFinal_ex(this->ctx_, this->digest_, &len); + this->calculated_ = true; + } +} +#else +MD5Digest::~MD5Digest() = default; +#endif // USE_HOST + } // namespace md5 } // namespace esphome #endif diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h index b0da2c0a3b..6ff651b02e 100644 --- a/esphome/components/md5/md5.h +++ b/esphome/components/md5/md5.h @@ -5,6 +5,10 @@ #include "esphome/core/hash_base.h" +#ifdef USE_HOST +#include +#endif + #ifdef USE_ESP32 #include "esp_rom_md5.h" #define MD5_CTX_TYPE md5_context_t @@ -31,7 +35,7 @@ namespace md5 { class MD5Digest : public HashBase { public: MD5Digest() = default; - ~MD5Digest() override = default; + ~MD5Digest() override; /// Initialize a new MD5 digest computation. void init() override; @@ -47,7 +51,12 @@ class MD5Digest : public HashBase { size_t get_size() const override { return 16; } protected: +#ifdef USE_HOST + EVP_MD_CTX *ctx_{nullptr}; + bool calculated_{false}; +#else MD5_CTX_TYPE ctx_{}; +#endif }; } // namespace md5 diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 4776bef22f..3088d8ad7e 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -157,15 +157,13 @@ async def to_code(config): return if CORE.using_arduino: - if CORE.is_esp32: - cg.add_library("ESPmDNS", None) - elif CORE.is_esp8266: + if CORE.is_esp8266: cg.add_library("ESP8266mDNS", None) elif CORE.is_rp2040: cg.add_library("LEAmDNS", None) - if CORE.using_esp_idf: - add_idf_component(name="espressif/mdns", ref="1.8.2") + if CORE.is_esp32: + add_idf_component(name="espressif/mdns", ref="1.9.1") cg.add_define("USE_MDNS") @@ -184,10 +182,8 @@ async def to_code(config): # Calculate compile-time dynamic TXT value count # Dynamic values are those that cannot be stored in flash at compile time + # Note: MAC address is now stored in a fixed char[13] buffer, not dynamic storage dynamic_txt_count = 0 - if "api" in CORE.config: - # Always: get_mac_address() - dynamic_txt_count += 1 # User-provided templatable TXT values (only lambdas, not static strings) dynamic_txt_count += sum( 1 @@ -196,8 +192,10 @@ async def to_code(config): if cg.is_template(txt_value) ) - # Ensure at least 1 to avoid zero-size array - cg.add_define("MDNS_DYNAMIC_TXT_COUNT", max(1, dynamic_txt_count)) + # Only add define if we actually need dynamic storage + if dynamic_txt_count > 0: + cg.add_define("USE_MDNS_DYNAMIC_TXT") + cg.add_define("MDNS_DYNAMIC_TXT_COUNT", dynamic_txt_count) # Enable storage if verbose logging is enabled (for dump_config) if get_logger_level() in ("VERBOSE", "VERY_VERBOSE"): diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 2c3150ff5d..47db92610a 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -21,8 +21,7 @@ #include "esphome/components/dashboard_import/dashboard_import.h" #endif -namespace esphome { -namespace mdns { +namespace esphome::mdns { static const char *const TAG = "mdns"; @@ -36,7 +35,7 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); // Wrap build-time defines into flash storage MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION); -void MDNSComponent::compile_records_(StaticVector &services) { +void MDNSComponent::compile_records_(StaticVector &services, char *mac_address_buf) { // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES // in mdns/__init__.py. If you add a new service here, update both locations. @@ -87,7 +86,9 @@ void MDNSComponent::compile_records_(StaticVectoradd_dynamic_txt_value(get_mac_address()))}); + + // MAC address: passed from caller (either member buffer or stack buffer depending on USE_MDNS_STORE_SERVICES) + txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(mac_address_buf)}); #ifdef USE_ESP8266 MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266"); @@ -119,7 +120,7 @@ void MDNSComponent::compile_records_(StaticVectorget_noise_ctx()->has_psk(); + bool has_psk = api::global_api_server->get_noise_ctx().has_psk(); const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED; txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)}); #endif @@ -135,8 +136,7 @@ void MDNSComponent::compile_records_(StaticVectordynamic_txt_values_.push_back(value); return this->dynamic_txt_values_[this->dynamic_txt_values_.size() - 1].c_str(); } +#endif - /// Storage for runtime-generated TXT values (MAC address, user lambdas) + protected: + /// Helper to set up services and MAC buffers, then call platform-specific registration + using PlatformRegisterFn = void (*)(MDNSComponent *, StaticVector &); + + void setup_buffers_and_register_(PlatformRegisterFn platform_register) { +#ifdef USE_MDNS_STORE_SERVICES + auto &services = this->services_; +#else + StaticVector services_storage; + auto &services = services_storage; +#endif + +#ifdef USE_API +#ifdef USE_MDNS_STORE_SERVICES + get_mac_address_into_buffer(this->mac_address_); + char *mac_ptr = this->mac_address_; +#else + char mac_address[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(mac_address); + char *mac_ptr = mac_address; +#endif +#else + char *mac_ptr = nullptr; +#endif + + this->compile_records_(services, mac_ptr); + platform_register(this, services); + } + +#ifdef USE_MDNS_DYNAMIC_TXT + /// Storage for runtime-generated TXT values from user lambdas /// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations. /// Static/compile-time values (version, board, etc.) are stored directly in flash and don't use this. StaticVector dynamic_txt_values_; +#endif - protected: +#if defined(USE_API) && defined(USE_MDNS_STORE_SERVICES) + /// Fixed buffer for MAC address (only needed when services are stored) + char mac_address_[MAC_ADDRESS_BUFFER_SIZE]; +#endif #ifdef USE_MDNS_STORE_SERVICES StaticVector services_{}; #endif - void compile_records_(StaticVector &services); + void compile_records_(StaticVector &services, char *mac_address_buf); }; -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index ecdc926cc9..e6b43e59cb 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -7,24 +7,15 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { static const char *const TAG = "mdns"; -void MDNSComponent::setup() { -#ifdef USE_MDNS_STORE_SERVICES - this->compile_records_(this->services_); - const auto &services = this->services_; -#else - StaticVector services; - this->compile_records_(services); -#endif - +static void register_esp32(MDNSComponent *comp, StaticVector &services) { esp_err_t err = mdns_init(); if (err != ESP_OK) { ESP_LOGW(TAG, "Init failed: %s", esp_err_to_name(err)); - this->mark_failed(); + comp->mark_failed(); return; } @@ -51,12 +42,13 @@ void MDNSComponent::setup() { } } +void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); } + void MDNSComponent::on_shutdown() { mdns_free(); delay(40); // Allow the mdns packets announcing service removal to be sent } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif // USE_ESP32 diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 9bbb406070..dcbe5ebd52 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -9,18 +9,9 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { - -void MDNSComponent::setup() { -#ifdef USE_MDNS_STORE_SERVICES - this->compile_records_(this->services_); - const auto &services = this->services_; -#else - StaticVector services; - this->compile_records_(services); -#endif +namespace esphome::mdns { +static void register_esp8266(MDNSComponent *, StaticVector &services) { MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { @@ -45,6 +36,8 @@ void MDNSComponent::setup() { } } +void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp8266); } + void MDNSComponent::loop() { MDNS.update(); } void MDNSComponent::on_shutdown() { @@ -52,7 +45,6 @@ void MDNSComponent::on_shutdown() { delay(10); } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp index f645d8d068..4d902319b8 100644 --- a/esphome/components/mdns/mdns_host.cpp +++ b/esphome/components/mdns/mdns_host.cpp @@ -6,16 +6,23 @@ #include "esphome/core/log.h" #include "mdns_component.h" -namespace esphome { -namespace mdns { +namespace esphome::mdns { void MDNSComponent::setup() { +#ifdef USE_MDNS_STORE_SERVICES +#ifdef USE_API + get_mac_address_into_buffer(this->mac_address_); + char *mac_ptr = this->mac_address_; +#else + char *mac_ptr = nullptr; +#endif + this->compile_records_(this->services_, mac_ptr); +#endif // Host platform doesn't have actual mDNS implementation } void MDNSComponent::on_shutdown() {} -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index fb2088f719..986099fa1f 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -9,18 +9,9 @@ #include -namespace esphome { -namespace mdns { - -void MDNSComponent::setup() { -#ifdef USE_MDNS_STORE_SERVICES - this->compile_records_(this->services_); - const auto &services = this->services_; -#else - StaticVector services; - this->compile_records_(services); -#endif +namespace esphome::mdns { +static void register_libretiny(MDNSComponent *, StaticVector &services) { MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { @@ -44,9 +35,10 @@ void MDNSComponent::setup() { } } +void MDNSComponent::setup() { this->setup_buffers_and_register_(register_libretiny); } + void MDNSComponent::on_shutdown() {} -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index a9f5349f14..e4a9b60cdb 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -9,18 +9,9 @@ #include -namespace esphome { -namespace mdns { - -void MDNSComponent::setup() { -#ifdef USE_MDNS_STORE_SERVICES - this->compile_records_(this->services_); - const auto &services = this->services_; -#else - StaticVector services; - this->compile_records_(services); -#endif +namespace esphome::mdns { +static void register_rp2040(MDNSComponent *, StaticVector &services) { MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { @@ -44,6 +35,8 @@ void MDNSComponent::setup() { } } +void MDNSComponent::setup() { this->setup_buffers_and_register_(register_rp2040); } + void MDNSComponent::loop() { MDNS.update(); } void MDNSComponent::on_shutdown() { @@ -51,7 +44,6 @@ void MDNSComponent::on_shutdown() { delay(40); } -} // namespace mdns -} // namespace esphome +} // namespace esphome::mdns #endif diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index 2f1c99115f..b753e2d088 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -157,7 +157,7 @@ class MediaPlayer : public EntityBase { virtual void control(const MediaPlayerCall &call) = 0; - CallbackManager state_callback_{}; + LazyCallbackManager state_callback_{}; }; } // namespace media_player diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index c3c8120362..00e6e14d85 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -13,6 +13,9 @@ static const uint8_t MHZ19_COMMAND_GET_PPM[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x static const uint8_t MHZ19_COMMAND_ABC_ENABLE[] = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00}; static const uint8_t MHZ19_COMMAND_ABC_DISABLE[] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00}; static const uint8_t MHZ19_COMMAND_CALIBRATE_ZERO[] = {0xFF, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x07, 0xD0}; +static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x13, 0x88}; +static const uint8_t MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM[] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x27, 0x10}; uint8_t mhz19_checksum(const uint8_t *command) { uint8_t sum = 0; @@ -28,6 +31,8 @@ void MHZ19Component::setup() { } else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) { this->abc_disable(); } + + this->range_set(this->detection_range_); } void MHZ19Component::update() { @@ -86,6 +91,26 @@ void MHZ19Component::abc_disable() { this->mhz19_write_command_(MHZ19_COMMAND_ABC_DISABLE, nullptr); } +void MHZ19Component::range_set(MHZ19DetectionRange detection_ppm) { + switch (detection_ppm) { + case MHZ19_DETECTION_RANGE_DEFAULT: + ESP_LOGV(TAG, "Using previously set detection range (no change)"); + break; + case MHZ19_DETECTION_RANGE_0_2000PPM: + ESP_LOGD(TAG, "Setting detection range to 0 to 2000ppm"); + this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM, nullptr); + break; + case MHZ19_DETECTION_RANGE_0_5000PPM: + ESP_LOGD(TAG, "Setting detection range to 0 to 5000ppm"); + this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM, nullptr); + break; + case MHZ19_DETECTION_RANGE_0_10000PPM: + ESP_LOGD(TAG, "Setting detection range to 0 to 10000ppm"); + this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM, nullptr); + break; + } +} + bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *response) { // Empty RX Buffer while (this->available()) @@ -99,7 +124,9 @@ bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *respo return this->read_array(response, MHZ19_RESPONSE_LENGTH); } + float MHZ19Component::get_setup_priority() const { return setup_priority::DATA; } + void MHZ19Component::dump_config() { ESP_LOGCONFIG(TAG, "MH-Z19:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); @@ -113,6 +140,23 @@ void MHZ19Component::dump_config() { } ESP_LOGCONFIG(TAG, " Warmup time: %" PRIu32 " s", this->warmup_seconds_); + + const char *range_str; + switch (this->detection_range_) { + case MHZ19_DETECTION_RANGE_DEFAULT: + range_str = "default"; + break; + case MHZ19_DETECTION_RANGE_0_2000PPM: + range_str = "0 to 2000ppm"; + break; + case MHZ19_DETECTION_RANGE_0_5000PPM: + range_str = "0 to 5000ppm"; + break; + case MHZ19_DETECTION_RANGE_0_10000PPM: + range_str = "0 to 10000ppm"; + break; + } + ESP_LOGCONFIG(TAG, " Detection range: %s", range_str); } } // namespace mhz19 diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index be36886d62..5898bab649 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -8,7 +8,18 @@ namespace esphome { namespace mhz19 { -enum MHZ19ABCLogic { MHZ19_ABC_NONE = 0, MHZ19_ABC_ENABLED, MHZ19_ABC_DISABLED }; +enum MHZ19ABCLogic { + MHZ19_ABC_NONE = 0, + MHZ19_ABC_ENABLED, + MHZ19_ABC_DISABLED, +}; + +enum MHZ19DetectionRange { + MHZ19_DETECTION_RANGE_DEFAULT = 0, + MHZ19_DETECTION_RANGE_0_2000PPM, + MHZ19_DETECTION_RANGE_0_5000PPM, + MHZ19_DETECTION_RANGE_0_10000PPM, +}; class MHZ19Component : public PollingComponent, public uart::UARTDevice { public: @@ -21,11 +32,13 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice { void calibrate_zero(); void abc_enable(); void abc_disable(); + void range_set(MHZ19DetectionRange detection_ppm); void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } void set_abc_enabled(bool abc_enabled) { abc_boot_logic_ = abc_enabled ? MHZ19_ABC_ENABLED : MHZ19_ABC_DISABLED; } void set_warmup_seconds(uint32_t seconds) { warmup_seconds_ = seconds; } + void set_detection_range(MHZ19DetectionRange detection_range) { detection_range_ = detection_range; } protected: bool mhz19_write_command_(const uint8_t *command, uint8_t *response); @@ -33,37 +46,32 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice { sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *co2_sensor_{nullptr}; MHZ19ABCLogic abc_boot_logic_{MHZ19_ABC_NONE}; + uint32_t warmup_seconds_; + + MHZ19DetectionRange detection_range_{MHZ19_DETECTION_RANGE_DEFAULT}; }; -template class MHZ19CalibrateZeroAction : public Action { +template class MHZ19CalibrateZeroAction : public Action, public Parented { public: - MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - - void play(const Ts &...x) override { this->mhz19_->calibrate_zero(); } - - protected: - MHZ19Component *mhz19_; + void play(const Ts &...x) override { this->parent_->calibrate_zero(); } }; -template class MHZ19ABCEnableAction : public Action { +template class MHZ19ABCEnableAction : public Action, public Parented { public: - MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - - void play(const Ts &...x) override { this->mhz19_->abc_enable(); } - - protected: - MHZ19Component *mhz19_; + void play(const Ts &...x) override { this->parent_->abc_enable(); } }; -template class MHZ19ABCDisableAction : public Action { +template class MHZ19ABCDisableAction : public Action, public Parented { public: - MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(const Ts &...x) override { this->parent_->abc_disable(); } +}; - void play(const Ts &...x) override { this->mhz19_->abc_disable(); } +template class MHZ19DetectionRangeSetAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(MHZ19DetectionRange, detection_range) - protected: - MHZ19Component *mhz19_; + void play(const Ts &...x) override { this->parent_->range_set(this->detection_range_.value(x...)); } }; } // namespace mhz19 diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 106636a6ba..1f698be404 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -19,14 +19,33 @@ DEPENDENCIES = ["uart"] CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration" CONF_WARMUP_TIME = "warmup_time" +CONF_DETECTION_RANGE = "detection_range" mhz19_ns = cg.esphome_ns.namespace("mhz19") MHZ19Component = mhz19_ns.class_("MHZ19Component", cg.PollingComponent, uart.UARTDevice) MHZ19CalibrateZeroAction = mhz19_ns.class_( - "MHZ19CalibrateZeroAction", automation.Action + "MHZ19CalibrateZeroAction", automation.Action, cg.Parented.template(MHZ19Component) ) -MHZ19ABCEnableAction = mhz19_ns.class_("MHZ19ABCEnableAction", automation.Action) -MHZ19ABCDisableAction = mhz19_ns.class_("MHZ19ABCDisableAction", automation.Action) +MHZ19ABCEnableAction = mhz19_ns.class_( + "MHZ19ABCEnableAction", automation.Action, cg.Parented.template(MHZ19Component) +) +MHZ19ABCDisableAction = mhz19_ns.class_( + "MHZ19ABCDisableAction", automation.Action, cg.Parented.template(MHZ19Component) +) +MHZ19DetectionRangeSetAction = mhz19_ns.class_( + "MHZ19DetectionRangeSetAction", + automation.Action, + cg.Parented.template(MHZ19Component), +) + +mhz19_detection_range = mhz19_ns.enum("MHZ19DetectionRange") +MHZ19_DETECTION_RANGE_ENUM = { + 2000: mhz19_detection_range.MHZ19_DETECTION_RANGE_0_2000PPM, + 5000: mhz19_detection_range.MHZ19_DETECTION_RANGE_0_5000PPM, + 10000: mhz19_detection_range.MHZ19_DETECTION_RANGE_0_10000PPM, +} + +_validate_ppm = cv.float_with_unit("parts per million", "ppm") CONFIG_SCHEMA = ( cv.Schema( @@ -49,6 +68,9 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_WARMUP_TIME, default="75s" ): cv.positive_time_period_seconds, + cv.Optional(CONF_DETECTION_RANGE): cv.All( + _validate_ppm, cv.enum(MHZ19_DETECTION_RANGE_ENUM) + ), } ) .extend(cv.polling_component_schema("60s")) @@ -78,8 +100,11 @@ async def to_code(config): cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME])) + if CONF_DETECTION_RANGE in config: + cg.add(var.set_detection_range(config[CONF_DETECTION_RANGE])) -CALIBRATION_ACTION_SCHEMA = maybe_simple_id( + +NO_ARGS_ACTION_SCHEMA = maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(MHZ19Component), } @@ -87,14 +112,37 @@ CALIBRATION_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( - "mhz19.calibrate_zero", MHZ19CalibrateZeroAction, CALIBRATION_ACTION_SCHEMA + "mhz19.calibrate_zero", MHZ19CalibrateZeroAction, NO_ARGS_ACTION_SCHEMA ) @automation.register_action( - "mhz19.abc_enable", MHZ19ABCEnableAction, CALIBRATION_ACTION_SCHEMA + "mhz19.abc_enable", MHZ19ABCEnableAction, NO_ARGS_ACTION_SCHEMA ) @automation.register_action( - "mhz19.abc_disable", MHZ19ABCDisableAction, CALIBRATION_ACTION_SCHEMA + "mhz19.abc_disable", MHZ19ABCDisableAction, NO_ARGS_ACTION_SCHEMA ) -async def mhz19_calibration_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) +async def mhz19_no_args_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +RANGE_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(MHZ19Component), + cv.Required(CONF_DETECTION_RANGE): cv.All( + _validate_ppm, cv.enum(MHZ19_DETECTION_RANGE_ENUM) + ), + } +) + + +@automation.register_action( + "mhz19.detection_range_set", MHZ19DetectionRangeSetAction, RANGE_ACTION_SCHEMA +) +async def mhz19_detection_range_set_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + detection_range = config.get(CONF_DETECTION_RANGE) + template_ = await cg.templatable(detection_range, args, mhz19_detection_range) + cg.add(var.set_detection_range(template_)) + return var diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 575fb97799..74696584da 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -7,7 +7,7 @@ from urllib.parse import urljoin from esphome import automation, external_files, git from esphome.automation import register_action, register_condition import esphome.codegen as cg -from esphome.components import esp32, microphone, socket +from esphome.components import esp32, microphone, ota, socket import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -368,7 +368,7 @@ CONFIG_SCHEMA = cv.All( ), } ).extend(cv.COMPONENT_SCHEMA), - cv.only_with_esp_idf, + cv.only_on_esp32, ) @@ -452,7 +452,7 @@ async def to_code(config): cg.add(var.set_microphone_source(mic_source)) cg.add_define("USE_MICRO_WAKE_WORD") - cg.add_define("USE_OTA_STATE_CALLBACK") + ota.request_ota_state_listeners() esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") diff --git a/esphome/components/micro_wake_word/automation.h b/esphome/components/micro_wake_word/automation.h index e1795a7e64..218ce9e4bc 100644 --- a/esphome/components/micro_wake_word/automation.h +++ b/esphome/components/micro_wake_word/automation.h @@ -3,7 +3,7 @@ #include "micro_wake_word.h" #include "streaming_model.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 namespace esphome { namespace micro_wake_word { diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index a0547b158e..d7e80efc84 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -1,6 +1,6 @@ #include "micro_wake_word.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/application.h" #include "esphome/core/hal.h" @@ -119,18 +119,21 @@ void MicroWakeWord::setup() { } }); -#ifdef USE_OTA - ota::get_global_ota_callback()->add_on_state_callback( - [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { - if (state == ota::OTA_STARTED) { - this->suspend_task_(); - } else if (state == ota::OTA_ERROR) { - this->resume_task_(); - } - }); +#ifdef USE_OTA_STATE_LISTENER + ota::get_global_ota_callback()->add_global_state_listener(this); #endif } +#ifdef USE_OTA_STATE_LISTENER +void MicroWakeWord::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + this->suspend_task_(); + } else if (state == ota::OTA_ERROR) { + this->resume_task_(); + } +} +#endif + void MicroWakeWord::inference_task(void *params) { MicroWakeWord *this_mww = (MicroWakeWord *) params; @@ -298,8 +301,7 @@ void MicroWakeWord::loop() { // uses floating point operations. if (!FrontendPopulateState(&this->frontend_config_, &this->frontend_state_, this->microphone_source_->get_audio_stream_info().get_sample_rate())) { - this->status_momentary_error( - "Failed to allocate buffers for spectrogram feature processor, attempting again in 1 second", 1000); + this->status_momentary_error("frontend_alloc", 1000); return; } @@ -308,7 +310,7 @@ void MicroWakeWord::loop() { if (this->inference_task_handle_ == nullptr) { FrontendFreeStateContents(&this->frontend_state_); // Deallocate frontend state - this->status_momentary_error("Task failed to start, attempting again in 1 second", 1000); + this->status_momentary_error("task_start", 1000); } } break; @@ -471,4 +473,4 @@ bool MicroWakeWord::update_model_probabilities_(const int8_t audio_features[PREP } // namespace micro_wake_word } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h index d46c40e48b..b427e4dfcb 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.h +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "preprocessor_settings.h" #include "streaming_model.h" @@ -9,8 +9,13 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/ring_buffer.h" +#ifdef USE_OTA_STATE_LISTENER +#include "esphome/components/ota/ota_backend.h" +#endif + #include #include @@ -26,13 +31,22 @@ enum State { STOPPED, }; -class MicroWakeWord : public Component { +class MicroWakeWord : public Component +#ifdef USE_OTA_STATE_LISTENER + , + public ota::OTAGlobalStateListener +#endif +{ public: void setup() override; void loop() override; float get_setup_priority() const override; void dump_config() override; +#ifdef USE_OTA_STATE_LISTENER + void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; +#endif + void start(); void stop(); @@ -126,4 +140,4 @@ class MicroWakeWord : public Component { } // namespace micro_wake_word } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 diff --git a/esphome/components/micro_wake_word/preprocessor_settings.h b/esphome/components/micro_wake_word/preprocessor_settings.h index 3de21de92e..c9d195b49b 100644 --- a/esphome/components/micro_wake_word/preprocessor_settings.h +++ b/esphome/components/micro_wake_word/preprocessor_settings.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 2b073cce56..47d2c70e13 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -1,6 +1,6 @@ #include "streaming_model.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/helpers.h" #include "esphome/core/log.h" diff --git a/esphome/components/micro_wake_word/streaming_model.h b/esphome/components/micro_wake_word/streaming_model.h index b7b22b9700..0811bfb19b 100644 --- a/esphome/components/micro_wake_word/streaming_model.h +++ b/esphome/components/micro_wake_word/streaming_model.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "preprocessor_settings.h" diff --git a/esphome/components/micronova/__init__.py b/esphome/components/micronova/__init__.py index 31abc11abf..d6ef93cf30 100644 --- a/esphome/components/micronova/__init__.py +++ b/esphome/components/micronova/__init__.py @@ -4,59 +4,73 @@ from esphome.components import uart import esphome.config_validation as cv from esphome.const import CONF_ID -CODEOWNERS = ["@jorre05"] +CODEOWNERS = ["@jorre05", "@edenhaus"] DEPENDENCIES = ["uart"] -CONF_MICRONOVA_ID = "micronova_id" +DOMAIN = "micronova" +CONF_MICRONOVA_ID = f"{DOMAIN}_id" CONF_ENABLE_RX_PIN = "enable_rx_pin" CONF_MEMORY_LOCATION = "memory_location" CONF_MEMORY_ADDRESS = "memory_address" +DEFAULT_POLLING_INTERVAL = "60s" -micronova_ns = cg.esphome_ns.namespace("micronova") +micronova_ns = cg.esphome_ns.namespace(DOMAIN) -MicroNovaFunctions = micronova_ns.enum("MicroNovaFunctions", is_class=True) -MICRONOVA_FUNCTIONS_ENUM = { - "STOVE_FUNCTION_SWITCH": MicroNovaFunctions.STOVE_FUNCTION_SWITCH, - "STOVE_FUNCTION_ROOM_TEMPERATURE": MicroNovaFunctions.STOVE_FUNCTION_ROOM_TEMPERATURE, - "STOVE_FUNCTION_THERMOSTAT_TEMPERATURE": MicroNovaFunctions.STOVE_FUNCTION_THERMOSTAT_TEMPERATURE, - "STOVE_FUNCTION_FUMES_TEMPERATURE": MicroNovaFunctions.STOVE_FUNCTION_FUMES_TEMPERATURE, - "STOVE_FUNCTION_STOVE_POWER": MicroNovaFunctions.STOVE_FUNCTION_STOVE_POWER, - "STOVE_FUNCTION_FAN_SPEED": MicroNovaFunctions.STOVE_FUNCTION_FAN_SPEED, - "STOVE_FUNCTION_STOVE_STATE": MicroNovaFunctions.STOVE_FUNCTION_STOVE_STATE, - "STOVE_FUNCTION_MEMORY_ADDRESS_SENSOR": MicroNovaFunctions.STOVE_FUNCTION_MEMORY_ADDRESS_SENSOR, - "STOVE_FUNCTION_WATER_TEMPERATURE": MicroNovaFunctions.STOVE_FUNCTION_WATER_TEMPERATURE, - "STOVE_FUNCTION_WATER_PRESSURE": MicroNovaFunctions.STOVE_FUNCTION_WATER_PRESSURE, - "STOVE_FUNCTION_POWER_LEVEL": MicroNovaFunctions.STOVE_FUNCTION_POWER_LEVEL, - "STOVE_FUNCTION_CUSTOM": MicroNovaFunctions.STOVE_FUNCTION_CUSTOM, -} +MicroNova = micronova_ns.class_("MicroNova", cg.Component, uart.UARTDevice) +MicroNovaListener = micronova_ns.class_("MicroNovaListener", cg.PollingComponent) -MicroNova = micronova_ns.class_("MicroNova", cg.PollingComponent, uart.UARTDevice) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(MicroNova), + cv.Required(CONF_ENABLE_RX_PIN): pins.gpio_output_pin_schema, + } +).extend(uart.UART_DEVICE_SCHEMA) -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(MicroNova), - cv.Required(CONF_ENABLE_RX_PIN): pins.gpio_output_pin_schema, - } - ) - .extend(uart.UART_DEVICE_SCHEMA) - .extend(cv.polling_component_schema("60s")) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + DOMAIN, + baud_rate=1200, + require_rx=True, + require_tx=True, + data_bits=8, + parity="NONE", + stop_bits=2, ) -def MICRONOVA_LISTENER_SCHEMA(default_memory_location, default_memory_address): - return cv.Schema( +def MICRONOVA_ADDRESS_SCHEMA( + *, + default_memory_location: int | None = None, + default_memory_address: int | None = None, + is_polling_component: bool, +): + location_key = ( + cv.Optional(CONF_MEMORY_LOCATION, default=default_memory_location) + if default_memory_location is not None + else cv.Required(CONF_MEMORY_LOCATION) + ) + address_key = ( + cv.Optional(CONF_MEMORY_ADDRESS, default=default_memory_address) + if default_memory_address is not None + else cv.Required(CONF_MEMORY_ADDRESS) + ) + schema = cv.Schema( { cv.GenerateID(CONF_MICRONOVA_ID): cv.use_id(MicroNova), - cv.Optional( - CONF_MEMORY_LOCATION, default=default_memory_location - ): cv.hex_int_range(), - cv.Optional( - CONF_MEMORY_ADDRESS, default=default_memory_address - ): cv.hex_int_range(), + location_key: cv.hex_int_range(min=0x00, max=0x79), + address_key: cv.hex_int_range(min=0x00, max=0xFF), } ) + if is_polling_component: + schema = schema.extend(cv.polling_component_schema(DEFAULT_POLLING_INTERVAL)) + return schema + + +async def to_code_micronova_listener(mv, var, config): + await cg.register_component(var, config) + cg.add(mv.register_micronova_listener(var)) + cg.add(var.set_memory_location(config[CONF_MEMORY_LOCATION])) + cg.add(var.set_memory_address(config[CONF_MEMORY_ADDRESS])) async def to_code(config): diff --git a/esphome/components/micronova/button/__init__.py b/esphome/components/micronova/button/__init__.py index 813d24efef..6adf8d96fe 100644 --- a/esphome/components/micronova/button/__init__.py +++ b/esphome/components/micronova/button/__init__.py @@ -6,9 +6,8 @@ from .. import ( CONF_MEMORY_ADDRESS, CONF_MEMORY_LOCATION, CONF_MICRONOVA_ID, - MICRONOVA_LISTENER_SCHEMA, + MICRONOVA_ADDRESS_SCHEMA, MicroNova, - MicroNovaFunctions, micronova_ns, ) @@ -24,8 +23,8 @@ CONFIG_SCHEMA = cv.Schema( MicroNovaButton, ) .extend( - MICRONOVA_LISTENER_SCHEMA( - default_memory_location=0xA0, default_memory_address=0x7D + MICRONOVA_ADDRESS_SCHEMA( + is_polling_component=False, ) ) .extend({cv.Required(CONF_MEMORY_DATA): cv.hex_int_range()}), @@ -38,7 +37,6 @@ async def to_code(config): if custom_button_config := config.get(CONF_CUSTOM_BUTTON): bt = await button.new_button(custom_button_config, mv) - cg.add(bt.set_memory_location(custom_button_config.get(CONF_MEMORY_LOCATION))) - cg.add(bt.set_memory_address(custom_button_config.get(CONF_MEMORY_ADDRESS))) + cg.add(bt.set_memory_location(custom_button_config[CONF_MEMORY_LOCATION])) + cg.add(bt.set_memory_address(custom_button_config[CONF_MEMORY_ADDRESS])) cg.add(bt.set_memory_data(custom_button_config[CONF_MEMORY_DATA])) - cg.add(bt.set_function(MicroNovaFunctions.STOVE_FUNCTION_CUSTOM)) diff --git a/esphome/components/micronova/button/micronova_button.cpp b/esphome/components/micronova/button/micronova_button.cpp index c1903fd878..3f49d4b5b3 100644 --- a/esphome/components/micronova/button/micronova_button.cpp +++ b/esphome/components/micronova/button/micronova_button.cpp @@ -1,18 +1,10 @@ #include "micronova_button.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { void MicroNovaButton::press_action() { - switch (this->get_function()) { - case MicroNovaFunctions::STOVE_FUNCTION_CUSTOM: - this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_); - break; - default: - break; - } - this->micronova_->update(); + this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_); + this->micronova_->request_update_listeners(); } -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/button/micronova_button.h b/esphome/components/micronova/button/micronova_button.h index 77649051d6..951ae8bba3 100644 --- a/esphome/components/micronova/button/micronova_button.h +++ b/esphome/components/micronova/button/micronova_button.h @@ -4,13 +4,15 @@ #include "esphome/core/component.h" #include "esphome/components/button/button.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { class MicroNovaButton : public Component, public button::Button, public MicroNovaButtonListener { public: MicroNovaButton(MicroNova *m) : MicroNovaButtonListener(m) {} - void dump_config() override { LOG_BUTTON("", "Micronova button", this); } + void dump_config() override { + LOG_BUTTON("", "Micronova button", this); + this->dump_base_config(); + } void set_memory_data(uint8_t f) { this->memory_data_ = f; } uint8_t get_memory_data() { return this->memory_data_; } @@ -19,5 +21,4 @@ class MicroNovaButton : public Component, public button::Button, public MicroNov void press_action() override; }; -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/micronova.cpp b/esphome/components/micronova/micronova.cpp index b96798ed12..22daef4fe6 100644 --- a/esphome/components/micronova/micronova.cpp +++ b/esphome/components/micronova/micronova.cpp @@ -1,8 +1,22 @@ #include "micronova.h" #include "esphome/core/log.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { + +static const int STOVE_REPLY_DELAY = 60; +static const uint8_t WRITE_BIT = 1 << 7; // 0x80 + +void MicroNovaBaseListener::dump_base_config() { + ESP_LOGCONFIG(TAG, + " Memory Location: %02X\n" + " Memory Address: %02X", + this->memory_location_, this->memory_address_); +} + +void MicroNovaListener::dump_base_config() { + MicroNovaBaseListener::dump_base_config(); + LOG_UPDATE_INTERVAL(this); +} void MicroNova::setup() { if (this->enable_rx_pin_ != nullptr) { @@ -22,16 +36,10 @@ void MicroNova::dump_config() { if (this->enable_rx_pin_ != nullptr) { LOG_PIN(" Enable RX Pin: ", this->enable_rx_pin_); } - - for (auto &mv_sensor : this->micronova_listeners_) { - mv_sensor->dump_config(); - ESP_LOGCONFIG(TAG, " sensor location:%02X, address:%02X", mv_sensor->get_memory_location(), - mv_sensor->get_memory_address()); - } } -void MicroNova::update() { - ESP_LOGD(TAG, "Schedule sensor update"); +void MicroNova::request_update_listeners() { + ESP_LOGD(TAG, "Schedule listener update"); for (auto &mv_listener : this->micronova_listeners_) { mv_listener->set_needs_update(true); } @@ -62,7 +70,7 @@ void MicroNova::loop() { } } -void MicroNova::request_address(uint8_t location, uint8_t address, MicroNovaSensorListener *listener) { +void MicroNova::request_address(uint8_t location, uint8_t address, MicroNovaListener *listener) { uint8_t write_data[2] = {0, 0}; uint8_t trash_rx; @@ -120,7 +128,8 @@ void MicroNova::write_address(uint8_t location, uint8_t address, uint8_t data) { uint16_t checksum = 0; if (this->reply_pending_mutex_.try_lock()) { - write_data[0] = location; + uint8_t write_location = location | WRITE_BIT; + write_data[0] = write_location; write_data[1] = address; write_data[2] = data; @@ -135,7 +144,7 @@ void MicroNova::write_address(uint8_t location, uint8_t address, uint8_t data) { this->enable_rx_pin_->digital_write(false); this->current_transmission_.request_transmission_time = millis(); - this->current_transmission_.memory_location = location; + this->current_transmission_.memory_location = write_location; this->current_transmission_.memory_address = address; this->current_transmission_.reply_pending = true; this->current_transmission_.initiating_listener = nullptr; @@ -144,5 +153,4 @@ void MicroNova::write_address(uint8_t location, uint8_t address, uint8_t data) { } } -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/micronova.h b/esphome/components/micronova/micronova.h index fc68d941cf..a70f355ead 100644 --- a/esphome/components/micronova/micronova.h +++ b/esphome/components/micronova/micronova.h @@ -8,39 +8,9 @@ #include -namespace esphome { -namespace micronova { +namespace esphome::micronova { static const char *const TAG = "micronova"; -static const int STOVE_REPLY_DELAY = 60; - -static const std::string STOVE_STATES[11] = {"Off", - "Start", - "Pellets loading", - "Ignition", - "Working", - "Brazier Cleaning", - "Final Cleaning", - "Standby", - "No pellets alarm", - "No ignition alarm", - "Undefined alarm"}; - -enum class MicroNovaFunctions { - STOVE_FUNCTION_VOID = 0, - STOVE_FUNCTION_SWITCH = 1, - STOVE_FUNCTION_ROOM_TEMPERATURE = 2, - STOVE_FUNCTION_THERMOSTAT_TEMPERATURE = 3, - STOVE_FUNCTION_FUMES_TEMPERATURE = 4, - STOVE_FUNCTION_STOVE_POWER = 5, - STOVE_FUNCTION_FAN_SPEED = 6, - STOVE_FUNCTION_STOVE_STATE = 7, - STOVE_FUNCTION_MEMORY_ADDRESS_SENSOR = 8, - STOVE_FUNCTION_WATER_TEMPERATURE = 9, - STOVE_FUNCTION_WATER_PRESSURE = 10, - STOVE_FUNCTION_POWER_LEVEL = 11, - STOVE_FUNCTION_CUSTOM = 12 -}; class MicroNova; @@ -50,64 +20,41 @@ class MicroNovaBaseListener { public: MicroNovaBaseListener() {} MicroNovaBaseListener(MicroNova *m) { this->micronova_ = m; } - virtual void dump_config(); void set_micronova_object(MicroNova *m) { this->micronova_ = m; } - void set_function(MicroNovaFunctions f) { this->function_ = f; } - MicroNovaFunctions get_function() { return this->function_; } - void set_memory_location(uint8_t l) { this->memory_location_ = l; } uint8_t get_memory_location() { return this->memory_location_; } void set_memory_address(uint8_t a) { this->memory_address_ = a; } uint8_t get_memory_address() { return this->memory_address_; } + void dump_base_config(); + protected: MicroNova *micronova_{nullptr}; - MicroNovaFunctions function_ = MicroNovaFunctions::STOVE_FUNCTION_VOID; uint8_t memory_location_ = 0; uint8_t memory_address_ = 0; }; -class MicroNovaSensorListener : public MicroNovaBaseListener { +class MicroNovaListener : public MicroNovaBaseListener, public PollingComponent { public: - MicroNovaSensorListener() {} - MicroNovaSensorListener(MicroNova *m) : MicroNovaBaseListener(m) {} + MicroNovaListener() {} + MicroNovaListener(MicroNova *m) : MicroNovaBaseListener(m) {} virtual void request_value_from_stove() = 0; virtual void process_value_from_stove(int value_from_stove) = 0; void set_needs_update(bool u) { this->needs_update_ = u; } bool get_needs_update() { return this->needs_update_; } - protected: - bool needs_update_ = false; -}; + void update() override { this->set_needs_update(true); } -class MicroNovaNumberListener : public MicroNovaBaseListener { - public: - MicroNovaNumberListener(MicroNova *m) : MicroNovaBaseListener(m) {} - virtual void request_value_from_stove() = 0; - virtual void process_value_from_stove(int value_from_stove) = 0; - - void set_needs_update(bool u) { this->needs_update_ = u; } - bool get_needs_update() { return this->needs_update_; } + void dump_base_config(); protected: bool needs_update_ = false; }; -class MicroNovaSwitchListener : public MicroNovaBaseListener { - public: - MicroNovaSwitchListener(MicroNova *m) : MicroNovaBaseListener(m) {} - virtual void set_stove_state(bool v) = 0; - virtual bool get_stove_state() = 0; - - protected: - uint8_t memory_data_on_ = 0; - uint8_t memory_data_off_ = 0; -}; - class MicroNovaButtonListener : public MicroNovaBaseListener { public: MicroNovaButtonListener(MicroNova *m) : MicroNovaBaseListener(m) {} @@ -118,31 +65,23 @@ class MicroNovaButtonListener : public MicroNovaBaseListener { ///////////////////////////////////////////////////////////////////// // Main component class -class MicroNova : public PollingComponent, public uart::UARTDevice { +class MicroNova : public Component, public uart::UARTDevice { public: MicroNova() {} void setup() override; void loop() override; - void update() override; void dump_config() override; - void register_micronova_listener(MicroNovaSensorListener *l) { this->micronova_listeners_.push_back(l); } + void register_micronova_listener(MicroNovaListener *l) { this->micronova_listeners_.push_back(l); } + void request_update_listeners(); - void request_address(uint8_t location, uint8_t address, MicroNovaSensorListener *listener); + void request_address(uint8_t location, uint8_t address, MicroNovaListener *listener); void write_address(uint8_t location, uint8_t address, uint8_t data); int read_stove_reply(); void set_enable_rx_pin(GPIOPin *enable_rx_pin) { this->enable_rx_pin_ = enable_rx_pin; } - void set_current_stove_state(uint8_t s) { this->current_stove_state_ = s; } - uint8_t get_current_stove_state() { return this->current_stove_state_; } - - void set_stove(MicroNovaSwitchListener *s) { this->stove_switch_ = s; } - MicroNovaSwitchListener *get_stove_switch() { return this->stove_switch_; } - protected: - uint8_t current_stove_state_ = 0; - GPIOPin *enable_rx_pin_{nullptr}; struct MicroNovaSerialTransmission { @@ -150,15 +89,13 @@ class MicroNova : public PollingComponent, public uart::UARTDevice { uint8_t memory_location; uint8_t memory_address; bool reply_pending; - MicroNovaSensorListener *initiating_listener; + MicroNovaListener *initiating_listener; }; Mutex reply_pending_mutex_; MicroNovaSerialTransmission current_transmission_; - std::vector micronova_listeners_{}; - MicroNovaSwitchListener *stove_switch_{nullptr}; + std::vector micronova_listeners_{}; }; -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/number/__init__.py b/esphome/components/micronova/number/__init__.py index b0eeaf8dd1..ef6cc0f7d7 100644 --- a/esphome/components/micronova/number/__init__.py +++ b/esphome/components/micronova/number/__init__.py @@ -4,22 +4,22 @@ import esphome.config_validation as cv from esphome.const import CONF_STEP, DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS from .. import ( - CONF_MEMORY_ADDRESS, - CONF_MEMORY_LOCATION, CONF_MICRONOVA_ID, - MICRONOVA_LISTENER_SCHEMA, + MICRONOVA_ADDRESS_SCHEMA, MicroNova, - MicroNovaFunctions, + MicroNovaListener, micronova_ns, + to_code_micronova_listener, ) ICON_FLASH = "mdi:flash" CONF_THERMOSTAT_TEMPERATURE = "thermostat_temperature" CONF_POWER_LEVEL = "power_level" -CONF_MEMORY_WRITE_LOCATION = "memory_write_location" -MicroNovaNumber = micronova_ns.class_("MicroNovaNumber", number.Number, cg.Component) +MicroNovaNumber = micronova_ns.class_( + "MicroNovaNumber", number.Number, MicroNovaListener +) CONFIG_SCHEMA = cv.Schema( { @@ -30,29 +30,26 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_TEMPERATURE, ) .extend( - MICRONOVA_LISTENER_SCHEMA( - default_memory_location=0x20, default_memory_address=0x7D + MICRONOVA_ADDRESS_SCHEMA( + default_memory_location=0x20, + default_memory_address=0x7D, + is_polling_component=True, ) ) .extend( { - cv.Optional( - CONF_MEMORY_WRITE_LOCATION, default=0xA0 - ): cv.hex_int_range(), cv.Optional(CONF_STEP, default=1.0): cv.float_range(min=0.1, max=10.0), } ), cv.Optional(CONF_POWER_LEVEL): number.number_schema( MicroNovaNumber, icon=ICON_FLASH, - ) - .extend( - MICRONOVA_LISTENER_SCHEMA( - default_memory_location=0x20, default_memory_address=0x7F + ).extend( + MICRONOVA_ADDRESS_SCHEMA( + default_memory_location=0x20, + default_memory_address=0x7F, + is_polling_component=True, ) - ) - .extend( - {cv.Optional(CONF_MEMORY_WRITE_LOCATION, default=0xA0): cv.hex_int_range()} ), } ) @@ -68,24 +65,9 @@ async def to_code(config): max_value=40, step=thermostat_temperature_config.get(CONF_STEP), ) + await to_code_micronova_listener(mv, numb, thermostat_temperature_config) cg.add(numb.set_micronova_object(mv)) - cg.add(mv.register_micronova_listener(numb)) - cg.add( - numb.set_memory_location( - thermostat_temperature_config[CONF_MEMORY_LOCATION] - ) - ) - cg.add( - numb.set_memory_address(thermostat_temperature_config[CONF_MEMORY_ADDRESS]) - ) - cg.add( - numb.set_memory_write_location( - thermostat_temperature_config.get(CONF_MEMORY_WRITE_LOCATION) - ) - ) - cg.add( - numb.set_function(MicroNovaFunctions.STOVE_FUNCTION_THERMOSTAT_TEMPERATURE) - ) + cg.add(numb.set_use_step_scaling(True)) if power_level_config := config.get(CONF_POWER_LEVEL): numb = await number.new_number( @@ -94,13 +76,5 @@ async def to_code(config): max_value=5, step=1, ) + await to_code_micronova_listener(mv, numb, power_level_config) cg.add(numb.set_micronova_object(mv)) - cg.add(mv.register_micronova_listener(numb)) - cg.add(numb.set_memory_location(power_level_config[CONF_MEMORY_LOCATION])) - cg.add(numb.set_memory_address(power_level_config[CONF_MEMORY_ADDRESS])) - cg.add( - numb.set_memory_write_location( - power_level_config.get(CONF_MEMORY_WRITE_LOCATION) - ) - ) - cg.add(numb.set_function(MicroNovaFunctions.STOVE_FUNCTION_POWER_LEVEL)) diff --git a/esphome/components/micronova/number/micronova_number.cpp b/esphome/components/micronova/number/micronova_number.cpp index 244eb7ee9f..8027947468 100644 --- a/esphome/components/micronova/number/micronova_number.cpp +++ b/esphome/components/micronova/number/micronova_number.cpp @@ -1,45 +1,29 @@ #include "micronova_number.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { void MicroNovaNumber::process_value_from_stove(int value_from_stove) { - float new_sensor_value = 0; - if (value_from_stove == -1) { this->publish_state(NAN); return; } - switch (this->get_function()) { - case MicroNovaFunctions::STOVE_FUNCTION_THERMOSTAT_TEMPERATURE: - new_sensor_value = ((float) value_from_stove) * this->traits.get_step(); - break; - case MicroNovaFunctions::STOVE_FUNCTION_POWER_LEVEL: - new_sensor_value = (float) value_from_stove; - break; - default: - break; + float new_value = static_cast(value_from_stove); + if (this->use_step_scaling_) { + new_value *= this->traits.get_step(); } - this->publish_state(new_sensor_value); + this->publish_state(new_value); } void MicroNovaNumber::control(float value) { - uint8_t new_number = 0; - - switch (this->get_function()) { - case MicroNovaFunctions::STOVE_FUNCTION_THERMOSTAT_TEMPERATURE: - new_number = (uint8_t) (value / this->traits.get_step()); - break; - case MicroNovaFunctions::STOVE_FUNCTION_POWER_LEVEL: - new_number = (uint8_t) value; - break; - default: - break; + uint8_t new_number; + if (this->use_step_scaling_) { + new_number = static_cast(value / this->traits.get_step()); + } else { + new_number = static_cast(value); } - this->micronova_->write_address(this->memory_write_location_, this->memory_address_, new_number); - this->micronova_->update(); + this->micronova_->write_address(this->memory_location_, this->memory_address_, new_number); + this->micronova_->request_update_listeners(); } -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/number/micronova_number.h b/esphome/components/micronova/number/micronova_number.h index 49c6358255..3fc5838a4f 100644 --- a/esphome/components/micronova/number/micronova_number.h +++ b/esphome/components/micronova/number/micronova_number.h @@ -3,26 +3,26 @@ #include "esphome/components/micronova/micronova.h" #include "esphome/components/number/number.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { -class MicroNovaNumber : public number::Number, public MicroNovaSensorListener { +class MicroNovaNumber : public number::Number, public MicroNovaListener { public: MicroNovaNumber() {} - MicroNovaNumber(MicroNova *m) : MicroNovaSensorListener(m) {} - void dump_config() override { LOG_NUMBER("", "Micronova number", this); } + MicroNovaNumber(MicroNova *m) : MicroNovaListener(m) {} + void dump_config() override { + LOG_NUMBER("", "Micronova number", this); + this->dump_base_config(); + } void control(float value) override; void request_value_from_stove() override { this->micronova_->request_address(this->memory_location_, this->memory_address_, this); } void process_value_from_stove(int value_from_stove) override; - void set_memory_write_location(uint8_t l) { this->memory_write_location_ = l; } - uint8_t get_memory_write_location() { return this->memory_write_location_; } + void set_use_step_scaling(bool v) { this->use_step_scaling_ = v; } protected: - uint8_t memory_write_location_ = 0; + bool use_step_scaling_ = false; }; -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/sensor/__init__.py b/esphome/components/micronova/sensor/__init__.py index ceb4a9ef77..e53c49aca5 100644 --- a/esphome/components/micronova/sensor/__init__.py +++ b/esphome/components/micronova/sensor/__init__.py @@ -10,18 +10,19 @@ from esphome.const import ( ) from .. import ( - CONF_MEMORY_ADDRESS, - CONF_MEMORY_LOCATION, CONF_MICRONOVA_ID, - MICRONOVA_LISTENER_SCHEMA, + MICRONOVA_ADDRESS_SCHEMA, MicroNova, - MicroNovaFunctions, + MicroNovaListener, micronova_ns, + to_code_micronova_listener, ) UNIT_BAR = "bar" -MicroNovaSensor = micronova_ns.class_("MicroNovaSensor", sensor.Sensor, cg.Component) +MicroNovaSensor = micronova_ns.class_( + "MicroNovaSensor", sensor.Sensor, MicroNovaListener +) CONF_ROOM_TEMPERATURE = "room_temperature" CONF_FUMES_TEMPERATURE = "fumes_temperature" @@ -42,8 +43,10 @@ CONFIG_SCHEMA = cv.Schema( state_class=STATE_CLASS_MEASUREMENT, accuracy_decimals=1, ).extend( - MICRONOVA_LISTENER_SCHEMA( - default_memory_location=0x00, default_memory_address=0x01 + MICRONOVA_ADDRESS_SCHEMA( + default_memory_location=0x00, + default_memory_address=0x01, + is_polling_component=True, ) ), cv.Optional(CONF_FUMES_TEMPERATURE): sensor.sensor_schema( @@ -53,8 +56,10 @@ CONFIG_SCHEMA = cv.Schema( state_class=STATE_CLASS_MEASUREMENT, accuracy_decimals=1, ).extend( - MICRONOVA_LISTENER_SCHEMA( - default_memory_location=0x00, default_memory_address=0x5A + MICRONOVA_ADDRESS_SCHEMA( + default_memory_location=0x00, + default_memory_address=0x5A, + is_polling_component=True, ) ), cv.Optional(CONF_STOVE_POWER): sensor.sensor_schema( @@ -62,8 +67,10 @@ CONFIG_SCHEMA = cv.Schema( state_class=STATE_CLASS_MEASUREMENT, accuracy_decimals=0, ).extend( - MICRONOVA_LISTENER_SCHEMA( - default_memory_location=0x00, default_memory_address=0x34 + MICRONOVA_ADDRESS_SCHEMA( + default_memory_location=0x00, + default_memory_address=0x34, + is_polling_component=True, ) ), cv.Optional(CONF_FAN_SPEED): sensor.sensor_schema( @@ -72,8 +79,10 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, ) .extend( - MICRONOVA_LISTENER_SCHEMA( - default_memory_location=0x00, default_memory_address=0x37 + MICRONOVA_ADDRESS_SCHEMA( + default_memory_location=0x00, + default_memory_address=0x37, + is_polling_component=True, ) ) .extend( @@ -86,8 +95,10 @@ CONFIG_SCHEMA = cv.Schema( state_class=STATE_CLASS_MEASUREMENT, accuracy_decimals=1, ).extend( - MICRONOVA_LISTENER_SCHEMA( - default_memory_location=0x00, default_memory_address=0x3B + MICRONOVA_ADDRESS_SCHEMA( + default_memory_location=0x00, + default_memory_address=0x3B, + is_polling_component=True, ) ), cv.Optional(CONF_WATER_PRESSURE): sensor.sensor_schema( @@ -97,15 +108,17 @@ CONFIG_SCHEMA = cv.Schema( state_class=STATE_CLASS_MEASUREMENT, accuracy_decimals=1, ).extend( - MICRONOVA_LISTENER_SCHEMA( - default_memory_location=0x00, default_memory_address=0x3C + MICRONOVA_ADDRESS_SCHEMA( + default_memory_location=0x00, + default_memory_address=0x3C, + is_polling_component=True, ) ), cv.Optional(CONF_MEMORY_ADDRESS_SENSOR): sensor.sensor_schema( MicroNovaSensor, ).extend( - MICRONOVA_LISTENER_SCHEMA( - default_memory_location=0x00, default_memory_address=0x00 + MICRONOVA_ADDRESS_SCHEMA( + is_polling_component=True, ) ), } @@ -115,58 +128,21 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): mv = await cg.get_variable(config[CONF_MICRONOVA_ID]) - if room_temperature_config := config.get(CONF_ROOM_TEMPERATURE): - sens = await sensor.new_sensor(room_temperature_config, mv) - cg.add(mv.register_micronova_listener(sens)) - cg.add(sens.set_memory_location(room_temperature_config[CONF_MEMORY_LOCATION])) - cg.add(sens.set_memory_address(room_temperature_config[CONF_MEMORY_ADDRESS])) - cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_ROOM_TEMPERATURE)) - - if fumes_temperature_config := config.get(CONF_FUMES_TEMPERATURE): - sens = await sensor.new_sensor(fumes_temperature_config, mv) - cg.add(mv.register_micronova_listener(sens)) - cg.add(sens.set_memory_location(fumes_temperature_config[CONF_MEMORY_LOCATION])) - cg.add(sens.set_memory_address(fumes_temperature_config[CONF_MEMORY_ADDRESS])) - cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_FUMES_TEMPERATURE)) - - if stove_power_config := config.get(CONF_STOVE_POWER): - sens = await sensor.new_sensor(stove_power_config, mv) - cg.add(mv.register_micronova_listener(sens)) - cg.add(sens.set_memory_location(stove_power_config[CONF_MEMORY_LOCATION])) - cg.add(sens.set_memory_address(stove_power_config[CONF_MEMORY_ADDRESS])) - cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_STOVE_POWER)) + for key, divisor in { + CONF_ROOM_TEMPERATURE: 2, + CONF_FUMES_TEMPERATURE: None, + CONF_STOVE_POWER: None, + CONF_MEMORY_ADDRESS_SENSOR: None, + CONF_WATER_TEMPERATURE: 2, + CONF_WATER_PRESSURE: 10, + }.items(): + if sensor_config := config.get(key): + sens = await sensor.new_sensor(sensor_config, mv) + await to_code_micronova_listener(mv, sens, sensor_config) + if divisor: + cg.add(sens.set_divisor(divisor)) if fan_speed_config := config.get(CONF_FAN_SPEED): sens = await sensor.new_sensor(fan_speed_config, mv) - cg.add(mv.register_micronova_listener(sens)) - cg.add(sens.set_memory_location(fan_speed_config[CONF_MEMORY_LOCATION])) - cg.add(sens.set_memory_address(fan_speed_config[CONF_MEMORY_ADDRESS])) - cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_FAN_SPEED)) + await to_code_micronova_listener(mv, sens, fan_speed_config) cg.add(sens.set_fan_speed_offset(fan_speed_config[CONF_FAN_RPM_OFFSET])) - - if memory_address_sensor_config := config.get(CONF_MEMORY_ADDRESS_SENSOR): - sens = await sensor.new_sensor(memory_address_sensor_config, mv) - cg.add(mv.register_micronova_listener(sens)) - cg.add( - sens.set_memory_location(memory_address_sensor_config[CONF_MEMORY_LOCATION]) - ) - cg.add( - sens.set_memory_address(memory_address_sensor_config[CONF_MEMORY_ADDRESS]) - ) - cg.add( - sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_MEMORY_ADDRESS_SENSOR) - ) - - if water_temperature_config := config.get(CONF_WATER_TEMPERATURE): - sens = await sensor.new_sensor(water_temperature_config, mv) - cg.add(mv.register_micronova_listener(sens)) - cg.add(sens.set_memory_location(water_temperature_config[CONF_MEMORY_LOCATION])) - cg.add(sens.set_memory_address(water_temperature_config[CONF_MEMORY_ADDRESS])) - cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_WATER_TEMPERATURE)) - - if water_pressure_config := config.get(CONF_WATER_PRESSURE): - sens = await sensor.new_sensor(water_pressure_config, mv) - cg.add(mv.register_micronova_listener(sens)) - cg.add(sens.set_memory_location(water_pressure_config[CONF_MEMORY_LOCATION])) - cg.add(sens.set_memory_address(water_pressure_config[CONF_MEMORY_ADDRESS])) - cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_WATER_PRESSURE)) diff --git a/esphome/components/micronova/sensor/micronova_sensor.cpp b/esphome/components/micronova/sensor/micronova_sensor.cpp index 3f0c0feaf8..d845e0ab3c 100644 --- a/esphome/components/micronova/sensor/micronova_sensor.cpp +++ b/esphome/components/micronova/sensor/micronova_sensor.cpp @@ -1,7 +1,6 @@ #include "micronova_sensor.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { void MicroNovaSensor::process_value_from_stove(int value_from_stove) { if (value_from_stove == -1) { @@ -9,27 +8,16 @@ void MicroNovaSensor::process_value_from_stove(int value_from_stove) { return; } - float new_sensor_value = (float) value_from_stove; - switch (this->get_function()) { - case MicroNovaFunctions::STOVE_FUNCTION_ROOM_TEMPERATURE: - new_sensor_value = new_sensor_value / 2; - break; - case MicroNovaFunctions::STOVE_FUNCTION_THERMOSTAT_TEMPERATURE: - break; - case MicroNovaFunctions::STOVE_FUNCTION_FAN_SPEED: - new_sensor_value = new_sensor_value == 0 ? 0 : (new_sensor_value * 10) + this->fan_speed_offset_; - break; - case MicroNovaFunctions::STOVE_FUNCTION_WATER_TEMPERATURE: - new_sensor_value = new_sensor_value / 2; - break; - case MicroNovaFunctions::STOVE_FUNCTION_WATER_PRESSURE: - new_sensor_value = new_sensor_value / 10; - break; - default: - break; + float new_sensor_value = static_cast(value_from_stove); + + // Fan speed has special calculation: value * 10 + offset (when non-zero) + if (this->is_fan_speed_) { + new_sensor_value = value_from_stove == 0 ? 0.0f : (new_sensor_value * 10) + this->fan_speed_offset_; + } else if (this->divisor_ > 1) { + new_sensor_value = new_sensor_value / this->divisor_; } + this->publish_state(new_sensor_value); } -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/sensor/micronova_sensor.h b/esphome/components/micronova/sensor/micronova_sensor.h index 9d5ae96b87..a2f232c7dc 100644 --- a/esphome/components/micronova/sensor/micronova_sensor.h +++ b/esphome/components/micronova/sensor/micronova_sensor.h @@ -3,25 +3,31 @@ #include "esphome/components/micronova/micronova.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { -class MicroNovaSensor : public sensor::Sensor, public MicroNovaSensorListener { +class MicroNovaSensor : public sensor::Sensor, public MicroNovaListener { public: - MicroNovaSensor(MicroNova *m) : MicroNovaSensorListener(m) {} - void dump_config() override { LOG_SENSOR("", "Micronova sensor", this); } + MicroNovaSensor(MicroNova *m) : MicroNovaListener(m) {} + void dump_config() override { + LOG_SENSOR("", "Micronova sensor", this); + this->dump_base_config(); + } void request_value_from_stove() override { this->micronova_->request_address(this->memory_location_, this->memory_address_, this); } void process_value_from_stove(int value_from_stove) override; - void set_fan_speed_offset(uint8_t f) { this->fan_speed_offset_ = f; } - uint8_t get_set_fan_speed_offset() { return this->fan_speed_offset_; } + void set_divisor(uint8_t d) { this->divisor_ = d; } + void set_fan_speed_offset(uint8_t offset) { + this->is_fan_speed_ = true; + this->fan_speed_offset_ = offset; + } protected: - int fan_speed_offset_ = 0; + uint8_t divisor_ = 1; + uint8_t fan_speed_offset_ = 0; + bool is_fan_speed_ = false; }; -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/switch/__init__.py b/esphome/components/micronova/switch/__init__.py index 43e5c9d844..c937a4cac9 100644 --- a/esphome/components/micronova/switch/__init__.py +++ b/esphome/components/micronova/switch/__init__.py @@ -4,20 +4,21 @@ import esphome.config_validation as cv from esphome.const import ICON_POWER from .. import ( - CONF_MEMORY_ADDRESS, - CONF_MEMORY_LOCATION, CONF_MICRONOVA_ID, - MICRONOVA_LISTENER_SCHEMA, + MICRONOVA_ADDRESS_SCHEMA, MicroNova, - MicroNovaFunctions, + MicroNovaListener, micronova_ns, + to_code_micronova_listener, ) CONF_STOVE = "stove" CONF_MEMORY_DATA_ON = "memory_data_on" CONF_MEMORY_DATA_OFF = "memory_data_off" -MicroNovaSwitch = micronova_ns.class_("MicroNovaSwitch", switch.Switch, cg.Component) +MicroNovaSwitch = micronova_ns.class_( + "MicroNovaSwitch", switch.Switch, MicroNovaListener +) CONFIG_SCHEMA = cv.Schema( { @@ -27,8 +28,10 @@ CONFIG_SCHEMA = cv.Schema( icon=ICON_POWER, ) .extend( - MICRONOVA_LISTENER_SCHEMA( - default_memory_location=0x80, default_memory_address=0x21 + MICRONOVA_ADDRESS_SCHEMA( + default_memory_location=0x00, + default_memory_address=0x21, + is_polling_component=True, ) ) .extend( @@ -46,9 +49,6 @@ async def to_code(config): if stove_config := config.get(CONF_STOVE): sw = await switch.new_switch(stove_config, mv) - cg.add(mv.set_stove(sw)) - cg.add(sw.set_memory_location(stove_config[CONF_MEMORY_LOCATION])) - cg.add(sw.set_memory_address(stove_config[CONF_MEMORY_ADDRESS])) + await to_code_micronova_listener(mv, sw, stove_config) cg.add(sw.set_memory_data_on(stove_config[CONF_MEMORY_DATA_ON])) cg.add(sw.set_memory_data_off(stove_config[CONF_MEMORY_DATA_OFF])) - cg.add(sw.set_function(MicroNovaFunctions.STOVE_FUNCTION_SWITCH)) diff --git a/esphome/components/micronova/switch/micronova_switch.cpp b/esphome/components/micronova/switch/micronova_switch.cpp index 28674acd96..9b9ad61018 100644 --- a/esphome/components/micronova/switch/micronova_switch.cpp +++ b/esphome/components/micronova/switch/micronova_switch.cpp @@ -1,35 +1,38 @@ #include "micronova_switch.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { void MicroNovaSwitch::write_state(bool state) { - switch (this->get_function()) { - case MicroNovaFunctions::STOVE_FUNCTION_SWITCH: - if (state) { - // Only send power-on when current state is Off - if (this->micronova_->get_current_stove_state() == 0) { - this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_); - this->publish_state(true); - } else { - ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", micronova_->get_current_stove_state()); - } - } else { - // don't send power-off when status is Off or Final cleaning - if (this->micronova_->get_current_stove_state() != 0 && micronova_->get_current_stove_state() != 6) { - this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_); - this->publish_state(false); - } else { - ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", micronova_->get_current_stove_state()); - } - } - this->micronova_->update(); - break; - - default: - break; + if (state) { + // Only send power-on when current state is Off + if (this->raw_state_ == 0) { + this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_); + this->publish_state(true); + } else { + ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", this->raw_state_); + } + } else { + // don't send power-off when status is Off or Final cleaning + if (this->raw_state_ != 0 && this->raw_state_ != 6) { + this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_); + this->publish_state(false); + } else { + ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", this->raw_state_); + } } + this->set_needs_update(true); } -} // namespace micronova -} // namespace esphome +void MicroNovaSwitch::process_value_from_stove(int value_from_stove) { + this->raw_state_ = value_from_stove; + if (value_from_stove == -1) { + ESP_LOGE(TAG, "Error reading stove state"); + return; + } + + // set the stove switch to on for any value but 0 + bool state = value_from_stove != 0; + this->publish_state(state); +} + +} // namespace esphome::micronova diff --git a/esphome/components/micronova/switch/micronova_switch.h b/esphome/components/micronova/switch/micronova_switch.h index b0ca33b497..96c2c14e9e 100644 --- a/esphome/components/micronova/switch/micronova_switch.h +++ b/esphome/components/micronova/switch/micronova_switch.h @@ -4,26 +4,30 @@ #include "esphome/core/component.h" #include "esphome/components/switch/switch.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { -class MicroNovaSwitch : public Component, public switch_::Switch, public MicroNovaSwitchListener { +class MicroNovaSwitch : public switch_::Switch, public MicroNovaListener { public: - MicroNovaSwitch(MicroNova *m) : MicroNovaSwitchListener(m) {} - void dump_config() override { LOG_SWITCH("", "Micronova switch", this); } - - void set_stove_state(bool v) override { this->publish_state(v); } - bool get_stove_state() override { return this->state; } + MicroNovaSwitch(MicroNova *m) : MicroNovaListener(m) {} + void dump_config() override { + LOG_SWITCH("", "Micronova switch", this); + this->dump_base_config(); + } + void request_value_from_stove() override { + this->micronova_->request_address(this->memory_location_, this->memory_address_, this); + } + void process_value_from_stove(int value_from_stove) override; void set_memory_data_on(uint8_t f) { this->memory_data_on_ = f; } - uint8_t get_memory_data_on() { return this->memory_data_on_; } void set_memory_data_off(uint8_t f) { this->memory_data_off_ = f; } - uint8_t get_memory_data_off() { return this->memory_data_off_; } protected: void write_state(bool state) override; + + uint8_t memory_data_on_ = 0; + uint8_t memory_data_off_ = 0; + uint8_t raw_state_ = 0; }; -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/text_sensor/__init__.py b/esphome/components/micronova/text_sensor/__init__.py index 474c30e13b..33d0779eae 100644 --- a/esphome/components/micronova/text_sensor/__init__.py +++ b/esphome/components/micronova/text_sensor/__init__.py @@ -3,19 +3,18 @@ from esphome.components import text_sensor import esphome.config_validation as cv from .. import ( - CONF_MEMORY_ADDRESS, - CONF_MEMORY_LOCATION, CONF_MICRONOVA_ID, - MICRONOVA_LISTENER_SCHEMA, + MICRONOVA_ADDRESS_SCHEMA, MicroNova, - MicroNovaFunctions, + MicroNovaListener, micronova_ns, + to_code_micronova_listener, ) CONF_STOVE_STATE = "stove_state" MicroNovaTextSensor = micronova_ns.class_( - "MicroNovaTextSensor", text_sensor.TextSensor, cg.Component + "MicroNovaTextSensor", text_sensor.TextSensor, MicroNovaListener ) CONFIG_SCHEMA = cv.Schema( @@ -24,8 +23,10 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_STOVE_STATE): text_sensor.text_sensor_schema( MicroNovaTextSensor ).extend( - MICRONOVA_LISTENER_SCHEMA( - default_memory_location=0x00, default_memory_address=0x21 + MICRONOVA_ADDRESS_SCHEMA( + default_memory_location=0x00, + default_memory_address=0x21, + is_polling_component=True, ) ), } @@ -37,7 +38,4 @@ async def to_code(config): if stove_state_config := config.get(CONF_STOVE_STATE): sens = await text_sensor.new_text_sensor(stove_state_config, mv) - cg.add(mv.register_micronova_listener(sens)) - cg.add(sens.set_memory_location(stove_state_config[CONF_MEMORY_LOCATION])) - cg.add(sens.set_memory_address(stove_state_config[CONF_MEMORY_ADDRESS])) - cg.add(sens.set_function(MicroNovaFunctions.STOVE_FUNCTION_STOVE_STATE)) + await to_code_micronova_listener(mv, sens, stove_state_config) diff --git a/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp b/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp index 03b192ffd1..2217ed6d6f 100644 --- a/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp +++ b/esphome/components/micronova/text_sensor/micronova_text_sensor.cpp @@ -1,7 +1,6 @@ #include "micronova_text_sensor.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { void MicroNovaTextSensor::process_value_from_stove(int value_from_stove) { if (value_from_stove == -1) { @@ -9,23 +8,7 @@ void MicroNovaTextSensor::process_value_from_stove(int value_from_stove) { return; } - switch (this->get_function()) { - case MicroNovaFunctions::STOVE_FUNCTION_STOVE_STATE: - this->micronova_->set_current_stove_state(value_from_stove); - this->publish_state(STOVE_STATES[value_from_stove]); - // set the stove switch to on for any value but 0 - if (value_from_stove != 0 && this->micronova_->get_stove_switch() != nullptr && - !this->micronova_->get_stove_switch()->get_stove_state()) { - this->micronova_->get_stove_switch()->set_stove_state(true); - } else if (value_from_stove == 0 && this->micronova_->get_stove_switch() != nullptr && - this->micronova_->get_stove_switch()->get_stove_state()) { - this->micronova_->get_stove_switch()->set_stove_state(false); - } - break; - default: - break; - } + this->publish_state(STOVE_STATES[value_from_stove]); } -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/micronova/text_sensor/micronova_text_sensor.h b/esphome/components/micronova/text_sensor/micronova_text_sensor.h index b4e5de9bb3..290f0ca45a 100644 --- a/esphome/components/micronova/text_sensor/micronova_text_sensor.h +++ b/esphome/components/micronova/text_sensor/micronova_text_sensor.h @@ -3,18 +3,31 @@ #include "esphome/components/micronova/micronova.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace micronova { +namespace esphome::micronova { -class MicroNovaTextSensor : public text_sensor::TextSensor, public MicroNovaSensorListener { +static const char *const STOVE_STATES[11] = {"Off", + "Start", + "Pellets loading", + "Ignition", + "Working", + "Brazier Cleaning", + "Final Cleaning", + "Standby", + "No pellets alarm", + "No ignition alarm", + "Undefined alarm"}; + +class MicroNovaTextSensor : public text_sensor::TextSensor, public MicroNovaListener { public: - MicroNovaTextSensor(MicroNova *m) : MicroNovaSensorListener(m) {} - void dump_config() override { LOG_TEXT_SENSOR("", "Micronova text sensor", this); } + MicroNovaTextSensor(MicroNova *m) : MicroNovaListener(m) {} + void dump_config() override { + LOG_TEXT_SENSOR("", "Micronova text sensor", this); + this->dump_base_config(); + } void request_value_from_stove() override { this->micronova_->request_address(this->memory_location_, this->memory_address_, this); } void process_value_from_stove(int value_from_stove) override; }; -} // namespace micronova -} // namespace esphome +} // namespace esphome::micronova diff --git a/esphome/components/microphone/__init__.py b/esphome/components/microphone/__init__.py index 1fc0df88a3..ce31484413 100644 --- a/esphome/components/microphone/__init__.py +++ b/esphome/components/microphone/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_GAIN_FACTOR, CONF_ID, CONF_MICROPHONE, + CONF_ON_DATA, CONF_TRIGGER_ID, ) from esphome.core import CORE @@ -19,8 +20,6 @@ CODEOWNERS = ["@jesserockz", "@kahrendt"] IS_PLATFORM_COMPONENT = True -CONF_ON_DATA = "on_data" - microphone_ns = cg.esphome_ns.namespace("microphone") Microphone = microphone_ns.class_("Microphone") diff --git a/esphome/components/midea_ir/midea_ir.cpp b/esphome/components/midea_ir/midea_ir.cpp index c269b2f7d9..eaee1c731c 100644 --- a/esphome/components/midea_ir/midea_ir.cpp +++ b/esphome/components/midea_ir/midea_ir.cpp @@ -165,7 +165,8 @@ bool MideaIR::on_receive(remote_base::RemoteReceiveData data) { } bool MideaIR::on_midea_(const MideaData &data) { - ESP_LOGV(TAG, "Decoded Midea IR data: %s", data.to_string().c_str()); + char buf[MideaData::TO_STR_BUFFER_SIZE]; + ESP_LOGV(TAG, "Decoded Midea IR data: %s", data.to_str(buf)); if (data.type() == MideaData::MIDEA_TYPE_CONTROL) { const ControlData status = data; if (status.get_mode() != climate::CLIMATE_MODE_FAN_ONLY) diff --git a/esphome/components/mipi_dsi/display.py b/esphome/components/mipi_dsi/display.py index 4fc837be67..c288b33cd2 100644 --- a/esphome/components/mipi_dsi/display.py +++ b/esphome/components/mipi_dsi/display.py @@ -12,7 +12,7 @@ from esphome.components.const import ( CONF_DRAW_ROUNDING, ) from esphome.components.display import CONF_SHOW_TEST_CARD -from esphome.components.esp32 import const, only_on_variant +from esphome.components.esp32 import VARIANT_ESP32P4, only_on_variant from esphome.components.mipi import ( COLOR_ORDERS, CONF_COLOR_DEPTH, @@ -165,8 +165,8 @@ def model_schema(config): ) return cv.All( schema, - only_on_variant(supported=[const.VARIANT_ESP32P4]), - cv.only_with_esp_idf, + cv.only_on_esp32, + only_on_variant(supported=[VARIANT_ESP32P4]), ) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index fbe251de41..18cafab684 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -1,16 +1,26 @@ #ifdef USE_ESP32_VARIANT_ESP32P4 #include #include "mipi_dsi.h" +#include "esphome/core/helpers.h" namespace esphome { namespace mipi_dsi { +// Maximum bytes to log for init commands (truncated if larger) +static constexpr size_t MIPI_DSI_MAX_CMD_LOG_BYTES = 64; + static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) { auto *sem = static_cast(user_ctx); BaseType_t need_yield = pdFALSE; xSemaphoreGiveFromISR(sem, &need_yield); return (need_yield == pdTRUE); } + +void MIPI_DSI::smark_failed(const LogString *message, esp_err_t err) { + ESP_LOGE(TAG, "%s: %s", LOG_STR_ARG(message), esp_err_to_name(err)); + this->mark_failed(message); +} + void MIPI_DSI::setup() { ESP_LOGCONFIG(TAG, "Running Setup"); @@ -31,7 +41,7 @@ void MIPI_DSI::setup() { }; auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_); if (err != ESP_OK) { - this->smark_failed("lcd_new_dsi_bus failed", err); + this->smark_failed(LOG_STR("lcd_new_dsi_bus failed"), err); return; } esp_lcd_dbi_io_config_t dbi_config = { @@ -41,7 +51,7 @@ void MIPI_DSI::setup() { }; err = esp_lcd_new_panel_io_dbi(this->bus_handle_, &dbi_config, &this->io_handle_); if (err != ESP_OK) { - this->smark_failed("new_panel_io_dbi failed", err); + this->smark_failed(LOG_STR("new_panel_io_dbi failed"), err); return; } auto pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; @@ -69,7 +79,7 @@ void MIPI_DSI::setup() { }}; err = esp_lcd_new_panel_dpi(this->bus_handle_, &dpi_config, &this->handle_); if (err != ESP_OK) { - this->smark_failed("esp_lcd_new_panel_dpi failed", err); + this->smark_failed(LOG_STR("esp_lcd_new_panel_dpi failed"), err); return; } if (this->reset_pin_ != nullptr) { @@ -86,14 +96,14 @@ void MIPI_DSI::setup() { auto when = millis() + 120; err = esp_lcd_panel_init(this->handle_); if (err != ESP_OK) { - this->smark_failed("esp_lcd_init failed", err); + this->smark_failed(LOG_STR("esp_lcd_init failed"), err); return; } size_t index = 0; auto &vec = this->init_sequence_; while (index != vec.size()) { if (vec.size() - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } uint8_t cmd = vec[index++]; @@ -104,7 +114,7 @@ void MIPI_DSI::setup() { } else { uint8_t num_args = x & 0x7F; if (vec.size() - index < num_args) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } if (cmd == SLEEP_OUT) { @@ -115,11 +125,14 @@ void MIPI_DSI::setup() { } } const auto *ptr = vec.data() + index; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(MIPI_DSI_MAX_CMD_LOG_BYTES)]; +#endif ESP_LOGVV(TAG, "Command %02X, length %d, byte(s) %s", cmd, num_args, - format_hex_pretty(ptr, num_args, '.', false).c_str()); + format_hex_pretty_to(hex_buf, ptr, num_args, '.')); err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args); if (err != ESP_OK) { - this->smark_failed("lcd_panel_io_tx_param failed", err); + this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err); return; } index += num_args; @@ -134,7 +147,7 @@ void MIPI_DSI::setup() { err = (esp_lcd_dpi_panel_register_event_callbacks(this->handle_, &cbs, this->io_lock_)); if (err != ESP_OK) { - this->smark_failed("Failed to register callbacks", err); + this->smark_failed(LOG_STR("Failed to register callbacks"), err); return; } @@ -216,7 +229,7 @@ bool MIPI_DSI::check_buffer_() { RAMAllocator allocator; this->buffer_ = allocator.allocate(this->height_ * this->width_ * bytes_per_pixel); if (this->buffer_ == nullptr) { - this->mark_failed("Could not allocate buffer for display!"); + this->mark_failed(LOG_STR("Could not allocate buffer for display!")); return false; } return true; @@ -287,6 +300,13 @@ void MIPI_DSI::draw_pixel_at(int x, int y, Color color) { void MIPI_DSI::fill(Color color) { if (!this->check_buffer_()) return; + + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + switch (this->color_depth_) { case display::COLOR_BITNESS_565: { auto *ptr_16 = reinterpret_cast(this->buffer_); diff --git a/esphome/components/mipi_dsi/mipi_dsi.h b/esphome/components/mipi_dsi/mipi_dsi.h index ce8a2a2236..1cffe3b178 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.h +++ b/esphome/components/mipi_dsi/mipi_dsi.h @@ -62,10 +62,7 @@ class MIPI_DSI : public display::Display { void set_lanes(uint8_t lanes) { this->lanes_ = lanes; } void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } - void smark_failed(const char *message, esp_err_t err) { - auto str = str_sprintf("Setup failed: %s: %s", message, esp_err_to_name(err)); - this->mark_failed(str.c_str()); - } + void smark_failed(const LogString *message, esp_err_t err); void update() override; diff --git a/esphome/components/mipi_dsi/models/guition.py b/esphome/components/mipi_dsi/models/guition.py index 5f7db4ebda..cd566633f9 100644 --- a/esphome/components/mipi_dsi/models/guition.py +++ b/esphome/components/mipi_dsi/models/guition.py @@ -35,3 +35,70 @@ DriverChip( (0x10, 0x0C), (0x11, 0x0C), (0x12, 0x0C), (0x13, 0x0C), (0x30, 0x00), ], ) + + +# JC4880P443 Driver Configuration (ST7701) +# Using parameters from esp_lcd_st7701.h and the working full init sequence +# ---------------------------------------------------------------------------------------------------------------------- +# * Resolution: 480x800 +# * PCLK Frequency: 34 MHz +# * DSI Lane Bit Rate: 500 Mbps (using 2-Lane DSI configuration) +# * Horizontal Timing (hsync_pulse_width=12, hsync_back_porch=42, hsync_front_porch=42) +# * Vertical Timing (vsync_pulse_width=2, vsync_back_porch=8, vsync_front_porch=166) +# ---------------------------------------------------------------------------------------------------------------------- +DriverChip( + "JC4880P443", + width=480, + height=800, + hsync_back_porch=42, + hsync_pulse_width=12, + hsync_front_porch=42, + vsync_back_porch=8, + vsync_pulse_width=2, + vsync_front_porch=166, + pclk_frequency="34MHz", + lane_bit_rate="500Mbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + reset_pin=5, + initsequence=[ + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x63, 0x00), + (0xC1, 0x0D, 0x02), + (0xC2, 0x10, 0x08), + (0xCC, 0x10), + (0xB0, 0x80, 0x09, 0x53, 0x0C, 0xD0, 0x07, 0x0C, 0x09, 0x09, 0x28, 0x06, 0xD4, 0x13, 0x69, 0x2B, 0x71), + (0xB1, 0x80, 0x94, 0x5A, 0x10, 0xD3, 0x06, 0x0A, 0x08, 0x08, 0x25, 0x03, 0xD3, 0x12, 0x66, 0x6A, 0x0D), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x5D), + (0xB1, 0x58), + (0xB2, 0x87), + (0xB3, 0x80), + (0xB5, 0x4E), + (0xB7, 0x85), + (0xB8, 0x21), + (0xB9, 0x10, 0x1F), + (0xBB, 0x03), + (0xBC, 0x00), + (0xC1, 0x78), + (0xC2, 0x78), + (0xD0, 0x88), + (0xE0, 0x00, 0x3A, 0x02), + (0xE1, 0x04, 0xA0, 0x00, 0xA0, 0x05, 0xA0, 0x00, 0xA0, 0x00, 0x40, 0x40), + (0xE2, 0x30, 0x00, 0x40, 0x40, 0x32, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00), + (0xE3, 0x00, 0x00, 0x33, 0x33), + (0xE4, 0x44, 0x44), + (0xE5, 0x09, 0x2E, 0xA0, 0xA0, 0x0B, 0x30, 0xA0, 0xA0, 0x05, 0x2A, 0xA0, 0xA0, 0x07, 0x2C, 0xA0, 0xA0), + (0xE6, 0x00, 0x00, 0x33, 0x33), + (0xE7, 0x44, 0x44), + (0xE8, 0x08, 0x2D, 0xA0, 0xA0, 0x0A, 0x2F, 0xA0, 0xA0, 0x04, 0x29, 0xA0, 0xA0, 0x06, 0x2B, 0xA0, 0xA0), + (0xEB, 0x00, 0x00, 0x4E, 0x4E, 0x00, 0x00, 0x00), + (0xEC, 0x08, 0x01), + (0xED, 0xB0, 0x2B, 0x98, 0xA4, 0x56, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0x65, 0x4A, 0x89, 0xB2, 0x0B), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3F, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00), + ] +) +# fmt: on diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index 9d6b1fa729..96e167b2e6 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -11,7 +11,7 @@ from esphome.components.const import ( CONF_DRAW_ROUNDING, ) from esphome.components.display import CONF_SHOW_TEST_CARD -from esphome.components.esp32 import const, only_on_variant +from esphome.components.esp32 import VARIANT_ESP32S3, only_on_variant from esphome.components.mipi import ( COLOR_ORDERS, CONF_DE_PIN, @@ -24,7 +24,7 @@ from esphome.components.mipi import ( CONF_VSYNC_BACK_PORCH, CONF_VSYNC_FRONT_PORCH, CONF_VSYNC_PULSE_WIDTH, - MODE_BGR, + MODE_RGB, PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, DriverChip, @@ -157,7 +157,7 @@ def model_schema(config): model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list( pins.gpio_output_pin_schema ), - model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(COLOR_ORDERS, upper=True), + model.option(CONF_COLOR_ORDER, MODE_RGB): cv.enum(COLOR_ORDERS, upper=True), model.option(CONF_DRAW_ROUNDING, 2): power_of_two, model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of( *pixel_modes, lower=True @@ -224,8 +224,8 @@ def _config_schema(config): schema = model_schema(config) return cv.All( schema, - only_on_variant(supported=[const.VARIANT_ESP32S3]), - cv.only_with_esp_idf, + cv.only_on_esp32, + only_on_variant(supported=[VARIANT_ESP32S3]), )(config) @@ -280,14 +280,9 @@ async def to_code(config): red_pins = config[CONF_DATA_PINS][CONF_RED] green_pins = config[CONF_DATA_PINS][CONF_GREEN] blue_pins = config[CONF_DATA_PINS][CONF_BLUE] - if config[CONF_COLOR_ORDER] == "BGR": - dpins.extend(red_pins) - dpins.extend(green_pins) - dpins.extend(blue_pins) - else: - dpins.extend(blue_pins) - dpins.extend(green_pins) - dpins.extend(red_pins) + dpins.extend(blue_pins) + dpins.extend(green_pins) + dpins.extend(red_pins) # swap bytes to match big-endian format dpins = dpins[8:16] + dpins[0:8] else: diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 00c9c8cbff..ef96da8a1c 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -1,5 +1,6 @@ #ifdef USE_ESP32_VARIANT_ESP32S3 #include "mipi_rgb.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" #include "esp_lcd_panel_rgb.h" @@ -8,6 +9,9 @@ namespace esphome { namespace mipi_rgb { static const uint8_t DELAY_FLAG = 0xFF; + +// Maximum bytes to log for init commands (truncated if larger) +static constexpr size_t MIPI_RGB_MAX_CMD_LOG_BYTES = 64; static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes @@ -73,7 +77,7 @@ void MipiRgbSpi::write_init_sequence_() { auto &vec = this->init_sequence_; while (index != vec.size()) { if (vec.size() - index < 2) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } uint8_t cmd = vec[index++]; @@ -84,15 +88,16 @@ void MipiRgbSpi::write_init_sequence_() { } else { uint8_t num_args = x & 0x7F; if (vec.size() - index < num_args) { - this->mark_failed("Malformed init sequence"); + this->mark_failed(LOG_STR("Malformed init sequence")); return; } if (cmd == SLEEP_OUT) { delay(120); // NOLINT } const auto *ptr = vec.data() + index; + char hex_buf[format_hex_pretty_size(MIPI_RGB_MAX_CMD_LOG_BYTES)]; ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, num_args, - format_hex_pretty(ptr, num_args, '.', false).c_str()); + format_hex_pretty_to(hex_buf, ptr, num_args, '.')); index += num_args; this->write_command_(cmd); while (num_args-- != 0) @@ -164,8 +169,8 @@ void MipiRgb::common_setup_() { if (err == ESP_OK) err = esp_lcd_panel_init(this->handle_); if (err != ESP_OK) { - auto msg = str_sprintf("lcd setup failed: %s", esp_err_to_name(err)); - this->mark_failed(msg.c_str()); + ESP_LOGE(TAG, "lcd setup failed: %s", esp_err_to_name(err)); + this->mark_failed(LOG_STR("lcd setup failed")); } ESP_LOGCONFIG(TAG, "MipiRgb setup complete"); } @@ -249,7 +254,7 @@ bool MipiRgb::check_buffer_() { RAMAllocator allocator; this->buffer_ = allocator.allocate(this->height_ * this->width_); if (this->buffer_ == nullptr) { - this->mark_failed("Could not allocate buffer for display!"); + this->mark_failed(LOG_STR("Could not allocate buffer for display!")); return false; } return true; @@ -300,6 +305,13 @@ void MipiRgb::draw_pixel_at(int x, int y, Color color) { void MipiRgb::fill(Color color) { if (!this->check_buffer_()) return; + + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + auto *ptr_16 = reinterpret_cast(this->buffer_); uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); @@ -350,6 +362,7 @@ void MipiRgb::dump_config() { "\n Width: %u" "\n Height: %u" "\n Rotation: %d degrees" + "\n PCLK Inverted: %s" "\n HSync Pulse Width: %u" "\n HSync Back Porch: %u" "\n HSync Front Porch: %u" @@ -357,30 +370,23 @@ void MipiRgb::dump_config() { "\n VSync Back Porch: %u" "\n VSync Front Porch: %u" "\n Invert Colors: %s" - "\n Pixel Clock: %dMHz" + "\n Pixel Clock: %uMHz" "\n Reset Pin: %s" "\n DE Pin: %s" "\n PCLK Pin: %s" "\n HSYNC Pin: %s" "\n VSYNC Pin: %s", - this->model_, this->width_, this->height_, this->rotation_, this->hsync_pulse_width_, - this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, - this->vsync_front_porch_, YESNO(this->invert_colors_), this->pclk_frequency_ / 1000000, - get_pin_name(this->reset_pin_).c_str(), get_pin_name(this->de_pin_).c_str(), - get_pin_name(this->pclk_pin_).c_str(), get_pin_name(this->hsync_pin_).c_str(), - get_pin_name(this->vsync_pin_).c_str()); + this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_), + this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, + this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_), + (unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(), + get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(), + get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str()); - if (this->madctl_ & MADCTL_BGR) { - this->dump_pins_(8, 13, "Blue", 0); - this->dump_pins_(13, 16, "Green", 0); - this->dump_pins_(0, 3, "Green", 3); - this->dump_pins_(3, 8, "Red", 0); - } else { - this->dump_pins_(8, 13, "Red", 0); - this->dump_pins_(13, 16, "Green", 0); - this->dump_pins_(0, 3, "Green", 3); - this->dump_pins_(3, 8, "Blue", 0); - } + this->dump_pins_(8, 13, "Blue", 0); + this->dump_pins_(13, 16, "Green", 0); + this->dump_pins_(0, 3, "Green", 3); + this->dump_pins_(3, 8, "Red", 0); } } // namespace mipi_rgb diff --git a/esphome/components/mipi_rgb/models/guition.py b/esphome/components/mipi_rgb/models/guition.py index da433e686e..915b8beda0 100644 --- a/esphome/components/mipi_rgb/models/guition.py +++ b/esphome/components/mipi_rgb/models/guition.py @@ -11,6 +11,7 @@ st7701s.extend( vsync_pin=17, pclk_pin=21, pclk_frequency="12MHz", + pclk_inverted=False, pixel_mode="18bit", mirror_x=True, mirror_y=True, diff --git a/esphome/components/mipi_rgb/models/lilygo.py b/esphome/components/mipi_rgb/models/lilygo.py index 109dc42af6..c0e91cd8ae 100644 --- a/esphome/components/mipi_rgb/models/lilygo.py +++ b/esphome/components/mipi_rgb/models/lilygo.py @@ -7,7 +7,6 @@ ST7701S( "T-PANEL-S3", width=480, height=480, - color_order="BGR", invert_colors=False, swap_xy=UNDEFINED, spi_mode="MODE3", @@ -56,7 +55,6 @@ t_rgb = ST7701S( "T-RGB-2.1", width=480, height=480, - color_order="BGR", pixel_mode="18bit", invert_colors=False, swap_xy=UNDEFINED, diff --git a/esphome/components/mipi_rgb/models/st7701s.py b/esphome/components/mipi_rgb/models/st7701s.py index bfd1c9aa3f..3c66380d04 100644 --- a/esphome/components/mipi_rgb/models/st7701s.py +++ b/esphome/components/mipi_rgb/models/st7701s.py @@ -24,6 +24,8 @@ class ST7701S(DriverChip): sdir = 0 if transform.get(CONF_MIRROR_X): sdir |= 0x04 + # XFLIP doesn't do anything in the ST7701S, + # it's set in the madctl byte just so it can be reported at runtime by logconfig madctl |= MADCTL_XFLIP sequence.append((SDIR_CMD, sdir)) return madctl @@ -80,7 +82,6 @@ st7701s.extend( "MAKERFABS-4", width=480, height=480, - color_order="RGB", invert_colors=True, pixel_mode="18bit", cs_pin=1, diff --git a/esphome/components/mipi_rgb/models/waveshare.py b/esphome/components/mipi_rgb/models/waveshare.py index 0fc765fd52..cd1fc341ef 100644 --- a/esphome/components/mipi_rgb/models/waveshare.py +++ b/esphome/components/mipi_rgb/models/waveshare.py @@ -1,13 +1,13 @@ -from esphome.components.mipi import DriverChip +from esphome.components.mipi import DriverChip, delay from esphome.config_validation import UNDEFINED from .st7701s import st7701s +# fmt: off wave_4_3 = DriverChip( "ESP32-S3-TOUCH-LCD-4.3", swap_xy=UNDEFINED, initsequence=(), - color_order="RGB", width=800, height=480, pclk_frequency="16MHz", @@ -55,10 +55,9 @@ wave_4_3.extend( ) st7701s.extend( - "WAVESHARE-4-480x480", + "WAVESHARE-4-480X480", data_rate="2MHz", spi_mode="MODE3", - color_order="BGR", pixel_mode="18bit", width=480, height=480, @@ -76,3 +75,72 @@ st7701s.extend( "blue": [5, 45, 48, 47, 21], }, ) + +st7701s.extend( + "WAVESHARE-3.16-320X820", + width=320, + height=820, + de_pin=40, + hsync_pin=38, + vsync_pin=39, + pclk_pin=41, + cs_pin={ + "number": 0, + "ignore_strapping_warning": True, + }, + pclk_frequency="18MHz", + reset_pin=16, + hsync_back_porch=30, + hsync_front_porch=30, + hsync_pulse_width=6, + vsync_back_porch=20, + vsync_front_porch=20, + vsync_pulse_width=40, + data_pins={ + "red": [17, 46, 3, 8, 18], + "green": [14, 13, 12, 11, 10, 9], + "blue": [21, 5, 45, 48, 47], + }, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0xE5, 0x02), + (0xC1, 0x15, 0x0A), + (0xC2, 0x07, 0x02), + (0xCC, 0x10), + (0xB0, 0x00, 0x08, 0x51, 0x0D, 0xCE, 0x06, 0x00, 0x08, 0x08, 0x24, 0x05, 0xD0, 0x0F, 0x6F, 0x36, 0x1F), + (0xB1, 0x00, 0x10, 0x4F, 0x0C, 0x11, 0x05, 0x00, 0x07, 0x07, 0x18, 0x02, 0xD3, 0x11, 0x6E, 0x34, 0x1F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x4D), + (0xB1, 0x37), + (0xB2, 0x87), + (0xB3, 0x80), + (0xB5, 0x4A), + (0xB7, 0x85), + (0xB8, 0x21), + (0xB9, 0x00, 0x13), + (0xC0, 0x09), + (0xC1, 0x78), + (0xC2, 0x78), + (0xD0, 0x88), + (0xE0, 0x80, 0x00, 0x02), + (0xE1, 0x0F, 0xA0, 0x00, 0x00, 0x10, 0xA0, 0x00, 0x00, 0x00, 0x60, 0x60), + (0xE2, 0x30, 0x30, 0x60, 0x60, 0x45, 0xA0, 0x00, 0x00, 0x46, 0xA0, 0x00, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x33, 0x33), + (0xE4, 0x44, 0x44), + (0xE5, 0x0F, 0x4A, 0xA0, 0xA0, 0x11, 0x4A, 0xA0, 0xA0, 0x13, 0x4A, 0xA0, 0xA0, 0x15, 0x4A, 0xA0, 0xA0), + (0xE6, 0x00, 0x00, 0x33, 0x33), + (0xE7, 0x44, 0x44), + (0xE8, 0x10, 0x4A, 0xA0, 0xA0, 0x12, 0x4A, 0xA0, 0xA0, 0x14, 0x4A, 0xA0, 0xA0, 0x16, 0x4A, 0xA0, 0xA0), + (0xEB, 0x02, 0x00, 0x4E, 0x4E, 0xEE, 0x44, 0x00), + (0xED, 0xFF, 0xFF, 0x04, 0x56, 0x72, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, 0x65, 0x40, 0xFF, 0xFF), + (0xEF, 0x08, 0x08, 0x08, 0x40, 0x3F, 0x64), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xE8, 0x00, 0x0E), + (0xE8, 0x00, 0x0C), + delay(10), + (0xE8, 0x00, 0x00), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00), + ) +) diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 50ea826eab..69bf133c68 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -224,7 +224,7 @@ def model_schema(config): } ) if bus_mode != TYPE_SINGLE: - return cv.All(schema, cv.only_with_esp_idf) + return cv.All(schema, cv.only_on_esp32) return schema diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 7e597d1c61..a59cb8104b 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -5,11 +5,15 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/display/display.h" #include "esphome/components/display/display_color_utils.h" +#include "esphome/core/helpers.h" namespace esphome { namespace mipi_spi { constexpr static const char *const TAG = "display.mipi_spi"; + +// Maximum bytes to log for commands (truncated if larger) +static constexpr size_t MIPI_SPI_MAX_CMD_LOG_BYTES = 64; static constexpr uint8_t SW_RESET_CMD = 0x01; static constexpr uint8_t SLEEP_OUT = 0x11; static constexpr uint8_t NORON = 0x13; @@ -241,7 +245,10 @@ class MipiSpi : public display::Display, // Writes a command to the display, with the given bytes. void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { - esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)]; + esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len)); +#endif if constexpr (BUS_TYPE == BUS_TYPE_QUAD) { this->enable(); this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); @@ -478,7 +485,7 @@ class MipiSpiBuffer : public MipiSpi allocator{}; this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION); if (this->buffer_ == nullptr) { - this->mark_failed("Buffer allocation failed"); + this->mark_failed(LOG_STR("Buffer allocation failed")); } } @@ -562,6 +569,12 @@ class MipiSpiBuffer : public MipiSpiget_clipping().is_set()) { + display::Display::fill(color); + return; + } + this->x_low_ = 0; this->y_low_ = this->start_line_; this->x_high_ = WIDTH - 1; diff --git a/esphome/components/mipi_spi/models/ili.py b/esphome/components/mipi_spi/models/ili.py index 0102c0f665..60a25c32a9 100644 --- a/esphome/components/mipi_spi/models/ili.py +++ b/esphome/components/mipi_spi/models/ili.py @@ -148,6 +148,19 @@ ILI9341 = DriverChip( ), ), ) +# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation +ILI9341.extend( + "M5CORE2", + width=320, + height=240, + mirror_x=False, + cs_pin=5, + dc_pin=15, + invert_colors=True, + pixel_mode="18bit", + data_rate="40MHz", +) + DriverChip( "ILI9481", mirror_x=True, diff --git a/esphome/components/mipi_spi/models/jc.py b/esphome/components/mipi_spi/models/jc.py index 5dbf049ded..5b936fd956 100644 --- a/esphome/components/mipi_spi/models/jc.py +++ b/esphome/components/mipi_spi/models/jc.py @@ -484,4 +484,109 @@ DriverChip( ), ) +DriverChip( + "JC4827W543", + height=272, + width=480, + offset_height=0, + offset_width=0, + cs_pin={CONF_NUMBER: 45, CONF_IGNORE_STRAPPING_WARNING: True}, + invert_colors=True, + color_order=MODE_RGB, + bus_mode=TYPE_QUAD, + data_rate="20MHz", + initsequence=( + (0xFF, 0xA5), + (0x41, 0x03), + (0x44, 0x15), + (0x45, 0x15), + (0x7D, 0x03), + (0xC1, 0xBB), + (0xC2, 0x05), + (0xC3, 0x10), + (0xC6, 0x3E), + (0xC7, 0x25), + (0xC8, 0x11), + (0x7A, 0x5F), + (0x6F, 0x44), + (0x78, 0x70), + (0xC9, 0x00), + (0x67, 0x21), + (0x51, 0x0A), + (0x52, 0x76), + (0x53, 0x0A), + (0x54, 0x76), + (0x46, 0x0A), + (0x47, 0x2A), + (0x48, 0x0A), + (0x49, 0x1A), + (0x56, 0x43), + (0x57, 0x42), + (0x58, 0x3C), + (0x59, 0x64), + (0x5A, 0x41), + (0x5B, 0x3C), + (0x5C, 0x02), + (0x5D, 0x3C), + (0x5E, 0x1F), + (0x60, 0x80), + (0x61, 0x3F), + (0x62, 0x21), + (0x63, 0x07), + (0x64, 0xE0), + (0x65, 0x02), + (0xCA, 0x20), + (0xCB, 0x52), + (0xCC, 0x10), + (0xCD, 0x42), + (0xD0, 0x20), + (0xD1, 0x52), + (0xD2, 0x10), + (0xD3, 0x42), + (0xD4, 0x0A), + (0xD5, 0x32), + (0x80, 0x00), + (0xA0, 0x00), + (0x81, 0x07), + (0xA1, 0x06), + (0x82, 0x02), + (0xA2, 0x01), + (0x86, 0x11), + (0xA6, 0x10), + (0x87, 0x27), + (0xA7, 0x27), + (0x83, 0x37), + (0xA3, 0x37), + (0x84, 0x35), + (0xA4, 0x35), + (0x85, 0x3F), + (0xA5, 0x3F), + (0x88, 0x0B), + (0xA8, 0x0B), + (0x89, 0x14), + (0xA9, 0x14), + (0x8A, 0x1A), + (0xAA, 0x1A), + (0x8B, 0x0A), + (0xAB, 0x0A), + (0x8C, 0x14), + (0xAC, 0x08), + (0x8D, 0x17), + (0xAD, 0x07), + (0x8E, 0x16), + (0xAE, 0x06), + (0x8F, 0x1B), + (0xAF, 0x07), + (0x90, 0x04), + (0xB0, 0x04), + (0x91, 0x0A), + (0xB1, 0x0A), + (0x92, 0x16), + (0xB2, 0x15), + (0xFF, 0x00), + (0x11, 0x00), + (0x29, 0x00), + ), +) + models = {} diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index 10ab4f3b5c..d80b7aeff5 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -1,4 +1,5 @@ #include "mitsubishi.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -6,6 +7,9 @@ namespace mitsubishi { static const char *const TAG = "mitsubishi.climate"; +// IR frame size for Mitsubishi climate +static constexpr size_t MITSUBISHI_FRAME_SIZE = 18; + const uint8_t MITSUBISHI_OFF = 0x00; const uint8_t MITSUBISHI_MODE_AUTO = 0x20; @@ -388,7 +392,10 @@ bool MitsubishiClimate::on_receive(remote_base::RemoteReceiveData data) { break; } - ESP_LOGV(TAG, "Receiving: %s", format_hex_pretty(state_frame, 18).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MITSUBISHI_FRAME_SIZE)]; +#endif + ESP_LOGV(TAG, "Receiving: %s", format_hex_pretty_to(hex_buf, state_frame, MITSUBISHI_FRAME_SIZE)); this->publish_state(); return true; diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index 46729f8eda..c4069851af 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -93,9 +93,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_NUM_CHANNELS): cv.int_range(min=1, max=2), cv.Optional(CONF_QUEUE_MODE, default=False): cv.boolean, - cv.SplitDefault(CONF_TASK_STACK_IN_PSRAM, esp32_idf=False): cv.All( - cv.boolean, cv.only_with_esp_idf - ), + cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean, } ), cv.only_on([PLATFORM_ESP32]), diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index b0b64f5709..043b629cf1 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -78,19 +78,20 @@ void SourceSpeaker::loop() { } else { switch (err) { case ESP_ERR_NO_MEM: - this->status_set_error("Failed to start mixer: not enough memory"); + this->status_set_error(LOG_STR("Failed to start mixer: not enough memory")); break; case ESP_ERR_NOT_SUPPORTED: - this->status_set_error("Failed to start mixer: unsupported bits per sample"); + this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample")); break; case ESP_ERR_INVALID_ARG: - this->status_set_error("Failed to start mixer: audio stream isn't compatible with the other audio stream."); + this->status_set_error( + LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream.")); break; case ESP_ERR_INVALID_STATE: - this->status_set_error("Failed to start mixer: mixer task failed to start"); + this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start")); break; default: - this->status_set_error("Failed to start mixer"); + this->status_set_error(LOG_STR("Failed to start mixer")); break; } @@ -317,7 +318,7 @@ void MixerSpeaker::loop() { xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING); } if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error("Failed to allocate the mixer's internal buffer"); + this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer")); xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM); } if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) { diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index f0d1044f3f..d6321eae8f 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -83,6 +83,7 @@ void MMC5603Component::dump_config() { ESP_LOGE(TAG, "The ID registers don't match - Is this really an MMC5603?"); } LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Auto set/reset: %s", ONOFF(this->auto_set_reset_)); LOG_SENSOR(" ", "X Axis", this->x_sensor_); LOG_SENSOR(" ", "Y Axis", this->y_sensor_); @@ -93,7 +94,8 @@ void MMC5603Component::dump_config() { float MMC5603Component::get_setup_priority() const { return setup_priority::DATA; } void MMC5603Component::update() { - if (!this->write_byte(MMC56X3_CTRL0_REG, 0x01)) { + uint8_t ctrl0 = (this->auto_set_reset_) ? 0x21 : 0x01; + if (!this->write_byte(MMC56X3_CTRL0_REG, ctrl0)) { this->status_set_warning(); return; } diff --git a/esphome/components/mmc5603/mmc5603.h b/esphome/components/mmc5603/mmc5603.h index cd0893053c..09718bd3b7 100644 --- a/esphome/components/mmc5603/mmc5603.h +++ b/esphome/components/mmc5603/mmc5603.h @@ -25,6 +25,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice { void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; } void set_z_sensor(sensor::Sensor *z_sensor) { z_sensor_ = z_sensor; } void set_heading_sensor(sensor::Sensor *heading_sensor) { heading_sensor_ = heading_sensor; } + void set_auto_set_reset(bool auto_set_reset) { auto_set_reset_ = auto_set_reset; } protected: MMC5603Datarate datarate_; @@ -32,6 +33,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *y_sensor_{nullptr}; sensor::Sensor *z_sensor_{nullptr}; sensor::Sensor *heading_sensor_{nullptr}; + bool auto_set_reset_{true}; enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, diff --git a/esphome/components/mmc5603/sensor.py b/esphome/components/mmc5603/sensor.py index 3223225271..5b3982cee6 100644 --- a/esphome/components/mmc5603/sensor.py +++ b/esphome/components/mmc5603/sensor.py @@ -16,6 +16,8 @@ from esphome.const import ( UNIT_MICROTESLA, ) +CONF_AUTO_SET_RESET = "auto_set_reset" + DEPENDENCIES = ["i2c"] mmc5603_ns = cg.esphome_ns.namespace("mmc5603") @@ -54,6 +56,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, cv.Optional(CONF_HEADING): heading_schema, + cv.Optional(CONF_AUTO_SET_RESET, default=True): cv.boolean, } ) .extend(cv.polling_component_schema("60s")) @@ -88,3 +91,5 @@ async def to_code(config): if CONF_HEADING in config: sens = await sensor.new_sensor(config[CONF_HEADING]) cg.add(var.set_heading_sensor(sens)) + if CONF_AUTO_SET_RESET in config: + cg.add(var.set_auto_set_reset(config[CONF_AUTO_SET_RESET])) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 20271b4bdb..5e9387b843 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -8,6 +8,9 @@ namespace modbus { static const char *const TAG = "modbus"; +// Maximum bytes to log for Modbus frames (truncated if larger) +static constexpr size_t MODBUS_MAX_LOG_BYTES = 64; + void Modbus::setup() { if (this->flow_control_pin_ != nullptr) { this->flow_control_pin_->setup(); @@ -193,12 +196,12 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { } void Modbus::dump_config() { - ESP_LOGCONFIG(TAG, "Modbus:"); - LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); ESP_LOGCONFIG(TAG, + "Modbus:\n" " Send Wait Time: %d ms\n" " CRC Disabled: %s", this->send_wait_time_, YESNO(this->disable_crc_)); + LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); } float Modbus::get_setup_priority() const { // After UART bus @@ -255,7 +258,10 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address this->flow_control_pin_->digital_write(false); waiting_for_response = address; last_send_ = millis(); - ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data.data(), data.size())); } // Helper function for lambdas @@ -276,7 +282,10 @@ void Modbus::send_raw(const std::vector &payload) { if (this->flow_control_pin_ != nullptr) this->flow_control_pin_->digital_write(false); waiting_for_response = payload[0]; - ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty(payload).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty_to(hex_buf, payload.data(), payload.size())); last_send_ = millis(); } diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index ea8467d5a3..4a3ec1fc41 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -1,5 +1,6 @@ #include #include "modbus_number.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -7,6 +8,9 @@ namespace modbus_controller { static const char *const TAG = "modbus.number"; +// Maximum uint16_t registers to log in verbose hex output +static constexpr size_t MODBUS_NUMBER_MAX_LOG_REGISTERS = 32; + void ModbusNumber::parse_and_publish(const std::vector &data) { float result = payload_to_float(data, *this) / this->multiply_by_; @@ -47,7 +51,11 @@ void ModbusNumber::control(float value) { } if (!data.empty()) { - ESP_LOGV(TAG, "Modbus Number write raw: %s", format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_uint16_size(MODBUS_NUMBER_MAX_LOG_REGISTERS)]; +#endif + ESP_LOGV(TAG, "Modbus Number write raw: %s", + format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); write_cmd = ModbusCommandItem::create_custom_command( this->parent_, data, [this, write_cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp index 45e786a704..f02d9397ca 100644 --- a/esphome/components/modbus_controller/output/modbus_output.cpp +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -7,6 +7,9 @@ namespace modbus_controller { static const char *const TAG = "modbus_controller.output"; +// Maximum bytes to log in verbose hex output +static constexpr size_t MODBUS_OUTPUT_MAX_LOG_BYTES = 64; + /** Write a value to the device * */ @@ -80,7 +83,11 @@ void ModbusBinaryOutput::write_state(bool state) { } } if (!data.empty()) { - ESP_LOGV(TAG, "Modbus binary output write raw: %s", format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_OUTPUT_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Modbus binary output write raw: %s", + format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); cmd = ModbusCommandItem::create_custom_command( this->parent_, data, [this, cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp index 21c4c1718d..68aa37c9ed 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.cpp +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -1,11 +1,15 @@ #include "modbus_switch.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { namespace modbus_controller { static const char *const TAG = "modbus_controller.switch"; +// Maximum bytes to log in verbose hex output +static constexpr size_t MODBUS_SWITCH_MAX_LOG_BYTES = 64; + void ModbusSwitch::setup() { optional initial_state = Switch::get_initial_state_with_restore_mode(); if (initial_state.has_value()) { @@ -71,7 +75,11 @@ void ModbusSwitch::write_state(bool state) { } } if (!data.empty()) { - ESP_LOGV(TAG, "Modbus Switch write raw: %s", format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MODBUS_SWITCH_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Modbus Switch write raw: %s", + format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); cmd = ModbusCommandItem::create_custom_command( this->parent_, data, [this, cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { diff --git a/esphome/components/mopeka_ble/mopeka_ble.cpp b/esphome/components/mopeka_ble/mopeka_ble.cpp index 07c8ac5d71..b926beaff2 100644 --- a/esphome/components/mopeka_ble/mopeka_ble.cpp +++ b/esphome/components/mopeka_ble/mopeka_ble.cpp @@ -36,6 +36,7 @@ static const uint8_t MANUFACTURER_NRF52_DATA_LENGTH = 10; */ bool MopekaListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; // Fetch information about BLE device. const auto &service_uuids = device.get_service_uuids(); if (service_uuids.size() != 1) { @@ -62,7 +63,7 @@ bool MopekaListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const bool sync_button_pressed = (manu_data.data[3] & 0x80) != 0; if (this->show_sensors_without_sync_ || sync_button_pressed) { - ESP_LOGI(TAG, "MOPEKA STD (CC2540) SENSOR FOUND: %s", device.address_str().c_str()); + ESP_LOGI(TAG, "MOPEKA STD (CC2540) SENSOR FOUND: %s", device.address_str_to(addr_buf)); } // Is the device maybe a Mopeka Pro (NRF52) sensor. @@ -78,7 +79,7 @@ bool MopekaListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const bool sync_button_pressed = (manu_data.data[2] & 0x80) != 0; if (this->show_sensors_without_sync_ || sync_button_pressed) { - ESP_LOGI(TAG, "MOPEKA PRO (NRF52) SENSOR FOUND: %s", device.address_str().c_str()); + ESP_LOGI(TAG, "MOPEKA PRO (NRF52) SENSOR FOUND: %s", device.address_str_to(addr_buf)); } } diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp index 9527f09f59..9bc9900a5a 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp @@ -31,7 +31,8 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str_to(addr_buf)); const auto &manu_datas = device.get_manufacturer_datas(); @@ -116,7 +117,7 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) // Get temperature of sensor if (this->temperature_ != nullptr) { - uint8_t temp_in_c = this->parse_temperature_(manu_data.data); + int8_t temp_in_c = this->parse_temperature_(manu_data.data); this->temperature_->publish_state(temp_in_c); } @@ -145,7 +146,7 @@ uint32_t MopekaProCheck::parse_distance_(const std::vector &message) { (MOPEKA_LPG_COEF[0] + MOPEKA_LPG_COEF[1] * raw_t + MOPEKA_LPG_COEF[2] * raw_t * raw_t)); } -uint8_t MopekaProCheck::parse_temperature_(const std::vector &message) { return (message[2] & 0x7F) - 40; } +int8_t MopekaProCheck::parse_temperature_(const std::vector &message) { return (message[2] & 0x7F) - 40; } SensorReadQuality MopekaProCheck::parse_read_quality_(const std::vector &message) { // Since a 8 bit value is being shifted and truncated to 2 bits all possible values are defined as enumeration diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index 4cbe8f2afe..41fb312152 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -61,7 +61,7 @@ class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi uint8_t parse_battery_level_(const std::vector &message); uint32_t parse_distance_(const std::vector &message); - uint8_t parse_temperature_(const std::vector &message); + int8_t parse_temperature_(const std::vector &message); SensorReadQuality parse_read_quality_(const std::vector &message); }; diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 0d8340f95f..6322b550c9 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -13,11 +13,16 @@ static const uint16_t SERVICE_UUID = 0xADA0; static const uint8_t MANUFACTURER_DATA_LENGTH = 23; static const uint16_t MANUFACTURER_ID = 0x000D; +// Maximum bytes to log in very verbose hex output +static constexpr size_t MOPEKA_MAX_LOG_BYTES = 32; + void MopekaStdCheck::dump_config() { - ESP_LOGCONFIG(TAG, "Mopeka Std Check"); - ESP_LOGCONFIG(TAG, " Propane Butane mix: %.0f%%", this->propane_butane_mix_ * 100); - ESP_LOGCONFIG(TAG, " Tank distance empty: %" PRIi32 "mm", this->empty_mm_); - ESP_LOGCONFIG(TAG, " Tank distance full: %" PRIi32 "mm", this->full_mm_); + ESP_LOGCONFIG(TAG, + "Mopeka Std Check\n" + " Propane Butane mix: %.0f%%\n" + " Tank distance empty: %" PRIi32 "mm\n" + " Tank distance full: %" PRIi32 "mm", + this->propane_butane_mix_ * 100, this->empty_mm_, this->full_mm_); LOG_SENSOR(" ", "Level", this->level_); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -30,15 +35,17 @@ void MopekaStdCheck::dump_config() { * update the sensor state data. */ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - { - // Validate address. - if (device.address_uint64() != this->address_) { - return false; - } - - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + // Validate address. + if (device.address_uint64() != this->address_) { + return false; } + // Stack buffer for MAC address formatting - reused throughout function + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); + { // Validate service uuid const auto &service_uuids = device.get_service_uuids(); @@ -54,16 +61,20 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const auto &manu_datas = device.get_manufacturer_datas(); if (manu_datas.size() != 1) { - ESP_LOGE(TAG, "[%s] Unexpected manu_datas size (%d)", device.address_str().c_str(), manu_datas.size()); + ESP_LOGE(TAG, "[%s] Unexpected manu_datas size (%d)", addr_str, manu_datas.size()); return false; } const auto &manu_data = manu_datas[0]; - ESP_LOGVV(TAG, "[%s] Manufacturer data: %s", device.address_str().c_str(), format_hex_pretty(manu_data.data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(MOPEKA_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, "[%s] Manufacturer data: %s", addr_str, + format_hex_pretty_to(hex_buf, manu_data.data.data(), manu_data.data.size())); if (manu_data.data.size() != MANUFACTURER_DATA_LENGTH) { - ESP_LOGE(TAG, "[%s] Unexpected manu_data size (%d)", device.address_str().c_str(), manu_data.data.size()); + ESP_LOGE(TAG, "[%s] Unexpected manu_data size (%d)", addr_str, manu_data.data.size()); return false; } @@ -73,21 +84,21 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const u_int8_t hardware_id = mopeka_data->data_1 & 0xCF; if (static_cast(hardware_id) != STANDARD && static_cast(hardware_id) != XL && static_cast(hardware_id) != ETRAILER && static_cast(hardware_id) != STANDARD_ALT) { - ESP_LOGE(TAG, "[%s] Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id); + ESP_LOGE(TAG, "[%s] Unsupported Sensor Type (0x%X)", addr_str, hardware_id); return false; } - ESP_LOGVV(TAG, "[%s] Sensor slow update rate: %d", device.address_str().c_str(), mopeka_data->slow_update_rate); - ESP_LOGVV(TAG, "[%s] Sensor sync pressed: %d", device.address_str().c_str(), mopeka_data->sync_pressed); + ESP_LOGVV(TAG, "[%s] Sensor slow update rate: %d", addr_str, mopeka_data->slow_update_rate); + ESP_LOGVV(TAG, "[%s] Sensor sync pressed: %d", addr_str, mopeka_data->sync_pressed); for (u_int8_t i = 0; i < 3; i++) { - ESP_LOGVV(TAG, "[%s] %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 1, - mopeka_data->val[i].value_0, mopeka_data->val[i].time_0); - ESP_LOGVV(TAG, "[%s] %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 2, - mopeka_data->val[i].value_1, mopeka_data->val[i].time_1); - ESP_LOGVV(TAG, "[%s] %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 3, - mopeka_data->val[i].value_2, mopeka_data->val[i].time_2); - ESP_LOGVV(TAG, "[%s] %u. Sensor data %u time %u.", device.address_str().c_str(), (i * 4) + 4, - mopeka_data->val[i].value_3, mopeka_data->val[i].time_3); + ESP_LOGVV(TAG, "[%s] %u. Sensor data %u time %u.", addr_str, (i * 4) + 1, mopeka_data->val[i].value_0, + mopeka_data->val[i].time_0); + ESP_LOGVV(TAG, "[%s] %u. Sensor data %u time %u.", addr_str, (i * 4) + 2, mopeka_data->val[i].value_1, + mopeka_data->val[i].time_1); + ESP_LOGVV(TAG, "[%s] %u. Sensor data %u time %u.", addr_str, (i * 4) + 3, mopeka_data->val[i].value_2, + mopeka_data->val[i].time_2); + ESP_LOGVV(TAG, "[%s] %u. Sensor data %u time %u.", addr_str, (i * 4) + 4, mopeka_data->val[i].value_3, + mopeka_data->val[i].time_3); } // Get battery level first @@ -154,12 +165,12 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) } } - ESP_LOGV(TAG, "[%s] Found %u values with best data %u time %u.", device.address_str().c_str(), - number_of_usable_values, best_value, best_time); + ESP_LOGV(TAG, "[%s] Found %u values with best data %u time %u.", addr_str, number_of_usable_values, best_value, + best_time); if (number_of_usable_values < 1 || best_value < 2 || best_time < 2) { // At least two measurement values must be present. - ESP_LOGW(TAG, "[%s] Poor read quality. Setting distance to 0.", device.address_str().c_str()); + ESP_LOGW(TAG, "[%s] Poor read quality. Setting distance to 0.", addr_str); if (this->distance_ != nullptr) { this->distance_->publish_state(0); } @@ -168,7 +179,7 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) } } else { float lpg_speed_of_sound = this->get_lpg_speed_of_sound_(temp_in_c); - ESP_LOGV(TAG, "[%s] Speed of sound in current fluid %f m/s", device.address_str().c_str(), lpg_speed_of_sound); + ESP_LOGV(TAG, "[%s] Speed of sound in current fluid %f m/s", addr_str, lpg_speed_of_sound); uint32_t distance_value = lpg_speed_of_sound * best_time / 100.0f; diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index 5a8a8e7205..4b358e384c 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -153,10 +153,8 @@ void MPR121GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_ - 4, value != this->inverted_); } -std::string MPR121GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "ELE%u on MPR121", this->pin_); - return buffer; +size_t MPR121GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "ELE%u on MPR121", this->pin_); } } // namespace mpr121 diff --git a/esphome/components/mpr121/mpr121.h b/esphome/components/mpr121/mpr121.h index 6dd2c38309..085018fff0 100644 --- a/esphome/components/mpr121/mpr121.h +++ b/esphome/components/mpr121/mpr121.h @@ -109,7 +109,7 @@ class MPR121GPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(MPR121Component *parent) { this->parent_ = parent; } void set_pin(uint8_t pin) { this->pin_ = pin; } diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 1fc0c30db1..e73de49fef 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -55,6 +55,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_RTL87XX, PlatformFramework, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -232,11 +233,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PASSWORD, default=""): cv.string, cv.Optional(CONF_CLEAN_SESSION, default=False): cv.boolean, cv.Optional(CONF_CLIENT_ID): cv.string, - cv.SplitDefault(CONF_IDF_SEND_ASYNC, esp32_idf=False): cv.All( - cv.boolean, cv.only_with_esp_idf + cv.SplitDefault(CONF_IDF_SEND_ASYNC, esp32=False): cv.All( + cv.boolean, cv.only_on_esp32 ), cv.Optional(CONF_CERTIFICATE_AUTHORITY): cv.All( - cv.string, cv.only_with_esp_idf + cv.string, cv.only_on_esp32 ), cv.Inclusive(CONF_CLIENT_CERTIFICATE, "cert-key-pair"): cv.All( cv.string, cv.only_on_esp32 @@ -244,8 +245,8 @@ CONFIG_SCHEMA = cv.All( cv.Inclusive(CONF_CLIENT_CERTIFICATE_KEY, "cert-key-pair"): cv.All( cv.string, cv.only_on_esp32 ), - cv.SplitDefault(CONF_SKIP_CERT_CN_CHECK, esp32_idf=False): cv.All( - cv.boolean, cv.only_with_esp_idf + cv.SplitDefault(CONF_SKIP_CERT_CN_CHECK, esp32=False): cv.All( + cv.boolean, cv.only_on_esp32 ), cv.Optional(CONF_DISCOVERY, default=True): cv.Any( cv.boolean, cv.one_of("CLEAN", upper=True) @@ -316,7 +317,7 @@ CONFIG_SCHEMA = cv.All( } ), validate_config, - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]), _consume_mqtt_sockets, ) diff --git a/esphome/components/mqtt/custom_mqtt_device.cpp b/esphome/components/mqtt/custom_mqtt_device.cpp index 787cc1153f..25a8a82066 100644 --- a/esphome/components/mqtt/custom_mqtt_device.cpp +++ b/esphome/components/mqtt/custom_mqtt_device.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.custom"; @@ -29,7 +28,6 @@ bool CustomMQTTDevice::publish_json(const std::string &topic, const json::json_b } bool CustomMQTTDevice::is_connected() { return global_mqtt_client != nullptr && global_mqtt_client->is_connected(); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_MQTT diff --git a/esphome/components/mqtt/custom_mqtt_device.h b/esphome/components/mqtt/custom_mqtt_device.h index 0852a17cf1..09ed7bd6d1 100644 --- a/esphome/components/mqtt/custom_mqtt_device.h +++ b/esphome/components/mqtt/custom_mqtt_device.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "mqtt_client.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { /** This class is a helper class for custom components that communicate using * MQTT. It has 5 helper functions that you can use (square brackets indicate optional): @@ -214,7 +213,6 @@ void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callba global_mqtt_client->subscribe_json(topic, f, qos); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index dd3df5f8aa..eb46c3b10c 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_ALARM_CONTROL_PANEL -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.alarm_control_panel"; @@ -59,22 +58,22 @@ void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendD JsonArray supported_features = root[MQTT_SUPPORTED_FEATURES].to(); const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features(); if (acp_supported_features & ACP_FEAT_ARM_AWAY) { - supported_features.add("arm_away"); + supported_features.add(ESPHOME_F("arm_away")); } if (acp_supported_features & ACP_FEAT_ARM_HOME) { - supported_features.add("arm_home"); + supported_features.add(ESPHOME_F("arm_home")); } if (acp_supported_features & ACP_FEAT_ARM_NIGHT) { - supported_features.add("arm_night"); + supported_features.add(ESPHOME_F("arm_night")); } if (acp_supported_features & ACP_FEAT_ARM_VACATION) { - supported_features.add("arm_vacation"); + supported_features.add(ESPHOME_F("arm_vacation")); } if (acp_supported_features & ACP_FEAT_ARM_CUSTOM_BYPASS) { - supported_features.add("arm_custom_bypass"); + supported_features.add(ESPHOME_F("arm_custom_bypass")); } if (acp_supported_features & ACP_FEAT_TRIGGER) { - supported_features.add("trigger"); + supported_features.add(ESPHOME_F("trigger")); } root[MQTT_CODE_DISARM_REQUIRED] = this->alarm_control_panel_->get_requires_code(); root[MQTT_CODE_ARM_REQUIRED] = this->alarm_control_panel_->get_requires_code_to_arm(); @@ -123,8 +122,7 @@ bool MQTTAlarmControlPanelComponent::publish_state() { return this->publish(this->get_state_topic_(), state_s); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.h b/esphome/components/mqtt/mqtt_alarm_control_panel.h index 4ad37b7314..cf4fac1511 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.h +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.h @@ -8,8 +8,7 @@ #include "mqtt_component.h" #include "esphome/components/alarm_control_panel/alarm_control_panel.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent { public: @@ -32,8 +31,7 @@ class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent { alarm_control_panel::AlarmControlPanel *alarm_control_panel_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_backend.h b/esphome/components/mqtt/mqtt_backend.h index 0c1720ec34..a7e3f1013d 100644 --- a/esphome/components/mqtt/mqtt_backend.h +++ b/esphome/components/mqtt/mqtt_backend.h @@ -6,8 +6,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { enum class MQTTClientDisconnectReason : int8_t { TCP_DISCONNECTED = 0, @@ -67,6 +66,5 @@ class MQTTBackend { virtual void loop() {} }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index dcc51ed60e..c12c79499f 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -8,8 +8,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.idf"; @@ -166,10 +165,12 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { case MQTT_EVENT_ERROR: ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); if (event.error_handle.error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { - ESP_LOGE(TAG, "Last error code reported from esp-tls: 0x%x", event.error_handle.esp_tls_last_esp_err); - ESP_LOGE(TAG, "Last tls stack error number: 0x%x", event.error_handle.esp_tls_stack_err); - ESP_LOGE(TAG, "Last captured errno : %d (%s)", event.error_handle.esp_transport_sock_errno, - strerror(event.error_handle.esp_transport_sock_errno)); + ESP_LOGE(TAG, + "Last error code reported from esp-tls: 0x%x\n" + "Last tls stack error number: 0x%x\n" + "Last captured errno : %d (%s)", + event.error_handle.esp_tls_last_esp_err, event.error_handle.esp_tls_stack_err, + event.error_handle.esp_transport_sock_errno, strerror(event.error_handle.esp_transport_sock_errno)); } else if (event.error_handle.error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { ESP_LOGE(TAG, "Connection refused error: 0x%x", event.error_handle.connect_return_code); } else { @@ -232,16 +233,6 @@ void MQTTBackendESP32::esphome_mqtt_task(void *params) { this_mqtt->mqtt_event_pool_.release(elem); } } - - // Clean up any remaining items in the queue - struct QueueElement *elem; - while ((elem = this_mqtt->mqtt_queue_.pop()) != nullptr) { - this_mqtt->mqtt_event_pool_.release(elem); - } - - // Note: EventPool destructor will clean up the pool itself - // Task will delete itself - vTaskDelete(nullptr); } bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload, @@ -278,7 +269,6 @@ bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, } #endif // USE_MQTT_IDF_ENQUEUE -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_ESP32 #endif diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index a24e75eaf9..bd2d2a67b2 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -15,8 +15,7 @@ #include "esphome/core/lock_free_queue.h" #include "esphome/core/event_pool.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { struct Event { esp_mqtt_event_id_t event_id; @@ -273,8 +272,7 @@ class MQTTBackendESP32 final : public MQTTBackend { #endif }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.h b/esphome/components/mqtt/mqtt_backend_esp8266.h index a979634bf4..470d1e6a8b 100644 --- a/esphome/components/mqtt/mqtt_backend_esp8266.h +++ b/esphome/components/mqtt/mqtt_backend_esp8266.h @@ -6,8 +6,7 @@ #include -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTBackendESP8266 final : public MQTTBackend { public: @@ -67,8 +66,7 @@ class MQTTBackendESP8266 final : public MQTTBackend { AsyncMqttClient mqtt_client_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // defined(USE_ESP8266) #endif diff --git a/esphome/components/mqtt/mqtt_backend_libretiny.h b/esphome/components/mqtt/mqtt_backend_libretiny.h index 2578ae9941..24bf018a90 100644 --- a/esphome/components/mqtt/mqtt_backend_libretiny.h +++ b/esphome/components/mqtt/mqtt_backend_libretiny.h @@ -6,8 +6,7 @@ #include -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTBackendLibreTiny final : public MQTTBackend { public: @@ -67,8 +66,7 @@ class MQTTBackendLibreTiny final : public MQTTBackend { AsyncMqttClient mqtt_client_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // defined(USE_LIBRETINY) #endif diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 479cee205a..146ca46f68 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_BINARY_SENSOR -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.binary_sensor"; @@ -57,8 +56,7 @@ bool MQTTBinarySensorComponent::publish_state(bool state) { return this->publish(this->get_state_topic_(), state_s); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_binary_sensor.h b/esphome/components/mqtt/mqtt_binary_sensor.h index f6579fcd19..82176ec97b 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.h +++ b/esphome/components/mqtt/mqtt_binary_sensor.h @@ -7,8 +7,7 @@ #include "mqtt_component.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTBinarySensorComponent : public mqtt::MQTTComponent { public: @@ -36,8 +35,7 @@ class MQTTBinarySensorComponent : public mqtt::MQTTComponent { binary_sensor::BinarySensor *binary_sensor_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index f8eb0eab2d..2b700a4962 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_BUTTON -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.button"; @@ -43,8 +42,7 @@ void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon std::string MQTTButtonComponent::component_type() const { return "button"; } const EntityBase *MQTTButtonComponent::get_entity() const { return this->button_; } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_button.h b/esphome/components/mqtt/mqtt_button.h index 42389caecc..ec802664df 100644 --- a/esphome/components/mqtt/mqtt_button.h +++ b/esphome/components/mqtt/mqtt_button.h @@ -8,8 +8,7 @@ #include "esphome/components/button/button.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTButtonComponent : public mqtt::MQTTComponent { public: @@ -33,8 +32,7 @@ class MQTTButtonComponent : public mqtt::MQTTComponent { button::Button *button_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 9055b4421e..652f55734b 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -22,8 +22,7 @@ #include "esphome/components/dashboard_import/dashboard_import.h" #endif -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt"; @@ -57,15 +56,7 @@ void MQTTClientComponent::setup() { }); #ifdef USE_LOGGER if (this->is_log_message_enabled() && logger::global_logger != nullptr) { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - if (level <= this->log_level_ && this->is_connected()) { - this->publish({.topic = this->log_message_.topic, - .payload = std::string(message, message_len), - .qos = this->log_message_.qos, - .retain = this->log_message_.retain}); - } - }); + logger::global_logger->add_log_listener(this); } #endif @@ -103,61 +94,77 @@ void MQTTClientComponent::send_device_info_() { index++; } } - root["name"] = App.get_name(); + root[ESPHOME_F("name")] = App.get_name(); if (!App.get_friendly_name().empty()) { - root["friendly_name"] = App.get_friendly_name(); + root[ESPHOME_F("friendly_name")] = App.get_friendly_name(); } #ifdef USE_API - root["port"] = api::global_api_server->get_port(); + root[ESPHOME_F("port")] = api::global_api_server->get_port(); #endif - root["version"] = ESPHOME_VERSION; - root["mac"] = get_mac_address(); + root[ESPHOME_F("version")] = ESPHOME_VERSION; + root[ESPHOME_F("mac")] = get_mac_address(); #ifdef USE_ESP8266 - root["platform"] = "ESP8266"; + root[ESPHOME_F("platform")] = ESPHOME_F("ESP8266"); #endif #ifdef USE_ESP32 - root["platform"] = "ESP32"; + root[ESPHOME_F("platform")] = ESPHOME_F("ESP32"); #endif #ifdef USE_LIBRETINY - root["platform"] = lt_cpu_get_model_name(); + root[ESPHOME_F("platform")] = lt_cpu_get_model_name(); #endif - root["board"] = ESPHOME_BOARD; + root[ESPHOME_F("board")] = ESPHOME_BOARD; #if defined(USE_WIFI) - root["network"] = "wifi"; + root[ESPHOME_F("network")] = ESPHOME_F("wifi"); #elif defined(USE_ETHERNET) - root["network"] = "ethernet"; + root[ESPHOME_F("network")] = ESPHOME_F("ethernet"); #endif #ifdef ESPHOME_PROJECT_NAME - root["project_name"] = ESPHOME_PROJECT_NAME; - root["project_version"] = ESPHOME_PROJECT_VERSION; + root[ESPHOME_F("project_name")] = ESPHOME_PROJECT_NAME; + root[ESPHOME_F("project_version")] = ESPHOME_PROJECT_VERSION; #endif // ESPHOME_PROJECT_NAME #ifdef USE_DASHBOARD_IMPORT - root["package_import_url"] = dashboard_import::get_package_import_url(); + root[ESPHOME_F("package_import_url")] = dashboard_import::get_package_import_url(); #endif #ifdef USE_API_NOISE - root[api::global_api_server->get_noise_ctx()->has_psk() ? "api_encryption" : "api_encryption_supported"] = - "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; + root[api::global_api_server->get_noise_ctx().has_psk() ? ESPHOME_F("api_encryption") + : ESPHOME_F("api_encryption_supported")] = + ESPHOME_F("Noise_NNpsk0_25519_ChaChaPoly_SHA256"); #endif }, 2, this->discovery_info_.retain); // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } +#ifdef USE_LOGGER +void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + (void) tag; + if (level <= this->log_level_ && this->is_connected()) { + this->publish({.topic = this->log_message_.topic, + .payload = std::string(message, message_len), + .qos = this->log_message_.qos, + .retain = this->log_message_.retain}); + } +} +#endif + void MQTTClientComponent::dump_config() { + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + // clang-format off ESP_LOGCONFIG(TAG, "MQTT:\n" " Server Address: %s:%u (%s)\n" " Username: " LOG_SECRET("'%s'") "\n" - " Client ID: " LOG_SECRET("'%s'") "\n" - " Clean Session: %s", - this->credentials_.address.c_str(), this->credentials_.port, this->ip_.str().c_str(), + " Client ID: " LOG_SECRET("'%s'") "\n" + " Clean Session: %s", + this->credentials_.address.c_str(), this->credentials_.port, this->ip_.str_to(ip_buf), this->credentials_.username.c_str(), this->credentials_.client_id.c_str(), YESNO(this->credentials_.clean_session)); + // clang-format on if (this->is_discovery_ip_enabled()) { ESP_LOGCONFIG(TAG, " Discovery IP enabled"); } @@ -242,7 +249,8 @@ void MQTTClientComponent::check_dnslookup_() { return; } - ESP_LOGD(TAG, "Resolved broker IP address to %s", this->ip_.str().c_str()); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGD(TAG, "Resolved broker IP address to %s", this->ip_.str_to(ip_buf)); this->start_connect_(); } #if defined(USE_ESP8266) && LWIP_VERSION_MAJOR == 1 @@ -743,7 +751,6 @@ void MQTTMessageTrigger::dump_config() { } float MQTTMessageTrigger::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 79383ee857..4189e7ae77 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -10,6 +10,9 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #if defined(USE_ESP32) #include "mqtt_backend_esp32.h" #elif defined(USE_ESP8266) @@ -21,8 +24,7 @@ #include -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { /** Callback for MQTT events. */ @@ -97,7 +99,12 @@ enum MQTTClientState { class MQTTComponent; -class MQTTClientComponent : public Component { +class MQTTClientComponent : public Component +#ifdef USE_LOGGER + , + public logger::LogListener +#endif +{ public: MQTTClientComponent(); @@ -238,6 +245,10 @@ class MQTTClientComponent : public Component { /// MQTT client setup priority float get_setup_priority() const override; +#ifdef USE_LOGGER + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; +#endif + void on_message(const std::string &topic, const std::string &payload); bool can_proceed() override; @@ -450,7 +461,6 @@ template class MQTTDisableAction : public Action { MQTTClientComponent *parent_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index aee2b38942..d402fff6e6 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_CLIMATE -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.climate"; @@ -32,18 +31,18 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo JsonArray modes = root[MQTT_MODES].to(); // sort array for nice UI in HA if (traits.supports_mode(CLIMATE_MODE_AUTO)) - modes.add("auto"); - modes.add("off"); + modes.add(ESPHOME_F("auto")); + modes.add(ESPHOME_F("off")); if (traits.supports_mode(CLIMATE_MODE_COOL)) - modes.add("cool"); + modes.add(ESPHOME_F("cool")); if (traits.supports_mode(CLIMATE_MODE_HEAT)) - modes.add("heat"); + modes.add(ESPHOME_F("heat")); if (traits.supports_mode(CLIMATE_MODE_FAN_ONLY)) - modes.add("fan_only"); + modes.add(ESPHOME_F("fan_only")); if (traits.supports_mode(CLIMATE_MODE_DRY)) - modes.add("dry"); + modes.add(ESPHOME_F("dry")); if (traits.supports_mode(CLIMATE_MODE_HEAT_COOL)) - modes.add("heat_cool"); + modes.add(ESPHOME_F("heat_cool")); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { @@ -91,21 +90,21 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // preset_mode_state_topic root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic(); // presets - JsonArray presets = root["preset_modes"].to(); + JsonArray presets = root[ESPHOME_F("preset_modes")].to(); if (traits.supports_preset(CLIMATE_PRESET_HOME)) - presets.add("home"); + presets.add(ESPHOME_F("home")); if (traits.supports_preset(CLIMATE_PRESET_AWAY)) - presets.add("away"); + presets.add(ESPHOME_F("away")); if (traits.supports_preset(CLIMATE_PRESET_BOOST)) - presets.add("boost"); + presets.add(ESPHOME_F("boost")); if (traits.supports_preset(CLIMATE_PRESET_COMFORT)) - presets.add("comfort"); + presets.add(ESPHOME_F("comfort")); if (traits.supports_preset(CLIMATE_PRESET_ECO)) - presets.add("eco"); + presets.add(ESPHOME_F("eco")); if (traits.supports_preset(CLIMATE_PRESET_SLEEP)) - presets.add("sleep"); + presets.add(ESPHOME_F("sleep")); if (traits.supports_preset(CLIMATE_PRESET_ACTIVITY)) - presets.add("activity"); + presets.add(ESPHOME_F("activity")); for (const auto &preset : traits.get_supported_custom_presets()) presets.add(preset); } @@ -121,27 +120,27 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // fan_mode_state_topic root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); // fan_modes - JsonArray fan_modes = root["fan_modes"].to(); + JsonArray fan_modes = root[ESPHOME_F("fan_modes")].to(); if (traits.supports_fan_mode(CLIMATE_FAN_ON)) - fan_modes.add("on"); + fan_modes.add(ESPHOME_F("on")); if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) - fan_modes.add("off"); + fan_modes.add(ESPHOME_F("off")); if (traits.supports_fan_mode(CLIMATE_FAN_AUTO)) - fan_modes.add("auto"); + fan_modes.add(ESPHOME_F("auto")); if (traits.supports_fan_mode(CLIMATE_FAN_LOW)) - fan_modes.add("low"); + fan_modes.add(ESPHOME_F("low")); if (traits.supports_fan_mode(CLIMATE_FAN_MEDIUM)) - fan_modes.add("medium"); + fan_modes.add(ESPHOME_F("medium")); if (traits.supports_fan_mode(CLIMATE_FAN_HIGH)) - fan_modes.add("high"); + fan_modes.add(ESPHOME_F("high")); if (traits.supports_fan_mode(CLIMATE_FAN_MIDDLE)) - fan_modes.add("middle"); + fan_modes.add(ESPHOME_F("middle")); if (traits.supports_fan_mode(CLIMATE_FAN_FOCUS)) - fan_modes.add("focus"); + fan_modes.add(ESPHOME_F("focus")); if (traits.supports_fan_mode(CLIMATE_FAN_DIFFUSE)) - fan_modes.add("diffuse"); + fan_modes.add(ESPHOME_F("diffuse")); if (traits.supports_fan_mode(CLIMATE_FAN_QUIET)) - fan_modes.add("quiet"); + fan_modes.add(ESPHOME_F("quiet")); for (const auto &fan_mode : traits.get_supported_custom_fan_modes()) fan_modes.add(fan_mode); } @@ -152,15 +151,15 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // swing_mode_state_topic root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); // swing_modes - JsonArray swing_modes = root["swing_modes"].to(); + JsonArray swing_modes = root[ESPHOME_F("swing_modes")].to(); if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) - swing_modes.add("off"); + swing_modes.add(ESPHOME_F("off")); if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) - swing_modes.add("both"); + swing_modes.add(ESPHOME_F("both")); if (traits.supports_swing_mode(CLIMATE_SWING_VERTICAL)) - swing_modes.add("vertical"); + swing_modes.add(ESPHOME_F("vertical")); if (traits.supports_swing_mode(CLIMATE_SWING_HORIZONTAL)) - swing_modes.add("horizontal"); + swing_modes.add(ESPHOME_F("horizontal")); } config.state_topic = false; @@ -460,8 +459,7 @@ bool MQTTClimateComponent::publish_state_() { return success; } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index 4e54230e68..f561627ac9 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -8,8 +8,7 @@ #include "esphome/components/climate/climate.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTClimateComponent : public mqtt::MQTTComponent { public: @@ -49,8 +48,7 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { climate::Climate *device_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 1cd818964e..ccbdb2ea91 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -9,8 +9,7 @@ #include "mqtt_const.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.component"; @@ -154,7 +153,15 @@ bool MQTTComponent::send_discovery_() { device_info[MQTT_DEVICE_MANUFACTURER] = model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME); #else - device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time() + ")"; + static const char ver_fmt[] PROGMEM = ESPHOME_VERSION " (config hash 0x%08" PRIx32 ")"; +#ifdef USE_ESP8266 + char fmt_buf[sizeof(ver_fmt)]; + strcpy_P(fmt_buf, ver_fmt); + const char *fmt = fmt_buf; +#else + const char *fmt = ver_fmt; +#endif + device_info[MQTT_DEVICE_SW_VERSION] = str_sprintf(fmt, App.get_config_hash()); device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; #if defined(USE_ESP8266) || defined(USE_ESP32) device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif"; @@ -298,7 +305,6 @@ bool MQTTComponent::is_internal() { return this->get_entity()->is_internal(); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 2f8dfcf64e..e5f9664f77 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -11,8 +11,7 @@ #include "esphome/core/string_ref.h" #include "mqtt_client.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { /// Simple Helper struct used for Home Assistant MQTT send_discovery(). struct SendDiscoveryConfig { @@ -205,7 +204,6 @@ class MQTTComponent : public Component { bool resend_state_{false}; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_MQTt diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 3ddd8fc5cc..221af00371 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -1,547 +1,332 @@ #pragma once #include "esphome/core/defines.h" +#include "esphome/core/progmem.h" #ifdef USE_MQTT -namespace esphome { -namespace mqtt { +// MQTT JSON Key Constants for Home Assistant Discovery +// +// This file defines string constants used as JSON keys in MQTT discovery payloads +// for Home Assistant integration. These are used exclusively with ArduinoJson as: +// root[MQTT_DEVICE_CLASS] = "temperature"; +// +// Implementation: +// - ESP8266: Stores strings in PROGMEM (flash) using __FlashStringHelper* pointers. +// ArduinoJson recognizes this type and reads from flash memory. +// - Other platforms: Uses constexpr const char* for compile-time optimization. +// - USE_MQTT_ABBREVIATIONS: When defined, uses shortened key names to reduce message size. +// +// Adding new keys: +// Add a single line to MQTT_KEYS_LIST: X(MQTT_NEW_KEY, "abbr", "full_name") +// The X-macro will generate the appropriate constants for each platform. +// +// Note: Other MQTT_* constants (e.g., MQTT_CLIENT_CONNECTED, MQTT_LEGACY_UNIQUE_ID_GENERATOR) +// are C++ enums defined in mqtt_client.h and mqtt_backend*.h - unrelated to these JSON keys. +// X-macro list: MQTT_KEYS_LIST(X) calls X(name, abbr, full) for each key +// clang-format off +#define MQTT_KEYS_LIST(X) \ + X(MQTT_ACTION_TEMPLATE, "act_tpl", "action_template") \ + X(MQTT_ACTION_TOPIC, "act_t", "action_topic") \ + X(MQTT_AUTOMATION_TYPE, "atype", "automation_type") \ + X(MQTT_AUX_COMMAND_TOPIC, "aux_cmd_t", "aux_command_topic") \ + X(MQTT_AUX_STATE_TEMPLATE, "aux_stat_tpl", "aux_state_template") \ + X(MQTT_AUX_STATE_TOPIC, "aux_stat_t", "aux_state_topic") \ + X(MQTT_AVAILABILITY, "avty", "availability") \ + X(MQTT_AVAILABILITY_MODE, "avty_mode", "availability_mode") \ + X(MQTT_AVAILABILITY_TOPIC, "avty_t", "availability_topic") \ + X(MQTT_AWAY_MODE_COMMAND_TOPIC, "away_mode_cmd_t", "away_mode_command_topic") \ + X(MQTT_AWAY_MODE_STATE_TEMPLATE, "away_mode_stat_tpl", "away_mode_state_template") \ + X(MQTT_AWAY_MODE_STATE_TOPIC, "away_mode_stat_t", "away_mode_state_topic") \ + X(MQTT_BATTERY_LEVEL_TEMPLATE, "bat_lev_tpl", "battery_level_template") \ + X(MQTT_BATTERY_LEVEL_TOPIC, "bat_lev_t", "battery_level_topic") \ + X(MQTT_BLUE_TEMPLATE, "b_tpl", "blue_template") \ + X(MQTT_BRIGHTNESS_COMMAND_TOPIC, "bri_cmd_t", "brightness_command_topic") \ + X(MQTT_BRIGHTNESS_SCALE, "bri_scl", "brightness_scale") \ + X(MQTT_BRIGHTNESS_STATE_TOPIC, "bri_stat_t", "brightness_state_topic") \ + X(MQTT_BRIGHTNESS_TEMPLATE, "bri_tpl", "brightness_template") \ + X(MQTT_BRIGHTNESS_VALUE_TEMPLATE, "bri_val_tpl", "brightness_value_template") \ + X(MQTT_CHARGING_TEMPLATE, "chrg_tpl", "charging_template") \ + X(MQTT_CHARGING_TOPIC, "chrg_t", "charging_topic") \ + X(MQTT_CLEANING_TEMPLATE, "cln_tpl", "cleaning_template") \ + X(MQTT_CLEANING_TOPIC, "cln_t", "cleaning_topic") \ + X(MQTT_CODE_ARM_REQUIRED, "cod_arm_req", "code_arm_required") \ + X(MQTT_CODE_DISARM_REQUIRED, "cod_dis_req", "code_disarm_required") \ + X(MQTT_COLOR_MODE, "clrm", "color_mode") \ + X(MQTT_COLOR_MODE_STATE_TOPIC, "clrm_stat_t", "color_mode_state_topic") \ + X(MQTT_COLOR_MODE_VALUE_TEMPLATE, "clrm_val_tpl", "color_mode_value_template") \ + X(MQTT_COLOR_TEMP_COMMAND_TEMPLATE, "clr_temp_cmd_tpl", "color_temp_command_template") \ + X(MQTT_COLOR_TEMP_COMMAND_TOPIC, "clr_temp_cmd_t", "color_temp_command_topic") \ + X(MQTT_COLOR_TEMP_STATE_TOPIC, "clr_temp_stat_t", "color_temp_state_topic") \ + X(MQTT_COLOR_TEMP_TEMPLATE, "clr_temp_tpl", "color_temp_template") \ + X(MQTT_COLOR_TEMP_VALUE_TEMPLATE, "clr_temp_val_tpl", "color_temp_value_template") \ + X(MQTT_COMMAND_OFF_TEMPLATE, "cmd_off_tpl", "command_off_template") \ + X(MQTT_COMMAND_ON_TEMPLATE, "cmd_on_tpl", "command_on_template") \ + X(MQTT_COMMAND_RETAIN, "ret", "retain") \ + X(MQTT_COMMAND_TEMPLATE, "cmd_tpl", "command_template") \ + X(MQTT_COMMAND_TOPIC, "cmd_t", "command_topic") \ + X(MQTT_CONFIGURATION_URL, "cu", "configuration_url") \ + X(MQTT_CURRENT_HUMIDITY_TEMPLATE, "curr_hum_tpl", "current_humidity_template") \ + X(MQTT_CURRENT_HUMIDITY_TOPIC, "curr_hum_t", "current_humidity_topic") \ + X(MQTT_CURRENT_TEMPERATURE_STEP, "precision", "precision") \ + X(MQTT_CURRENT_TEMPERATURE_TEMPLATE, "curr_temp_tpl", "current_temperature_template") \ + X(MQTT_CURRENT_TEMPERATURE_TOPIC, "curr_temp_t", "current_temperature_topic") \ + X(MQTT_DEVICE, "dev", "device") \ + X(MQTT_DEVICE_CLASS, "dev_cla", "device_class") \ + X(MQTT_DEVICE_CONNECTIONS, "cns", "connections") \ + X(MQTT_DEVICE_IDENTIFIERS, "ids", "identifiers") \ + X(MQTT_DEVICE_MANUFACTURER, "mf", "manufacturer") \ + X(MQTT_DEVICE_MODEL, "mdl", "model") \ + X(MQTT_DEVICE_NAME, "name", "name") \ + X(MQTT_DEVICE_SUGGESTED_AREA, "sa", "suggested_area") \ + X(MQTT_DEVICE_SW_VERSION, "sw", "sw_version") \ + X(MQTT_DEVICE_HW_VERSION, "hw", "hw_version") \ + X(MQTT_DIRECTION_COMMAND_TOPIC, "dir_cmd_t", "direction_command_topic") \ + X(MQTT_DIRECTION_STATE_TOPIC, "dir_stat_t", "direction_state_topic") \ + X(MQTT_DOCKED_TEMPLATE, "dock_tpl", "docked_template") \ + X(MQTT_DOCKED_TOPIC, "dock_t", "docked_topic") \ + X(MQTT_EFFECT_COMMAND_TOPIC, "fx_cmd_t", "effect_command_topic") \ + X(MQTT_EFFECT_LIST, "fx_list", "effect_list") \ + X(MQTT_EFFECT_STATE_TOPIC, "fx_stat_t", "effect_state_topic") \ + X(MQTT_EFFECT_TEMPLATE, "fx_tpl", "effect_template") \ + X(MQTT_EFFECT_VALUE_TEMPLATE, "fx_val_tpl", "effect_value_template") \ + X(MQTT_ENABLED_BY_DEFAULT, "en", "enabled_by_default") \ + X(MQTT_ENTITY_CATEGORY, "ent_cat", "entity_category") \ + X(MQTT_ERROR_TEMPLATE, "err_tpl", "error_template") \ + X(MQTT_ERROR_TOPIC, "err_t", "error_topic") \ + X(MQTT_EVENT_TYPE, "event_type", "event_type") \ + X(MQTT_EVENT_TYPES, "evt_typ", "event_types") \ + X(MQTT_EXPIRE_AFTER, "exp_aft", "expire_after") \ + X(MQTT_FAN_MODE_COMMAND_TEMPLATE, "fan_mode_cmd_tpl", "fan_mode_command_template") \ + X(MQTT_FAN_MODE_COMMAND_TOPIC, "fan_mode_cmd_t", "fan_mode_command_topic") \ + X(MQTT_FAN_MODE_STATE_TEMPLATE, "fan_mode_stat_tpl", "fan_mode_state_template") \ + X(MQTT_FAN_MODE_STATE_TOPIC, "fan_mode_stat_t", "fan_mode_state_topic") \ + X(MQTT_FAN_SPEED_LIST, "fanspd_lst", "fan_speed_list") \ + X(MQTT_FAN_SPEED_TEMPLATE, "fanspd_tpl", "fan_speed_template") \ + X(MQTT_FAN_SPEED_TOPIC, "fanspd_t", "fan_speed_topic") \ + X(MQTT_FLASH_TIME_LONG, "flsh_tlng", "flash_time_long") \ + X(MQTT_FLASH_TIME_SHORT, "flsh_tsht", "flash_time_short") \ + X(MQTT_FORCE_UPDATE, "frc_upd", "force_update") \ + X(MQTT_GREEN_TEMPLATE, "g_tpl", "green_template") \ + X(MQTT_HOLD_COMMAND_TEMPLATE, "hold_cmd_tpl", "hold_command_template") \ + X(MQTT_HOLD_COMMAND_TOPIC, "hold_cmd_t", "hold_command_topic") \ + X(MQTT_HOLD_STATE_TEMPLATE, "hold_stat_tpl", "hold_state_template") \ + X(MQTT_HOLD_STATE_TOPIC, "hold_stat_t", "hold_state_topic") \ + X(MQTT_HS_COMMAND_TOPIC, "hs_cmd_t", "hs_command_topic") \ + X(MQTT_HS_STATE_TOPIC, "hs_stat_t", "hs_state_topic") \ + X(MQTT_HS_VALUE_TEMPLATE, "hs_val_tpl", "hs_value_template") \ + X(MQTT_ICON, "ic", "icon") \ + X(MQTT_INITIAL, "init", "initial") \ + X(MQTT_JSON_ATTRIBUTES, "json_attr", "json_attributes") \ + X(MQTT_JSON_ATTRIBUTES_TEMPLATE, "json_attr_tpl", "json_attributes_template") \ + X(MQTT_JSON_ATTRIBUTES_TOPIC, "json_attr_t", "json_attributes_topic") \ + X(MQTT_LAST_RESET_TOPIC, "lrst_t", "last_reset_topic") \ + X(MQTT_LAST_RESET_VALUE_TEMPLATE, "lrst_val_tpl", "last_reset_value_template") \ + X(MQTT_MAX, "max", "max") \ + X(MQTT_MAX_HUMIDITY, "max_hum", "max_humidity") \ + X(MQTT_MAX_MIREDS, "max_mirs", "max_mireds") \ + X(MQTT_MAX_TEMP, "max_temp", "max_temp") \ + X(MQTT_MIN, "min", "min") \ + X(MQTT_MIN_HUMIDITY, "min_hum", "min_humidity") \ + X(MQTT_MIN_MIREDS, "min_mirs", "min_mireds") \ + X(MQTT_MIN_TEMP, "min_temp", "min_temp") \ + X(MQTT_MODE, "mode", "mode") \ + X(MQTT_MODE_COMMAND_TEMPLATE, "mode_cmd_tpl", "mode_command_template") \ + X(MQTT_MODE_COMMAND_TOPIC, "mode_cmd_t", "mode_command_topic") \ + X(MQTT_MODE_STATE_TEMPLATE, "mode_stat_tpl", "mode_state_template") \ + X(MQTT_MODE_STATE_TOPIC, "mode_stat_t", "mode_state_topic") \ + X(MQTT_MODES, "modes", "modes") \ + X(MQTT_NAME, "name", "name") \ + X(MQTT_OBJECT_ID, "obj_id", "object_id") \ + X(MQTT_OFF_DELAY, "off_dly", "off_delay") \ + X(MQTT_ON_COMMAND_TYPE, "on_cmd_type", "on_command_type") \ + X(MQTT_OPTIMISTIC, "opt", "optimistic") \ + X(MQTT_OPTIONS, "ops", "options") \ + X(MQTT_OSCILLATION_COMMAND_TEMPLATE, "osc_cmd_tpl", "oscillation_command_template") \ + X(MQTT_OSCILLATION_COMMAND_TOPIC, "osc_cmd_t", "oscillation_command_topic") \ + X(MQTT_OSCILLATION_STATE_TOPIC, "osc_stat_t", "oscillation_state_topic") \ + X(MQTT_OSCILLATION_VALUE_TEMPLATE, "osc_val_tpl", "oscillation_value_template") \ + X(MQTT_PAYLOAD, "pl", "payload") \ + X(MQTT_PAYLOAD_ARM_AWAY, "pl_arm_away", "payload_arm_away") \ + X(MQTT_PAYLOAD_ARM_CUSTOM_BYPASS, "pl_arm_custom_b", "payload_arm_custom_bypass") \ + X(MQTT_PAYLOAD_ARM_HOME, "pl_arm_home", "payload_arm_home") \ + X(MQTT_PAYLOAD_ARM_NIGHT, "pl_arm_nite", "payload_arm_night") \ + X(MQTT_PAYLOAD_ARM_VACATION, "pl_arm_vacation", "payload_arm_vacation") \ + X(MQTT_PAYLOAD_AVAILABLE, "pl_avail", "payload_available") \ + X(MQTT_PAYLOAD_CLEAN_SPOT, "pl_cln_sp", "payload_clean_spot") \ + X(MQTT_PAYLOAD_CLOSE, "pl_cls", "payload_close") \ + X(MQTT_PAYLOAD_DISARM, "pl_disarm", "payload_disarm") \ + X(MQTT_PAYLOAD_HIGH_SPEED, "pl_hi_spd", "payload_high_speed") \ + X(MQTT_PAYLOAD_HOME, "pl_home", "payload_home") \ + X(MQTT_PAYLOAD_INSTALL, "pl_inst", "payload_install") \ + X(MQTT_PAYLOAD_LOCATE, "pl_loc", "payload_locate") \ + X(MQTT_PAYLOAD_LOCK, "pl_lock", "payload_lock") \ + X(MQTT_PAYLOAD_LOW_SPEED, "pl_lo_spd", "payload_low_speed") \ + X(MQTT_PAYLOAD_MEDIUM_SPEED, "pl_med_spd", "payload_medium_speed") \ + X(MQTT_PAYLOAD_NOT_AVAILABLE, "pl_not_avail", "payload_not_available") \ + X(MQTT_PAYLOAD_NOT_HOME, "pl_not_home", "payload_not_home") \ + X(MQTT_PAYLOAD_OFF, "pl_off", "payload_off") \ + X(MQTT_PAYLOAD_OFF_SPEED, "pl_off_spd", "payload_off_speed") \ + X(MQTT_PAYLOAD_ON, "pl_on", "payload_on") \ + X(MQTT_PAYLOAD_OPEN, "pl_open", "payload_open") \ + X(MQTT_PAYLOAD_OSCILLATION_OFF, "pl_osc_off", "payload_oscillation_off") \ + X(MQTT_PAYLOAD_OSCILLATION_ON, "pl_osc_on", "payload_oscillation_on") \ + X(MQTT_PAYLOAD_PAUSE, "pl_paus", "payload_pause") \ + X(MQTT_PAYLOAD_RESET, "pl_rst", "payload_reset") \ + X(MQTT_PAYLOAD_RESET_HUMIDITY, "pl_rst_hum", "payload_reset_humidity") \ + X(MQTT_PAYLOAD_RESET_MODE, "pl_rst_mode", "payload_reset_mode") \ + X(MQTT_PAYLOAD_RESET_PERCENTAGE, "pl_rst_pct", "payload_reset_percentage") \ + X(MQTT_PAYLOAD_RESET_PRESET_MODE, "pl_rst_pr_mode", "payload_reset_preset_mode") \ + X(MQTT_PAYLOAD_RETURN_TO_BASE, "pl_ret", "payload_return_to_base") \ + X(MQTT_PAYLOAD_START, "pl_strt", "payload_start") \ + X(MQTT_PAYLOAD_START_PAUSE, "pl_stpa", "payload_start_pause") \ + X(MQTT_PAYLOAD_STOP, "pl_stop", "payload_stop") \ + X(MQTT_PAYLOAD_TURN_OFF, "pl_toff", "payload_turn_off") \ + X(MQTT_PAYLOAD_TURN_ON, "pl_ton", "payload_turn_on") \ + X(MQTT_PAYLOAD_UNLOCK, "pl_unlk", "payload_unlock") \ + X(MQTT_PERCENTAGE_COMMAND_TEMPLATE, "pct_cmd_tpl", "percentage_command_template") \ + X(MQTT_PERCENTAGE_COMMAND_TOPIC, "pct_cmd_t", "percentage_command_topic") \ + X(MQTT_PERCENTAGE_STATE_TOPIC, "pct_stat_t", "percentage_state_topic") \ + X(MQTT_PERCENTAGE_VALUE_TEMPLATE, "pct_val_tpl", "percentage_value_template") \ + X(MQTT_POSITION_CLOSED, "pos_clsd", "position_closed") \ + X(MQTT_POSITION_OPEN, "pos_open", "position_open") \ + X(MQTT_POSITION_TEMPLATE, "pos_tpl", "position_template") \ + X(MQTT_POSITION_TOPIC, "pos_t", "position_topic") \ + X(MQTT_POWER_COMMAND_TOPIC, "pow_cmd_t", "power_command_topic") \ + X(MQTT_POWER_STATE_TEMPLATE, "pow_stat_tpl", "power_state_template") \ + X(MQTT_POWER_STATE_TOPIC, "pow_stat_t", "power_state_topic") \ + X(MQTT_PRESET_MODE_COMMAND_TEMPLATE, "pr_mode_cmd_tpl", "preset_mode_command_template") \ + X(MQTT_PRESET_MODE_COMMAND_TOPIC, "pr_mode_cmd_t", "preset_mode_command_topic") \ + X(MQTT_PRESET_MODE_STATE_TOPIC, "pr_mode_stat_t", "preset_mode_state_topic") \ + X(MQTT_PRESET_MODE_VALUE_TEMPLATE, "pr_mode_val_tpl", "preset_mode_value_template") \ + X(MQTT_PRESET_MODES, "pr_modes", "preset_modes") \ + X(MQTT_QOS, "qos", "qos") \ + X(MQTT_RED_TEMPLATE, "r_tpl", "red_template") \ + X(MQTT_RETAIN, "ret", "retain") \ + X(MQTT_RGB_COMMAND_TEMPLATE, "rgb_cmd_tpl", "rgb_command_template") \ + X(MQTT_RGB_COMMAND_TOPIC, "rgb_cmd_t", "rgb_command_topic") \ + X(MQTT_RGB_STATE_TOPIC, "rgb_stat_t", "rgb_state_topic") \ + X(MQTT_RGB_VALUE_TEMPLATE, "rgb_val_tpl", "rgb_value_template") \ + X(MQTT_RGBW_COMMAND_TEMPLATE, "rgbw_cmd_tpl", "rgbw_command_template") \ + X(MQTT_RGBW_COMMAND_TOPIC, "rgbw_cmd_t", "rgbw_command_topic") \ + X(MQTT_RGBW_STATE_TOPIC, "rgbw_stat_t", "rgbw_state_topic") \ + X(MQTT_RGBW_VALUE_TEMPLATE, "rgbw_val_tpl", "rgbw_value_template") \ + X(MQTT_RGBWW_COMMAND_TEMPLATE, "rgbww_cmd_tpl", "rgbww_command_template") \ + X(MQTT_RGBWW_COMMAND_TOPIC, "rgbww_cmd_t", "rgbww_command_topic") \ + X(MQTT_RGBWW_STATE_TOPIC, "rgbww_stat_t", "rgbww_state_topic") \ + X(MQTT_RGBWW_VALUE_TEMPLATE, "rgbww_val_tpl", "rgbww_value_template") \ + X(MQTT_SEND_COMMAND_TOPIC, "send_cmd_t", "send_command_topic") \ + X(MQTT_SEND_IF_OFF, "send_if_off", "send_if_off") \ + X(MQTT_SET_FAN_SPEED_TOPIC, "set_fan_spd_t", "set_fan_speed_topic") \ + X(MQTT_SET_POSITION_TEMPLATE, "set_pos_tpl", "set_position_template") \ + X(MQTT_SET_POSITION_TOPIC, "set_pos_t", "set_position_topic") \ + X(MQTT_SOURCE_TYPE, "src_type", "source_type") \ + X(MQTT_SPEED_COMMAND_TOPIC, "spd_cmd_t", "speed_command_topic") \ + X(MQTT_SPEED_RANGE_MAX, "spd_rng_max", "speed_range_max") \ + X(MQTT_SPEED_RANGE_MIN, "spd_rng_min", "speed_range_min") \ + X(MQTT_SPEED_STATE_TOPIC, "spd_stat_t", "speed_state_topic") \ + X(MQTT_SPEED_VALUE_TEMPLATE, "spd_val_tpl", "speed_value_template") \ + X(MQTT_SPEEDS, "spds", "speeds") \ + X(MQTT_STATE_CLASS, "stat_cla", "state_class") \ + X(MQTT_STATE_CLOSED, "stat_clsd", "state_closed") \ + X(MQTT_STATE_CLOSING, "stat_closing", "state_closing") \ + X(MQTT_STATE_LOCKED, "stat_locked", "state_locked") \ + X(MQTT_STATE_OFF, "stat_off", "state_off") \ + X(MQTT_STATE_ON, "stat_on", "state_on") \ + X(MQTT_STATE_OPEN, "stat_open", "state_open") \ + X(MQTT_STATE_OPENING, "stat_opening", "state_opening") \ + X(MQTT_STATE_STOPPED, "stat_stopped", "state_stopped") \ + X(MQTT_STATE_TEMPLATE, "stat_tpl", "state_template") \ + X(MQTT_STATE_TOPIC, "stat_t", "state_topic") \ + X(MQTT_STATE_UNLOCKED, "stat_unlocked", "state_unlocked") \ + X(MQTT_STATE_VALUE_TEMPLATE, "stat_val_tpl", "state_value_template") \ + X(MQTT_STEP, "step", "step") \ + X(MQTT_SUBTYPE, "stype", "subtype") \ + X(MQTT_SUPPORTED_COLOR_MODES, "sup_clrm", "supported_color_modes") \ + X(MQTT_SUPPORTED_FEATURES, "sup_feat", "supported_features") \ + X(MQTT_SWING_MODE_COMMAND_TEMPLATE, "swing_mode_cmd_tpl", "swing_mode_command_template") \ + X(MQTT_SWING_MODE_COMMAND_TOPIC, "swing_mode_cmd_t", "swing_mode_command_topic") \ + X(MQTT_SWING_MODE_STATE_TEMPLATE, "swing_mode_stat_tpl", "swing_mode_state_template") \ + X(MQTT_SWING_MODE_STATE_TOPIC, "swing_mode_stat_t", "swing_mode_state_topic") \ + X(MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE, "hum_cmd_tpl", "target_humidity_command_template") \ + X(MQTT_TARGET_HUMIDITY_COMMAND_TOPIC, "hum_cmd_t", "target_humidity_command_topic") \ + X(MQTT_TARGET_HUMIDITY_STATE_TEMPLATE, "hum_state_tpl", "target_humidity_state_template") \ + X(MQTT_TARGET_HUMIDITY_STATE_TOPIC, "hum_stat_t", "target_humidity_state_topic") \ + X(MQTT_TARGET_TEMPERATURE_STEP, "temp_step", "temp_step") \ + X(MQTT_TEMPERATURE_COMMAND_TEMPLATE, "temp_cmd_tpl", "temperature_command_template") \ + X(MQTT_TEMPERATURE_COMMAND_TOPIC, "temp_cmd_t", "temperature_command_topic") \ + X(MQTT_TEMPERATURE_HIGH_COMMAND_TEMPLATE, "temp_hi_cmd_tpl", "temperature_high_command_template") \ + X(MQTT_TEMPERATURE_HIGH_COMMAND_TOPIC, "temp_hi_cmd_t", "temperature_high_command_topic") \ + X(MQTT_TEMPERATURE_HIGH_STATE_TEMPLATE, "temp_hi_stat_tpl", "temperature_high_state_template") \ + X(MQTT_TEMPERATURE_HIGH_STATE_TOPIC, "temp_hi_stat_t", "temperature_high_state_topic") \ + X(MQTT_TEMPERATURE_LOW_COMMAND_TEMPLATE, "temp_lo_cmd_tpl", "temperature_low_command_template") \ + X(MQTT_TEMPERATURE_LOW_COMMAND_TOPIC, "temp_lo_cmd_t", "temperature_low_command_topic") \ + X(MQTT_TEMPERATURE_LOW_STATE_TEMPLATE, "temp_lo_stat_tpl", "temperature_low_state_template") \ + X(MQTT_TEMPERATURE_LOW_STATE_TOPIC, "temp_lo_stat_t", "temperature_low_state_topic") \ + X(MQTT_TEMPERATURE_STATE_TEMPLATE, "temp_stat_tpl", "temperature_state_template") \ + X(MQTT_TEMPERATURE_STATE_TOPIC, "temp_stat_t", "temperature_state_topic") \ + X(MQTT_TEMPERATURE_UNIT, "temp_unit", "temperature_unit") \ + X(MQTT_TILT_CLOSED_VALUE, "tilt_clsd_val", "tilt_closed_value") \ + X(MQTT_TILT_COMMAND_TEMPLATE, "tilt_cmd_tpl", "tilt_command_template") \ + X(MQTT_TILT_COMMAND_TOPIC, "tilt_cmd_t", "tilt_command_topic") \ + X(MQTT_TILT_INVERT_STATE, "tilt_inv_stat", "tilt_invert_state") \ + X(MQTT_TILT_MAX, "tilt_max", "tilt_max") \ + X(MQTT_TILT_MIN, "tilt_min", "tilt_min") \ + X(MQTT_TILT_OPENED_VALUE, "tilt_opnd_val", "tilt_opened_value") \ + X(MQTT_TILT_OPTIMISTIC, "tilt_opt", "tilt_optimistic") \ + X(MQTT_TILT_STATUS_TEMPLATE, "tilt_status_tpl", "tilt_status_template") \ + X(MQTT_TILT_STATUS_TOPIC, "tilt_status_t", "tilt_status_topic") \ + X(MQTT_TOPIC, "t", "topic") \ + X(MQTT_UNIQUE_ID, "uniq_id", "unique_id") \ + X(MQTT_UNIT_OF_MEASUREMENT, "unit_of_meas", "unit_of_measurement") \ + X(MQTT_VALUE_TEMPLATE, "val_tpl", "value_template") \ + X(MQTT_WHITE_COMMAND_TOPIC, "whit_cmd_t", "white_command_topic") \ + X(MQTT_WHITE_SCALE, "whit_scl", "white_scale") \ + X(MQTT_WHITE_VALUE_COMMAND_TOPIC, "whit_val_cmd_t", "white_value_command_topic") \ + X(MQTT_WHITE_VALUE_SCALE, "whit_val_scl", "white_value_scale") \ + X(MQTT_WHITE_VALUE_STATE_TOPIC, "whit_val_stat_t", "white_value_state_topic") \ + X(MQTT_WHITE_VALUE_TEMPLATE, "whit_val_tpl", "white_value_template") \ + X(MQTT_XY_COMMAND_TOPIC, "xy_cmd_t", "xy_command_topic") \ + X(MQTT_XY_STATE_TOPIC, "xy_stat_t", "xy_state_topic") \ + X(MQTT_XY_VALUE_TEMPLATE, "xy_val_tpl", "xy_value_template") +// clang-format on + +#ifdef USE_ESP8266 +// ESP8266: Store strings in PROGMEM (flash) and expose as __FlashStringHelper* pointers. +// ArduinoJson recognizes this type and reads from flash memory. +namespace esphome::mqtt { + +// Generate PROGMEM data arrays #ifdef USE_MQTT_ABBREVIATIONS - -constexpr const char *const MQTT_ACTION_TEMPLATE = "act_tpl"; -constexpr const char *const MQTT_ACTION_TOPIC = "act_t"; -constexpr const char *const MQTT_AUTOMATION_TYPE = "atype"; -constexpr const char *const MQTT_AUX_COMMAND_TOPIC = "aux_cmd_t"; -constexpr const char *const MQTT_AUX_STATE_TEMPLATE = "aux_stat_tpl"; -constexpr const char *const MQTT_AUX_STATE_TOPIC = "aux_stat_t"; -constexpr const char *const MQTT_AVAILABILITY = "avty"; -constexpr const char *const MQTT_AVAILABILITY_MODE = "avty_mode"; -constexpr const char *const MQTT_AVAILABILITY_TOPIC = "avty_t"; -constexpr const char *const MQTT_AWAY_MODE_COMMAND_TOPIC = "away_mode_cmd_t"; -constexpr const char *const MQTT_AWAY_MODE_STATE_TEMPLATE = "away_mode_stat_tpl"; -constexpr const char *const MQTT_AWAY_MODE_STATE_TOPIC = "away_mode_stat_t"; -constexpr const char *const MQTT_BATTERY_LEVEL_TEMPLATE = "bat_lev_tpl"; -constexpr const char *const MQTT_BATTERY_LEVEL_TOPIC = "bat_lev_t"; -constexpr const char *const MQTT_BLUE_TEMPLATE = "b_tpl"; -constexpr const char *const MQTT_BRIGHTNESS_COMMAND_TOPIC = "bri_cmd_t"; -constexpr const char *const MQTT_BRIGHTNESS_SCALE = "bri_scl"; -constexpr const char *const MQTT_BRIGHTNESS_STATE_TOPIC = "bri_stat_t"; -constexpr const char *const MQTT_BRIGHTNESS_TEMPLATE = "bri_tpl"; -constexpr const char *const MQTT_BRIGHTNESS_VALUE_TEMPLATE = "bri_val_tpl"; -constexpr const char *const MQTT_CHARGING_TEMPLATE = "chrg_tpl"; -constexpr const char *const MQTT_CHARGING_TOPIC = "chrg_t"; -constexpr const char *const MQTT_CLEANING_TEMPLATE = "cln_tpl"; -constexpr const char *const MQTT_CLEANING_TOPIC = "cln_t"; -constexpr const char *const MQTT_CODE_ARM_REQUIRED = "cod_arm_req"; -constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "cod_dis_req"; -constexpr const char *const MQTT_COLOR_MODE = "clrm"; -constexpr const char *const MQTT_COLOR_MODE_STATE_TOPIC = "clrm_stat_t"; -constexpr const char *const MQTT_COLOR_MODE_VALUE_TEMPLATE = "clrm_val_tpl"; -constexpr const char *const MQTT_COLOR_TEMP_COMMAND_TEMPLATE = "clr_temp_cmd_tpl"; -constexpr const char *const MQTT_COLOR_TEMP_COMMAND_TOPIC = "clr_temp_cmd_t"; -constexpr const char *const MQTT_COLOR_TEMP_STATE_TOPIC = "clr_temp_stat_t"; -constexpr const char *const MQTT_COLOR_TEMP_TEMPLATE = "clr_temp_tpl"; -constexpr const char *const MQTT_COLOR_TEMP_VALUE_TEMPLATE = "clr_temp_val_tpl"; -constexpr const char *const MQTT_COMMAND_OFF_TEMPLATE = "cmd_off_tpl"; -constexpr const char *const MQTT_COMMAND_ON_TEMPLATE = "cmd_on_tpl"; -constexpr const char *const MQTT_COMMAND_RETAIN = "ret"; -constexpr const char *const MQTT_COMMAND_TEMPLATE = "cmd_tpl"; -constexpr const char *const MQTT_COMMAND_TOPIC = "cmd_t"; -constexpr const char *const MQTT_CONFIGURATION_URL = "cu"; -constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "curr_hum_tpl"; -constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "curr_hum_t"; -constexpr const char *const MQTT_CURRENT_TEMPERATURE_STEP = "precision"; -constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "curr_temp_tpl"; -constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "curr_temp_t"; -constexpr const char *const MQTT_DEVICE = "dev"; -constexpr const char *const MQTT_DEVICE_CLASS = "dev_cla"; -constexpr const char *const MQTT_DEVICE_CONNECTIONS = "cns"; -constexpr const char *const MQTT_DEVICE_IDENTIFIERS = "ids"; -constexpr const char *const MQTT_DEVICE_MANUFACTURER = "mf"; -constexpr const char *const MQTT_DEVICE_MODEL = "mdl"; -constexpr const char *const MQTT_DEVICE_NAME = "name"; -constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "sa"; -constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw"; -constexpr const char *const MQTT_DEVICE_HW_VERSION = "hw"; -constexpr const char *const MQTT_DIRECTION_COMMAND_TOPIC = "dir_cmd_t"; -constexpr const char *const MQTT_DIRECTION_STATE_TOPIC = "dir_stat_t"; -constexpr const char *const MQTT_DOCKED_TEMPLATE = "dock_tpl"; -constexpr const char *const MQTT_DOCKED_TOPIC = "dock_t"; -constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "fx_cmd_t"; -constexpr const char *const MQTT_EFFECT_LIST = "fx_list"; -constexpr const char *const MQTT_EFFECT_STATE_TOPIC = "fx_stat_t"; -constexpr const char *const MQTT_EFFECT_TEMPLATE = "fx_tpl"; -constexpr const char *const MQTT_EFFECT_VALUE_TEMPLATE = "fx_val_tpl"; -constexpr const char *const MQTT_ENABLED_BY_DEFAULT = "en"; -constexpr const char *const MQTT_ENTITY_CATEGORY = "ent_cat"; -constexpr const char *const MQTT_ERROR_TEMPLATE = "err_tpl"; -constexpr const char *const MQTT_ERROR_TOPIC = "err_t"; -constexpr const char *const MQTT_EVENT_TYPE = "event_type"; -constexpr const char *const MQTT_EVENT_TYPES = "evt_typ"; -constexpr const char *const MQTT_EXPIRE_AFTER = "exp_aft"; -constexpr const char *const MQTT_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_cmd_tpl"; -constexpr const char *const MQTT_FAN_MODE_COMMAND_TOPIC = "fan_mode_cmd_t"; -constexpr const char *const MQTT_FAN_MODE_STATE_TEMPLATE = "fan_mode_stat_tpl"; -constexpr const char *const MQTT_FAN_MODE_STATE_TOPIC = "fan_mode_stat_t"; -constexpr const char *const MQTT_FAN_SPEED_LIST = "fanspd_lst"; -constexpr const char *const MQTT_FAN_SPEED_TEMPLATE = "fanspd_tpl"; -constexpr const char *const MQTT_FAN_SPEED_TOPIC = "fanspd_t"; -constexpr const char *const MQTT_FLASH_TIME_LONG = "flsh_tlng"; -constexpr const char *const MQTT_FLASH_TIME_SHORT = "flsh_tsht"; -constexpr const char *const MQTT_FORCE_UPDATE = "frc_upd"; -constexpr const char *const MQTT_GREEN_TEMPLATE = "g_tpl"; -constexpr const char *const MQTT_HOLD_COMMAND_TEMPLATE = "hold_cmd_tpl"; -constexpr const char *const MQTT_HOLD_COMMAND_TOPIC = "hold_cmd_t"; -constexpr const char *const MQTT_HOLD_STATE_TEMPLATE = "hold_stat_tpl"; -constexpr const char *const MQTT_HOLD_STATE_TOPIC = "hold_stat_t"; -constexpr const char *const MQTT_HS_COMMAND_TOPIC = "hs_cmd_t"; -constexpr const char *const MQTT_HS_STATE_TOPIC = "hs_stat_t"; -constexpr const char *const MQTT_HS_VALUE_TEMPLATE = "hs_val_tpl"; -constexpr const char *const MQTT_ICON = "ic"; -constexpr const char *const MQTT_INITIAL = "init"; -constexpr const char *const MQTT_JSON_ATTRIBUTES = "json_attr"; -constexpr const char *const MQTT_JSON_ATTRIBUTES_TEMPLATE = "json_attr_tpl"; -constexpr const char *const MQTT_JSON_ATTRIBUTES_TOPIC = "json_attr_t"; -constexpr const char *const MQTT_LAST_RESET_TOPIC = "lrst_t"; -constexpr const char *const MQTT_LAST_RESET_VALUE_TEMPLATE = "lrst_val_tpl"; -constexpr const char *const MQTT_MAX = "max"; -constexpr const char *const MQTT_MAX_HUMIDITY = "max_hum"; -constexpr const char *const MQTT_MAX_MIREDS = "max_mirs"; -constexpr const char *const MQTT_MAX_TEMP = "max_temp"; -constexpr const char *const MQTT_MIN = "min"; -constexpr const char *const MQTT_MIN_HUMIDITY = "min_hum"; -constexpr const char *const MQTT_MIN_MIREDS = "min_mirs"; -constexpr const char *const MQTT_MIN_TEMP = "min_temp"; -constexpr const char *const MQTT_MODE = "mode"; -constexpr const char *const MQTT_MODE_COMMAND_TEMPLATE = "mode_cmd_tpl"; -constexpr const char *const MQTT_MODE_COMMAND_TOPIC = "mode_cmd_t"; -constexpr const char *const MQTT_MODE_STATE_TEMPLATE = "mode_stat_tpl"; -constexpr const char *const MQTT_MODE_STATE_TOPIC = "mode_stat_t"; -constexpr const char *const MQTT_MODES = "modes"; -constexpr const char *const MQTT_NAME = "name"; -constexpr const char *const MQTT_OBJECT_ID = "obj_id"; -constexpr const char *const MQTT_OFF_DELAY = "off_dly"; -constexpr const char *const MQTT_ON_COMMAND_TYPE = "on_cmd_type"; -constexpr const char *const MQTT_OPTIMISTIC = "opt"; -constexpr const char *const MQTT_OPTIONS = "ops"; -constexpr const char *const MQTT_OSCILLATION_COMMAND_TEMPLATE = "osc_cmd_tpl"; -constexpr const char *const MQTT_OSCILLATION_COMMAND_TOPIC = "osc_cmd_t"; -constexpr const char *const MQTT_OSCILLATION_STATE_TOPIC = "osc_stat_t"; -constexpr const char *const MQTT_OSCILLATION_VALUE_TEMPLATE = "osc_val_tpl"; -constexpr const char *const MQTT_PAYLOAD = "pl"; -constexpr const char *const MQTT_PAYLOAD_ARM_AWAY = "pl_arm_away"; -constexpr const char *const MQTT_PAYLOAD_ARM_CUSTOM_BYPASS = "pl_arm_custom_b"; -constexpr const char *const MQTT_PAYLOAD_ARM_HOME = "pl_arm_home"; -constexpr const char *const MQTT_PAYLOAD_ARM_NIGHT = "pl_arm_nite"; -constexpr const char *const MQTT_PAYLOAD_ARM_VACATION = "pl_arm_vacation"; -constexpr const char *const MQTT_PAYLOAD_AVAILABLE = "pl_avail"; -constexpr const char *const MQTT_PAYLOAD_CLEAN_SPOT = "pl_cln_sp"; -constexpr const char *const MQTT_PAYLOAD_CLOSE = "pl_cls"; -constexpr const char *const MQTT_PAYLOAD_DISARM = "pl_disarm"; -constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "pl_hi_spd"; -constexpr const char *const MQTT_PAYLOAD_HOME = "pl_home"; -constexpr const char *const MQTT_PAYLOAD_INSTALL = "pl_inst"; -constexpr const char *const MQTT_PAYLOAD_LOCATE = "pl_loc"; -constexpr const char *const MQTT_PAYLOAD_LOCK = "pl_lock"; -constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "pl_lo_spd"; -constexpr const char *const MQTT_PAYLOAD_MEDIUM_SPEED = "pl_med_spd"; -constexpr const char *const MQTT_PAYLOAD_NOT_AVAILABLE = "pl_not_avail"; -constexpr const char *const MQTT_PAYLOAD_NOT_HOME = "pl_not_home"; -constexpr const char *const MQTT_PAYLOAD_OFF = "pl_off"; -constexpr const char *const MQTT_PAYLOAD_OFF_SPEED = "pl_off_spd"; -constexpr const char *const MQTT_PAYLOAD_ON = "pl_on"; -constexpr const char *const MQTT_PAYLOAD_OPEN = "pl_open"; -constexpr const char *const MQTT_PAYLOAD_OSCILLATION_OFF = "pl_osc_off"; -constexpr const char *const MQTT_PAYLOAD_OSCILLATION_ON = "pl_osc_on"; -constexpr const char *const MQTT_PAYLOAD_PAUSE = "pl_paus"; -constexpr const char *const MQTT_PAYLOAD_RESET = "pl_rst"; -constexpr const char *const MQTT_PAYLOAD_RESET_HUMIDITY = "pl_rst_hum"; -constexpr const char *const MQTT_PAYLOAD_RESET_MODE = "pl_rst_mode"; -constexpr const char *const MQTT_PAYLOAD_RESET_PERCENTAGE = "pl_rst_pct"; -constexpr const char *const MQTT_PAYLOAD_RESET_PRESET_MODE = "pl_rst_pr_mode"; -constexpr const char *const MQTT_PAYLOAD_RETURN_TO_BASE = "pl_ret"; -constexpr const char *const MQTT_PAYLOAD_START = "pl_strt"; -constexpr const char *const MQTT_PAYLOAD_START_PAUSE = "pl_stpa"; -constexpr const char *const MQTT_PAYLOAD_STOP = "pl_stop"; -constexpr const char *const MQTT_PAYLOAD_TURN_OFF = "pl_toff"; -constexpr const char *const MQTT_PAYLOAD_TURN_ON = "pl_ton"; -constexpr const char *const MQTT_PAYLOAD_UNLOCK = "pl_unlk"; -constexpr const char *const MQTT_PERCENTAGE_COMMAND_TEMPLATE = "pct_cmd_tpl"; -constexpr const char *const MQTT_PERCENTAGE_COMMAND_TOPIC = "pct_cmd_t"; -constexpr const char *const MQTT_PERCENTAGE_STATE_TOPIC = "pct_stat_t"; -constexpr const char *const MQTT_PERCENTAGE_VALUE_TEMPLATE = "pct_val_tpl"; -constexpr const char *const MQTT_POSITION_CLOSED = "pos_clsd"; -constexpr const char *const MQTT_POSITION_OPEN = "pos_open"; -constexpr const char *const MQTT_POSITION_TEMPLATE = "pos_tpl"; -constexpr const char *const MQTT_POSITION_TOPIC = "pos_t"; -constexpr const char *const MQTT_POWER_COMMAND_TOPIC = "pow_cmd_t"; -constexpr const char *const MQTT_POWER_STATE_TEMPLATE = "pow_stat_tpl"; -constexpr const char *const MQTT_POWER_STATE_TOPIC = "pow_stat_t"; -constexpr const char *const MQTT_PRESET_MODE_COMMAND_TEMPLATE = "pr_mode_cmd_tpl"; -constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC = "pr_mode_cmd_t"; -constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC = "pr_mode_stat_t"; -constexpr const char *const MQTT_PRESET_MODE_VALUE_TEMPLATE = "pr_mode_val_tpl"; -constexpr const char *const MQTT_PRESET_MODES = "pr_modes"; -constexpr const char *const MQTT_QOS = "qos"; -constexpr const char *const MQTT_RED_TEMPLATE = "r_tpl"; -constexpr const char *const MQTT_RETAIN = "ret"; -constexpr const char *const MQTT_RGB_COMMAND_TEMPLATE = "rgb_cmd_tpl"; -constexpr const char *const MQTT_RGB_COMMAND_TOPIC = "rgb_cmd_t"; -constexpr const char *const MQTT_RGB_STATE_TOPIC = "rgb_stat_t"; -constexpr const char *const MQTT_RGB_VALUE_TEMPLATE = "rgb_val_tpl"; -constexpr const char *const MQTT_RGBW_COMMAND_TEMPLATE = "rgbw_cmd_tpl"; -constexpr const char *const MQTT_RGBW_COMMAND_TOPIC = "rgbw_cmd_t"; -constexpr const char *const MQTT_RGBW_STATE_TOPIC = "rgbw_stat_t"; -constexpr const char *const MQTT_RGBW_VALUE_TEMPLATE = "rgbw_val_tpl"; -constexpr const char *const MQTT_RGBWW_COMMAND_TEMPLATE = "rgbww_cmd_tpl"; -constexpr const char *const MQTT_RGBWW_COMMAND_TOPIC = "rgbww_cmd_t"; -constexpr const char *const MQTT_RGBWW_STATE_TOPIC = "rgbww_stat_t"; -constexpr const char *const MQTT_RGBWW_VALUE_TEMPLATE = "rgbww_val_tpl"; -constexpr const char *const MQTT_SEND_COMMAND_TOPIC = "send_cmd_t"; -constexpr const char *const MQTT_SEND_IF_OFF = "send_if_off"; -constexpr const char *const MQTT_SET_FAN_SPEED_TOPIC = "set_fan_spd_t"; -constexpr const char *const MQTT_SET_POSITION_TEMPLATE = "set_pos_tpl"; -constexpr const char *const MQTT_SET_POSITION_TOPIC = "set_pos_t"; -constexpr const char *const MQTT_SOURCE_TYPE = "src_type"; -constexpr const char *const MQTT_SPEED_COMMAND_TOPIC = "spd_cmd_t"; -constexpr const char *const MQTT_SPEED_RANGE_MAX = "spd_rng_max"; -constexpr const char *const MQTT_SPEED_RANGE_MIN = "spd_rng_min"; -constexpr const char *const MQTT_SPEED_STATE_TOPIC = "spd_stat_t"; -constexpr const char *const MQTT_SPEED_VALUE_TEMPLATE = "spd_val_tpl"; -constexpr const char *const MQTT_SPEEDS = "spds"; -constexpr const char *const MQTT_STATE_CLASS = "stat_cla"; -constexpr const char *const MQTT_STATE_CLOSED = "stat_clsd"; -constexpr const char *const MQTT_STATE_CLOSING = "stat_closing"; -constexpr const char *const MQTT_STATE_LOCKED = "stat_locked"; -constexpr const char *const MQTT_STATE_OFF = "stat_off"; -constexpr const char *const MQTT_STATE_ON = "stat_on"; -constexpr const char *const MQTT_STATE_OPEN = "stat_open"; -constexpr const char *const MQTT_STATE_OPENING = "stat_opening"; -constexpr const char *const MQTT_STATE_STOPPED = "stat_stopped"; -constexpr const char *const MQTT_STATE_TEMPLATE = "stat_tpl"; -constexpr const char *const MQTT_STATE_TOPIC = "stat_t"; -constexpr const char *const MQTT_STATE_UNLOCKED = "stat_unlocked"; -constexpr const char *const MQTT_STATE_VALUE_TEMPLATE = "stat_val_tpl"; -constexpr const char *const MQTT_STEP = "step"; -constexpr const char *const MQTT_SUBTYPE = "stype"; -constexpr const char *const MQTT_SUPPORTED_COLOR_MODES = "sup_clrm"; -constexpr const char *const MQTT_SUPPORTED_FEATURES = "sup_feat"; -constexpr const char *const MQTT_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_cmd_tpl"; -constexpr const char *const MQTT_SWING_MODE_COMMAND_TOPIC = "swing_mode_cmd_t"; -constexpr const char *const MQTT_SWING_MODE_STATE_TEMPLATE = "swing_mode_stat_tpl"; -constexpr const char *const MQTT_SWING_MODE_STATE_TOPIC = "swing_mode_stat_t"; -constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE = "hum_cmd_tpl"; -constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TOPIC = "hum_cmd_t"; -constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TEMPLATE = "hum_state_tpl"; -constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC = "hum_stat_t"; -constexpr const char *const MQTT_TARGET_TEMPERATURE_STEP = "temp_step"; -constexpr const char *const MQTT_TEMPERATURE_COMMAND_TEMPLATE = "temp_cmd_tpl"; -constexpr const char *const MQTT_TEMPERATURE_COMMAND_TOPIC = "temp_cmd_t"; -constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TEMPLATE = "temp_hi_cmd_tpl"; -constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TOPIC = "temp_hi_cmd_t"; -constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TEMPLATE = "temp_hi_stat_tpl"; -constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TOPIC = "temp_hi_stat_t"; -constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TEMPLATE = "temp_lo_cmd_tpl"; -constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TOPIC = "temp_lo_cmd_t"; -constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TEMPLATE = "temp_lo_stat_tpl"; -constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TOPIC = "temp_lo_stat_t"; -constexpr const char *const MQTT_TEMPERATURE_STATE_TEMPLATE = "temp_stat_tpl"; -constexpr const char *const MQTT_TEMPERATURE_STATE_TOPIC = "temp_stat_t"; -constexpr const char *const MQTT_TEMPERATURE_UNIT = "temp_unit"; -constexpr const char *const MQTT_TILT_CLOSED_VALUE = "tilt_clsd_val"; -constexpr const char *const MQTT_TILT_COMMAND_TEMPLATE = "tilt_cmd_tpl"; -constexpr const char *const MQTT_TILT_COMMAND_TOPIC = "tilt_cmd_t"; -constexpr const char *const MQTT_TILT_INVERT_STATE = "tilt_inv_stat"; -constexpr const char *const MQTT_TILT_MAX = "tilt_max"; -constexpr const char *const MQTT_TILT_MIN = "tilt_min"; -constexpr const char *const MQTT_TILT_OPENED_VALUE = "tilt_opnd_val"; -constexpr const char *const MQTT_TILT_OPTIMISTIC = "tilt_opt"; -constexpr const char *const MQTT_TILT_STATUS_TEMPLATE = "tilt_status_tpl"; -constexpr const char *const MQTT_TILT_STATUS_TOPIC = "tilt_status_t"; -constexpr const char *const MQTT_TOPIC = "t"; -constexpr const char *const MQTT_UNIQUE_ID = "uniq_id"; -constexpr const char *const MQTT_UNIT_OF_MEASUREMENT = "unit_of_meas"; -constexpr const char *const MQTT_VALUE_TEMPLATE = "val_tpl"; -constexpr const char *const MQTT_WHITE_COMMAND_TOPIC = "whit_cmd_t"; -constexpr const char *const MQTT_WHITE_SCALE = "whit_scl"; -constexpr const char *const MQTT_WHITE_VALUE_COMMAND_TOPIC = "whit_val_cmd_t"; -constexpr const char *const MQTT_WHITE_VALUE_SCALE = "whit_val_scl"; -constexpr const char *const MQTT_WHITE_VALUE_STATE_TOPIC = "whit_val_stat_t"; -constexpr const char *const MQTT_WHITE_VALUE_TEMPLATE = "whit_val_tpl"; -constexpr const char *const MQTT_XY_COMMAND_TOPIC = "xy_cmd_t"; -constexpr const char *const MQTT_XY_STATE_TOPIC = "xy_stat_t"; -constexpr const char *const MQTT_XY_VALUE_TEMPLATE = "xy_val_tpl"; - +#define MQTT_DATA(name, abbr, full) static const char name##_data[] PROGMEM = abbr; #else +#define MQTT_DATA(name, abbr, full) static const char name##_data[] PROGMEM = full; +#endif +MQTT_KEYS_LIST(MQTT_DATA) +#undef MQTT_DATA -constexpr const char *const MQTT_ACTION_TEMPLATE = "action_template"; -constexpr const char *const MQTT_ACTION_TOPIC = "action_topic"; -constexpr const char *const MQTT_AUTOMATION_TYPE = "automation_type"; -constexpr const char *const MQTT_AUX_COMMAND_TOPIC = "aux_command_topic"; -constexpr const char *const MQTT_AUX_STATE_TEMPLATE = "aux_state_template"; -constexpr const char *const MQTT_AUX_STATE_TOPIC = "aux_state_topic"; -constexpr const char *const MQTT_AVAILABILITY = "availability"; -constexpr const char *const MQTT_AVAILABILITY_MODE = "availability_mode"; -constexpr const char *const MQTT_AVAILABILITY_TOPIC = "availability_topic"; -constexpr const char *const MQTT_AWAY_MODE_COMMAND_TOPIC = "away_mode_command_topic"; -constexpr const char *const MQTT_AWAY_MODE_STATE_TEMPLATE = "away_mode_state_template"; -constexpr const char *const MQTT_AWAY_MODE_STATE_TOPIC = "away_mode_state_topic"; -constexpr const char *const MQTT_BATTERY_LEVEL_TEMPLATE = "battery_level_template"; -constexpr const char *const MQTT_BATTERY_LEVEL_TOPIC = "battery_level_topic"; -constexpr const char *const MQTT_BLUE_TEMPLATE = "blue_template"; -constexpr const char *const MQTT_BRIGHTNESS_COMMAND_TOPIC = "brightness_command_topic"; -constexpr const char *const MQTT_BRIGHTNESS_SCALE = "brightness_scale"; -constexpr const char *const MQTT_BRIGHTNESS_STATE_TOPIC = "brightness_state_topic"; -constexpr const char *const MQTT_BRIGHTNESS_TEMPLATE = "brightness_template"; -constexpr const char *const MQTT_BRIGHTNESS_VALUE_TEMPLATE = "brightness_value_template"; -constexpr const char *const MQTT_CHARGING_TEMPLATE = "charging_template"; -constexpr const char *const MQTT_CHARGING_TOPIC = "charging_topic"; -constexpr const char *const MQTT_CLEANING_TEMPLATE = "cleaning_template"; -constexpr const char *const MQTT_CLEANING_TOPIC = "cleaning_topic"; -constexpr const char *const MQTT_CODE_ARM_REQUIRED = "code_arm_required"; -constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "code_disarm_required"; -constexpr const char *const MQTT_COLOR_MODE = "color_mode"; -constexpr const char *const MQTT_COLOR_MODE_STATE_TOPIC = "color_mode_state_topic"; -constexpr const char *const MQTT_COLOR_MODE_VALUE_TEMPLATE = "color_mode_value_template"; -constexpr const char *const MQTT_COLOR_TEMP_COMMAND_TEMPLATE = "color_temp_command_template"; -constexpr const char *const MQTT_COLOR_TEMP_COMMAND_TOPIC = "color_temp_command_topic"; -constexpr const char *const MQTT_COLOR_TEMP_STATE_TOPIC = "color_temp_state_topic"; -constexpr const char *const MQTT_COLOR_TEMP_TEMPLATE = "color_temp_template"; -constexpr const char *const MQTT_COLOR_TEMP_VALUE_TEMPLATE = "color_temp_value_template"; -constexpr const char *const MQTT_COMMAND_OFF_TEMPLATE = "command_off_template"; -constexpr const char *const MQTT_COMMAND_ON_TEMPLATE = "command_on_template"; -constexpr const char *const MQTT_COMMAND_RETAIN = "retain"; -constexpr const char *const MQTT_COMMAND_TEMPLATE = "command_template"; -constexpr const char *const MQTT_COMMAND_TOPIC = "command_topic"; -constexpr const char *const MQTT_CONFIGURATION_URL = "configuration_url"; -constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template"; -constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic"; -constexpr const char *const MQTT_CURRENT_TEMPERATURE_STEP = "precision"; -constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "current_temperature_template"; -constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "current_temperature_topic"; -constexpr const char *const MQTT_DEVICE = "device"; -constexpr const char *const MQTT_DEVICE_CLASS = "device_class"; -constexpr const char *const MQTT_DEVICE_CONNECTIONS = "connections"; -constexpr const char *const MQTT_DEVICE_IDENTIFIERS = "identifiers"; -constexpr const char *const MQTT_DEVICE_MANUFACTURER = "manufacturer"; -constexpr const char *const MQTT_DEVICE_MODEL = "model"; -constexpr const char *const MQTT_DEVICE_NAME = "name"; -constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "suggested_area"; -constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw_version"; -constexpr const char *const MQTT_DEVICE_HW_VERSION = "hw_version"; -constexpr const char *const MQTT_DIRECTION_COMMAND_TOPIC = "direction_command_topic"; -constexpr const char *const MQTT_DIRECTION_STATE_TOPIC = "direction_state_topic"; -constexpr const char *const MQTT_DOCKED_TEMPLATE = "docked_template"; -constexpr const char *const MQTT_DOCKED_TOPIC = "docked_topic"; -constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "effect_command_topic"; -constexpr const char *const MQTT_EFFECT_LIST = "effect_list"; -constexpr const char *const MQTT_EFFECT_STATE_TOPIC = "effect_state_topic"; -constexpr const char *const MQTT_EFFECT_TEMPLATE = "effect_template"; -constexpr const char *const MQTT_EFFECT_VALUE_TEMPLATE = "effect_value_template"; -constexpr const char *const MQTT_ENABLED_BY_DEFAULT = "enabled_by_default"; -constexpr const char *const MQTT_ENTITY_CATEGORY = "entity_category"; -constexpr const char *const MQTT_ERROR_TEMPLATE = "error_template"; -constexpr const char *const MQTT_ERROR_TOPIC = "error_topic"; -constexpr const char *const MQTT_EVENT_TYPE = "event_type"; -constexpr const char *const MQTT_EVENT_TYPES = "event_types"; -constexpr const char *const MQTT_EXPIRE_AFTER = "expire_after"; -constexpr const char *const MQTT_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template"; -constexpr const char *const MQTT_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic"; -constexpr const char *const MQTT_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template"; -constexpr const char *const MQTT_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic"; -constexpr const char *const MQTT_FAN_SPEED_LIST = "fan_speed_list"; -constexpr const char *const MQTT_FAN_SPEED_TEMPLATE = "fan_speed_template"; -constexpr const char *const MQTT_FAN_SPEED_TOPIC = "fan_speed_topic"; -constexpr const char *const MQTT_FLASH_TIME_LONG = "flash_time_long"; -constexpr const char *const MQTT_FLASH_TIME_SHORT = "flash_time_short"; -constexpr const char *const MQTT_FORCE_UPDATE = "force_update"; -constexpr const char *const MQTT_GREEN_TEMPLATE = "green_template"; -constexpr const char *const MQTT_HOLD_COMMAND_TEMPLATE = "hold_command_template"; -constexpr const char *const MQTT_HOLD_COMMAND_TOPIC = "hold_command_topic"; -constexpr const char *const MQTT_HOLD_STATE_TEMPLATE = "hold_state_template"; -constexpr const char *const MQTT_HOLD_STATE_TOPIC = "hold_state_topic"; -constexpr const char *const MQTT_HS_COMMAND_TOPIC = "hs_command_topic"; -constexpr const char *const MQTT_HS_STATE_TOPIC = "hs_state_topic"; -constexpr const char *const MQTT_HS_VALUE_TEMPLATE = "hs_value_template"; -constexpr const char *const MQTT_ICON = "icon"; -constexpr const char *const MQTT_INITIAL = "initial"; -constexpr const char *const MQTT_JSON_ATTRIBUTES = "json_attributes"; -constexpr const char *const MQTT_JSON_ATTRIBUTES_TEMPLATE = "json_attributes_template"; -constexpr const char *const MQTT_JSON_ATTRIBUTES_TOPIC = "json_attributes_topic"; -constexpr const char *const MQTT_LAST_RESET_TOPIC = "last_reset_topic"; -constexpr const char *const MQTT_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"; -constexpr const char *const MQTT_MAX = "max"; -constexpr const char *const MQTT_MAX_HUMIDITY = "max_humidity"; -constexpr const char *const MQTT_MAX_MIREDS = "max_mireds"; -constexpr const char *const MQTT_MAX_TEMP = "max_temp"; -constexpr const char *const MQTT_MIN = "min"; -constexpr const char *const MQTT_MIN_HUMIDITY = "min_humidity"; -constexpr const char *const MQTT_MIN_MIREDS = "min_mireds"; -constexpr const char *const MQTT_MIN_TEMP = "min_temp"; -constexpr const char *const MQTT_MODE = "mode"; -constexpr const char *const MQTT_MODE_COMMAND_TEMPLATE = "mode_command_template"; -constexpr const char *const MQTT_MODE_COMMAND_TOPIC = "mode_command_topic"; -constexpr const char *const MQTT_MODE_STATE_TEMPLATE = "mode_state_template"; -constexpr const char *const MQTT_MODE_STATE_TOPIC = "mode_state_topic"; -constexpr const char *const MQTT_MODES = "modes"; -constexpr const char *const MQTT_NAME = "name"; -constexpr const char *const MQTT_OBJECT_ID = "object_id"; -constexpr const char *const MQTT_OFF_DELAY = "off_delay"; -constexpr const char *const MQTT_ON_COMMAND_TYPE = "on_command_type"; -constexpr const char *const MQTT_OPTIMISTIC = "optimistic"; -constexpr const char *const MQTT_OPTIONS = "options"; -constexpr const char *const MQTT_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template"; -constexpr const char *const MQTT_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"; -constexpr const char *const MQTT_OSCILLATION_STATE_TOPIC = "oscillation_state_topic"; -constexpr const char *const MQTT_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template"; -constexpr const char *const MQTT_PAYLOAD = "payload"; -constexpr const char *const MQTT_PAYLOAD_ARM_AWAY = "payload_arm_away"; -constexpr const char *const MQTT_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass"; -constexpr const char *const MQTT_PAYLOAD_ARM_HOME = "payload_arm_home"; -constexpr const char *const MQTT_PAYLOAD_ARM_NIGHT = "payload_arm_night"; -constexpr const char *const MQTT_PAYLOAD_ARM_VACATION = "payload_arm_vacation"; -constexpr const char *const MQTT_PAYLOAD_AVAILABLE = "payload_available"; -constexpr const char *const MQTT_PAYLOAD_CLEAN_SPOT = "payload_clean_spot"; -constexpr const char *const MQTT_PAYLOAD_CLOSE = "payload_close"; -constexpr const char *const MQTT_PAYLOAD_DISARM = "payload_disarm"; -constexpr const char *const MQTT_PAYLOAD_HIGH_SPEED = "payload_high_speed"; -constexpr const char *const MQTT_PAYLOAD_HOME = "payload_home"; -constexpr const char *const MQTT_PAYLOAD_INSTALL = "payload_install"; -constexpr const char *const MQTT_PAYLOAD_LOCATE = "payload_locate"; -constexpr const char *const MQTT_PAYLOAD_LOCK = "payload_lock"; -constexpr const char *const MQTT_PAYLOAD_LOW_SPEED = "payload_low_speed"; -constexpr const char *const MQTT_PAYLOAD_MEDIUM_SPEED = "payload_medium_speed"; -constexpr const char *const MQTT_PAYLOAD_NOT_AVAILABLE = "payload_not_available"; -constexpr const char *const MQTT_PAYLOAD_NOT_HOME = "payload_not_home"; -constexpr const char *const MQTT_PAYLOAD_OFF = "payload_off"; -constexpr const char *const MQTT_PAYLOAD_OFF_SPEED = "payload_off_speed"; -constexpr const char *const MQTT_PAYLOAD_ON = "payload_on"; -constexpr const char *const MQTT_PAYLOAD_OPEN = "payload_open"; -constexpr const char *const MQTT_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off"; -constexpr const char *const MQTT_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on"; -constexpr const char *const MQTT_PAYLOAD_PAUSE = "payload_pause"; -constexpr const char *const MQTT_PAYLOAD_RESET = "payload_reset"; -constexpr const char *const MQTT_PAYLOAD_RESET_HUMIDITY = "payload_reset_humidity"; -constexpr const char *const MQTT_PAYLOAD_RESET_MODE = "payload_reset_mode"; -constexpr const char *const MQTT_PAYLOAD_RESET_PERCENTAGE = "payload_reset_percentage"; -constexpr const char *const MQTT_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode"; -constexpr const char *const MQTT_PAYLOAD_RETURN_TO_BASE = "payload_return_to_base"; -constexpr const char *const MQTT_PAYLOAD_START = "payload_start"; -constexpr const char *const MQTT_PAYLOAD_START_PAUSE = "payload_start_pause"; -constexpr const char *const MQTT_PAYLOAD_STOP = "payload_stop"; -constexpr const char *const MQTT_PAYLOAD_TURN_OFF = "payload_turn_off"; -constexpr const char *const MQTT_PAYLOAD_TURN_ON = "payload_turn_on"; -constexpr const char *const MQTT_PAYLOAD_UNLOCK = "payload_unlock"; -constexpr const char *const MQTT_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template"; -constexpr const char *const MQTT_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic"; -constexpr const char *const MQTT_PERCENTAGE_STATE_TOPIC = "percentage_state_topic"; -constexpr const char *const MQTT_PERCENTAGE_VALUE_TEMPLATE = "percentage_value_template"; -constexpr const char *const MQTT_POSITION_CLOSED = "position_closed"; -constexpr const char *const MQTT_POSITION_OPEN = "position_open"; -constexpr const char *const MQTT_POSITION_TEMPLATE = "position_template"; -constexpr const char *const MQTT_POSITION_TOPIC = "position_topic"; -constexpr const char *const MQTT_POWER_COMMAND_TOPIC = "power_command_topic"; -constexpr const char *const MQTT_POWER_STATE_TEMPLATE = "power_state_template"; -constexpr const char *const MQTT_POWER_STATE_TOPIC = "power_state_topic"; -constexpr const char *const MQTT_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template"; -constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic"; -constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC = "preset_mode_state_topic"; -constexpr const char *const MQTT_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template"; -constexpr const char *const MQTT_PRESET_MODES = "preset_modes"; -constexpr const char *const MQTT_QOS = "qos"; -constexpr const char *const MQTT_RED_TEMPLATE = "red_template"; -constexpr const char *const MQTT_RETAIN = "retain"; -constexpr const char *const MQTT_RGB_COMMAND_TEMPLATE = "rgb_command_template"; -constexpr const char *const MQTT_RGB_COMMAND_TOPIC = "rgb_command_topic"; -constexpr const char *const MQTT_RGB_STATE_TOPIC = "rgb_state_topic"; -constexpr const char *const MQTT_RGB_VALUE_TEMPLATE = "rgb_value_template"; -constexpr const char *const MQTT_RGBW_COMMAND_TEMPLATE = "rgbw_command_template"; -constexpr const char *const MQTT_RGBW_COMMAND_TOPIC = "rgbw_command_topic"; -constexpr const char *const MQTT_RGBW_STATE_TOPIC = "rgbw_state_topic"; -constexpr const char *const MQTT_RGBW_VALUE_TEMPLATE = "rgbw_value_template"; -constexpr const char *const MQTT_RGBWW_COMMAND_TEMPLATE = "rgbww_command_template"; -constexpr const char *const MQTT_RGBWW_COMMAND_TOPIC = "rgbww_command_topic"; -constexpr const char *const MQTT_RGBWW_STATE_TOPIC = "rgbww_state_topic"; -constexpr const char *const MQTT_RGBWW_VALUE_TEMPLATE = "rgbww_value_template"; -constexpr const char *const MQTT_SEND_COMMAND_TOPIC = "send_command_topic"; -constexpr const char *const MQTT_SEND_IF_OFF = "send_if_off"; -constexpr const char *const MQTT_SET_FAN_SPEED_TOPIC = "set_fan_speed_topic"; -constexpr const char *const MQTT_SET_POSITION_TEMPLATE = "set_position_template"; -constexpr const char *const MQTT_SET_POSITION_TOPIC = "set_position_topic"; -constexpr const char *const MQTT_SOURCE_TYPE = "source_type"; -constexpr const char *const MQTT_SPEED_COMMAND_TOPIC = "speed_command_topic"; -constexpr const char *const MQTT_SPEED_RANGE_MAX = "speed_range_max"; -constexpr const char *const MQTT_SPEED_RANGE_MIN = "speed_range_min"; -constexpr const char *const MQTT_SPEED_STATE_TOPIC = "speed_state_topic"; -constexpr const char *const MQTT_SPEED_VALUE_TEMPLATE = "speed_value_template"; -constexpr const char *const MQTT_SPEEDS = "speeds"; -constexpr const char *const MQTT_STATE_CLASS = "state_class"; -constexpr const char *const MQTT_STATE_CLOSED = "state_closed"; -constexpr const char *const MQTT_STATE_CLOSING = "state_closing"; -constexpr const char *const MQTT_STATE_LOCKED = "state_locked"; -constexpr const char *const MQTT_STATE_OFF = "state_off"; -constexpr const char *const MQTT_STATE_ON = "state_on"; -constexpr const char *const MQTT_STATE_OPEN = "state_open"; -constexpr const char *const MQTT_STATE_OPENING = "state_opening"; -constexpr const char *const MQTT_STATE_STOPPED = "state_stopped"; -constexpr const char *const MQTT_STATE_TEMPLATE = "state_template"; -constexpr const char *const MQTT_STATE_TOPIC = "state_topic"; -constexpr const char *const MQTT_STATE_UNLOCKED = "state_unlocked"; -constexpr const char *const MQTT_STATE_VALUE_TEMPLATE = "state_value_template"; -constexpr const char *const MQTT_STEP = "step"; -constexpr const char *const MQTT_SUBTYPE = "subtype"; -constexpr const char *const MQTT_SUPPORTED_COLOR_MODES = "supported_color_modes"; -constexpr const char *const MQTT_SUPPORTED_FEATURES = "supported_features"; -constexpr const char *const MQTT_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template"; -constexpr const char *const MQTT_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic"; -constexpr const char *const MQTT_SWING_MODE_STATE_TEMPLATE = "swing_mode_state_template"; -constexpr const char *const MQTT_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic"; -constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template"; -constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic"; -constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template"; -constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic"; -constexpr const char *const MQTT_TARGET_TEMPERATURE_STEP = "temp_step"; -constexpr const char *const MQTT_TEMPERATURE_COMMAND_TEMPLATE = "temperature_command_template"; -constexpr const char *const MQTT_TEMPERATURE_COMMAND_TOPIC = "temperature_command_topic"; -constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TEMPLATE = "temperature_high_command_template"; -constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TOPIC = "temperature_high_command_topic"; -constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TEMPLATE = "temperature_high_state_template"; -constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TOPIC = "temperature_high_state_topic"; -constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TEMPLATE = "temperature_low_command_template"; -constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TOPIC = "temperature_low_command_topic"; -constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TEMPLATE = "temperature_low_state_template"; -constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TOPIC = "temperature_low_state_topic"; -constexpr const char *const MQTT_TEMPERATURE_STATE_TEMPLATE = "temperature_state_template"; -constexpr const char *const MQTT_TEMPERATURE_STATE_TOPIC = "temperature_state_topic"; -constexpr const char *const MQTT_TEMPERATURE_UNIT = "temperature_unit"; -constexpr const char *const MQTT_TILT_CLOSED_VALUE = "tilt_closed_value"; -constexpr const char *const MQTT_TILT_COMMAND_TEMPLATE = "tilt_command_template"; -constexpr const char *const MQTT_TILT_COMMAND_TOPIC = "tilt_command_topic"; -constexpr const char *const MQTT_TILT_INVERT_STATE = "tilt_invert_state"; -constexpr const char *const MQTT_TILT_MAX = "tilt_max"; -constexpr const char *const MQTT_TILT_MIN = "tilt_min"; -constexpr const char *const MQTT_TILT_OPENED_VALUE = "tilt_opened_value"; -constexpr const char *const MQTT_TILT_OPTIMISTIC = "tilt_optimistic"; -constexpr const char *const MQTT_TILT_STATUS_TEMPLATE = "tilt_status_template"; -constexpr const char *const MQTT_TILT_STATUS_TOPIC = "tilt_status_topic"; -constexpr const char *const MQTT_TOPIC = "topic"; -constexpr const char *const MQTT_UNIQUE_ID = "unique_id"; -constexpr const char *const MQTT_UNIT_OF_MEASUREMENT = "unit_of_measurement"; -constexpr const char *const MQTT_VALUE_TEMPLATE = "value_template"; -constexpr const char *const MQTT_WHITE_COMMAND_TOPIC = "white_command_topic"; -constexpr const char *const MQTT_WHITE_SCALE = "white_scale"; -constexpr const char *const MQTT_WHITE_VALUE_COMMAND_TOPIC = "white_value_command_topic"; -constexpr const char *const MQTT_WHITE_VALUE_SCALE = "white_value_scale"; -constexpr const char *const MQTT_WHITE_VALUE_STATE_TOPIC = "white_value_state_topic"; -constexpr const char *const MQTT_WHITE_VALUE_TEMPLATE = "white_value_template"; -constexpr const char *const MQTT_XY_COMMAND_TOPIC = "xy_command_topic"; -constexpr const char *const MQTT_XY_STATE_TOPIC = "xy_state_topic"; -constexpr const char *const MQTT_XY_VALUE_TEMPLATE = "xy_value_template"; +// Generate flash string pointers from the PROGMEM data +// NOLINTNEXTLINE(bugprone-macro-parentheses) +#define MQTT_PTR(name, abbr, full) \ + static const __FlashStringHelper *const name = reinterpret_cast(name##_data); +MQTT_KEYS_LIST(MQTT_PTR) +#undef MQTT_PTR +} // namespace esphome::mqtt +#else +// Other platforms: constexpr in namespace +namespace esphome::mqtt { +#ifdef USE_MQTT_ABBREVIATIONS +// NOLINTNEXTLINE(bugprone-macro-parentheses) +#define MQTT_CONST(name, abbr, full) constexpr const char *name = abbr; +#else +// NOLINTNEXTLINE(bugprone-macro-parentheses) +#define MQTT_CONST(name, abbr, full) constexpr const char *name = full; +#endif +MQTT_KEYS_LIST(MQTT_CONST) +#undef MQTT_CONST +} // namespace esphome::mqtt #endif -} // namespace mqtt -} // namespace esphome - #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index b63aa66d29..e628ac37a9 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_COVER -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.cover"; @@ -119,8 +118,7 @@ bool MQTTCoverComponent::publish_state() { return success; } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_cover.h b/esphome/components/mqtt/mqtt_cover.h index f3e6053d0b..6b874af16a 100644 --- a/esphome/components/mqtt/mqtt_cover.h +++ b/esphome/components/mqtt/mqtt_cover.h @@ -8,8 +8,7 @@ #include "esphome/components/cover/cover.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTCoverComponent : public mqtt::MQTTComponent { public: @@ -36,8 +35,7 @@ class MQTTCoverComponent : public mqtt::MQTTComponent { cover::Cover *cover_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_date.cpp b/esphome/components/mqtt/mqtt_date.cpp index 0f0a334ae7..1715384c5f 100644 --- a/esphome/components/mqtt/mqtt_date.cpp +++ b/esphome/components/mqtt/mqtt_date.cpp @@ -8,8 +8,7 @@ #ifdef USE_MQTT #ifdef USE_DATETIME_DATE -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.datetime"; @@ -20,14 +19,14 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {} void MQTTDateComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { auto call = this->date_->make_call(); - if (root["year"].is()) { - call.set_year(root["year"]); + if (root[ESPHOME_F("year")].is()) { + call.set_year(root[ESPHOME_F("year")]); } - if (root["month"].is()) { - call.set_month(root["month"]); + if (root[ESPHOME_F("month")].is()) { + call.set_month(root[ESPHOME_F("month")]); } - if (root["day"].is()) { - call.set_day(root["day"]); + if (root[ESPHOME_F("day")].is()) { + call.set_day(root[ESPHOME_F("day")]); } call.perform(); }); @@ -56,14 +55,13 @@ bool MQTTDateComponent::send_initial_state() { bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) { return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - root["year"] = year; - root["month"] = month; - root["day"] = day; + root[ESPHOME_F("year")] = year; + root[ESPHOME_F("month")] = month; + root[ESPHOME_F("day")] = day; }); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_DATETIME_DATE #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_date.h b/esphome/components/mqtt/mqtt_date.h index 5147afe7e7..380bb69e0e 100644 --- a/esphome/components/mqtt/mqtt_date.h +++ b/esphome/components/mqtt/mqtt_date.h @@ -8,8 +8,7 @@ #include "esphome/components/datetime/date_entity.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTDateComponent : public mqtt::MQTTComponent { public: @@ -38,8 +37,7 @@ class MQTTDateComponent : public mqtt::MQTTComponent { datetime::DateEntity *date_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_DATETIME_DATE #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_datetime.cpp b/esphome/components/mqtt/mqtt_datetime.cpp index 5c56baabe0..79a2c82180 100644 --- a/esphome/components/mqtt/mqtt_datetime.cpp +++ b/esphome/components/mqtt/mqtt_datetime.cpp @@ -8,8 +8,7 @@ #ifdef USE_MQTT #ifdef USE_DATETIME_DATETIME -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.datetime.datetime"; @@ -20,23 +19,23 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim void MQTTDateTimeComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { auto call = this->datetime_->make_call(); - if (root["year"].is()) { - call.set_year(root["year"]); + if (root[ESPHOME_F("year")].is()) { + call.set_year(root[ESPHOME_F("year")]); } - if (root["month"].is()) { - call.set_month(root["month"]); + if (root[ESPHOME_F("month")].is()) { + call.set_month(root[ESPHOME_F("month")]); } - if (root["day"].is()) { - call.set_day(root["day"]); + if (root[ESPHOME_F("day")].is()) { + call.set_day(root[ESPHOME_F("day")]); } - if (root["hour"].is()) { - call.set_hour(root["hour"]); + if (root[ESPHOME_F("hour")].is()) { + call.set_hour(root[ESPHOME_F("hour")]); } - if (root["minute"].is()) { - call.set_minute(root["minute"]); + if (root[ESPHOME_F("minute")].is()) { + call.set_minute(root[ESPHOME_F("minute")]); } - if (root["second"].is()) { - call.set_second(root["second"]); + if (root[ESPHOME_F("second")].is()) { + call.set_second(root[ESPHOME_F("second")]); } call.perform(); }); @@ -69,17 +68,16 @@ bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t uint8_t second) { return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - root["year"] = year; - root["month"] = month; - root["day"] = day; - root["hour"] = hour; - root["minute"] = minute; - root["second"] = second; + root[ESPHOME_F("year")] = year; + root[ESPHOME_F("month")] = month; + root[ESPHOME_F("day")] = day; + root[ESPHOME_F("hour")] = hour; + root[ESPHOME_F("minute")] = minute; + root[ESPHOME_F("second")] = second; }); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_DATETIME_DATETIME #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_datetime.h b/esphome/components/mqtt/mqtt_datetime.h index ba81c06cb3..8706bfcf75 100644 --- a/esphome/components/mqtt/mqtt_datetime.h +++ b/esphome/components/mqtt/mqtt_datetime.h @@ -8,8 +8,7 @@ #include "esphome/components/datetime/datetime_entity.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTDateTimeComponent : public mqtt::MQTTComponent { public: @@ -38,8 +37,7 @@ class MQTTDateTimeComponent : public mqtt::MQTTComponent { datetime::DateTimeEntity *datetime_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_DATETIME_DATETIME #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index fd095ea041..67a7aab5bd 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_EVENT -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.event"; @@ -54,8 +53,7 @@ bool MQTTEventComponent::publish_event_(const std::string &event_type) { std::string MQTTEventComponent::component_type() const { return "event"; } const EntityBase *MQTTEventComponent::get_entity() const { return this->event_; } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_event.h b/esphome/components/mqtt/mqtt_event.h index 4335820e53..fc6e778d44 100644 --- a/esphome/components/mqtt/mqtt_event.h +++ b/esphome/components/mqtt/mqtt_event.h @@ -8,8 +8,7 @@ #include "esphome/components/event/event.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTEventComponent : public mqtt::MQTTComponent { public: @@ -32,8 +31,7 @@ class MQTTEventComponent : public mqtt::MQTTComponent { event::Event *event_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 2aefc3a4db..ffecd9c663 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_FAN -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.fan"; @@ -182,8 +181,7 @@ bool MQTTFanComponent::publish_state() { return !failed; } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_fan.h b/esphome/components/mqtt/mqtt_fan.h index 78641d224f..16ce246853 100644 --- a/esphome/components/mqtt/mqtt_fan.h +++ b/esphome/components/mqtt/mqtt_fan.h @@ -8,8 +8,7 @@ #include "esphome/components/fan/fan.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTFanComponent : public mqtt::MQTTComponent { public: @@ -47,8 +46,7 @@ class MQTTFanComponent : public mqtt::MQTTComponent { fan::Fan *state_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 883b67ffc6..0dafe487ff 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -8,8 +8,7 @@ #ifdef USE_LIGHT #include "esphome/components/light/light_json_schema.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.light"; @@ -25,8 +24,11 @@ void MQTTJSONLightComponent::setup() { call.perform(); }); - auto f = std::bind(&MQTTJSONLightComponent::publish_state_, this); - this->state_->add_new_remote_values_callback([this, f]() { this->defer("send", f); }); + this->state_->add_remote_values_listener(this); +} + +void MQTTJSONLightComponent::on_light_remote_values_update() { + this->defer("send", [this]() { this->publish_state_(); }); } MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {} @@ -41,33 +43,33 @@ LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - root["schema"] = "json"; + root[ESPHOME_F("schema")] = ESPHOME_F("json"); auto traits = this->state_->get_traits(); root[MQTT_COLOR_MODE] = true; // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - JsonArray color_modes = root["supported_color_modes"].to(); + JsonArray color_modes = root[ESPHOME_F("supported_color_modes")].to(); if (traits.supports_color_mode(ColorMode::ON_OFF)) - color_modes.add("onoff"); + color_modes.add(ESPHOME_F("onoff")); if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) - color_modes.add("brightness"); + color_modes.add(ESPHOME_F("brightness")); if (traits.supports_color_mode(ColorMode::WHITE)) - color_modes.add("white"); + color_modes.add(ESPHOME_F("white")); if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) || traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) - color_modes.add("color_temp"); + color_modes.add(ESPHOME_F("color_temp")); if (traits.supports_color_mode(ColorMode::RGB)) - color_modes.add("rgb"); + color_modes.add(ESPHOME_F("rgb")); if (traits.supports_color_mode(ColorMode::RGB_WHITE) || // HA doesn't support RGBCT, and there's no CWWW->CT emulation in ESPHome yet, so ignore CT control for now traits.supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE)) - color_modes.add("rgbw"); + color_modes.add(ESPHOME_F("rgbw")); if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE)) - color_modes.add("rgbww"); + color_modes.add(ESPHOME_F("rgbww")); // legacy API if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) - root["brightness"] = true; + root[ESPHOME_F("brightness")] = true; if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) || traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) { @@ -76,11 +78,11 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery } if (this->state_->supports_effects()) { - root["effect"] = true; + root[ESPHOME_F("effect")] = true; JsonArray effect_list = root[MQTT_EFFECT_LIST].to(); for (auto *effect : this->state_->get_effects()) effect_list.add(effect->get_name()); - effect_list.add("None"); + effect_list.add(ESPHOME_F("None")); } } bool MQTTJSONLightComponent::send_initial_state() { return this->publish_state_(); } @@ -89,8 +91,7 @@ void MQTTJSONLightComponent::dump_config() { LOG_MQTT_COMPONENT(true, true) } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_light.h b/esphome/components/mqtt/mqtt_light.h index 3d1e770d4d..2cc631c901 100644 --- a/esphome/components/mqtt/mqtt_light.h +++ b/esphome/components/mqtt/mqtt_light.h @@ -8,10 +8,9 @@ #include "mqtt_component.h" #include "esphome/components/light/light_state.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { -class MQTTJSONLightComponent : public mqtt::MQTTComponent { +class MQTTJSONLightComponent : public mqtt::MQTTComponent, public light::LightRemoteValuesListener { public: explicit MQTTJSONLightComponent(light::LightState *state); @@ -25,6 +24,9 @@ class MQTTJSONLightComponent : public mqtt::MQTTComponent { bool send_initial_state() override; + // LightRemoteValuesListener interface + void on_light_remote_values_update() override; + protected: std::string component_type() const override; const EntityBase *get_entity() const override; @@ -34,8 +36,7 @@ class MQTTJSONLightComponent : public mqtt::MQTTComponent { light::LightState *state_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 0e15377ba4..58fa675eb7 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_LOCK -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.lock"; @@ -48,12 +47,17 @@ void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfi bool MQTTLockComponent::send_initial_state() { return this->publish_state(); } bool MQTTLockComponent::publish_state() { - std::string payload = lock_state_to_string(this->lock_->state); - return this->publish(this->get_state_topic_(), payload); +#ifdef USE_STORE_LOG_STR_IN_FLASH + char buf[LOCK_STATE_STR_SIZE]; + strncpy_P(buf, (PGM_P) lock_state_to_string(this->lock_->state), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + return this->publish(this->get_state_topic_(), buf); +#else + return this->publish(this->get_state_topic_(), LOG_STR_ARG(lock_state_to_string(this->lock_->state))); +#endif } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_lock.h b/esphome/components/mqtt/mqtt_lock.h index 789f74c795..6fb4998b25 100644 --- a/esphome/components/mqtt/mqtt_lock.h +++ b/esphome/components/mqtt/mqtt_lock.h @@ -8,8 +8,7 @@ #include "esphome/components/lock/lock.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTLockComponent : public mqtt::MQTTComponent { public: @@ -34,8 +33,7 @@ class MQTTLockComponent : public mqtt::MQTTComponent { lock::Lock *lock_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index f419eac130..381574ae56 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_NUMBER -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.number"; @@ -80,8 +79,7 @@ bool MQTTNumberComponent::publish_state(float value) { return this->publish(this->get_state_topic_(), buffer); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_number.h b/esphome/components/mqtt/mqtt_number.h index 10500c8333..b89e78a454 100644 --- a/esphome/components/mqtt/mqtt_number.h +++ b/esphome/components/mqtt/mqtt_number.h @@ -8,8 +8,7 @@ #include "esphome/components/number/number.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTNumberComponent : public mqtt::MQTTComponent { public: @@ -39,8 +38,7 @@ class MQTTNumberComponent : public mqtt::MQTTComponent { number::Number *number_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index e1660b07ea..5edc5c50dc 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_SELECT -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.select"; @@ -21,8 +20,7 @@ void MQTTSelectComponent::setup() { call.set_option(state); call.perform(); }); - this->select_->add_on_state_callback( - [this](const std::string &state, size_t index) { this->publish_state(this->select_->option_at(index)); }); + this->select_->add_on_state_callback([this](size_t index) { this->publish_state(this->select_->option_at(index)); }); } void MQTTSelectComponent::dump_config() { @@ -54,8 +52,7 @@ bool MQTTSelectComponent::publish_state(const std::string &value) { return this->publish(this->get_state_topic_(), value); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_select.h b/esphome/components/mqtt/mqtt_select.h index e0d8ac2417..19aad662e5 100644 --- a/esphome/components/mqtt/mqtt_select.h +++ b/esphome/components/mqtt/mqtt_select.h @@ -8,8 +8,7 @@ #include "esphome/components/select/select.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTSelectComponent : public mqtt::MQTTComponent { public: @@ -39,8 +38,7 @@ class MQTTSelectComponent : public mqtt::MQTTComponent { select::Select *select_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 010ac3013e..bd79ae40fe 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -11,8 +11,7 @@ #include "esphome/components/deep_sleep/deep_sleep_component.h" #endif -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.sensor"; @@ -86,8 +85,7 @@ bool MQTTSensorComponent::publish_state(float value) { return this->publish(this->get_state_topic_(), value_accuracy_to_string(value, accuracy)); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_sensor.h b/esphome/components/mqtt/mqtt_sensor.h index 15ea703ad4..8c60199e1b 100644 --- a/esphome/components/mqtt/mqtt_sensor.h +++ b/esphome/components/mqtt/mqtt_sensor.h @@ -8,8 +8,7 @@ #include "esphome/components/sensor/sensor.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTSensorComponent : public mqtt::MQTTComponent { public: @@ -51,8 +50,7 @@ class MQTTSensorComponent : public mqtt::MQTTComponent { optional expire_after_; // Override the expire after advertised to Home Assistant }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index b3a35420b9..a35ae8f9b6 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_SWITCH -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.switch"; @@ -57,8 +56,7 @@ bool MQTTSwitchComponent::publish_state(bool state) { return this->publish(this->get_state_topic_(), state_s); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_switch.h b/esphome/components/mqtt/mqtt_switch.h index c4d3f7164c..fb6a13f172 100644 --- a/esphome/components/mqtt/mqtt_switch.h +++ b/esphome/components/mqtt/mqtt_switch.h @@ -8,8 +8,7 @@ #include "esphome/components/switch/switch.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTSwitchComponent : public mqtt::MQTTComponent { public: @@ -34,8 +33,7 @@ class MQTTSwitchComponent : public mqtt::MQTTComponent { switch_::Switch *switch_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_text.cpp b/esphome/components/mqtt/mqtt_text.cpp index 5ab0ca9688..3cb851fd38 100644 --- a/esphome/components/mqtt/mqtt_text.cpp +++ b/esphome/components/mqtt/mqtt_text.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_TEXT -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.text"; @@ -57,8 +56,7 @@ bool MQTTTextComponent::publish_state(const std::string &value) { return this->publish(this->get_state_topic_(), value); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_text.h b/esphome/components/mqtt/mqtt_text.h index d9486fcbf8..0480b89395 100644 --- a/esphome/components/mqtt/mqtt_text.h +++ b/esphome/components/mqtt/mqtt_text.h @@ -8,8 +8,7 @@ #include "esphome/components/text/text.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTTextComponent : public mqtt::MQTTComponent { public: @@ -39,8 +38,7 @@ class MQTTTextComponent : public mqtt::MQTTComponent { text::Text *text_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index e6e7cf04e8..c87f22fb8e 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_TEXT_SENSOR -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.text_sensor"; @@ -43,8 +42,7 @@ bool MQTTTextSensor::send_initial_state() { std::string MQTTTextSensor::component_type() const { return "sensor"; } const EntityBase *MQTTTextSensor::get_entity() const { return this->sensor_; } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_text_sensor.h b/esphome/components/mqtt/mqtt_text_sensor.h index 9a14efdd16..d4d38d7eb2 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.h +++ b/esphome/components/mqtt/mqtt_text_sensor.h @@ -8,8 +8,7 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTTextSensor : public mqtt::MQTTComponent { public: @@ -32,8 +31,7 @@ class MQTTTextSensor : public mqtt::MQTTComponent { text_sensor::TextSensor *sensor_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_time.cpp b/esphome/components/mqtt/mqtt_time.cpp index 0c95bd8147..01b8dd3483 100644 --- a/esphome/components/mqtt/mqtt_time.cpp +++ b/esphome/components/mqtt/mqtt_time.cpp @@ -8,8 +8,7 @@ #ifdef USE_MQTT #ifdef USE_DATETIME_TIME -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.datetime.time"; @@ -20,14 +19,14 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {} void MQTTTimeComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { auto call = this->time_->make_call(); - if (root["hour"].is()) { - call.set_hour(root["hour"]); + if (root[ESPHOME_F("hour")].is()) { + call.set_hour(root[ESPHOME_F("hour")]); } - if (root["minute"].is()) { - call.set_minute(root["minute"]); + if (root[ESPHOME_F("minute")].is()) { + call.set_minute(root[ESPHOME_F("minute")]); } - if (root["second"].is()) { - call.set_second(root["second"]); + if (root[ESPHOME_F("second")].is()) { + call.set_second(root[ESPHOME_F("second")]); } call.perform(); }); @@ -56,14 +55,13 @@ bool MQTTTimeComponent::send_initial_state() { bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) { return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - root["hour"] = hour; - root["minute"] = minute; - root["second"] = second; + root[ESPHOME_F("hour")] = hour; + root[ESPHOME_F("minute")] = minute; + root[ESPHOME_F("second")] = second; }); } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_DATETIME_TIME #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_time.h b/esphome/components/mqtt/mqtt_time.h index b9dd822a73..60345c37ae 100644 --- a/esphome/components/mqtt/mqtt_time.h +++ b/esphome/components/mqtt/mqtt_time.h @@ -8,8 +8,7 @@ #include "esphome/components/datetime/time_entity.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTTimeComponent : public mqtt::MQTTComponent { public: @@ -38,8 +37,7 @@ class MQTTTimeComponent : public mqtt::MQTTComponent { datetime::TimeEntity *time_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_DATETIME_DATE #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp index 20f3a69a9e..aedf2414c1 100644 --- a/esphome/components/mqtt/mqtt_update.cpp +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_UPDATE -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.update"; @@ -30,20 +29,20 @@ void MQTTUpdateComponent::setup() { bool MQTTUpdateComponent::publish_state() { return this->publish_json(this->get_state_topic_(), [this](JsonObject root) { - root["installed_version"] = this->update_->update_info.current_version; - root["latest_version"] = this->update_->update_info.latest_version; - root["title"] = this->update_->update_info.title; + root[ESPHOME_F("installed_version")] = this->update_->update_info.current_version; + root[ESPHOME_F("latest_version")] = this->update_->update_info.latest_version; + root[ESPHOME_F("title")] = this->update_->update_info.title; if (!this->update_->update_info.summary.empty()) - root["release_summary"] = this->update_->update_info.summary; + root[ESPHOME_F("release_summary")] = this->update_->update_info.summary; if (!this->update_->update_info.release_url.empty()) - root["release_url"] = this->update_->update_info.release_url; + root[ESPHOME_F("release_url")] = this->update_->update_info.release_url; }); } void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - root["schema"] = "json"; - root[MQTT_PAYLOAD_INSTALL] = "INSTALL"; + root[ESPHOME_F("schema")] = ESPHOME_F("json"); + root[MQTT_PAYLOAD_INSTALL] = ESPHOME_F("INSTALL"); } bool MQTTUpdateComponent::send_initial_state() { return this->publish_state(); } @@ -56,8 +55,7 @@ void MQTTUpdateComponent::dump_config() { std::string MQTTUpdateComponent::component_type() const { return "update"; } const EntityBase *MQTTUpdateComponent::get_entity() const { return this->update_; } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_UPDATE #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_update.h b/esphome/components/mqtt/mqtt_update.h index 6fe04c4ea7..d04d22d25f 100644 --- a/esphome/components/mqtt/mqtt_update.h +++ b/esphome/components/mqtt/mqtt_update.h @@ -8,8 +8,7 @@ #include "esphome/components/update/update_entity.h" #include "mqtt_component.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTUpdateComponent : public mqtt::MQTTComponent { public: @@ -34,8 +33,7 @@ class MQTTUpdateComponent : public mqtt::MQTTComponent { update::UpdateEntity *update_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif // USE_UPDATE #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index ae60670748..8ee693121b 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -6,8 +6,7 @@ #ifdef USE_MQTT #ifdef USE_VALVE -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { static const char *const TAG = "mqtt.valve"; @@ -89,8 +88,7 @@ bool MQTTValveComponent::publish_state() { return success; } -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_valve.h b/esphome/components/mqtt/mqtt_valve.h index 63a0462193..9e5221e495 100644 --- a/esphome/components/mqtt/mqtt_valve.h +++ b/esphome/components/mqtt/mqtt_valve.h @@ -8,8 +8,7 @@ #include "esphome/components/valve/valve.h" -namespace esphome { -namespace mqtt { +namespace esphome::mqtt { class MQTTValveComponent : public mqtt::MQTTComponent { public: @@ -34,8 +33,7 @@ class MQTTValveComponent : public mqtt::MQTTComponent { valve::Valve *valve_; }; -} // namespace mqtt -} // namespace esphome +} // namespace esphome::mqtt #endif #endif // USE_MQTT diff --git a/esphome/components/my9231/my9231.cpp b/esphome/components/my9231/my9231.cpp index fba7ac2bf3..5b77a49e72 100644 --- a/esphome/components/my9231/my9231.cpp +++ b/esphome/components/my9231/my9231.cpp @@ -58,14 +58,14 @@ void MY9231OutputComponent::setup() { } } void MY9231OutputComponent::dump_config() { - ESP_LOGCONFIG(TAG, "MY9231:"); - LOG_PIN(" DI Pin: ", this->pin_di_); - LOG_PIN(" DCKI Pin: ", this->pin_dcki_); ESP_LOGCONFIG(TAG, + "MY9231:\n" " Total number of channels: %u\n" " Number of chips: %u\n" " Bit depth: %u", this->num_channels_, this->num_chips_, this->bit_depth_); + LOG_PIN(" DI Pin: ", this->pin_di_); + LOG_PIN(" DCKI Pin: ", this->pin_dcki_); } void MY9231OutputComponent::loop() { if (!this->update_) diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index 6a31b754f7..5edbc79862 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -131,9 +131,9 @@ void NAU7802Sensor::dump_config() { } // Note these may differ from the values on the device if calbration has been run ESP_LOGCONFIG(TAG, - " Offset Calibration: %s\n" + " Offset Calibration: %" PRId32 "\n" " Gain Calibration: %f", - to_string(this->offset_calibration_).c_str(), this->gain_calibration_); + this->offset_calibration_, this->gain_calibration_); std::string voltage = "unknown"; switch (this->ldo_) { @@ -278,7 +278,7 @@ void NAU7802Sensor::loop() { this->set_calibration_failure_(true); this->state_ = CalibrationState::INACTIVE; ESP_LOGE(TAG, "Failed to calibrate sensor"); - this->status_set_error("Calibration Failed"); + this->status_set_error(LOG_STR("Calibration Failed")); return; } @@ -289,7 +289,7 @@ void NAU7802Sensor::loop() { this->status_clear_error(); int32_t ocal = this->read_value_(OCAL1_B2_REG, 3); - ESP_LOGI(TAG, "New Offset: %s", to_string(ocal).c_str()); + ESP_LOGI(TAG, "New Offset: %" PRId32, ocal); uint32_t gcal = this->read_value_(GCAL1_B3_REG, 4); float gcal_f = ((float) gcal / (float) (1 << GCAL1_FRACTIONAL)); ESP_LOGI(TAG, "New Gain: %f", gcal_f); diff --git a/esphome/components/neopixelbus/_methods.py b/esphome/components/neopixelbus/_methods.py index 5a00fa2804..9072f78035 100644 --- a/esphome/components/neopixelbus/_methods.py +++ b/esphome/components/neopixelbus/_methods.py @@ -2,12 +2,12 @@ from dataclasses import dataclass from typing import Any import esphome.codegen as cg -from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import ( +from esphome.components.esp32 import ( VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3, + get_esp32_variant, ) import esphome.config_validation as cv from esphome.const import ( diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 0c9604e932..c77217243c 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -1,8 +1,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import light -from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import VARIANT_ESP32C3, VARIANT_ESP32S3 +from esphome.components.esp32 import VARIANT_ESP32C3, VARIANT_ESP32S3, get_esp32_variant import esphome.config_validation as cv from esphome.const import ( CONF_CHANNEL, @@ -195,6 +194,14 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + if CORE.is_esp8266: + # NeoPixelBus library unconditionally includes NeoEsp8266UartMethod.h + # which references Serial and Serial1, so we must enable both + from esphome.components.esp8266.const import enable_serial, enable_serial1 + + enable_serial() + enable_serial1() + has_white = "W" in config[CONF_TYPE] method = config[CONF_METHOD] diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index d7a51fb0c6..5b63bbfce9 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -156,7 +156,7 @@ async def to_code(config): "High performance networking disabled by user configuration (overriding component request)" ) - if CORE.is_esp32 and CORE.using_esp_idf and should_enable: + if CORE.is_esp32 and should_enable: # Check if PSRAM is guaranteed (set by psram component during final validation) psram_guaranteed = psram_is_guaranteed() @@ -210,12 +210,12 @@ async def to_code(config): "USE_NETWORK_MIN_IPV6_ADDR_COUNT", config[CONF_MIN_IPV6_ADDR_COUNT] ) if CORE.is_esp32: - if CORE.using_esp_idf: - add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6) - add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6) - else: + if CORE.using_arduino: add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", True) add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", True) + else: + add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6) + add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6) elif enable_ipv6: cg.add_build_flag("-DCONFIG_LWIP_IPV6") cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG") diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 5ec6450cce..b719d1a70e 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -8,7 +8,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/macros.h" -#if defined(USE_ESP_IDF) || defined(USE_LIBRETINY) || USE_ARDUINO_VERSION_CODE > VERSION_CODE(3, 0, 0) +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || USE_ARDUINO_VERSION_CODE > VERSION_CODE(3, 0, 0) #include #endif @@ -40,6 +40,9 @@ using ip4_addr_t = in_addr; namespace esphome { namespace network { +/// Buffer size for IP address string (IPv6 max: 39 chars + null) +static constexpr size_t IP_ADDRESS_BUFFER_SIZE = 40; + struct IPAddress { public: #ifdef USE_HOST @@ -50,6 +53,10 @@ struct IPAddress { IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } std::string str() const { return str_lower_case(inet_ntoa(ip_addr_)); } + /// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes. + char *str_to(char *buf) const { + return const_cast(inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE)); + } #else IPAddress() { ip_addr_set_zero(&ip_addr_); } IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { @@ -81,7 +88,12 @@ struct IPAddress { ip_addr_.type = IPADDR_TYPE_V6; } #endif /* LWIP_IPV6 */ - IPAddress(esp_ip4_addr_t *other_ip) { memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); } + IPAddress(esp_ip4_addr_t *other_ip) { + memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(esp_ip4_addr_t)); +#if LWIP_IPV6 + ip_addr_.type = IPADDR_TYPE_V4; +#endif + } IPAddress(esp_ip_addr_t *other_ip) { #if LWIP_IPV6 memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip_addr_)); @@ -123,6 +135,8 @@ struct IPAddress { bool is_ip6() const { return IP_IS_V6(&ip_addr_); } bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); } + /// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes. + char *str_to(char *buf) const { return ipaddr_ntoa_r(&ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE); } bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); } bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); } IPAddress &operator+=(uint8_t increase) { diff --git a/esphome/components/nextion/__init__.py b/esphome/components/nextion/__init__.py index 8adc49d68c..38f449dc03 100644 --- a/esphome/components/nextion/__init__.py +++ b/esphome/components/nextion/__init__.py @@ -13,14 +13,16 @@ CONF_SEND_TO_NEXTION = "send_to_nextion" FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "nextion_upload_arduino.cpp": { + "nextion_upload_esp32.cpp": { PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "nextion_upload_arduino.cpp": { PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2040_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, - "nextion_upload_idf.cpp": {PlatformFramework.ESP32_IDF}, } ) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index ed6cd93027..b95df55a61 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -154,14 +154,11 @@ async def to_code(config): cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) if CORE.is_esp32: - if CORE.using_arduino: - cg.add_library("NetworkClientSecure", None) - cg.add_library("HTTPClient", None) esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) esp32.add_idf_sdkconfig_option( "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True ) - elif CORE.is_esp8266 and CORE.using_arduino: + elif CORE.is_esp8266: cg.add_library("ESP8266HTTPClient", None) if CONF_TOUCH_SLEEP_TIMEOUT in config: diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 61068b52fc..331e901578 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -13,17 +13,12 @@ #include "esphome/components/display/display_color_utils.h" #ifdef USE_NEXTION_TFT_UPLOAD -#ifdef USE_ARDUINO #ifdef USE_ESP32 -#include -#endif // USE_ESP32 -#ifdef USE_ESP8266 +#include +#elif defined(USE_ESP8266) #include #include -#endif // USE_ESP8266 -#elif defined(USE_ESP_IDF) -#include -#endif // ARDUINO vs USE_ESP_IDF +#endif // USE_ESP32 vs USE_ESP8266 #endif // USE_NEXTION_TFT_UPLOAD namespace esphome { @@ -171,7 +166,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * This will change the image of the component `pic` to the image with ID `4`. */ - void set_component_picture(const char *component, uint8_t picture_id); + void set_component_picture(const char *component, uint8_t picture_id) { set_component_picc(component, picture_id); }; /** * Set the background color of a component. @@ -374,7 +369,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * This will change the picture id of the component `textview`. */ - void set_component_pic(const char *component, uint8_t pic_id); + void set_component_pic(const char *component, uint16_t pic_id); /** * Set the background picture id of component. @@ -388,7 +383,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * * This will change the background picture id of the component `textview`. */ - void set_component_picc(const char *component, uint8_t pic_id); + void set_component_picc(const char *component, uint16_t pic_id); /** * Set the font color of a component. @@ -910,7 +905,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * Draws a QR code with a Wi-Fi network credentials starting at the given coordinates (25,25). */ void qrcode(uint16_t x1, uint16_t y1, const char *content, uint16_t size = 200, uint16_t background_color = 65535, - uint16_t foreground_color = 0, uint8_t logo_pic = -1, uint8_t border_width = 8); + uint16_t foreground_color = 0, int32_t logo_pic = -1, uint8_t border_width = 8); /** * Draws a QR code in the screen @@ -935,7 +930,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void qrcode(uint16_t x1, uint16_t y1, const char *content, uint16_t size, Color background_color = Color(255, 255, 255), Color foreground_color = Color(0, 0, 0), - uint8_t logo_pic = -1, uint8_t border_width = 8); + int32_t logo_pic = -1, uint8_t border_width = 8); /** Set the brightness of the backlight. * @@ -1078,7 +1073,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe #ifdef USE_NEXTION_TFT_UPLOAD /** - * Set the tft file URL. https seems problematic with Arduino.. + * Set the tft file URL. */ void set_tft_url(const std::string &tft_url) { this->tft_url_ = tft_url; } @@ -1422,16 +1417,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe uint32_t original_baud_rate_ = 0; bool upload_first_chunk_sent_ = false; -#ifdef USE_ARDUINO - /** - * will request chunk_size chunks from the web server - * and send each to the nextion - * @param HTTPClient http_client HTTP client handler. - * @param int range_start Position of next byte to transfer. - * @return position of last byte transferred, -1 for failure. - */ - int upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start); -#elif defined(USE_ESP_IDF) +#ifdef USE_ESP32 /** * will request 4096 bytes chunks from the web server * and send each to Nextion @@ -1440,7 +1426,16 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * @return position of last byte transferred, -1 for failure. */ int upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &range_start); -#endif // USE_ARDUINO vs USE_ESP_IDF +#elif defined(USE_ARDUINO) + /** + * will request chunk_size chunks from the web server + * and send each to the nextion + * @param HTTPClient http_client HTTP client handler. + * @param int range_start Position of next byte to transfer. + * @return position of last byte transferred, -1 for failure. + */ + int upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start); +#endif // USE_ESP32 vs USE_ARDUINO /** * Ends the upload process, restart Nextion and, if successful, @@ -1450,12 +1445,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ bool upload_end_(bool successful); - /** - * Returns the ESP Free Heap memory. This is framework independent. - * @return Free Heap in bytes. - */ - uint32_t get_free_heap_(); - #endif // USE_NEXTION_TFT_UPLOAD bool check_connect_(); diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index cfaae7e3e0..2adf314a2e 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -143,12 +143,12 @@ void Nextion::set_component_pressed_font_color(const char *component, Color colo } // Set picture -void Nextion::set_component_pic(const char *component, uint8_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.pic=%" PRIu8, component, pic_id); +void Nextion::set_component_pic(const char *component, uint16_t pic_id) { + this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.pic=%" PRIu16, component, pic_id); } -void Nextion::set_component_picc(const char *component, uint8_t pic_id) { - this->add_no_result_to_queue_with_printf_("set_component_picc", "%s.picc=%" PRIu8, component, pic_id); +void Nextion::set_component_picc(const char *component, uint16_t pic_id) { + this->add_no_result_to_queue_with_printf_("set_component_picc", "%s.picc=%" PRIu16, component, pic_id); } // Set video @@ -217,10 +217,6 @@ void Nextion::disable_component_touch(const char *component) { this->add_no_result_to_queue_with_printf_("disable_component_touch", "tsw %s,0", component); } -void Nextion::set_component_picture(const char *component, uint8_t picture_id) { - this->add_no_result_to_queue_with_printf_("set_component_picture", "%s.pic=%" PRIu8, component, picture_id); -} - void Nextion::set_component_text(const char *component, const char *text) { this->add_no_result_to_queue_with_printf_("set_component_text", "%s.txt=\"%s\"", component, text); } @@ -330,14 +326,14 @@ void Nextion::filled_circle(uint16_t center_x, uint16_t center_y, uint16_t radiu } void Nextion::qrcode(uint16_t x1, uint16_t y1, const char *content, uint16_t size, uint16_t background_color, - uint16_t foreground_color, uint8_t logo_pic, uint8_t border_width) { + uint16_t foreground_color, int32_t logo_pic, uint8_t border_width) { this->add_no_result_to_queue_with_printf_( "qrcode", "qrcode %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu8 ",%" PRIu8 ",\"%s\"", x1, y1, size, background_color, foreground_color, logo_pic, border_width, content); } void Nextion::qrcode(uint16_t x1, uint16_t y1, const char *content, uint16_t size, Color background_color, - Color foreground_color, uint8_t logo_pic, uint8_t border_width) { + Color foreground_color, int32_t logo_pic, uint8_t border_width) { this->add_no_result_to_queue_with_printf_( "qrcode", "qrcode %" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu16 ",%" PRIu8 ",%" PRIu8 ",\"%s\"", x1, y1, size, display::ColorUtil::color_to_565(background_color), display::ColorUtil::color_to_565(foreground_color), diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index b4d217d7aa..d210bad004 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -1,43 +1,35 @@ #include "nextion.h" #ifdef USE_NEXTION_TFT_UPLOAD -#ifdef USE_ARDUINO +#ifndef USE_ESP32 #include #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" -#ifdef USE_ESP32 -#include -#endif - namespace esphome { namespace nextion { static const char *const TAG = "nextion.upload.arduino"; +static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 -inline uint32_t Nextion::get_free_heap_() { -#if defined(USE_ESP32) - return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); -#elif defined(USE_ESP8266) - return EspClass::getFreeHeap(); -#endif // USE_ESP32 vs USE_ESP8266 -} - int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { uint32_t range_size = this->tft_size_ - range_start; - ESP_LOGV(TAG, "Heap: %" PRIu32, this->get_free_heap_()); + ESP_LOGV(TAG, "Heap: %" PRIu32, EspClass::getFreeHeap()); uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { - ESP_LOGD(TAG, "Range end: %" PRIu32, range_end); - ESP_LOGD(TAG, "Range size: %" PRIu32, range_size); ESP_LOGE(TAG, "Invalid range"); + ESP_LOGD(TAG, + "Range end: %" PRIu32 "\n" + "Range size: %" PRIu32, + range_end, range_size); return -1; } @@ -95,18 +87,14 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true); this->content_length_ -= read_len; const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_; -#if defined(USE_ESP32) && defined(USE_PSRAM) - ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 "+%" PRIu32 ")", upload_percentage, - this->content_length_, static_cast(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)), - static_cast(heap_caps_get_free_size(MALLOC_CAP_SPIRAM))); -#else ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_, - this->get_free_heap_()); -#endif + EspClass::getFreeHeap()); upload_first_chunk_sent_ = true; if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request - ESP_LOGD(TAG, "Recv: [%s]", - format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; + ESP_LOGD( + TAG, "Recv: [%s]", + format_hex_pretty_to(hex_buf, reinterpret_cast(recv_string.data()), recv_string.size())); uint32_t result = 0; for (int j = 0; j < 4; ++j) { result += static_cast(recv_string[j + 1]) << (8 * j); @@ -123,8 +111,10 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { buffer = nullptr; return range_end + 1; } else if (recv_string[0] != 0x05 and recv_string[0] != 0x08) { // 0x05 == "ok" - ESP_LOGE(TAG, "Invalid response: [%s]", - format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; + ESP_LOGE( + TAG, "Invalid response: [%s]", + format_hex_pretty_to(hex_buf, reinterpret_cast(recv_string.data()), recv_string.size())); // Deallocate buffer allocator.deallocate(buffer, 4096); buffer = nullptr; @@ -148,9 +138,11 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { - ESP_LOGD(TAG, "TFT upload requested"); - ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse)); - ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); + ESP_LOGD(TAG, + "TFT upload requested\n" + "Exit reparse: %s\n" + "URL: %s", + YESNO(exit_reparse), this->tft_url_.c_str()); if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); @@ -174,18 +166,20 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); + if (baud_rate <= 0) { + baud_rate = this->original_baud_rate_; + } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client - ESP_LOGV(TAG, "Init HTTP client"); - ESP_LOGV(TAG, "Heap: %" PRIu32, this->get_free_heap_()); + ESP_LOGV(TAG, + "Init HTTP client\n" + "Heap: %" PRIu32, + EspClass::getFreeHeap()); HTTPClient http_client; http_client.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along bool begin_status = false; -#ifdef USE_ESP32 - begin_status = http_client.begin(this->tft_url_.c_str()); -#endif #ifdef USE_ESP8266 #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); @@ -253,22 +247,24 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { this->send_command_("sleep=0"); this->send_command_("dim=100"); delay(250); // NOLINT - ESP_LOGV(TAG, "Heap: %" PRIu32, this->get_free_heap_()); + ESP_LOGV(TAG, "Heap: %" PRIu32, EspClass::getFreeHeap()); App.feed_wdt(); char command[128]; // Tells the Nextion the content length of the tft file and baud rate it will be sent at // Once the Nextion accepts the command it will wait until the file is successfully uploaded // If it fails for any reason a power cycle of the display will be needed - sprintf(command, "whmi-wris %d,%d,1", this->content_length_, baud_rate); + snprintf(command, sizeof(command), "whmi-wris %" PRIu32 ",%" PRIu32 ",1", this->content_length_, baud_rate); // Clear serial receive buffer ESP_LOGV(TAG, "Clear RX buffer"); this->reset_(false); delay(250); // NOLINT - ESP_LOGV(TAG, "Heap: %" PRIu32, this->get_free_heap_()); - ESP_LOGV(TAG, "Upload cmd: %s", command); + ESP_LOGV(TAG, + "Heap: %" PRIu32 "\n" + "Upload cmd: %s", + EspClass::getFreeHeap(), command); this->send_command_(command); if (baud_rate != this->original_baud_rate_) { @@ -284,10 +280,11 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { this->recv_ret_string_(response, 5000, true); // This can take some time to return // The Nextion display will, if it's ready to accept data, send a 0x05 byte. + char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; ESP_LOGD(TAG, "Upload resp: [%s] %zu B", - format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str(), + format_hex_pretty_to(hex_buf, reinterpret_cast(response.data()), response.size()), response.length()); - ESP_LOGV(TAG, "Heap: %" PRIu32, this->get_free_heap_()); + ESP_LOGV(TAG, "Heap: %" PRIu32, EspClass::getFreeHeap()); if (response.find(0x05) != std::string::npos) { ESP_LOGV(TAG, "Upload prep done"); @@ -299,10 +296,12 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return this->upload_end_(false); } - ESP_LOGD(TAG, "Upload TFT:"); - ESP_LOGD(TAG, " URL: %s", this->tft_url_.c_str()); - ESP_LOGD(TAG, " Size: %d bytes", this->content_length_); - ESP_LOGD(TAG, " Heap: %" PRIu32, this->get_free_heap_()); + ESP_LOGD(TAG, + "Upload TFT:\n" + " URL: %s\n" + " Size: %d bytes\n" + " Heap: %" PRIu32, + this->tft_url_.c_str(), this->content_length_, EspClass::getFreeHeap()); // Proceed with the content download as before @@ -319,7 +318,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return this->upload_end_(false); } App.feed_wdt(); - ESP_LOGV(TAG, "Heap: %" PRIu32 " left: %" PRIu32, this->get_free_heap_(), this->content_length_); + ESP_LOGV(TAG, "Heap: %" PRIu32 " left: %" PRIu32, EspClass::getFreeHeap(), this->content_length_); } ESP_LOGD(TAG, "Upload complete"); @@ -353,5 +352,5 @@ WiFiClient *Nextion::get_wifi_client_() { } // namespace nextion } // namespace esphome -#endif // USE_ARDUINO +#endif // NOT USE_ESP32 #endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp similarity index 84% rename from esphome/components/nextion/nextion_upload_idf.cpp rename to esphome/components/nextion/nextion_upload_esp32.cpp index 3b0d65643d..712fa8e78e 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -1,7 +1,7 @@ #include "nextion.h" #ifdef USE_NEXTION_TFT_UPLOAD -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include #include @@ -9,12 +9,14 @@ #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" namespace esphome { namespace nextion { -static const char *const TAG = "nextion.upload.idf"; +static const char *const TAG = "nextion.upload.esp32"; +static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16; // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 @@ -25,8 +27,10 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { - ESP_LOGD(TAG, "Range end: %" PRIu32, range_end); - ESP_LOGD(TAG, "Range size: %" PRIu32, range_size); + ESP_LOGD(TAG, + "Range end: %" PRIu32 "\n" + "Range size: %" PRIu32, + range_end, range_size); ESP_LOGE(TAG, "Invalid range"); return -1; } @@ -108,8 +112,10 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r #endif upload_first_chunk_sent_ = true; if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request - ESP_LOGD(TAG, "Recv: [%s]", - format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; + ESP_LOGD( + TAG, "Recv: [%s]", + format_hex_pretty_to(hex_buf, reinterpret_cast(recv_string.data()), recv_string.size())); uint32_t result = 0; for (int j = 0; j < 4; ++j) { result += static_cast(recv_string[j + 1]) << (8 * j); @@ -126,8 +132,10 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r buffer = nullptr; return range_end + 1; } else if (recv_string[0] != 0x05 and recv_string[0] != 0x08) { // 0x05 == "ok" - ESP_LOGE(TAG, "Invalid response: [%s]", - format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; + ESP_LOGE( + TAG, "Invalid response: [%s]", + format_hex_pretty_to(hex_buf, reinterpret_cast(recv_string.data()), recv_string.size())); // Deallocate buffer allocator.deallocate(buffer, 4096); buffer = nullptr; @@ -151,9 +159,11 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { - ESP_LOGD(TAG, "TFT upload requested"); - ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse)); - ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); + ESP_LOGD(TAG, + "TFT upload requested\n" + "Exit reparse: %s\n" + "URL: %s", + YESNO(exit_reparse), this->tft_url_.c_str()); if (this->connection_state_.is_updating_) { ESP_LOGW(TAG, "Upload in progress"); @@ -177,11 +187,16 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); + if (baud_rate <= 0) { + baud_rate = this->original_baud_rate_; + } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client - ESP_LOGV(TAG, "Init HTTP client"); - ESP_LOGV(TAG, "Heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGV(TAG, + "Init HTTP client\n" + "Heap: %" PRIu32, + esp_get_free_heap_size()); esp_http_client_config_t config = { .url = this->tft_url_.c_str(), .cert_pem = nullptr, @@ -205,8 +220,10 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // Perform the HTTP request - ESP_LOGV(TAG, "Check connection"); - ESP_LOGV(TAG, "Heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGV(TAG, + "Check connection\n" + "Heap: %" PRIu32, + esp_get_free_heap_size()); err = esp_http_client_perform(http_client); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP failed: %s", esp_err_to_name(err)); @@ -215,8 +232,10 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // Check the HTTP Status Code - ESP_LOGV(TAG, "Check status"); - ESP_LOGV(TAG, "Heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGV(TAG, + "Check status\n" + "Heap: %" PRIu32, + esp_get_free_heap_size()); int status_code = esp_http_client_get_status_code(http_client); if (status_code != 200 && status_code != 206) { return this->upload_end_(false); @@ -252,7 +271,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { // Tells the Nextion the content length of the tft file and baud rate it will be sent at // Once the Nextion accepts the command it will wait until the file is successfully uploaded // If it fails for any reason a power cycle of the display will be needed - sprintf(command, "whmi-wris %" PRIu32 ",%" PRIu32 ",1", this->content_length_, baud_rate); + snprintf(command, sizeof(command), "whmi-wris %" PRIu32 ",%" PRIu32 ",1", this->content_length_, baud_rate); // Clear serial receive buffer ESP_LOGV(TAG, "Clear RX buffer"); @@ -274,8 +293,9 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { this->recv_ret_string_(response, 5000, true); // This can take some time to return // The Nextion display will, if it's ready to accept data, send a 0x05 byte. + char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)]; ESP_LOGD(TAG, "Upload resp: [%s] %zu B", - format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str(), + format_hex_pretty_to(hex_buf, reinterpret_cast(response.data()), response.size()), response.length()); ESP_LOGV(TAG, "Heap: %" PRIu32, esp_get_free_heap_size()); @@ -297,10 +317,12 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { return this->upload_end_(false); } - ESP_LOGD(TAG, "Uploading TFT:"); - ESP_LOGD(TAG, " URL: %s", this->tft_url_.c_str()); - ESP_LOGD(TAG, " Size: %" PRIu32 " bytes", this->content_length_); - ESP_LOGD(TAG, " Heap: %" PRIu32, esp_get_free_heap_size()); + ESP_LOGD(TAG, + "Uploading TFT:\n" + " URL: %s\n" + " Size: %" PRIu32 " bytes\n" + " Heap: %" PRIu32, + this->tft_url_.c_str(), this->content_length_, esp_get_free_heap_size()); // Proceed with the content download as before @@ -321,9 +343,8 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGV(TAG, "Heap: %" PRIu32 " left: %" PRIu32, esp_get_free_heap_size(), this->content_length_); } - ESP_LOGD(TAG, "TFT upload complete"); - - ESP_LOGD(TAG, "Close HTTP"); + ESP_LOGD(TAG, "TFT upload complete\n" + "Close HTTP"); esp_http_client_close(http_client); esp_http_client_cleanup(http_client); ESP_LOGV(TAG, "Connection closed"); @@ -333,5 +354,5 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { } // namespace nextion } // namespace esphome -#endif // USE_ESP_IDF +#endif // USE_ESP32 #endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 03927e8ea2..bf90a41df5 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -12,6 +12,7 @@ from esphome.components.zephyr import ( zephyr_add_prj_conf, zephyr_data, zephyr_set_core_data, + zephyr_setup_preferences, zephyr_to_code, ) from esphome.components.zephyr.const import ( @@ -49,7 +50,7 @@ from .const import ( from .gpio import nrf52_pin_to_code # noqa CODEOWNERS = ["@tomaszduda23"] -AUTO_LOAD = ["zephyr"] +AUTO_LOAD = ["zephyr", "preferences"] IS_TARGET_PLATFORM = True _LOGGER = logging.getLogger(__name__) @@ -194,6 +195,7 @@ async def to_code(config: ConfigType) -> None: cg.add_platformio_option("board_upload.require_upload_port", "true") cg.add_platformio_option("board_upload.wait_for_upload_port", "true") + zephyr_setup_preferences() zephyr_to_code(config) if dfu_config := config.get(CONF_DFU): @@ -206,6 +208,18 @@ async def to_code(config: ConfigType) -> None: if reg0_config[CONF_UICR_ERASE]: cg.add_define("USE_NRF52_UICR_ERASE") + # c++ support + zephyr_add_prj_conf("CPLUSPLUS", True) + zephyr_add_prj_conf("LIB_CPLUSPLUS", True) + # watchdog + zephyr_add_prj_conf("WATCHDOG", True) + zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False) + # disable console + zephyr_add_prj_conf("UART_CONSOLE", False) + zephyr_add_prj_conf("CONSOLE", False) + # use NFC pins as GPIO + zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) + @coroutine_with_priority(CoroPriority.DIAGNOSTICS) async def _dfu_to_code(dfu_config): diff --git a/esphome/components/nrf52/gpio.py b/esphome/components/nrf52/gpio.py index 17329042b2..498e8cc330 100644 --- a/esphome/components/nrf52/gpio.py +++ b/esphome/components/nrf52/gpio.py @@ -71,8 +71,15 @@ NRF52_PIN_SCHEMA = cv.All( @pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_NRF52, NRF52_PIN_SCHEMA) async def nrf52_pin_to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] + port = num // 32 + pin_name_prefix = f"P{port}." + var = cg.new_Pvariable( + config[CONF_ID], + cg.RawExpression(f"DEVICE_DT_GET_OR_NULL(DT_NODELABEL(gpio{port}))"), + 32, + pin_name_prefix, + ) cg.add(var.set_pin(num)) # Only set if true to avoid bloating setup() function # (inverted bit in pin_flags_ bitfield is zero-initialized to false) diff --git a/esphome/components/number/automation.cpp b/esphome/components/number/automation.cpp index bfc59d0465..78ffc255fe 100644 --- a/esphome/components/number/automation.cpp +++ b/esphome/components/number/automation.cpp @@ -1,8 +1,7 @@ #include "automation.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number.automation"; @@ -52,5 +51,4 @@ void ValueRangeTrigger::on_state_(float state) { this->rtc_.save(&in_range); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index 79eba883c4..a7cd04f083 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace number { +namespace esphome::number { class NumberStateTrigger : public Trigger { public: @@ -91,5 +90,4 @@ template class NumberInRangeCondition : public Condition float max_{NAN}; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index f12e0e9e1e..992100ead0 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -3,8 +3,7 @@ #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; @@ -43,5 +42,4 @@ void Number::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index da91d70d53..0425714702 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -6,8 +6,7 @@ #include "number_call.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { class Number; void log_number(const char *tag, const char *prefix, const char *type, Number *obj); @@ -50,8 +49,7 @@ class Number : public EntityBase { */ virtual void control(float value) = 0; - CallbackManager state_callback_; + LazyCallbackManager state_callback_; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp index 669dd65184..27a857c112 100644 --- a/esphome/components/number/number_call.cpp +++ b/esphome/components/number/number_call.cpp @@ -2,8 +2,7 @@ #include "number.h" #include "esphome/core/log.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; @@ -125,5 +124,4 @@ void NumberCall::perform() { this->parent_->control(target_value); } -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 807207f0ec..584c13f413 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -4,12 +4,11 @@ #include "esphome/core/log.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { class Number; -enum NumberOperation { +enum NumberOperation : uint8_t { NUMBER_OP_NONE, NUMBER_OP_SET, NUMBER_OP_INCREMENT, @@ -39,10 +38,9 @@ class NumberCall { float limit); Number *const parent_; - NumberOperation operation_{NUMBER_OP_NONE}; optional value_; - bool cycle_; + NumberOperation operation_{NUMBER_OP_NONE}; + bool cycle_{false}; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_traits.cpp b/esphome/components/number/number_traits.cpp index 89035661f5..1e4239ceca 100644 --- a/esphome/components/number/number_traits.cpp +++ b/esphome/components/number/number_traits.cpp @@ -1,10 +1,8 @@ #include "esphome/core/log.h" #include "number_traits.h" -namespace esphome { -namespace number { +namespace esphome::number { static const char *const TAG = "number"; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h index fa68c2390a..5ccbb9ba48 100644 --- a/esphome/components/number/number_traits.h +++ b/esphome/components/number/number_traits.h @@ -3,8 +3,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" -namespace esphome { -namespace number { +namespace esphome::number { enum NumberMode : uint8_t { NUMBER_MODE_AUTO = 0, @@ -35,5 +34,4 @@ class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeas NumberMode mode_{NUMBER_MODE_AUTO}; }; -} // namespace number -} // namespace esphome +} // namespace esphome::number diff --git a/esphome/components/one_wire/__init__.py b/esphome/components/one_wire/__init__.py index e12cca3e27..9173b7014b 100644 --- a/esphome/components/one_wire/__init__.py +++ b/esphome/components/one_wire/__init__.py @@ -32,7 +32,7 @@ async def register_one_wire_device(var, config): Sets the 1-wire bus to use and the 1-wire address. - This is a coroutine, you need to await it with a 'yield' expression! + This is a coroutine, you need to await it with an 'await' expression! """ parent = await cg.get_variable(config[CONF_ONE_WIRE_ID]) cg.add(var.set_one_wire_bus(parent)) diff --git a/esphome/components/one_wire/one_wire_bus.cpp b/esphome/components/one_wire/one_wire_bus.cpp index c2542177cf..27b7d58a0f 100644 --- a/esphome/components/one_wire/one_wire_bus.cpp +++ b/esphome/components/one_wire/one_wire_bus.cpp @@ -49,7 +49,8 @@ void OneWireBus::search() { break; auto *address8 = reinterpret_cast(&address); if (crc8(address8, 7) != address8[7]) { - ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", format_hex(address).c_str()); + char hex_buf[17]; + ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", format_hex_to(hex_buf, address)); } else { this->devices_.push_back(address); } @@ -82,8 +83,9 @@ void OneWireBus::dump_devices_(const char *tag) { ESP_LOGW(tag, " Found no devices!"); } else { ESP_LOGCONFIG(tag, " Found devices:"); + char hex_buf[17]; // uint64_t = 16 hex chars + null for (auto &address : this->devices_) { - ESP_LOGCONFIG(tag, " 0x%s (%s)", format_hex(address).c_str(), LOG_STR_ARG(get_model_str(address & 0xff))); + ESP_LOGCONFIG(tag, " 0x%s (%s)", format_hex_to(hex_buf, address), LOG_STR_ARG(get_model_str(address & 0xff))); } } } diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp index 2038d09ed0..ce9d3bdc91 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/online_image/png_image.cpp @@ -2,6 +2,7 @@ #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include "esphome/components/display/display_buffer.h" +#include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -38,6 +39,14 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); Color color(rgba[0], rgba[1], rgba[2], rgba[3]); decoder->draw(x, y, w, h, color); + + // Feed watchdog periodically to avoid triggering during long decode operations. + // Feed every 1024 pixels to balance efficiency and responsiveness. + uint32_t pixels = w * h; + decoder->increment_pixels_decoded(pixels); + if ((decoder->get_pixels_decoded() % 1024) < pixels) { + App.feed_wdt(); + } } PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) { diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h index 46519f8ef4..40e85dde33 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/online_image/png_image.h @@ -25,9 +25,13 @@ class PngDecoder : public ImageDecoder { int prepare(size_t download_size) override; int HOT decode(uint8_t *buffer, size_t size) override; + void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; } + uint32_t get_pixels_decoded() const { return this->pixels_decoded_; } + protected: RAMAllocator allocator_; pngle_t *pngle_; + uint32_t pixels_decoded_{0}; }; } // namespace online_image diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp index b23792fc7a..7a0cdc7f80 100644 --- a/esphome/components/opentherm/hub.cpp +++ b/esphome/components/opentherm/hub.cpp @@ -395,10 +395,8 @@ void OpenthermHub::dump_config() { this->write_initial_messages_(initial_messages); this->write_repeating_messages_(repeating_messages); - ESP_LOGCONFIG(TAG, "OpenTherm:"); - LOG_PIN(" In: ", this->in_pin_); - LOG_PIN(" Out: ", this->out_pin_); ESP_LOGCONFIG(TAG, + "OpenTherm:\n" " Sync mode: %s\n" " Sensors: %s\n" " Binary sensors: %s\n" @@ -409,6 +407,8 @@ void OpenthermHub::dump_config() { YESNO(this->sync_mode_), SHOW(OPENTHERM_SENSOR_LIST(ID, )), SHOW(OPENTHERM_BINARY_SENSOR_LIST(ID, )), SHOW(OPENTHERM_SWITCH_LIST(ID, )), SHOW(OPENTHERM_INPUT_SENSOR_LIST(ID, )), SHOW(OPENTHERM_OUTPUT_LIST(ID, )), SHOW(OPENTHERM_NUMBER_LIST(ID, ))); + LOG_PIN(" In: ", this->in_pin_); + LOG_PIN(" Out: ", this->out_pin_); ESP_LOGCONFIG(TAG, " Initial requests:"); for (auto type : initial_messages) { ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str(type)); diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index d59b9584d1..c6443f1282 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -7,6 +7,7 @@ #include "opentherm.h" #include "esphome/core/helpers.h" +#include #ifdef USE_ESP32 #include "driver/timer.h" #include "esp_err.h" @@ -20,7 +21,6 @@ namespace esphome { namespace opentherm { using std::string; -using std::to_string; static const char *const TAG = "opentherm"; @@ -563,14 +563,13 @@ const char *OpenTherm::message_id_to_str(MessageId id) { void OpenTherm::debug_data(OpenthermData &data) { ESP_LOGD(TAG, "%s %s %s %s", format_bin(data.type).c_str(), format_bin(data.id).c_str(), format_bin(data.valueHB).c_str(), format_bin(data.valueLB).c_str()); - ESP_LOGD(TAG, "type: %s; id: %s; HB: %s; LB: %s; uint_16: %s; float: %s", - this->message_type_to_str((MessageType) data.type), to_string(data.id).c_str(), - to_string(data.valueHB).c_str(), to_string(data.valueLB).c_str(), to_string(data.u16()).c_str(), - to_string(data.f88()).c_str()); + ESP_LOGD(TAG, "type: %s; id: %u; HB: %u; LB: %u; uint_16: %u; float: %f", + this->message_type_to_str((MessageType) data.type), data.id, data.valueHB, data.valueLB, data.u16(), + data.f88()); } void OpenTherm::debug_error(OpenThermError &error) const { - ESP_LOGD(TAG, "data: %s; clock: %s; capture: %s; bit_pos: %s", format_hex(error.data).c_str(), - to_string(clock_).c_str(), format_bin(error.capture).c_str(), to_string(error.bit_pos).c_str()); + ESP_LOGD(TAG, "data: 0x%08" PRIx32 "; clock: %u; capture: 0x%08" PRIx32 "; bit_pos: %u", error.data, this->clock_, + error.capture, error.bit_pos); } float OpenthermData::f88() { return ((float) this->s16()) / 256.0; } diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index e3ad3ed76c..26c05a0a86 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components.esp32 import ( + VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2, add_idf_sdkconfig_option, @@ -90,7 +91,7 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5) - # TODO: Add suport for synchronized sleepy end devices (SSED) + # TODO: Add support for synchronized sleepy end devices (SSED) add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True) @@ -101,7 +102,7 @@ OpenThreadSrpComponent = openthread_ns.class_("OpenThreadSrpComponent", cg.Compo _CONNECTION_SCHEMA = cv.Schema( { cv.Optional(CONF_PAN_ID): cv.hex_int, - cv.Optional(CONF_CHANNEL): cv.int_, + cv.Optional(CONF_CHANNEL): cv.int_range(min=11, max=26), cv.Optional(CONF_NETWORK_KEY): cv.hex_int, cv.Optional(CONF_EXT_PAN_ID): cv.hex_int, cv.Optional(CONF_NETWORK_NAME): cv.string_strict, @@ -151,8 +152,7 @@ CONFIG_SCHEMA = cv.All( } ).extend(_CONNECTION_SCHEMA), cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), - cv.only_with_esp_idf, - only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), + only_on_variant(supported=[VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2]), _validate, _require_vfs_select, ) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 721ab89326..90da17e2d3 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -21,8 +21,7 @@ static const char *const TAG = "openthread"; -namespace esphome { -namespace openthread { +namespace esphome::openthread { OpenThreadComponent *global_openthread_component = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -275,7 +274,5 @@ const char *OpenThreadComponent::get_use_address() const { return this->use_addr void OpenThreadComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } -} // namespace openthread -} // namespace esphome - +} // namespace esphome::openthread #endif diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 546128b366..3c60acaadd 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -13,8 +13,7 @@ #include #include -namespace esphome { -namespace openthread { +namespace esphome::openthread { class InstanceLock; @@ -91,6 +90,5 @@ class InstanceLock { InstanceLock() {} }; -} // namespace openthread -} // namespace esphome +} // namespace esphome::openthread #endif diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 72dc521091..a9aff3cce4 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -1,5 +1,5 @@ #include "esphome/core/defines.h" -#if defined(USE_OPENTHREAD) && defined(USE_ESP_IDF) +#if defined(USE_OPENTHREAD) && defined(USE_ESP32) #include #include "openthread.h" @@ -24,8 +24,7 @@ static const char *const TAG = "openthread"; -namespace esphome { -namespace openthread { +namespace esphome::openthread { void OpenThreadComponent::setup() { // Used eventfds: @@ -127,9 +126,12 @@ void OpenThreadComponent::ot_main() { ESP_LOGE(TAG, "Failed to set OpenThread linkmode."); } link_mode_config = otThreadGetLinkMode(esp_openthread_get_instance()); - ESP_LOGD(TAG, "Link Mode Device Type: %s", link_mode_config.mDeviceType ? "true" : "false"); - ESP_LOGD(TAG, "Link Mode Network Data: %s", link_mode_config.mNetworkData ? "true" : "false"); - ESP_LOGD(TAG, "Link Mode RX On When Idle: %s", link_mode_config.mRxOnWhenIdle ? "true" : "false"); + ESP_LOGD(TAG, + "Link Mode Device Type: %s\n" + "Link Mode Network Data: %s\n" + "Link Mode RX On When Idle: %s", + link_mode_config.mDeviceType ? "true" : "false", link_mode_config.mNetworkData ? "true" : "false", + link_mode_config.mRxOnWhenIdle ? "true" : "false"); // Run the main loop #if CONFIG_OPENTHREAD_CLI @@ -145,8 +147,8 @@ void OpenThreadComponent::ot_main() { // Make sure the length is 0 so we fallback to the configuration dataset.mLength = 0; } else { - ESP_LOGI(TAG, "Found OpenThread-managed dataset, ignoring esphome configuration"); - ESP_LOGI(TAG, "(set force_dataset: true to override)"); + ESP_LOGI(TAG, "Found OpenThread-managed dataset, ignoring esphome configuration\n" + "(set force_dataset: true to override)"); } #endif @@ -209,6 +211,5 @@ otInstance *InstanceLock::get_instance() { return esp_openthread_get_instance(); InstanceLock::~InstanceLock() { esp_openthread_lock_release(); } -} // namespace openthread -} // namespace esphome +} // namespace esphome::openthread #endif diff --git a/esphome/components/openthread_info/openthread_info_text_sensor.cpp b/esphome/components/openthread_info/openthread_info_text_sensor.cpp index 10724f3e2f..fc61ad81b2 100644 --- a/esphome/components/openthread_info/openthread_info_text_sensor.cpp +++ b/esphome/components/openthread_info/openthread_info_text_sensor.cpp @@ -3,8 +3,7 @@ #ifdef USE_OPENTHREAD #include "esphome/core/log.h" -namespace esphome { -namespace openthread_info { +namespace esphome::openthread_info { static const char *const TAG = "openthread_info"; @@ -19,6 +18,5 @@ void NetworkKeyOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "Network Key" void PanIdOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "PAN ID", this); } void ExtPanIdOpenThreadInfo::dump_config() { LOG_TEXT_SENSOR("", "Extended PAN ID", this); } -} // namespace openthread_info -} // namespace esphome +} // namespace esphome::openthread_info #endif diff --git a/esphome/components/openthread_info/openthread_info_text_sensor.h b/esphome/components/openthread_info/openthread_info_text_sensor.h index bbcd2d4655..ac5623e0c1 100644 --- a/esphome/components/openthread_info/openthread_info_text_sensor.h +++ b/esphome/components/openthread_info/openthread_info_text_sensor.h @@ -5,8 +5,7 @@ #include "esphome/core/component.h" #ifdef USE_OPENTHREAD -namespace esphome { -namespace openthread_info { +namespace esphome::openthread_info { using esphome::openthread::InstanceLock; @@ -34,13 +33,12 @@ class IPAddressOpenThreadInfo : public PollingComponent, public text_sensor::Tex return; } - char address_as_string[40]; - otIp6AddressToString(&*address, address_as_string, 40); - std::string ip = address_as_string; + char buf[OT_IP6_ADDRESS_STRING_SIZE]; + otIp6AddressToString(&*address, buf, sizeof(buf)); - if (this->last_ip_ != ip) { - this->last_ip_ = ip; - this->publish_state(this->last_ip_); + if (this->last_ip_ != buf) { + this->last_ip_ = buf; + this->publish_state(buf); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -90,7 +88,9 @@ class ExtAddrOpenThreadInfo : public OpenThreadInstancePollingComponent, public const auto *extaddr = otLinkGetExtendedAddress(instance); if (!std::equal(this->last_extaddr_.begin(), this->last_extaddr_.end(), extaddr->m8)) { std::copy(extaddr->m8, extaddr->m8 + 8, this->last_extaddr_.begin()); - this->publish_state(format_hex(extaddr->m8, 8)); + char buf[format_hex_size(8)]; + format_hex_to(buf, extaddr->m8, 8); + this->publish_state(buf); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -108,7 +108,9 @@ class Eui64OpenThreadInfo : public OpenThreadInstancePollingComponent, public te if (!std::equal(this->last_eui64_.begin(), this->last_eui64_.end(), addr.m8)) { std::copy(addr.m8, addr.m8 + 8, this->last_eui64_.begin()); - this->publish_state(format_hex(this->last_eui64_.begin(), 8)); + char buf[format_hex_size(8)]; + format_hex_to(buf, this->last_eui64_.data(), 8); + this->publish_state(buf); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -124,7 +126,9 @@ class ChannelOpenThreadInfo : public OpenThreadInstancePollingComponent, public uint8_t channel = otLinkGetChannel(instance); if (this->last_channel_ != channel) { this->last_channel_ = channel; - this->publish_state(std::to_string(this->last_channel_)); + char buf[4]; // max "255" + null + snprintf(buf, sizeof(buf), "%u", channel); + this->publish_state(buf); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -169,7 +173,9 @@ class NetworkKeyOpenThreadInfo : public DatasetOpenThreadInfo, public text_senso void update_dataset(otOperationalDataset *dataset) override { if (!std::equal(this->last_key_.begin(), this->last_key_.end(), dataset->mNetworkKey.m8)) { std::copy(dataset->mNetworkKey.m8, dataset->mNetworkKey.m8 + 16, this->last_key_.begin()); - this->publish_state(format_hex(dataset->mNetworkKey.m8, 16)); + char buf[format_hex_size(16)]; + format_hex_to(buf, dataset->mNetworkKey.m8, 16); + this->publish_state(buf); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } @@ -202,7 +208,9 @@ class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor: void update_dataset(otOperationalDataset *dataset) override { if (!std::equal(this->last_extpanid_.begin(), this->last_extpanid_.end(), dataset->mExtendedPanId.m8)) { std::copy(dataset->mExtendedPanId.m8, dataset->mExtendedPanId.m8 + 8, this->last_extpanid_.begin()); - this->publish_state(format_hex(this->last_extpanid_.begin(), 8)); + char buf[format_hex_size(8)]; + format_hex_to(buf, this->last_extpanid_.data(), 8); + this->publish_state(buf); } } @@ -213,6 +221,5 @@ class ExtPanIdOpenThreadInfo : public DatasetOpenThreadInfo, public text_sensor: std::array last_extpanid_{}; }; -} // namespace openthread_info -} // namespace esphome +} // namespace esphome::openthread_info #endif diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index eec39668db..ee54d5f8d3 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -1,3 +1,5 @@ +import logging + from esphome import automation import esphome.codegen as cg from esphome.config_helpers import filter_source_files_from_platform @@ -13,6 +15,8 @@ from esphome.const import ( from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +OTA_STATE_LISTENER_KEY = "ota_state_listener" + CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["md5", "safe_mode"] @@ -25,6 +29,8 @@ CONF_ON_PROGRESS = "on_progress" CONF_ON_STATE_CHANGE = "on_state_change" +_LOGGER = logging.getLogger(__name__) + ota_ns = cg.esphome_ns.namespace("ota") OTAComponent = ota_ns.class_("OTAComponent", cg.Component) OTAState = ota_ns.enum("OTAState") @@ -43,6 +49,10 @@ def _ota_final_validate(config): raise cv.Invalid( f"At least one platform must be specified for '{CONF_OTA}'; add '{CONF_PLATFORM}: {CONF_ESPHOME}' for original OTA functionality" ) + if CORE.is_host: + _LOGGER.warning( + "OTA not available for platform 'host'. OTA functionality disabled." + ) FINAL_VALIDATE_SCHEMA = _ota_final_validate @@ -86,9 +96,7 @@ BASE_OTA_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.OTA_UPDATES) async def to_code(config): cg.add_define("USE_OTA") - - if CORE.is_esp32 and CORE.using_arduino: - cg.add_library("Update", None) + CORE.add_job(final_step) if CORE.is_rp2040 and CORE.using_arduino: cg.add_library("Updater", None) @@ -122,14 +130,33 @@ async def ota_to_code(var, config): await automation.build_automation(trigger, [(cg.uint8, "x")], conf) use_state_callback = True if use_state_callback: - cg.add_define("USE_OTA_STATE_CALLBACK") + request_ota_state_listeners() + + +def request_ota_state_listeners() -> None: + """Request that OTA state listeners be compiled in. + + Components that need to be notified about OTA state changes (start, progress, + complete, error) should call this function during their code generation. + This enables the add_state_listener() API on OTAComponent. + """ + CORE.data[OTA_STATE_LISTENER_KEY] = True + + +@coroutine_with_priority(CoroPriority.FINAL) +async def final_step(): + """Final code generation step to configure optional OTA features.""" + if CORE.data.get(OTA_STATE_LISTENER_KEY, False): + cg.add_define("USE_OTA_STATE_LISTENER") FILTER_SOURCE_FILES = filter_source_files_from_platform( { - "ota_backend_arduino_esp32.cpp": {PlatformFramework.ESP32_ARDUINO}, - "ota_backend_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, - "ota_backend_arduino_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, + "ota_backend_esp_idf.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, + "ota_backend_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, "ota_backend_arduino_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, "ota_backend_arduino_libretiny.cpp": { PlatformFramework.BK72XX_ARDUINO, diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h index 7e1a60f3ce..92c0050ba0 100644 --- a/esphome/components/ota/automation.h +++ b/esphome/components/ota/automation.h @@ -1,5 +1,5 @@ #pragma once -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER #include "ota_backend.h" #include "esphome/core/automation.h" @@ -7,70 +7,64 @@ namespace esphome { namespace ota { -class OTAStateChangeTrigger : public Trigger { +class OTAStateChangeTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAStateChangeTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (!parent->is_failed()) { - trigger(state); - } - }); + explicit OTAStateChangeTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (!this->parent_->is_failed()) { + this->trigger(state); + } } + + protected: + OTAComponent *parent_; }; -class OTAStartTrigger : public Trigger<> { +template class OTAStateTrigger final : public Trigger<>, public OTAStateListener { public: - explicit OTAStartTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_STARTED && !parent->is_failed()) { - trigger(); - } - }); + explicit OTAStateTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == State && !this->parent_->is_failed()) { + this->trigger(); + } } + + protected: + OTAComponent *parent_; }; -class OTAProgressTrigger : public Trigger { +using OTAStartTrigger = OTAStateTrigger; +using OTAEndTrigger = OTAStateTrigger; +using OTAAbortTrigger = OTAStateTrigger; + +class OTAProgressTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAProgressTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_IN_PROGRESS && !parent->is_failed()) { - trigger(progress); - } - }); + explicit OTAProgressTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); } + + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == OTA_IN_PROGRESS && !this->parent_->is_failed()) { + this->trigger(progress); + } } + + protected: + OTAComponent *parent_; }; -class OTAEndTrigger : public Trigger<> { +class OTAErrorTrigger final : public Trigger, public OTAStateListener { public: - explicit OTAEndTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_COMPLETED && !parent->is_failed()) { - trigger(); - } - }); - } -}; + explicit OTAErrorTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); } -class OTAAbortTrigger : public Trigger<> { - public: - explicit OTAAbortTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_ABORT && !parent->is_failed()) { - trigger(); - } - }); + void on_ota_state(OTAState state, float progress, uint8_t error) override { + if (state == OTA_ERROR && !this->parent_->is_failed()) { + this->trigger(error); + } } -}; -class OTAErrorTrigger : public Trigger { - public: - explicit OTAErrorTrigger(OTAComponent *parent) { - parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { - if (state == OTA_ERROR && !parent->is_failed()) { - trigger(error); - } - }); - } + protected: + OTAComponent *parent_; }; } // namespace ota diff --git a/esphome/components/ota/ota_backend.cpp b/esphome/components/ota/ota_backend.cpp index 30de4ec4b3..8fb9f67214 100644 --- a/esphome/components/ota/ota_backend.cpp +++ b/esphome/components/ota/ota_backend.cpp @@ -3,7 +3,7 @@ namespace esphome { namespace ota { -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) OTAGlobalCallback *get_global_ota_callback() { @@ -13,7 +13,12 @@ OTAGlobalCallback *get_global_ota_callback() { return global_ota_callback; } -void register_ota_platform(OTAComponent *ota_caller) { get_global_ota_callback()->register_ota(ota_caller); } +void OTAComponent::notify_state_(OTAState state, float progress, uint8_t error) { + for (auto *listener : this->state_listeners_) { + listener->on_ota_state(state, progress, error); + } + get_global_ota_callback()->notify_ota_state(state, progress, error, this); +} #endif } // namespace ota diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 64ee0b9f7c..e03afd4fc6 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -4,8 +4,8 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#ifdef USE_OTA_STATE_CALLBACK -#include "esphome/core/automation.h" +#ifdef USE_OTA_STATE_LISTENER +#include #endif namespace esphome { @@ -60,62 +60,75 @@ class OTABackend { virtual bool supports_compression() = 0; }; -class OTAComponent : public Component { -#ifdef USE_OTA_STATE_CALLBACK +/** Listener interface for OTA state changes. + * + * Components can implement this interface to receive OTA state updates + * without the overhead of std::function callbacks. + */ +class OTAStateListener { public: - void add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); - } + virtual ~OTAStateListener() = default; + virtual void on_ota_state(OTAState state, float progress, uint8_t error) = 0; +}; + +class OTAComponent : public Component { +#ifdef USE_OTA_STATE_LISTENER + public: + void add_state_listener(OTAStateListener *listener) { this->state_listeners_.push_back(listener); } protected: - /** Extended callback manager with deferred call support. + void notify_state_(OTAState state, float progress, uint8_t error); + + /** Notify state with deferral to main loop (for thread safety). * - * This adds a call_deferred() method for thread-safe execution from other tasks. + * This should be used by OTA implementations that run in separate tasks + * (like web_server OTA) to ensure listeners execute in the main loop. */ - class StateCallbackManager : public CallbackManager { - public: - StateCallbackManager(OTAComponent *component) : component_(component) {} + void notify_state_deferred_(OTAState state, float progress, uint8_t error) { + this->defer([this, state, progress, error]() { this->notify_state_(state, progress, error); }); + } - /** Call callbacks with deferral to main loop (for thread safety). - * - * This should be used by OTA implementations that run in separate tasks - * (like web_server OTA) to ensure callbacks execute in the main loop. - */ - void call_deferred(ota::OTAState state, float progress, uint8_t error) { - component_->defer([this, state, progress, error]() { this->call(state, progress, error); }); - } - - private: - OTAComponent *component_; - }; - - StateCallbackManager state_callback_{this}; + std::vector state_listeners_; #endif }; -#ifdef USE_OTA_STATE_CALLBACK +#ifdef USE_OTA_STATE_LISTENER + +/** Listener interface for global OTA state changes (includes OTA component pointer). + * + * Used by OTAGlobalCallback to aggregate state from multiple OTA components. + */ +class OTAGlobalStateListener { + public: + virtual ~OTAGlobalStateListener() = default; + virtual void on_ota_global_state(OTAState state, float progress, uint8_t error, OTAComponent *component) = 0; +}; + +/** Global callback that aggregates OTA state from all OTA components. + * + * OTA components call notify_ota_state() directly with their pointer, + * which forwards the event to all registered global listeners. + */ class OTAGlobalCallback { public: - void register_ota(OTAComponent *ota_caller) { - ota_caller->add_on_state_callback([this, ota_caller](OTAState state, float progress, uint8_t error) { - this->state_callback_.call(state, progress, error, ota_caller); - }); - } - void add_on_state_callback(std::function &&callback) { - this->state_callback_.add(std::move(callback)); + void add_global_state_listener(OTAGlobalStateListener *listener) { this->global_listeners_.push_back(listener); } + + void notify_ota_state(OTAState state, float progress, uint8_t error, OTAComponent *component) { + for (auto *listener : this->global_listeners_) { + listener->on_ota_global_state(state, progress, error, component); + } } protected: - CallbackManager state_callback_{}; + std::vector global_listeners_; }; OTAGlobalCallback *get_global_ota_callback(); -void register_ota_platform(OTAComponent *ota_caller); // OTA implementations should use: -// - state_callback_.call() when already in main loop (e.g., esphome OTA) -// - state_callback_.call_deferred() when in separate task (e.g., web_server OTA) -// This ensures proper callback execution in all contexts. +// - notify_state_() when already in main loop (e.g., esphome OTA) +// - notify_state_deferred_() when in separate task (e.g., web_server OTA) +// This ensures proper listener execution in all contexts. #endif std::unique_ptr make_ota_backend(); diff --git a/esphome/components/ota/ota_backend_arduino_esp32.cpp b/esphome/components/ota/ota_backend_arduino_esp32.cpp deleted file mode 100644 index 5c6230f2ce..0000000000 --- a/esphome/components/ota/ota_backend_arduino_esp32.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include "esphome/core/defines.h" -#include "esphome/core/log.h" - -#include "ota_backend.h" -#include "ota_backend_arduino_esp32.h" - -#include - -namespace esphome { -namespace ota { - -static const char *const TAG = "ota.arduino_esp32"; - -std::unique_ptr make_ota_backend() { return make_unique(); } - -OTAResponseTypes ArduinoESP32OTABackend::begin(size_t image_size) { - // Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA - // where the exact firmware size is unknown due to multipart encoding - if (image_size == 0) { - image_size = UPDATE_SIZE_UNKNOWN; - } - bool ret = Update.begin(image_size, U_FLASH); - if (ret) { - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - if (error == UPDATE_ERROR_SIZE) - return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE; - - ESP_LOGE(TAG, "Begin error: %d", error); - - return OTA_RESPONSE_ERROR_UNKNOWN; -} - -void ArduinoESP32OTABackend::set_update_md5(const char *md5) { - Update.setMD5(md5); - this->md5_set_ = true; -} - -OTAResponseTypes ArduinoESP32OTABackend::write(uint8_t *data, size_t len) { - size_t written = Update.write(data, len); - if (written == len) { - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - ESP_LOGE(TAG, "Write error: %d", error); - - return OTA_RESPONSE_ERROR_WRITING_FLASH; -} - -OTAResponseTypes ArduinoESP32OTABackend::end() { - // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 - // This matches the behavior of the old web_server OTA implementation - if (Update.end(!this->md5_set_)) { - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - ESP_LOGE(TAG, "End error: %d", error); - - return OTA_RESPONSE_ERROR_UPDATE_END; -} - -void ArduinoESP32OTABackend::abort() { Update.abort(); } - -} // namespace ota -} // namespace esphome - -#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/ota/ota_backend_arduino_esp32.h b/esphome/components/ota/ota_backend_arduino_esp32.h deleted file mode 100644 index 6615cf3dc0..0000000000 --- a/esphome/components/ota/ota_backend_arduino_esp32.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include "ota_backend.h" - -#include "esphome/core/defines.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace ota { - -class ArduinoESP32OTABackend : public OTABackend { - public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; - bool supports_compression() override { return false; } - - private: - bool md5_set_{false}; -}; - -} // namespace ota -} // namespace esphome - -#endif // USE_ESP32_FRAMEWORK_ARDUINO diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.cpp b/esphome/components/ota/ota_backend_arduino_esp8266.cpp deleted file mode 100644 index 375c4e7200..0000000000 --- a/esphome/components/ota/ota_backend_arduino_esp8266.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#ifdef USE_ARDUINO -#ifdef USE_ESP8266 -#include "ota_backend_arduino_esp8266.h" -#include "ota_backend.h" - -#include "esphome/components/esp8266/preferences.h" -#include "esphome/core/defines.h" -#include "esphome/core/log.h" - -#include - -namespace esphome { -namespace ota { - -static const char *const TAG = "ota.arduino_esp8266"; - -std::unique_ptr make_ota_backend() { return make_unique(); } - -OTAResponseTypes ArduinoESP8266OTABackend::begin(size_t image_size) { - // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space - if (image_size == 0) { - // NOLINTNEXTLINE(readability-static-accessed-through-instance) - image_size = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; - } - bool ret = Update.begin(image_size, U_FLASH); - if (ret) { - esp8266::preferences_prevent_write(true); - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - if (error == UPDATE_ERROR_BOOTSTRAP) - return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; - if (error == UPDATE_ERROR_NEW_FLASH_CONFIG) - return OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; - if (error == UPDATE_ERROR_FLASH_CONFIG) - return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; - if (error == UPDATE_ERROR_SPACE) - return OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE; - - ESP_LOGE(TAG, "Begin error: %d", error); - - return OTA_RESPONSE_ERROR_UNKNOWN; -} - -void ArduinoESP8266OTABackend::set_update_md5(const char *md5) { - Update.setMD5(md5); - this->md5_set_ = true; -} - -OTAResponseTypes ArduinoESP8266OTABackend::write(uint8_t *data, size_t len) { - size_t written = Update.write(data, len); - if (written == len) { - return OTA_RESPONSE_OK; - } - - uint8_t error = Update.getError(); - ESP_LOGE(TAG, "Write error: %d", error); - - return OTA_RESPONSE_ERROR_WRITING_FLASH; -} - -OTAResponseTypes ArduinoESP8266OTABackend::end() { - // Use strict validation (false) when MD5 is set, lenient validation (true) when no MD5 - // This matches the behavior of the old web_server OTA implementation - bool success = Update.end(!this->md5_set_); - - // On ESP8266, Update.end() might return false even with error code 0 - // Check the actual error code to determine success - uint8_t error = Update.getError(); - - if (success || error == UPDATE_ERROR_OK) { - return OTA_RESPONSE_OK; - } - - ESP_LOGE(TAG, "End error: %d", error); - return OTA_RESPONSE_ERROR_UPDATE_END; -} - -void ArduinoESP8266OTABackend::abort() { - Update.end(); - esp8266::preferences_prevent_write(false); -} - -} // namespace ota -} // namespace esphome - -#endif -#endif diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h deleted file mode 100644 index e1b9015cc7..0000000000 --- a/esphome/components/ota/ota_backend_arduino_esp8266.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once -#ifdef USE_ARDUINO -#ifdef USE_ESP8266 -#include "ota_backend.h" - -#include "esphome/core/defines.h" -#include "esphome/core/macros.h" - -namespace esphome { -namespace ota { - -class ArduinoESP8266OTABackend : public OTABackend { - public: - OTAResponseTypes begin(size_t image_size) override; - void set_update_md5(const char *md5) override; - OTAResponseTypes write(uint8_t *data, size_t len) override; - OTAResponseTypes end() override; - void abort() override; -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) - bool supports_compression() override { return true; } -#else - bool supports_compression() override { return false; } -#endif - - private: - bool md5_set_{false}; -}; - -} // namespace ota -} // namespace esphome - -#endif -#endif diff --git a/esphome/components/ota/ota_backend_esp8266.cpp b/esphome/components/ota/ota_backend_esp8266.cpp new file mode 100644 index 0000000000..4b84708cd9 --- /dev/null +++ b/esphome/components/ota/ota_backend_esp8266.cpp @@ -0,0 +1,356 @@ +#ifdef USE_ESP8266 +#include "ota_backend_esp8266.h" +#include "ota_backend.h" + +#include "esphome/components/esp8266/preferences.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include +#include + +#include + +extern "C" { +#include +#include +#include +#include +#include +} + +// Note: FLASH_SECTOR_SIZE (0x1000) is already defined in spi_flash_geometry.h + +// Flash header offsets +static constexpr uint8_t FLASH_MODE_OFFSET = 2; + +// Firmware magic bytes +static constexpr uint8_t FIRMWARE_MAGIC = 0xE9; +static constexpr uint8_t GZIP_MAGIC_1 = 0x1F; +static constexpr uint8_t GZIP_MAGIC_2 = 0x8B; + +// ESP8266 flash memory base address (memory-mapped flash starts here) +static constexpr uint32_t FLASH_BASE_ADDRESS = 0x40200000; + +// Boot mode extraction from GPI register (bits 16-19 contain boot mode) +static constexpr int BOOT_MODE_SHIFT = 16; +static constexpr int BOOT_MODE_MASK = 0xf; + +// Boot mode indicating UART download mode (OTA not possible) +static constexpr int BOOT_MODE_UART_DOWNLOAD = 1; + +// Minimum buffer size when memory is constrained +static constexpr size_t MIN_BUFFER_SIZE = 256; + +namespace esphome::ota { + +static const char *const TAG = "ota.esp8266"; + +std::unique_ptr make_ota_backend() { return make_unique(); } + +OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) { + // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space + if (image_size == 0) { + // Round down to sector boundary: subtract one sector, then mask to sector alignment + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + image_size = (ESP.getFreeSketchSpace() - FLASH_SECTOR_SIZE) & ~(FLASH_SECTOR_SIZE - 1); + } + + // Check boot mode - if boot mode is UART download mode, + // we will not be able to reset into normal mode once update is done + int boot_mode = (GPI >> BOOT_MODE_SHIFT) & BOOT_MODE_MASK; + if (boot_mode == BOOT_MODE_UART_DOWNLOAD) { + return OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; + } + + // Check flash configuration - real size must be >= configured size + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + if (!ESP.checkFlashConfig(false)) { + return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; + } + + // Get current sketch size + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + uint32_t sketch_size = ESP.getSketchSize(); + + // Size of current sketch rounded to sector boundary + uint32_t current_sketch_size = (sketch_size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1)); + + // Size of update rounded to sector boundary + uint32_t rounded_size = (image_size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1)); + + // End of available space for sketch and update (start of filesystem) + uint32_t update_end_address = FS_start - FLASH_BASE_ADDRESS; + + // Calculate start address for the update (write from end backwards) + this->start_address_ = (update_end_address > rounded_size) ? (update_end_address - rounded_size) : 0; + + // Check if there's enough space for both current sketch and update + if (this->start_address_ < current_sketch_size) { + return OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE; + } + + // Allocate buffer for sector writes (use smaller buffer if memory constrained) + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + this->buffer_size_ = (ESP.getFreeHeap() > 2 * FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : MIN_BUFFER_SIZE; + + // ESP8266's umm_malloc guarantees 4-byte aligned allocations, which is required + // for spi_flash_write(). This is the same pattern used by Arduino's Updater class. + this->buffer_ = make_unique(this->buffer_size_); + if (!this->buffer_) { + return OTA_RESPONSE_ERROR_UNKNOWN; + } + + this->current_address_ = this->start_address_; + this->image_size_ = image_size; + this->buffer_len_ = 0; + this->md5_set_ = false; + + // Disable WiFi sleep during update + wifi_set_sleep_type(NONE_SLEEP_T); + + // Prevent preference writes during update + esp8266::preferences_prevent_write(true); + + // Initialize MD5 computation + this->md5_.init(); + + ESP_LOGD(TAG, "OTA begin: start=0x%08" PRIX32 ", size=%zu", this->start_address_, image_size); + + return OTA_RESPONSE_OK; +} + +void ESP8266OTABackend::set_update_md5(const char *md5) { + // Parse hex string to bytes + if (parse_hex(md5, this->expected_md5_, 16)) { + this->md5_set_ = true; + } +} + +OTAResponseTypes ESP8266OTABackend::write(uint8_t *data, size_t len) { + if (!this->buffer_) { + return OTA_RESPONSE_ERROR_UNKNOWN; + } + + size_t written = 0; + while (written < len) { + // Calculate how much we can buffer + size_t to_buffer = std::min(len - written, this->buffer_size_ - this->buffer_len_); + memcpy(this->buffer_.get() + this->buffer_len_, data + written, to_buffer); + this->buffer_len_ += to_buffer; + written += to_buffer; + + // If buffer is full, write to flash + if (this->buffer_len_ == this->buffer_size_ && !this->write_buffer_()) { + return OTA_RESPONSE_ERROR_WRITING_FLASH; + } + } + + return OTA_RESPONSE_OK; +} + +bool ESP8266OTABackend::erase_sector_if_needed_() { + if ((this->current_address_ % FLASH_SECTOR_SIZE) != 0) { + return true; // Not at sector boundary + } + + App.feed_wdt(); + if (spi_flash_erase_sector(this->current_address_ / FLASH_SECTOR_SIZE) != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Flash erase failed at 0x%08" PRIX32, this->current_address_); + return false; + } + return true; +} + +bool ESP8266OTABackend::flash_write_() { + App.feed_wdt(); + if (spi_flash_write(this->current_address_, reinterpret_cast(this->buffer_.get()), this->buffer_len_) != + SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Flash write failed at 0x%08" PRIX32, this->current_address_); + return false; + } + return true; +} + +bool ESP8266OTABackend::write_buffer_() { + if (this->buffer_len_ == 0) { + return true; + } + + if (!this->erase_sector_if_needed_()) { + return false; + } + + // Patch flash mode in first sector if needed + // This is analogous to what esptool.py does when it receives a --flash_mode argument + bool is_first_sector = (this->current_address_ == this->start_address_); + uint8_t original_flash_mode = 0; + bool patched_flash_mode = false; + + // Only patch if we have enough bytes to access flash mode offset and it's not GZIP + if (is_first_sector && this->buffer_len_ > FLASH_MODE_OFFSET && this->buffer_[0] != GZIP_MAGIC_1) { + // Not GZIP compressed - check and patch flash mode + uint8_t current_flash_mode = this->get_flash_chip_mode_(); + uint8_t buffer_flash_mode = this->buffer_[FLASH_MODE_OFFSET]; + + if (buffer_flash_mode != current_flash_mode) { + original_flash_mode = buffer_flash_mode; + this->buffer_[FLASH_MODE_OFFSET] = current_flash_mode; + patched_flash_mode = true; + } + } + + if (!this->flash_write_()) { + return false; + } + + // Restore original flash mode for MD5 calculation + if (patched_flash_mode) { + this->buffer_[FLASH_MODE_OFFSET] = original_flash_mode; + } + + // Update MD5 with original (unpatched) data + this->md5_.add(this->buffer_.get(), this->buffer_len_); + + this->current_address_ += this->buffer_len_; + this->buffer_len_ = 0; + + return true; +} + +bool ESP8266OTABackend::write_buffer_final_() { + // Similar to write_buffer_(), but without flash mode patching or MD5 update (for final padded write) + if (this->buffer_len_ == 0) { + return true; + } + + if (!this->erase_sector_if_needed_() || !this->flash_write_()) { + return false; + } + + this->current_address_ += this->buffer_len_; + this->buffer_len_ = 0; + + return true; +} + +OTAResponseTypes ESP8266OTABackend::end() { + // Write any remaining buffered data + if (this->buffer_len_ > 0) { + // Add actual data to MD5 before padding + this->md5_.add(this->buffer_.get(), this->buffer_len_); + + // Pad to 4-byte alignment for flash write + while (this->buffer_len_ % 4 != 0) { + this->buffer_[this->buffer_len_++] = 0xFF; + } + if (!this->write_buffer_final_()) { + this->abort(); + return OTA_RESPONSE_ERROR_WRITING_FLASH; + } + } + + // Calculate actual bytes written + size_t actual_size = this->current_address_ - this->start_address_; + + // Check if any data was written + if (actual_size == 0) { + ESP_LOGE(TAG, "No data written"); + this->abort(); + return OTA_RESPONSE_ERROR_UPDATE_END; + } + + // Verify MD5 if set (strict mode), otherwise use lenient mode + // In lenient mode (no MD5), we accept whatever was written + if (this->md5_set_) { + this->md5_.calculate(); + if (!this->md5_.equals_bytes(this->expected_md5_)) { + ESP_LOGE(TAG, "MD5 mismatch"); + this->abort(); + return OTA_RESPONSE_ERROR_MD5_MISMATCH; + } + } else { + // Lenient mode: adjust size to what was actually written + // This matches Arduino's Update.end(true) behavior + this->image_size_ = actual_size; + } + + // Verify firmware header + if (!this->verify_end_()) { + this->abort(); + return OTA_RESPONSE_ERROR_UPDATE_END; + } + + // Write eboot command to copy firmware on next boot + eboot_command ebcmd; + ebcmd.action = ACTION_COPY_RAW; + ebcmd.args[0] = this->start_address_; + ebcmd.args[1] = 0x00000; // Destination: start of flash + ebcmd.args[2] = this->image_size_; + eboot_command_write(&ebcmd); + + ESP_LOGI(TAG, "OTA update staged: 0x%08" PRIX32 " -> 0x00000, size=%zu", this->start_address_, this->image_size_); + + // Clean up + this->buffer_.reset(); + esp8266::preferences_prevent_write(false); + + return OTA_RESPONSE_OK; +} + +void ESP8266OTABackend::abort() { + this->buffer_.reset(); + this->buffer_len_ = 0; + this->image_size_ = 0; + esp8266::preferences_prevent_write(false); +} + +bool ESP8266OTABackend::verify_end_() { + uint32_t buf; + if (spi_flash_read(this->start_address_, &buf, 4) != SPI_FLASH_RESULT_OK) { + ESP_LOGE(TAG, "Failed to read firmware header"); + return false; + } + + uint8_t *bytes = reinterpret_cast(&buf); + + // Check for GZIP (compressed firmware) + if (bytes[0] == GZIP_MAGIC_1 && bytes[1] == GZIP_MAGIC_2) { + // GZIP compressed - can't verify further + return true; + } + + // Check firmware magic byte + if (bytes[0] != FIRMWARE_MAGIC) { + ESP_LOGE(TAG, "Invalid firmware magic: 0x%02X (expected 0x%02X)", bytes[0], FIRMWARE_MAGIC); + return false; + } + +#if !FLASH_MAP_SUPPORT + // Check if new firmware's flash size fits (only when auto-detection is disabled) + // With FLASH_MAP_SUPPORT (modern cores), flash size is auto-detected from chip + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + uint32_t bin_flash_size = ESP.magicFlashChipSize((bytes[3] & 0xf0) >> 4); + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + if (bin_flash_size > ESP.getFlashChipRealSize()) { + ESP_LOGE(TAG, "Firmware flash size (%" PRIu32 ") exceeds chip size (%" PRIu32 ")", bin_flash_size, + ESP.getFlashChipRealSize()); + return false; + } +#endif + + return true; +} + +uint8_t ESP8266OTABackend::get_flash_chip_mode_() { + uint32_t data; + if (spi_flash_read(0x0000, &data, 4) != SPI_FLASH_RESULT_OK) { + return 0; // Default to QIO + } + return (reinterpret_cast(&data))[FLASH_MODE_OFFSET]; +} + +} // namespace esphome::ota +#endif // USE_ESP8266 diff --git a/esphome/components/ota/ota_backend_esp8266.h b/esphome/components/ota/ota_backend_esp8266.h new file mode 100644 index 0000000000..a9d6dd2ccc --- /dev/null +++ b/esphome/components/ota/ota_backend_esp8266.h @@ -0,0 +1,58 @@ +#pragma once +#ifdef USE_ESP8266 +#include "ota_backend.h" + +#include "esphome/components/md5/md5.h" +#include "esphome/core/defines.h" + +#include + +namespace esphome::ota { + +/// OTA backend for ESP8266 using native SDK functions. +/// This implementation bypasses the Arduino Updater library to save ~228 bytes of RAM +/// by not having a global Update object in .bss. +class ESP8266OTABackend : public OTABackend { + public: + OTAResponseTypes begin(size_t image_size) override; + void set_update_md5(const char *md5) override; + OTAResponseTypes write(uint8_t *data, size_t len) override; + OTAResponseTypes end() override; + void abort() override; + // Compression supported in all ESP8266 Arduino versions ESPHome supports (>= 2.7.0) + bool supports_compression() override { return true; } + + protected: + /// Erase flash sector if current address is at sector boundary + bool erase_sector_if_needed_(); + + /// Write buffer to flash (does not update address or clear buffer) + bool flash_write_(); + + /// Write buffered data to flash and update MD5 + bool write_buffer_(); + + /// Write buffered data to flash without MD5 update (for final padded write) + bool write_buffer_final_(); + + /// Verify the firmware header is valid + bool verify_end_(); + + /// Get current flash chip mode from flash header + uint8_t get_flash_chip_mode_(); + + std::unique_ptr buffer_; + size_t buffer_size_{0}; + size_t buffer_len_{0}; + + uint32_t start_address_{0}; + uint32_t current_address_{0}; + size_t image_size_{0}; + + md5::MD5Digest md5_{}; + uint8_t expected_md5_[16]; // Fixed-size buffer for 128-bit (16-byte) MD5 digest + bool md5_set_{false}; +}; + +} // namespace esphome::ota +#endif // USE_ESP8266 diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 97aae09bd9..f278c3741f 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -1,4 +1,4 @@ -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "ota_backend_esp_idf.h" #include "esphome/components/md5/md5.h" @@ -107,4 +107,4 @@ void IDFOTABackend::abort() { } // namespace ota } // namespace esphome -#endif +#endif // USE_ESP32 diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index 6e93982131..764010e614 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -1,5 +1,5 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "ota_backend.h" #include "esphome/components/md5/md5.h" @@ -29,4 +29,4 @@ class IDFOTABackend : public OTABackend { } // namespace ota } // namespace esphome -#endif +#endif // USE_ESP32 diff --git a/esphome/components/ota/ota_backend_host.cpp b/esphome/components/ota/ota_backend_host.cpp new file mode 100644 index 0000000000..ddab174bed --- /dev/null +++ b/esphome/components/ota/ota_backend_host.cpp @@ -0,0 +1,24 @@ +#ifdef USE_HOST +#include "ota_backend_host.h" + +#include "esphome/core/defines.h" + +namespace esphome::ota { + +// Stub implementation - OTA is not supported on host platform. +// All methods return error codes to allow compilation of configs with OTA triggers. + +std::unique_ptr make_ota_backend() { return make_unique(); } + +OTAResponseTypes HostOTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UPDATE_PREPARE; } + +void HostOTABackend::set_update_md5(const char *expected_md5) {} + +OTAResponseTypes HostOTABackend::write(uint8_t *data, size_t len) { return OTA_RESPONSE_ERROR_WRITING_FLASH; } + +OTAResponseTypes HostOTABackend::end() { return OTA_RESPONSE_ERROR_UPDATE_END; } + +void HostOTABackend::abort() {} + +} // namespace esphome::ota +#endif diff --git a/esphome/components/ota/ota_backend_host.h b/esphome/components/ota/ota_backend_host.h new file mode 100644 index 0000000000..ae7d0cb0b3 --- /dev/null +++ b/esphome/components/ota/ota_backend_host.h @@ -0,0 +1,21 @@ +#pragma once +#ifdef USE_HOST +#include "ota_backend.h" + +namespace esphome::ota { + +/// Stub OTA backend for host platform - allows compilation but does not implement OTA. +/// All operations return error codes immediately. This enables configurations with +/// OTA triggers to compile for host platform during development. +class HostOTABackend : public OTABackend { + public: + OTAResponseTypes begin(size_t image_size) override; + void set_update_md5(const char *md5) override; + OTAResponseTypes write(uint8_t *data, size_t len) override; + OTAResponseTypes end() override; + void abort() override; + bool supports_compression() override { return false; } +}; + +} // namespace esphome::ota +#endif diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 04057c07f2..6d353ccf11 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -1,7 +1,13 @@ +from collections import UserDict +from collections.abc import Callable +from functools import reduce +import logging from pathlib import Path +from typing import Any from esphome import git, yaml_util -from esphome.config_helpers import merge_config +from esphome.components.substitutions.jinja import has_jinja +from esphome.config_helpers import Remove, merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ESPHOME, @@ -13,6 +19,7 @@ from esphome.const import ( CONF_PATH, CONF_REF, CONF_REFRESH, + CONF_SUBSTITUTIONS, CONF_URL, CONF_USERNAME, CONF_VARS, @@ -20,18 +27,57 @@ from esphome.const import ( ) from esphome.core import EsphomeError +_LOGGER = logging.getLogger(__name__) + DOMAIN = CONF_PACKAGES -def validate_git_package(config: dict): - if CONF_URL not in config: - return config - config = BASE_SCHEMA(config) - new_config = config +def validate_has_jinja(value: Any): + if not isinstance(value, str) or not has_jinja(value): + raise cv.Invalid("string does not contain Jinja syntax") + return value + + +def valid_package_contents(allow_jinja: bool = True) -> Callable[[Any], dict]: + """Returns a validator that checks if a package_config that will be merged looks as + much as possible to a valid config to fail early on obvious mistakes.""" + + def validator(package_config: dict) -> dict: + if isinstance(package_config, dict): + if CONF_URL in package_config: + # If a URL key is found, then make sure the config conforms to a remote package schema: + return REMOTE_PACKAGE_SCHEMA(package_config) + + # Validate manually since Voluptuous would regenerate dicts and lose metadata + # such as ESPHomeDataBase + for k, v in package_config.items(): + if not isinstance(k, str): + raise cv.Invalid("Package content keys must be strings") + if isinstance(v, (dict, list, Remove)): + continue # e.g. script: [], psram: !remove, logger: {level: debug} + if v is None: + continue # e.g. web_server: + if allow_jinja and isinstance(v, str) and has_jinja(v): + # e.g: remote package shorthand: + # package_name: github://esphome/repo/file.yaml@${ branch }, or: + # switch: ${ expression that evals to a switch } + continue + + raise cv.Invalid("Invalid component content in package definition") + return package_config + + raise cv.Invalid("Package contents must be a dict") + + return validator + + +def expand_file_to_files(config: dict): if CONF_FILE in config: + new_config = config new_config[CONF_FILES] = [config[CONF_FILE]] del new_config[CONF_FILE] - return new_config + return new_config + return config def validate_yaml_filename(value): @@ -45,7 +91,7 @@ def validate_yaml_filename(value): def validate_source_shorthand(value): if not isinstance(value, str): - raise cv.Invalid("Shorthand only for strings") + raise cv.Invalid("Git URL shorthand only for strings") git_file = git.GitFile.from_shorthand(value) @@ -56,10 +102,26 @@ def validate_source_shorthand(value): if git_file.ref: conf[CONF_REF] = git_file.ref - return BASE_SCHEMA(conf) + return REMOTE_PACKAGE_SCHEMA(conf) -BASE_SCHEMA = cv.All( +def deprecate_single_package(config): + _LOGGER.warning( + """ + Including a single package under `packages:`, i.e., `packages: !include mypackage.yaml` is deprecated. + This method for including packages will go away in 2026.7.0 + Please use a list instead: + + packages: + - !include mypackage.yaml + + See https://github.com/esphome/esphome/pull/12116 + """ + ) + return config + + +REMOTE_PACKAGE_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_URL): cv.url, @@ -90,23 +152,33 @@ BASE_SCHEMA = cv.All( } ), cv.has_at_least_one_key(CONF_FILE, CONF_FILES), + expand_file_to_files, ) -PACKAGE_SCHEMA = cv.All( - cv.Any(validate_source_shorthand, BASE_SCHEMA, dict), validate_git_package +PACKAGE_SCHEMA = cv.Any( # A package definition is either: + validate_source_shorthand, # A git URL shorthand string that expands to a remote package schema, or + REMOTE_PACKAGE_SCHEMA, # a valid remote package schema, or + validate_has_jinja, # a Jinja string that may resolve to a package, or + valid_package_contents( + allow_jinja=True + ), # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}} + # which will have to be fully validated later as per each component's schema. ) -CONFIG_SCHEMA = cv.Any( +CONFIG_SCHEMA = cv.Any( # under `packages:` we can have either: cv.Schema( { - str: PACKAGE_SCHEMA, + str: PACKAGE_SCHEMA, # a named dict of package definitions, or } ), - [PACKAGE_SCHEMA], + [PACKAGE_SCHEMA], # a list of package definitions, or + cv.All( # a single package definition (deprecated) + cv.ensure_list(PACKAGE_SCHEMA), deprecate_single_package + ), ) -def _process_base_package(config: dict, skip_update: bool = False) -> dict: +def _process_remote_package(config: dict, skip_update: bool = False) -> dict: # When skip_update is True, use NEVER_REFRESH to prevent updates actual_refresh = git.NEVER_REFRESH if skip_update else config[CONF_REFRESH] repo_dir, revert = git.clone_or_update( @@ -182,32 +254,84 @@ def _process_base_package(config: dict, skip_update: bool = False) -> dict: return {"packages": packages} -def _process_package(package_config, config, skip_update: bool = False): - recursive_package = package_config - if CONF_URL in package_config: - package_config = _process_base_package(package_config, skip_update) - if isinstance(package_config, dict): - recursive_package = do_packages_pass(package_config, skip_update) - return merge_config(recursive_package, config) - - -def do_packages_pass(config: dict, skip_update: bool = False): +def _walk_packages( + config: dict, callback: Callable[[dict], dict], validate_deprecated: bool = True +) -> dict: if CONF_PACKAGES not in config: return config packages = config[CONF_PACKAGES] - with cv.prepend_path(CONF_PACKAGES): + + # The following block and `validate_deprecated` parameter can be safely removed + # once single-package deprecation is effective + if validate_deprecated: packages = CONFIG_SCHEMA(packages) + + with cv.prepend_path(CONF_PACKAGES): if isinstance(packages, dict): for package_name, package_config in reversed(packages.items()): with cv.prepend_path(package_name): - config = _process_package(package_config, config, skip_update) + package_config = callback(package_config) + packages[package_name] = _walk_packages(package_config, callback) elif isinstance(packages, list): - for package_config in reversed(packages): - config = _process_package(package_config, config, skip_update) + for idx in reversed(range(len(packages))): + with cv.prepend_path(idx): + package_config = callback(packages[idx]) + packages[idx] = _walk_packages(package_config, callback) else: raise cv.Invalid( f"Packages must be a key to value mapping or list, got {type(packages)} instead" ) - - del config[CONF_PACKAGES] + config[CONF_PACKAGES] = packages + return config + + +def do_packages_pass(config: dict, skip_update: bool = False) -> dict: + """Processes, downloads and validates all packages in the config. + Also extracts and merges all substitutions found in packages into the main config substitutions. + """ + if CONF_PACKAGES not in config: + return config + + substitutions = UserDict(config.pop(CONF_SUBSTITUTIONS, {})) + + def process_package_callback(package_config: dict) -> dict: + """This will be called for each package found in the config.""" + package_config = PACKAGE_SCHEMA(package_config) + if isinstance(package_config, str): + return package_config # Jinja string, skip processing + if CONF_URL in package_config: + package_config = _process_remote_package(package_config, skip_update) + # Extract substitutions from the package and merge them into the main substitutions: + substitutions.data = merge_config( + package_config.pop(CONF_SUBSTITUTIONS, {}), substitutions.data + ) + return package_config + + _walk_packages(config, process_package_callback) + + if substitutions: + config[CONF_SUBSTITUTIONS] = substitutions.data + + return config + + +def merge_packages(config: dict) -> dict: + """Merges all packages into the main config and removes the `packages:` key.""" + if CONF_PACKAGES not in config: + return config + + # Build flat list of all package configs to merge in priority order: + merge_list: list[dict] = [] + + validate_package = valid_package_contents(allow_jinja=False) + + def process_package_callback(package_config: dict) -> dict: + """This will be called for each package found in the config.""" + merge_list.append(validate_package(package_config)) + return package_config + + _walk_packages(config, process_package_callback, validate_deprecated=False) + # Merge all packages into the main config: + config = reduce(lambda new, old: merge_config(old, new), merge_list, config) + del config[CONF_PACKAGES] return config diff --git a/esphome/components/packet_transport/__init__.py b/esphome/components/packet_transport/__init__.py index 43da7740fe..1930e45e85 100644 --- a/esphome/components/packet_transport/__init__.py +++ b/esphome/components/packet_transport/__init__.py @@ -176,17 +176,22 @@ async def register_packet_transport(var, config): if encryption := provider.get(CONF_ENCRYPTION): cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) + is_provider = False for sens_conf in config.get(CONF_SENSORS, ()): + is_provider = True sens_id = sens_conf[CONF_ID] sensor = await cg.get_variable(sens_id) bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) cg.add(var.add_sensor(bcst_id, sensor)) for sens_conf in config.get(CONF_BINARY_SENSORS, ()): + is_provider = True sens_id = sens_conf[CONF_ID] sensor = await cg.get_variable(sens_id) bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) cg.add(var.add_binary_sensor(bcst_id, sensor)) + if is_provider: + cg.add(var.set_is_provider(True)) if encryption := config.get(CONF_ENCRYPTION): cg.add(var.set_encryption_key(hash_encryption_key(encryption))) return providers diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 8bde4ee505..4a53ab110b 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -1,11 +1,15 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "packet_transport.h" #include "esphome/components/xxtea/xxtea.h" namespace esphome { namespace packet_transport { + +// Maximum bytes to log in hex output (168 * 3 = 504, under TX buffer size of 512) +static constexpr size_t PACKET_MAX_LOG_BYTES = 168; /** * Structure of a data packet; everything is little-endian * @@ -195,8 +199,8 @@ static void add(std::vector &vec, const char *str) { void PacketTransport::setup() { this->name_ = App.get_name().c_str(); if (strlen(this->name_) > 255) { + this->status_set_error(LOG_STR("Device name exceeds 255 chars")); this->mark_failed(); - this->status_set_error("Device name exceeds 255 chars"); return; } this->resend_ping_key_ = this->ping_pong_enable_; @@ -263,6 +267,8 @@ void PacketTransport::flush_() { xxtea::encrypt((uint32_t *) (encode_buffer.data() + header_len), len / 4, (uint32_t *) this->encryption_key_.data()); } + char hex_buf[format_hex_pretty_size(PACKET_MAX_LOG_BYTES)]; + ESP_LOGVV(TAG, "Sending packet %s", format_hex_pretty_to(hex_buf, encode_buffer.data(), encode_buffer.size())); this->send_packet(encode_buffer); } @@ -316,6 +322,9 @@ void PacketTransport::send_data_(bool all) { } void PacketTransport::update() { + // resend all sensors if required + if (this->is_provider_) + this->send_data_(true); if (!this->ping_pong_enable_) { return; } @@ -501,8 +510,9 @@ void PacketTransport::process_(const std::vector &data) { } if (decoder.get(byte) == DECODE_OK) { ESP_LOGW(TAG, "Unknown key %X", byte); + char hex_buf[format_hex_pretty_size(PACKET_MAX_LOG_BYTES)]; ESP_LOGD(TAG, "Buffer pos: %zu contents: %s", data.size() - decoder.get_remaining_size(), - format_hex_pretty(data).c_str()); + format_hex_pretty_to(hex_buf, data.data(), data.size())); } break; } @@ -551,7 +561,7 @@ void PacketTransport::loop() { if (this->resend_ping_key_) this->send_ping_pong_request_(); if (this->updated_) { - this->send_data_(this->resend_data_); + this->send_data_(false); } } diff --git a/esphome/components/packet_transport/packet_transport.h b/esphome/components/packet_transport/packet_transport.h index a2370e9749..86ec564fce 100644 --- a/esphome/components/packet_transport/packet_transport.h +++ b/esphome/components/packet_transport/packet_transport.h @@ -91,6 +91,7 @@ class PacketTransport : public PollingComponent { } } + void set_is_provider(bool is_provider) { this->is_provider_ = is_provider; } void set_encryption_key(std::vector key) { this->encryption_key_ = std::move(key); } void set_rolling_code_enable(bool enable) { this->rolling_code_enable_ = enable; } void set_ping_pong_enable(bool enable) { this->ping_pong_enable_ = enable; } @@ -129,7 +130,7 @@ class PacketTransport : public PollingComponent { uint32_t ping_pong_recyle_time_{}; uint32_t last_key_time_{}; bool resend_ping_key_{}; - bool resend_data_{}; + bool is_provider_{}; const char *name_{}; ESPPreferenceObject pref_{}; diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index c0056e780b..909bac5f05 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -180,10 +180,8 @@ void PCA6416AGPIOPin::setup() { pin_mode(flags_); } void PCA6416AGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCA6416AGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void PCA6416AGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string PCA6416AGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via PCA6416A", pin_); - return buffer; +size_t PCA6416AGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via PCA6416A", this->pin_); } } // namespace pca6416a diff --git a/esphome/components/pca6416a/pca6416a.h b/esphome/components/pca6416a/pca6416a.h index 10a4a64e9b..138a51cc20 100644 --- a/esphome/components/pca6416a/pca6416a.h +++ b/esphome/components/pca6416a/pca6416a.h @@ -52,7 +52,7 @@ class PCA6416AGPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(PCA6416AComponent *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index e8d49f66e2..a6f9c2396c 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -129,10 +129,8 @@ void PCA9554GPIOPin::setup() { pin_mode(flags_); } void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCA9554GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void PCA9554GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string PCA9554GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via PCA9554", pin_); - return buffer; +size_t PCA9554GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via PCA9554", this->pin_); } } // namespace pca9554 diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index 7b356b4068..bf752e50c9 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -59,7 +59,7 @@ class PCA9554GPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(PCA9554Component *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/pca9685/__init__.py b/esphome/components/pca9685/__init__.py index 50f58cdfb9..0e238ff7da 100644 --- a/esphome/components/pca9685/__init__.py +++ b/esphome/components/pca9685/__init__.py @@ -1,7 +1,12 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv -from esphome.const import CONF_EXTERNAL_CLOCK_INPUT, CONF_FREQUENCY, CONF_ID +from esphome.const import ( + CONF_EXTERNAL_CLOCK_INPUT, + CONF_FREQUENCY, + CONF_ID, + CONF_PHASE_BALANCER, +) DEPENDENCIES = ["i2c"] MULTI_CONF = True @@ -9,6 +14,12 @@ MULTI_CONF = True pca9685_ns = cg.esphome_ns.namespace("pca9685") PCA9685Output = pca9685_ns.class_("PCA9685Output", cg.Component, i2c.I2CDevice) +phase_balancer = pca9685_ns.enum("PhaseBalancer", is_class=True) +PHASE_BALANCERS = { + "none": phase_balancer.NONE, + "linear": phase_balancer.LINEAR, +} + def validate_frequency(config): if config[CONF_EXTERNAL_CLOCK_INPUT]: @@ -27,9 +38,12 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(PCA9685Output), cv.Optional(CONF_FREQUENCY): cv.All( - cv.frequency, cv.Range(min=23.84, max=1525.88) + cv.frequency, cv.float_range(min=23.84, max=1525.88) ), cv.Optional(CONF_EXTERNAL_CLOCK_INPUT, default=False): cv.boolean, + cv.Optional(CONF_PHASE_BALANCER, default="linear"): cv.enum( + PHASE_BALANCERS + ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -43,5 +57,6 @@ async def to_code(config): if CONF_FREQUENCY in config: cg.add(var.set_frequency(config[CONF_FREQUENCY])) cg.add(var.set_extclk(config[CONF_EXTERNAL_CLOCK_INPUT])) + cg.add(var.set_phase_balancer(config[CONF_PHASE_BALANCER])) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp index 6df708ac84..77e3d5a6c6 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -105,7 +105,18 @@ void PCA9685Output::loop() { const uint16_t num_channels = this->max_channel_ - this->min_channel_ + 1; const uint16_t phase_delta_begin = 4096 / num_channels; for (uint8_t channel = this->min_channel_; channel <= this->max_channel_; channel++) { - uint16_t phase_begin = (channel - this->min_channel_) * phase_delta_begin; + uint16_t phase_begin; + switch (this->balancer_) { + case PhaseBalancer::NONE: + phase_begin = 0; + break; + case PhaseBalancer::LINEAR: + phase_begin = (channel - this->min_channel_) * phase_delta_begin; + break; + default: + ESP_LOGE(TAG, "Unknown phase balancer %d", static_cast(this->balancer_)); + return; + } uint16_t phase_end; uint16_t amount = this->pwm_amounts_[channel]; if (amount == 0) { diff --git a/esphome/components/pca9685/pca9685_output.h b/esphome/components/pca9685/pca9685_output.h index 8e547d0032..288c923d4c 100644 --- a/esphome/components/pca9685/pca9685_output.h +++ b/esphome/components/pca9685/pca9685_output.h @@ -7,6 +7,11 @@ namespace esphome { namespace pca9685 { +enum class PhaseBalancer { + NONE = 0x00, + LINEAR = 0x01, +}; + /// Inverts polarity of channel output signal extern const uint8_t PCA9685_MODE_INVERTED; /// Channel update happens upon ACK (post-set) rather than on STOP (endTransmission) @@ -47,6 +52,7 @@ class PCA9685Output : public Component, public i2c::I2CDevice { void loop() override; void set_extclk(bool extclk) { this->extclk_ = extclk; } void set_frequency(float frequency) { this->frequency_ = frequency; } + void set_phase_balancer(PhaseBalancer balancer) { this->balancer_ = balancer; } protected: friend PCA9685Channel; @@ -60,6 +66,7 @@ class PCA9685Output : public Component, public i2c::I2CDevice { float frequency_; uint8_t mode_; bool extclk_ = false; + PhaseBalancer balancer_ = PhaseBalancer::LINEAR; uint8_t min_channel_{0xFF}; uint8_t max_channel_{0x00}; diff --git a/esphome/components/pcd8544/pcd_8544.cpp b/esphome/components/pcd8544/pcd_8544.cpp index f5b018b127..95d91ff18a 100644 --- a/esphome/components/pcd8544/pcd_8544.cpp +++ b/esphome/components/pcd8544/pcd_8544.cpp @@ -117,6 +117,12 @@ void PCD8544::update() { } void PCD8544::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + uint8_t fill = color.is_on() ? 0xFF : 0x00; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index 72d8865d7f..8bdd312ab9 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -21,9 +21,11 @@ void PCF8574Component::loop() { this->reset_pin_cache_(); } void PCF8574Component::dump_config() { - ESP_LOGCONFIG(TAG, "PCF8574:"); + ESP_LOGCONFIG(TAG, + "PCF8574:\n" + " Is PCF8575: %s", + YESNO(this->pcf8575_)); LOG_I2C_DEVICE(this) - ESP_LOGCONFIG(TAG, " Is PCF8575: %s", YESNO(this->pcf8575_)); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } @@ -104,10 +106,8 @@ void PCF8574GPIOPin::setup() { pin_mode(flags_); } void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void PCF8574GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string PCF8574GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via PCF8574", pin_); - return buffer; +size_t PCF8574GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via PCF8574", this->pin_); } } // namespace pcf8574 diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index fd1ea8af63..5203030142 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -54,7 +54,7 @@ class PCF8574GPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(PCF8574Component *parent) { parent_ = parent; } void set_pin(uint8_t pin) { pin_ = pin; } diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 517ca833e6..f3a1f013d9 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -164,7 +164,9 @@ bool PI4IOE5V6408GPIOPin::digital_read() { return this->parent_->digital_read(th void PI4IOE5V6408GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string PI4IOE5V6408GPIOPin::dump_summary() const { return str_sprintf("%u via PI4IOE5V6408", this->pin_); } +size_t PI4IOE5V6408GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via PI4IOE5V6408", this->pin_); +} } // namespace pi4ioe5v6408 } // namespace esphome diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h index 82b3076fab..4dc31201ce 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h @@ -52,7 +52,7 @@ class PI4IOE5V6408GPIOPin : public GPIOPin, public Parentedpin_ = pin; } void set_inverted(bool inverted) { this->inverted_ = inverted; } diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index 28d16e17ab..d1d9c200cf 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -138,20 +138,21 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce } void PIDAutotuner::dump_config() { if (this->state_ == AUTOTUNE_SUCCEEDED) { - ESP_LOGI(TAG, "%s: PID Autotune:", this->id_.c_str()); - ESP_LOGI(TAG, " State: Succeeded!"); + ESP_LOGI(TAG, + "%s: PID Autotune:\n" + " State: Succeeded!", + this->id_.c_str()); bool has_issue = false; if (!this->amplitude_detector_.is_amplitude_convergent()) { - ESP_LOGW(TAG, " Could not reliably determine oscillation amplitude, PID parameters may be inaccurate!"); - ESP_LOGW(TAG, " Please make sure you eliminate all outside influences on the measured temperature."); + ESP_LOGW(TAG, " Could not reliably determine oscillation amplitude, PID parameters may be inaccurate!\n" + " Please make sure you eliminate all outside influences on the measured temperature."); has_issue = true; } if (!this->frequency_detector_.is_increase_decrease_symmetrical()) { - ESP_LOGW(TAG, " Oscillation Frequency is not symmetrical. PID parameters may be inaccurate!"); - ESP_LOGW( - TAG, - " This is usually because the heat and cool processes do not change the temperature at the same rate."); ESP_LOGW(TAG, + " Oscillation Frequency is not symmetrical. PID parameters may be inaccurate!\n" + " This is usually because the heat and cool processes do not change the temperature at the same " + "rate.\n" " Please try reducing the positive_output value (or increase negative_output in case of a cooler)"); has_issue = true; } @@ -160,18 +161,23 @@ void PIDAutotuner::dump_config() { } auto fac = get_ziegler_nichols_pid_(); - ESP_LOGI(TAG, " Calculated PID parameters (\"Ziegler-Nichols PID\" rule):"); - ESP_LOGI(TAG, " "); - ESP_LOGI(TAG, " control_parameters:"); - ESP_LOGI(TAG, " kp: %.5f", fac.kp); - ESP_LOGI(TAG, " ki: %.5f", fac.ki); - ESP_LOGI(TAG, " kd: %.5f", fac.kd); - ESP_LOGI(TAG, " "); - ESP_LOGI(TAG, " Please copy these values into your YAML configuration! They will reset on the next reboot."); + ESP_LOGI(TAG, + " Calculated PID parameters (\"Ziegler-Nichols PID\" rule):\n" + "\n" + " control_parameters:\n" + " kp: %.5f\n" + " ki: %.5f\n" + " kd: %.5f\n" + "\n" + " Please copy these values into your YAML configuration! They will reset on the next reboot.", + fac.kp, fac.ki, fac.kd); - ESP_LOGV(TAG, " Oscillation Period: %f", this->frequency_detector_.get_mean_oscillation_period()); - ESP_LOGV(TAG, " Oscillation Amplitude: %f", this->amplitude_detector_.get_mean_oscillation_amplitude()); - ESP_LOGV(TAG, " Ku: %f, Pu: %f", this->ku_, this->pu_); + ESP_LOGV(TAG, + " Oscillation Period: %f\n" + " Oscillation Amplitude: %f\n" + " Ku: %f, Pu: %f", + this->frequency_detector_.get_mean_oscillation_period(), + this->amplitude_detector_.get_mean_oscillation_amplitude(), this->ku_, this->pu_); ESP_LOGD(TAG, " Alternative Rules:"); // http://www.mstarlabs.com/control/znrule.html @@ -183,13 +189,16 @@ void PIDAutotuner::dump_config() { } if (this->state_ == AUTOTUNE_RUNNING) { - ESP_LOGD(TAG, "%s: PID Autotune:", this->id_.c_str()); - ESP_LOGD(TAG, " Autotune is still running!"); - ESP_LOGD(TAG, " Status: Trying to reach %.2f °C", setpoint_ - relay_function_.current_target_error()); - ESP_LOGD(TAG, " Stats so far:"); - ESP_LOGD(TAG, " Phases: %" PRIu32, relay_function_.phase_count); - ESP_LOGD(TAG, " Detected %zu zero-crossings", frequency_detector_.zerocrossing_intervals.size()); - ESP_LOGD(TAG, " Current Phase Min: %.2f, Max: %.2f", amplitude_detector_.phase_min, + ESP_LOGD(TAG, + "%s: PID Autotune:\n" + " Autotune is still running!\n" + " Status: Trying to reach %.2f °C\n" + " Stats so far:\n" + " Phases: %" PRIu32 "\n" + " Detected %zu zero-crossings\n" + " Current Phase Min: %.2f, Max: %.2f", + this->id_.c_str(), setpoint_ - relay_function_.current_target_error(), relay_function_.phase_count, + frequency_detector_.zerocrossing_intervals.size(), amplitude_detector_.phase_min, amplitude_detector_.phase_max); } } @@ -205,8 +214,10 @@ PIDAutotuner::PIDResult PIDAutotuner::calculate_pid_(float kp_factor, float ki_f } void PIDAutotuner::print_rule_(const char *name, float kp_factor, float ki_factor, float kd_factor) { auto fac = calculate_pid_(kp_factor, ki_factor, kd_factor); - ESP_LOGD(TAG, " Rule '%s':", name); - ESP_LOGD(TAG, " kp: %.5f, ki: %.5f, kd: %.5f", fac.kp, fac.ki, fac.kd); + ESP_LOGD(TAG, + " Rule '%s':\n" + " kp: %.5f, ki: %.5f, kd: %.5f", + name, fac.kp, fac.ki, fac.kd); } // ================== RelayFunction ================== diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index fd74eabd87..2094c0e942 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -162,14 +162,14 @@ void PIDClimate::start_autotune(std::unique_ptr &&autotune) { float min_value = this->supports_cool_() ? -1.0f : 0.0f; float max_value = this->supports_heat_() ? 1.0f : 0.0f; this->autotuner_->config(min_value, max_value); - this->autotuner_->set_autotuner_id(this->get_object_id()); + this->autotuner_->set_autotuner_id(this->get_name()); ESP_LOGI(TAG, "%s: Autotune has started. This can take a long time depending on the " "responsiveness of your system. Your system " "output will be altered to deliberately oscillate above and below the setpoint multiple times. " "Until your sensor provides a reading, the autotuner may display \'nan\'", - this->get_object_id().c_str()); + this->get_name().c_str()); this->set_interval("autotune-progress", 10000, [this]() { if (this->autotuner_ != nullptr && !this->autotuner_->is_finished()) @@ -178,7 +178,7 @@ void PIDClimate::start_autotune(std::unique_ptr &&autotune) { if (mode != climate::CLIMATE_MODE_HEAT_COOL) { ESP_LOGW(TAG, "%s: !!! For PID autotuner you need to set AUTO (also called heat/cool) mode!", - this->get_object_id().c_str()); + this->get_name().c_str()); } } diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py index c693cfea19..8ff21ab069 100644 --- a/esphome/components/pm1006/sensor.py +++ b/esphome/components/pm1006/sensor.py @@ -7,10 +7,10 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, DEVICE_CLASS_PM25, ICON_BLUR, + SCHEDULER_DONT_RUN, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ) -from esphome.core import TimePeriodMilliseconds CODEOWNERS = ["@habbie"] DEPENDENCIES = ["uart"] @@ -41,16 +41,12 @@ CONFIG_SCHEMA = cv.All( def validate_interval_uart(config): - require_tx = False - interval = config.get(CONF_UPDATE_INTERVAL) - - if isinstance(interval, TimePeriodMilliseconds): - # 'never' is encoded as a very large int, not as a TimePeriodMilliseconds objects - require_tx = True - uart.final_validate_device_schema( - "pm1006", baud_rate=9600, require_rx=True, require_tx=require_tx + "pm1006", + baud_rate=9600, + require_rx=True, + require_tx=interval.total_milliseconds != SCHEDULER_DONT_RUN, )(config) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index eb10d19c91..bb167033d1 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -18,6 +18,8 @@ static const uint16_t PMS_CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automaticall static const uint16_t PMS_CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode static const uint16_t PMS_CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode +void PMSX003Component::setup() {} + void PMSX003Component::dump_config() { ESP_LOGCONFIG(TAG, "PMSX003:"); LOG_SENSOR(" ", "PM1.0STD", this->pm_1_0_std_sensor_); @@ -39,21 +41,36 @@ void PMSX003Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + + if (this->update_interval_ <= PMS_STABILISING_MS) { + ESP_LOGCONFIG(TAG, " Mode: active continuous (sensor default)"); + } else { + ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles"); + } + this->check_uart_settings(9600); } void PMSX003Component::loop() { const uint32_t now = App.get_loop_component_start_time(); + // Initialize sensor mode on first loop + if (this->initialised_ == 0) { + if (this->update_interval_ > PMS_STABILISING_MS) { + // Long update interval: use passive mode with sleep/wake cycles + this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE); + this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP); + } else { + // Short/zero update interval: use active continuous mode + this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_ACTIVE); + } + this->initialised_ = 1; + } + // If we update less often than it takes the device to stabilise, spin the fan down // rather than running it constantly. It does take some time to stabilise, so we // need to keep track of what state we're in. if (this->update_interval_ > PMS_STABILISING_MS) { - if (this->initialised_ == 0) { - this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE); - this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP); - this->initialised_ = 1; - } switch (this->state_) { case PMSX003_STATE_IDLE: // Power on the sensor now so it'll be ready when we hit the update time diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index ba607b4487..f48121800e 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -31,6 +31,7 @@ enum PMSX003State { class PMSX003Component : public uart::UARTDevice, public Component { public: PMSX003Component() = default; + void setup() override; void dump_config() override; void loop() override; diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index 3f04e8e1cc..6f679ed10a 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -55,7 +55,7 @@ def CONFIG_SCHEMA(conf): if conf: raise cv.Invalid( "This component has been moved in 1.16, please see the docs for updated " - "instructions. https://esphome.io/components/binary_sensor/pn532.html" + "instructions. https://esphome.io/components/binary_sensor/pn532/" ) diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index ef4022db4b..d5e892a576 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -31,8 +31,10 @@ void PN532::setup() { this->mark_failed(); return; } - ESP_LOGD(TAG, "Found chip PN5%02X", version_data[0]); - ESP_LOGD(TAG, "Firmware ver. %d.%d", version_data[1], version_data[2]); + ESP_LOGD(TAG, + "Found chip PN5%02X\n" + "Firmware ver. %d.%d", + version_data[0], version_data[1], version_data[2]); if (!this->write_command_({ PN532_COMMAND_SAMCONFIGURATION, diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp index 0871f7acab..118421c47f 100644 --- a/esphome/components/pn532_spi/pn532_spi.cpp +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -1,4 +1,5 @@ #include "pn532_spi.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" // Based on: @@ -11,6 +12,9 @@ namespace pn532_spi { static const char *const TAG = "pn532_spi"; +// Maximum bytes to log in verbose hex output +static constexpr size_t PN532_MAX_LOG_BYTES = 64; + void PN532Spi::setup() { this->spi_setup(); @@ -32,7 +36,10 @@ bool PN532Spi::write_data(const std::vector &data) { delay(2); // First byte, communication mode: Write data this->write_byte(0x01); - ESP_LOGV(TAG, "Writing data: %s", format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(PN532_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Writing data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); this->write_array(data.data(), data.size()); this->disable(); @@ -55,7 +62,10 @@ bool PN532Spi::read_data(std::vector &data, uint8_t len) { this->read_array(data.data(), len); this->disable(); data.insert(data.begin(), 0x01); - ESP_LOGV(TAG, "Read data: %s", format_hex_pretty(data).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(PN532_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Read data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); return true; } @@ -73,7 +83,10 @@ bool PN532Spi::read_response(uint8_t command, std::vector &data) { std::vector header(7); this->read_array(header.data(), 7); - ESP_LOGV(TAG, "Header data: %s", format_hex_pretty(header).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(PN532_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Header data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), header.data(), header.size())); if (header[0] != 0x00 && header[1] != 0x00 && header[2] != 0xFF) { // invalid packet @@ -103,7 +116,7 @@ bool PN532Spi::read_response(uint8_t command, std::vector &data) { this->read_array(data.data(), len + 1); this->disable(); - ESP_LOGV(TAG, "Response data: %s", format_hex_pretty(data).c_str()); + ESP_LOGV(TAG, "Response data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size())); uint8_t checksum = header[5] + header[6]; // TFI + Command response code for (int i = 0; i < len - 1; i++) { diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index f827bd151a..f6ddcb0767 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -240,8 +240,11 @@ uint8_t PN7150::reset_core_(const bool reset_config, const bool power) { return nfc::STATUS_FAILED; } - ESP_LOGD(TAG, "Configuration %s", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? "reset" : "retained"); - ESP_LOGD(TAG, "NCI version: %s", rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? "2.0" : "1.0"); + ESP_LOGD(TAG, + "Configuration %s\n" + "NCI version: %s", + rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? "reset" : "retained", + rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? "2.0" : "1.0"); return nfc::STATUS_OK; } @@ -266,11 +269,13 @@ uint8_t PN7150::init_core_() { uint8_t flash_major_version = rx.get_message()[18 + rx.get_message()[8]]; uint8_t flash_minor_version = rx.get_message()[19 + rx.get_message()[8]]; - ESP_LOGD(TAG, "Manufacturer ID: 0x%02X", manf_id); - ESP_LOGD(TAG, "Hardware version: 0x%02X", hw_version); - ESP_LOGD(TAG, "ROM code version: 0x%02X", rom_code_version); - ESP_LOGD(TAG, "FLASH major version: 0x%02X", flash_major_version); - ESP_LOGD(TAG, "FLASH minor version: 0x%02X", flash_minor_version); + ESP_LOGD(TAG, + "Manufacturer ID: 0x%02X\n" + "Hardware version: 0x%02X\n" + "ROM code version: 0x%02X\n" + "FLASH major version: 0x%02X\n" + "FLASH minor version: 0x%02X", + manf_id, hw_version, rom_code_version, flash_major_version, flash_minor_version); return rx.get_simple_status_response(); } @@ -847,8 +852,8 @@ void PN7150::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoi case EP_WRITE: if (this->next_task_message_to_write_ != nullptr) { - ESP_LOGD(TAG, " Tag writing"); - ESP_LOGD(TAG, " Tag formatting"); + ESP_LOGD(TAG, " Tag writing\n" + " Tag formatting"); if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { ESP_LOGE(TAG, " Tag could not be formatted for writing"); } else { diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index a8edfadd8e..8c8028b04a 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -262,9 +262,12 @@ uint8_t PN7160::reset_core_(const bool reset_config, const bool power) { return nfc::STATUS_FAILED; } - ESP_LOGD(TAG, "Configuration %s", rx.get_message()[4] ? "reset" : "retained"); - ESP_LOGD(TAG, "NCI version: %s", rx.get_message()[5] == 0x20 ? "2.0" : "1.0"); - ESP_LOGD(TAG, "Manufacturer ID: 0x%02X", rx.get_message()[6]); + ESP_LOGD(TAG, + "Configuration %s\n" + "NCI version: %s\n" + "Manufacturer ID: 0x%02X", + rx.get_message()[4] ? "reset" : "retained", rx.get_message()[5] == 0x20 ? "2.0" : "1.0", + rx.get_message()[6]); rx.get_message().erase(rx.get_message().begin(), rx.get_message().begin() + 8); ESP_LOGD(TAG, "Manufacturer info: %s", nfc::format_bytes(rx.get_message()).c_str()); @@ -291,11 +294,13 @@ uint8_t PN7160::init_core_() { uint8_t flash_minor_version = rx.get_message()[20 + rx.get_message()[8]]; std::vector features(rx.get_message().begin() + 4, rx.get_message().begin() + 8); - ESP_LOGD(TAG, "Hardware version: %u", hw_version); - ESP_LOGD(TAG, "ROM code version: %u", rom_code_version); - ESP_LOGD(TAG, "FLASH major version: %u", flash_major_version); - ESP_LOGD(TAG, "FLASH minor version: %u", flash_minor_version); - ESP_LOGD(TAG, "Features: %s", nfc::format_bytes(features).c_str()); + ESP_LOGD(TAG, + "Hardware version: %u\n" + "ROM code version: %u\n" + "FLASH major version: %u\n" + "FLASH minor version: %u\n" + "Features: %s", + hw_version, rom_code_version, flash_major_version, flash_minor_version, nfc::format_bytes(features).c_str()); return rx.get_simple_status_response(); } @@ -871,8 +876,8 @@ void PN7160::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoi case EP_WRITE: if (this->next_task_message_to_write_ != nullptr) { - ESP_LOGD(TAG, " Tag writing"); - ESP_LOGD(TAG, " Tag formatting"); + ESP_LOGD(TAG, " Tag writing\n" + " Tag formatting"); if (this->format_endpoint_(working_endpoint.tag->get_uid()) != nfc::STATUS_OK) { ESP_LOGE(TAG, " Tag could not be formatted for writing"); } else { diff --git a/esphome/components/preferences/__init__.py b/esphome/components/preferences/__init__.py index 1da6d02045..c6bede891a 100644 --- a/esphome/components/preferences/__init__.py +++ b/esphome/components/preferences/__init__.py @@ -1,6 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID +from esphome.core import coroutine_with_priority +from esphome.coroutine import CoroPriority CODEOWNERS = ["@esphome/core"] @@ -16,6 +18,7 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) +@coroutine_with_priority(CoroPriority.PREFERENCES) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_write_interval(config[CONF_FLASH_WRITE_INTERVAL])) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 5cfcacf0cb..88b357041a 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -53,6 +53,18 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { this->lock_row_(stream, obj, area, node, friendly_name); #endif +#ifdef USE_EVENT + this->event_type_(stream); + for (auto *obj : App.get_events()) + this->event_row_(stream, obj, area, node, friendly_name); +#endif + +#ifdef USE_TEXT + this->text_type_(stream); + for (auto *obj : App.get_texts()) + this->text_row_(stream, obj, area, node, friendly_name); +#endif + #ifdef USE_TEXT_SENSOR this->text_sensor_type_(stream); for (auto *obj : App.get_text_sensors()) @@ -100,7 +112,11 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { std::string PrometheusHandler::relabel_id_(EntityBase *obj) { auto item = relabel_map_id_.find(obj); - return item == relabel_map_id_.end() ? obj->get_object_id() : item->second; + if (item != relabel_map_id_.end()) { + return item->second; + } + char object_id_buf[OBJECT_ID_MAX_LEN]; + return obj->get_object_id_to(object_id_buf).str(); } std::string PrometheusHandler::relabel_name_(EntityBase *obj) { @@ -129,6 +145,24 @@ void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, st } } +#ifdef USE_ESP8266 +void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, const __FlashStringHelper *metric_name, + EntityBase *obj, std::string &area, std::string &node, + std::string &friendly_name) { +#else +void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, const char *metric_name, EntityBase *obj, + std::string &area, std::string &node, std::string &friendly_name) { +#endif + stream->print(metric_name); + stream->print(ESPHOME_F("{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); +} + // Type-specific implementation #ifdef USE_SENSOR void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { @@ -291,13 +325,7 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat if (obj->is_internal() && !this->include_internal_) return; // State - stream->print(ESPHOME_F("esphome_light_state{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); + print_metric_labels_(stream, ESPHOME_F("esphome_light_state"), obj, area, node, friendly_name); stream->print(ESPHOME_F("\"} ")); stream->print(obj->remote_values.is_on()); stream->print(ESPHOME_F("\n")); @@ -306,78 +334,45 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat float brightness, r, g, b, w; color.as_brightness(&brightness); color.as_rgbw(&r, &g, &b, &w); - stream->print(ESPHOME_F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(ESPHOME_F("\",channel=\"brightness\"} ")); - stream->print(brightness); - stream->print(ESPHOME_F("\n")); - stream->print(ESPHOME_F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(ESPHOME_F("\",channel=\"r\"} ")); - stream->print(r); - stream->print(ESPHOME_F("\n")); - stream->print(ESPHOME_F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(ESPHOME_F("\",channel=\"g\"} ")); - stream->print(g); - stream->print(ESPHOME_F("\n")); - stream->print(ESPHOME_F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(ESPHOME_F("\",channel=\"b\"} ")); - stream->print(b); - stream->print(ESPHOME_F("\n")); - stream->print(ESPHOME_F("esphome_light_color{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(ESPHOME_F("\",channel=\"w\"} ")); - stream->print(w); - stream->print(ESPHOME_F("\n")); - // Effect - std::string effect = obj->get_effect_name(); - if (effect == "None") { - stream->print(ESPHOME_F("esphome_light_effect_active{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); - stream->print(ESPHOME_F("\",effect=\"None\"} 0\n")); - } else { - stream->print(ESPHOME_F("esphome_light_effect_active{id=\"")); - stream->print(relabel_id_(obj).c_str()); - add_area_label_(stream, area); - add_node_label_(stream, node); - add_friendly_name_label_(stream, friendly_name); - stream->print(ESPHOME_F("\",name=\"")); - stream->print(relabel_name_(obj).c_str()); + if (obj->get_traits().supports_color_capability(light::ColorCapability::BRIGHTNESS)) { + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"brightness\"} ")); + stream->print(brightness); + stream->print(ESPHOME_F("\n")); + } + if (obj->get_traits().supports_color_capability(light::ColorCapability::RGB)) { + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"r\"} ")); + stream->print(r); + stream->print(ESPHOME_F("\n")); + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"g\"} ")); + stream->print(g); + stream->print(ESPHOME_F("\n")); + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"b\"} ")); + stream->print(b); + stream->print(ESPHOME_F("\n")); + } + if (obj->get_traits().supports_color_capability(light::ColorCapability::WHITE)) { + print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name); + stream->print(ESPHOME_F("\",channel=\"w\"} ")); + stream->print(w); + stream->print(ESPHOME_F("\n")); + } + // Skip effect metrics if light has no effects + if (!obj->get_effects().empty()) { + // Effect + std::string effect = obj->get_effect_name(); + print_metric_labels_(stream, ESPHOME_F("esphome_light_effect_active"), obj, area, node, friendly_name); stream->print(ESPHOME_F("\",effect=\"")); - stream->print(effect.c_str()); - stream->print(ESPHOME_F("\"} 1\n")); + // Only vary based on effect + if (effect == "None") { + stream->print(ESPHOME_F("None\"} 0\n")); + } else { + stream->print(effect.c_str()); + stream->print(ESPHOME_F("\"} 1\n")); + } } } #endif @@ -547,6 +542,100 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso } #endif +// Type-specific implementation +#ifdef USE_TEXT +void PrometheusHandler::text_type_(AsyncResponseStream *stream) { + stream->print(ESPHOME_F("#TYPE esphome_text_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_text_failed gauge\n")); +} +void PrometheusHandler::text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node, + std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + if (obj->has_state()) { + // We have a valid value, output this value + stream->print(ESPHOME_F("esphome_text_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 0\n")); + // Data itself + stream->print(ESPHOME_F("esphome_text_value{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\",value=\"")); + stream->print(obj->state.c_str()); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); + } else { + // Invalid state + stream->print(ESPHOME_F("esphome_text_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 1\n")); + } +} +#endif + +// Type-specific implementation +#ifdef USE_EVENT +void PrometheusHandler::event_type_(AsyncResponseStream *stream) { + stream->print(ESPHOME_F("#TYPE esphome_event_value gauge\n")); + stream->print(ESPHOME_F("#TYPE esphome_event_failed gauge\n")); +} +void PrometheusHandler::event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node, + std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + if (obj->get_last_event_type() != nullptr) { + // We have a valid event type, output this value + stream->print(ESPHOME_F("esphome_event_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 0\n")); + // Data itself + stream->print(ESPHOME_F("esphome_event_value{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\",last_event_type=\"")); + stream->print(obj->get_last_event_type()); + stream->print(ESPHOME_F("\"} ")); + stream->print(ESPHOME_F("1.0")); + stream->print(ESPHOME_F("\n")); + } else { + // No event triggered yet + stream->print(ESPHOME_F("esphome_event_failed{id=\"")); + stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); + stream->print(ESPHOME_F("\",name=\"")); + stream->print(relabel_name_(obj).c_str()); + stream->print(ESPHOME_F("\"} 1\n")); + } +} +#endif + // Type-specific implementation #ifdef USE_NUMBER void PrometheusHandler::number_type_(AsyncResponseStream *stream) { @@ -620,7 +709,7 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(ESPHOME_F("\",value=\"")); - stream->print(obj->state.c_str()); + stream->print(obj->current_option()); stream->print(ESPHOME_F("\"} ")); stream->print(ESPHOME_F("1.0")); stream->print(ESPHOME_F("\n")); @@ -810,7 +899,11 @@ void PrometheusHandler::valve_row_(AsyncResponseStream *stream, valve::Valve *ob stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(ESPHOME_F("\",operation=\"")); - stream->print(valve::valve_operation_to_str(obj->current_operation)); +#ifdef USE_STORE_LOG_STR_IN_FLASH + stream->print((const __FlashStringHelper *) valve::valve_operation_to_str(obj->current_operation)); +#else + stream->print((const char *) valve::valve_operation_to_str(obj->current_operation)); +#endif stream->print(ESPHOME_F("\"} ")); stream->print(ESPHOME_F("1.0")); stream->print(ESPHOME_F("\n")); diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index c4598f44b0..24243c8c98 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -66,6 +66,14 @@ class PrometheusHandler : public AsyncWebHandler, public Component { void add_area_label_(AsyncResponseStream *stream, std::string &area); void add_node_label_(AsyncResponseStream *stream, std::string &node); void add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name); + /// Print metric name and common labels (id, area, node, friendly_name, name) +#ifdef USE_ESP8266 + void print_metric_labels_(AsyncResponseStream *stream, const __FlashStringHelper *metric_name, EntityBase *obj, + std::string &area, std::string &node, std::string &friendly_name); +#else + void print_metric_labels_(AsyncResponseStream *stream, const char *metric_name, EntityBase *obj, std::string &area, + std::string &node, std::string &friendly_name); +#endif #ifdef USE_SENSOR /// Return the type for prometheus @@ -123,6 +131,22 @@ class PrometheusHandler : public AsyncWebHandler, public Component { std::string &friendly_name); #endif +#ifdef USE_EVENT + /// Return the type for prometheus + void event_type_(AsyncResponseStream *stream); + /// Return the event values state as prometheus data point + void event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + +#ifdef USE_TEXT + /// Return the type for prometheus + void text_type_(AsyncResponseStream *stream); + /// Return the text values state as prometheus data point + void text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + #ifdef USE_TEXT_SENSOR /// Return the type for prometheus void text_sensor_type_(AsyncResponseStream *stream); diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index c50c599855..39afb407f1 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -7,13 +7,12 @@ from esphome.components.esp32 import ( CONF_CPU_FREQUENCY, CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, VARIANT_ESP32, - add_idf_sdkconfig_option, - get_esp32_variant, -) -from esphome.components.esp32.const import ( + VARIANT_ESP32C5, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + add_idf_sdkconfig_option, + get_esp32_variant, ) import esphome.config_validation as cv from esphome.const import ( @@ -53,6 +52,7 @@ CONF_ENABLE_ECC = "enable_ecc" SPIRAM_MODES = { VARIANT_ESP32: (TYPE_QUAD,), + VARIANT_ESP32C5: (TYPE_QUAD,), VARIANT_ESP32S2: (TYPE_QUAD,), VARIANT_ESP32S3: (TYPE_QUAD, TYPE_OCTAL), VARIANT_ESP32P4: (TYPE_HEX,), @@ -61,6 +61,7 @@ SPIRAM_MODES = { SPIRAM_SPEEDS = { VARIANT_ESP32: (40, 80, 120), + VARIANT_ESP32C5: (40, 80, 120), VARIANT_ESP32S2: (40, 80, 120), VARIANT_ESP32S3: (40, 80, 120), VARIANT_ESP32P4: (20, 100, 200), @@ -196,7 +197,6 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_SPIRAM_SPEED", speed) if config[CONF_MODE] == TYPE_OCTAL and speed == 120: add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True) - add_idf_sdkconfig_option("CONFIG_BOOTLOADER_FLASH_DC_AWARE", True) if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 4, 0): add_idf_sdkconfig_option( "CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 6300d6fe96..c0d74cef4a 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -68,8 +68,10 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { next_pcnt_channel = pcnt_channel_t(int(next_pcnt_channel) + 1); } - ESP_LOGCONFIG(TAG, " PCNT Unit Number: %u", this->pcnt_unit); - ESP_LOGCONFIG(TAG, " PCNT Channel Number: %u", this->pcnt_channel); + ESP_LOGCONFIG(TAG, + " PCNT Unit Number: %u\n" + " PCNT Channel Number: %u", + this->pcnt_unit, this->pcnt_channel); pcnt_count_mode_t rising = PCNT_COUNT_DIS, falling = PCNT_COUNT_DIS; switch (this->rising_edge_mode) { diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index b6916ad68f..4d4a5466bb 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -1,4 +1,5 @@ #include "pvvx_display.h" +#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,14 +9,16 @@ namespace pvvx_mithermometer { static const char *const TAG = "display.pvvx_mithermometer"; void PVVXDisplay::dump_config() { + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; ESP_LOGCONFIG(TAG, "PVVX MiThermometer display:\n" " MAC address : %s\n" " Service UUID : %s\n" " Characteristic UUID : %s\n" " Auto clear : %s", - this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(), - this->char_uuid_.to_string().c_str(), YESNO(this->auto_clear_enabled_)); + this->parent_->address_str(), this->service_uuid_.to_str(service_buf), + this->char_uuid_.to_str(char_buf), YESNO(this->auto_clear_enabled_)); #ifdef USE_TIME ESP_LOGCONFIG(TAG, " Set time on connection: %s", YESNO(this->time_ != nullptr)); #endif @@ -28,12 +31,12 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t switch (event) { case ESP_GATTC_OPEN_EVT: if (param->open.status == ESP_GATT_OK) { - ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str().c_str()); + ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str()); this->delayed_disconnect_(); } break; case ESP_GATTC_DISCONNECT_EVT: - ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str().c_str()); + ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str()); this->connection_established_ = false; this->cancel_timeout("disconnect"); this->char_handle_ = 0; @@ -41,7 +44,7 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t case ESP_GATTC_SEARCH_CMPL_EVT: { auto *chr = this->parent_->get_characteristic(this->service_uuid_, this->char_uuid_); if (chr == nullptr) { - ESP_LOGW(TAG, "[%s] Characteristic not found.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Characteristic not found.", this->parent_->address_str()); break; } this->connection_established_ = true; @@ -66,11 +69,11 @@ void PVVXDisplay::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb return; if (param->ble_security.auth_cmpl.success) { - ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str()); // Now that pairing is complete, perform the pending writes this->sync_time_and_display_(); } else { - ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str()); } break; } @@ -89,22 +92,20 @@ void PVVXDisplay::update() { void PVVXDisplay::display() { if (!this->parent_->enabled) { - ESP_LOGD(TAG, "[%s] BLE client not enabled. Init connection.", this->parent_->address_str().c_str()); + ESP_LOGD(TAG, "[%s] BLE client not enabled. Init connection.", this->parent_->address_str()); this->parent_->set_enabled(true); return; } if (!this->connection_established_) { - ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", - this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", this->parent_->address_str()); return; } if (!this->char_handle_) { - ESP_LOGW(TAG, "[%s] No ble handle to BLE client. State update can not be written.", - this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] No ble handle to BLE client. State update can not be written.", this->parent_->address_str()); return; } ESP_LOGD(TAG, "[%s] Send to display: bignum %d, smallnum: %d, cfg: 0x%02x, validity period: %u.", - this->parent_->address_str().c_str(), this->bignum_, this->smallnum_, this->cfg_, this->validity_period_); + this->parent_->address_str(), this->bignum_, this->smallnum_, this->cfg_, this->validity_period_); uint8_t blk[8] = {}; blk[0] = 0x22; blk[1] = this->bignum_ & 0xff; @@ -128,16 +129,16 @@ void PVVXDisplay::setcfgbit_(uint8_t bit, bool value) { void PVVXDisplay::send_to_setup_char_(uint8_t *blk, size_t size) { if (!this->connection_established_) { - ESP_LOGW(TAG, "[%s] Not connected to BLE client.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Not connected to BLE client.", this->parent_->address_str()); return; } auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, size, blk, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); } else { - ESP_LOGV(TAG, "[%s] send %u bytes", this->parent_->address_str().c_str(), size); + ESP_LOGV(TAG, "[%s] send %u bytes", this->parent_->address_str(), size); this->delayed_disconnect_(); } } @@ -161,21 +162,21 @@ void PVVXDisplay::sync_time_() { if (this->time_ == nullptr) return; if (!this->connection_established_) { - ESP_LOGW(TAG, "[%s] Not connected to BLE client. Time can not be synced.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Not connected to BLE client. Time can not be synced.", this->parent_->address_str()); return; } if (!this->char_handle_) { - ESP_LOGW(TAG, "[%s] No ble handle to BLE client. Time can not be synced.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] No ble handle to BLE client. Time can not be synced.", this->parent_->address_str()); return; } auto time = this->time_->now(); if (!time.is_valid()) { - ESP_LOGW(TAG, "[%s] Time is not yet valid. Time can not be synced.", this->parent_->address_str().c_str()); + ESP_LOGW(TAG, "[%s] Time is not yet valid. Time can not be synced.", this->parent_->address_str()); return; } time.recalc_timestamp_utc(true); // calculate timestamp of local time uint8_t blk[5] = {}; - ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str().c_str(), time.timestamp); + ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str(), time.timestamp); blk[0] = 0x23; blk[1] = time.timestamp & 0xff; blk[2] = (time.timestamp >> 8) & 0xff; diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.h b/esphome/components/pvvx_mithermometer/display/pvvx_display.h index 8637506bae..06837b94ab 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.h +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -60,13 +60,13 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { * Valid values are from -99.5 to 1999.5. Smaller values are displayed as Lo, higher as Hi. * It will printed as it fits in the screen. */ - void print_bignum(float bignum) { this->bignum_ = bignum * 10; } + void print_bignum(float bignum) { this->bignum_ = static_cast(bignum * 10); } /** * Print the small number * * Valid values are from -9 to 99. Smaller values are displayed as Lo, higher as Hi. */ - void print_smallnum(float smallnum) { this->smallnum_ = smallnum; } + void print_smallnum(float smallnum) { this->smallnum_ = static_cast(smallnum); } /** * Print a happy face * @@ -107,8 +107,8 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { bool auto_clear_enabled_{true}; uint32_t disconnect_delay_ms_ = 5000; uint16_t validity_period_ = 300; - uint16_t bignum_ = 0; - uint16_t smallnum_ = 0; + int16_t bignum_ = 0; + int16_t smallnum_ = 0; uint8_t cfg_ = 0; void setcfgbit_(uint8_t bit, bool value); diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp index 6975109952..5712447909 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp @@ -21,7 +21,9 @@ bool PVVXMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &devic ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -32,7 +34,7 @@ bool PVVXMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &devic if (!(parse_message_(service_data.data, *res))) { continue; } - if (!(report_results_(res, device.address_str()))) { + if (!(report_results_(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) @@ -111,13 +113,13 @@ bool PVVXMiThermometer::parse_message_(const std::vector &message, Pars return true; } -bool PVVXMiThermometer::report_results_(const optional &result, const std::string &address) { +bool PVVXMiThermometer::report_results_(const optional &result, const char *address) { if (!result.has_value()) { ESP_LOGVV(TAG, "report_results(): no results available."); return false; } - ESP_LOGD(TAG, "Got PVVX MiThermometer (%s):", address.c_str()); + ESP_LOGD(TAG, "Got PVVX MiThermometer (%s):", address); if (result->temperature.has_value()) { ESP_LOGD(TAG, " Temperature: %.2f °C", *result->temperature); diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h index 9614a3c586..c15e1e7e22 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -41,7 +41,7 @@ class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevic optional parse_header_(const esp32_ble_tracker::ServiceData &service_data); bool parse_message_(const std::vector &message, ParseResult &result); - bool report_results_(const optional &result, const std::string &address); + bool report_results_(const optional &result, const char *address); }; } // namespace pvvx_mithermometer diff --git a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp index 55e02f3e33..8175477cb2 100644 --- a/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp +++ b/esphome/components/pylontech/text_sensor/pylontech_text_sensor.cpp @@ -25,16 +25,16 @@ void PylontechTextSensor::on_line_read(PylontechListener::LineContents *line) { return; } if (this->base_state_text_sensor_ != nullptr) { - this->base_state_text_sensor_->publish_state(std::string(line->base_st)); + this->base_state_text_sensor_->publish_state(line->base_st); } if (this->voltage_state_text_sensor_ != nullptr) { - this->voltage_state_text_sensor_->publish_state(std::string(line->volt_st)); + this->voltage_state_text_sensor_->publish_state(line->volt_st); } if (this->current_state_text_sensor_ != nullptr) { - this->current_state_text_sensor_->publish_state(std::string(line->curr_st)); + this->current_state_text_sensor_->publish_state(line->curr_st); } if (this->temperature_state_text_sensor_ != nullptr) { - this->temperature_state_text_sensor_->publish_state(std::string(line->temp_st)); + this->temperature_state_text_sensor_->publish_state(line->temp_st); } } diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 61fde186d7..4e1ef27d5e 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -127,9 +127,11 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.qmp6988_cali.COE_b21 = (int16_t) encode_uint16(a_data_uint8_tr[14], a_data_uint8_tr[15]); qmp6988_data_.qmp6988_cali.COE_bp3 = (int16_t) encode_uint16(a_data_uint8_tr[16], a_data_uint8_tr[17]); - ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n"); - ESP_LOGV(TAG, "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_a0, - qmp6988_data_.qmp6988_cali.COE_a1, qmp6988_data_.qmp6988_cali.COE_a2, qmp6988_data_.qmp6988_cali.COE_b00); + ESP_LOGV(TAG, + "<-----------calibration data-------------->\n" + "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]", + qmp6988_data_.qmp6988_cali.COE_a0, qmp6988_data_.qmp6988_cali.COE_a1, qmp6988_data_.qmp6988_cali.COE_a2, + qmp6988_data_.qmp6988_cali.COE_b00); ESP_LOGV(TAG, "COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bt1, qmp6988_data_.qmp6988_cali.COE_bt2, qmp6988_data_.qmp6988_cali.COE_bp1, qmp6988_data_.qmp6988_cali.COE_b11); ESP_LOGV(TAG, "COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bp2, @@ -150,9 +152,10 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.ik.b12 = 6846L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b12 + 85590281L; // 29Q53 qmp6988_data_.ik.b21 = 13836L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60 qmp6988_data_.ik.bp3 = 2915L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65 - ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n"); - ESP_LOGV(TAG, "a0[%d] a1[%d] a2[%d] b00[%d]\r\n", qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, - qmp6988_data_.ik.b00); + ESP_LOGV(TAG, + "<----------- int calibration data -------------->\n" + "a0[%d] a1[%d] a2[%d] b00[%d]", + qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00); ESP_LOGV(TAG, "bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\r\n", qmp6988_data_.ik.bt1, qmp6988_data_.ik.bt2, qmp6988_data_.ik.bp1, qmp6988_data_.ik.b11); ESP_LOGV(TAG, "bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]\r\n", qmp6988_data_.ik.bp2, qmp6988_data_.ik.b12, @@ -310,7 +313,7 @@ void QMP6988Component::calculate_pressure_() { void QMP6988Component::setup() { if (!this->device_check_()) { - this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } @@ -330,8 +333,10 @@ void QMP6988Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); ESP_LOGCONFIG(TAG, " Temperature Oversampling: %s", oversampling_to_str(this->temperature_oversampling_)); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); - ESP_LOGCONFIG(TAG, " Pressure Oversampling: %s", oversampling_to_str(this->pressure_oversampling_)); - ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_)); + ESP_LOGCONFIG(TAG, + " Pressure Oversampling: %s\n" + " IIR Filter: %s", + oversampling_to_str(this->pressure_oversampling_), iir_filter_to_str(this->iir_filter_)); } void QMP6988Component::update() { diff --git a/esphome/components/qspi_dbi/display.py b/esphome/components/qspi_dbi/display.py index 74d837a794..e4440c9b81 100644 --- a/esphome/components/qspi_dbi/display.py +++ b/esphome/components/qspi_dbi/display.py @@ -154,7 +154,7 @@ CONFIG_SCHEMA = cv.All( upper=True, key=CONF_MODEL, ), - cv.only_with_esp_idf, + cv.only_on_esp32, ) diff --git a/esphome/components/qspi_dbi/qspi_dbi.cpp b/esphome/components/qspi_dbi/qspi_dbi.cpp index 6c95bb7cf2..d42f95dca3 100644 --- a/esphome/components/qspi_dbi/qspi_dbi.cpp +++ b/esphome/components/qspi_dbi/qspi_dbi.cpp @@ -1,10 +1,14 @@ -#if defined(USE_ESP_IDF) && defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32) && defined(USE_ESP32_VARIANT_ESP32S3) #include "qspi_dbi.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { namespace qspi_dbi { +// Maximum bytes to log in verbose hex output +static constexpr size_t QSPI_DBI_MAX_LOG_BYTES = 64; + void QspiDbi::setup() { this->spi_setup(); if (this->enable_pin_ != nullptr) { @@ -174,7 +178,11 @@ void QspiDbi::write_to_display_(int x_start, int y_start, int w, int h, const ui this->disable(); } void QspiDbi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) { - ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(QSPI_DBI_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, + format_hex_pretty_to(hex_buf, sizeof(hex_buf), bytes, len)); this->enable(); this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len); this->disable(); @@ -208,12 +216,14 @@ void QspiDbi::write_sequence_(const std::vector &vec) { void QspiDbi::dump_config() { ESP_LOGCONFIG("", "QSPI_DBI Display"); ESP_LOGCONFIG("", "Model: %s", this->model_); - ESP_LOGCONFIG(TAG, " Height: %u", this->height_); - ESP_LOGCONFIG(TAG, " Width: %u", this->width_); - ESP_LOGCONFIG(TAG, " Draw rounding: %u", this->draw_rounding_); + ESP_LOGCONFIG(TAG, + " Height: %u\n" + " Width: %u\n" + " Draw rounding: %u\n" + " SPI Data rate: %uMHz", + this->height_, this->width_, this->draw_rounding_, (unsigned) (this->data_rate_ / 1000000)); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" Reset Pin: ", this->reset_pin_); - ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000)); } } // namespace qspi_dbi diff --git a/esphome/components/qspi_dbi/qspi_dbi.h b/esphome/components/qspi_dbi/qspi_dbi.h index f35f0e519c..3eee9bec47 100644 --- a/esphome/components/qspi_dbi/qspi_dbi.h +++ b/esphome/components/qspi_dbi/qspi_dbi.h @@ -3,7 +3,7 @@ // #pragma once -#if defined(USE_ESP_IDF) && defined(USE_ESP32_VARIANT_ESP32S3) +#if defined(USE_ESP32) && defined(USE_ESP32_VARIANT_ESP32S3) #include "esphome/components/spi/spi.h" #include "esphome/components/display/display.h" #include "esphome/components/display/display_buffer.h" diff --git a/esphome/components/radon_eye_ble/radon_eye_listener.cpp b/esphome/components/radon_eye_ble/radon_eye_listener.cpp index 0c6165c691..2c3ef77add 100644 --- a/esphome/components/radon_eye_ble/radon_eye_listener.cpp +++ b/esphome/components/radon_eye_ble/radon_eye_listener.cpp @@ -1,7 +1,6 @@ #include "radon_eye_listener.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include -#include #ifdef USE_ESP32 @@ -11,17 +10,11 @@ namespace radon_eye_ble { static const char *const TAG = "radon_eye_ble"; bool RadonEyeListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - if (not device.get_name().empty()) { - // Vector containing the prefixes to search for - std::vector prefixes = {"FR:R", "FR:I", "FR:H"}; - - // Check if the device name starts with any of the prefixes - if (std::any_of(prefixes.begin(), prefixes.end(), - [&](const std::string &prefix) { return device.get_name().starts_with(prefix); })) { - // Device found - ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().c_str(), - device.address_str().c_str()); - } + // Radon Eye devices have names starting with "FR:" + if (device.get_name().starts_with("FR:")) { + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().c_str(), + device.address_str_to(addr_buf)); } return false; } diff --git a/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp b/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp index 3959178b94..f2d32d51de 100644 --- a/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp +++ b/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp @@ -1,4 +1,7 @@ #include "radon_eye_rd200.h" +#include "esphome/components/esp32_ble/ble_uuid.h" + +#include #ifdef USE_ESP32 @@ -7,6 +10,22 @@ namespace radon_eye_rd200 { static const char *const TAG = "radon_eye_rd200"; +static const esp32_ble_tracker::ESPBTUUID SERVICE_UUID_V1 = + esp32_ble_tracker::ESPBTUUID::from_raw("00001523-1212-efde-1523-785feabcd123"); +static const esp32_ble_tracker::ESPBTUUID WRITE_CHARACTERISTIC_UUID_V1 = + esp32_ble_tracker::ESPBTUUID::from_raw("00001524-1212-efde-1523-785feabcd123"); +static const esp32_ble_tracker::ESPBTUUID READ_CHARACTERISTIC_UUID_V1 = + esp32_ble_tracker::ESPBTUUID::from_raw("00001525-1212-efde-1523-785feabcd123"); +static const uint8_t WRITE_COMMAND_V1 = 0x50; + +static const esp32_ble_tracker::ESPBTUUID SERVICE_UUID_V2 = + esp32_ble_tracker::ESPBTUUID::from_raw("00001523-0000-1000-8000-00805f9b34fb"); +static const esp32_ble_tracker::ESPBTUUID WRITE_CHARACTERISTIC_UUID_V2 = + esp32_ble_tracker::ESPBTUUID::from_raw("00001524-0000-1000-8000-00805f9b34fb"); +static const esp32_ble_tracker::ESPBTUUID READ_CHARACTERISTIC_UUID_V2 = + esp32_ble_tracker::ESPBTUUID::from_raw("00001525-0000-1000-8000-00805f9b34fb"); +static const uint8_t WRITE_COMMAND_V2 = 0x40; + void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { switch (event) { @@ -23,105 +42,150 @@ void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } case ESP_GATTC_SEARCH_CMPL_EVT: { + if (this->parent()->get_service(SERVICE_UUID_V1) != nullptr) { + service_uuid_ = SERVICE_UUID_V1; + sensors_write_characteristic_uuid_ = WRITE_CHARACTERISTIC_UUID_V1; + sensors_read_characteristic_uuid_ = READ_CHARACTERISTIC_UUID_V1; + write_command_ = WRITE_COMMAND_V1; + } else if (this->parent()->get_service(SERVICE_UUID_V2) != nullptr) { + service_uuid_ = SERVICE_UUID_V2; + sensors_write_characteristic_uuid_ = WRITE_CHARACTERISTIC_UUID_V2; + sensors_read_characteristic_uuid_ = READ_CHARACTERISTIC_UUID_V2; + write_command_ = WRITE_COMMAND_V2; + } else { + ESP_LOGW(TAG, "No supported device has been found, disconnecting"); + parent()->set_enabled(false); + break; + } + this->read_handle_ = 0; auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_read_characteristic_uuid_); if (chr == nullptr) { - ESP_LOGW(TAG, "No sensor read characteristic found at service %s char %s", service_uuid_.to_string().c_str(), - sensors_read_characteristic_uuid_.to_string().c_str()); + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; + ESP_LOGW(TAG, "No sensor read characteristic found at service %s char %s", service_uuid_.to_str(service_buf), + sensors_read_characteristic_uuid_.to_str(char_buf)); break; } this->read_handle_ = chr->handle; - // Write a 0x50 to the write characteristic. auto *write_chr = this->parent()->get_characteristic(service_uuid_, sensors_write_characteristic_uuid_); if (write_chr == nullptr) { - ESP_LOGW(TAG, "No sensor write characteristic found at service %s char %s", service_uuid_.to_string().c_str(), - sensors_read_characteristic_uuid_.to_string().c_str()); + char service_buf[esp32_ble::UUID_STR_LEN]; + char char_buf[esp32_ble::UUID_STR_LEN]; + ESP_LOGW(TAG, "No sensor write characteristic found at service %s char %s", service_uuid_.to_str(service_buf), + sensors_write_characteristic_uuid_.to_str(char_buf)); break; } this->write_handle_ = write_chr->handle; - this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; - - write_query_message_(); - - request_read_values_(); + esp_err_t status = + esp_ble_gattc_register_for_notify(gattc_if, this->parent()->get_remote_bda(), this->read_handle_); + if (status) { + ESP_LOGW(TAG, "Error registering for sensor notify, status=%d", status); + } break; } - case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent()->get_conn_id()) - break; - if (param->read.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + case ESP_GATTC_WRITE_DESCR_EVT: { + if (param->write.status != ESP_GATT_OK) { + ESP_LOGE(TAG, "write descr failed, error status = %x", param->write.status); break; } - if (param->read.handle == this->read_handle_) { - read_sensors_(param->read.value, param->read.value_len); + ESP_LOGV(TAG, "Write descr success, writing 0x%02X at write_handle=%d", this->write_command_, + this->write_handle_); + esp_err_t status = + esp_ble_gattc_write_char(gattc_if, this->parent()->get_conn_id(), this->write_handle_, sizeof(write_command_), + (uint8_t *) &write_command_, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "Error writing 0x%02x command, status=%d", write_command_, status); } break; } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.is_notify) { + ESP_LOGV(TAG, "ESP_GATTC_NOTIFY_EVT, receive notify value, %d bytes", param->notify.value_len); + } else { + ESP_LOGV(TAG, "ESP_GATTC_NOTIFY_EVT, receive indicate value, %d bytes", param->notify.value_len); + } + read_sensors_(param->notify.value, param->notify.value_len); + break; + } + default: break; } } void RadonEyeRD200::read_sensors_(uint8_t *value, uint16_t value_len) { - if (value_len < 20) { - ESP_LOGD(TAG, "Invalid read"); + if (value_len < 1) { + ESP_LOGW(TAG, "Unexpected empty message"); return; } - // Example data - // [13:08:47][D][radon_eye_rd200:107]: result bytes: 5010 85EBB940 00000000 00000000 2200 2500 0000 - ESP_LOGV(TAG, "result bytes: %02X%02X %02X%02X%02X%02X %02X%02X%02X%02X %02X%02X%02X%02X %02X%02X %02X%02X %02X%02X", - value[0], value[1], value[2], value[3], value[4], value[5], value[6], value[7], value[8], value[9], - value[10], value[11], value[12], value[13], value[14], value[15], value[16], value[17], value[18], - value[19]); + uint8_t command = value[0]; - if (value[0] != 0x50) { - // This isn't a sensor reading. + if ((command == WRITE_COMMAND_V1 && value_len < 20) || (command == WRITE_COMMAND_V2 && value_len < 68)) { + ESP_LOGW(TAG, "Unexpected command 0x%02X message length %d", command, value_len); return; } + // Example data V1: + // 501085EBB9400000000000000000220025000000 + // Example data V2: + // 4042323230313033525532303338330652443230304e56322e302e3200014a00060a00080000000300010079300000e01108001c00020000003822005c8f423fa4709d3f + ESP_LOGV(TAG, "radon sensors raw bytes"); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, value, value_len, ESP_LOG_VERBOSE); + // Convert from pCi/L to Bq/m³ constexpr float convert_to_bwpm3 = 37.0; - RadonValue radon_value; - radon_value.chars[0] = value[2]; - radon_value.chars[1] = value[3]; - radon_value.chars[2] = value[4]; - radon_value.chars[3] = value[5]; - float radon_now = radon_value.number * convert_to_bwpm3; - if (is_valid_radon_value_(radon_now)) { - radon_sensor_->publish_state(radon_now); + float radon_now; // in Bq/m³ + float radon_day; // in Bq/m³ + float radon_month; // in Bq/m³ + if (command == WRITE_COMMAND_V1) { + // Use memcpy to avoid unaligned memory access + float temp; + memcpy(&temp, value + 2, sizeof(float)); + radon_now = temp * convert_to_bwpm3; + memcpy(&temp, value + 6, sizeof(float)); + radon_day = temp * convert_to_bwpm3; + memcpy(&temp, value + 10, sizeof(float)); + radon_month = temp * convert_to_bwpm3; + } else if (command == WRITE_COMMAND_V2) { + // Use memcpy to avoid unaligned memory access + uint16_t temp; + memcpy(&temp, value + 33, sizeof(uint16_t)); + radon_now = temp; + memcpy(&temp, value + 35, sizeof(uint16_t)); + radon_day = temp; + memcpy(&temp, value + 37, sizeof(uint16_t)); + radon_month = temp; + } else { + ESP_LOGW(TAG, "Unexpected command value: 0x%02X", command); + return; } - radon_value.chars[0] = value[6]; - radon_value.chars[1] = value[7]; - radon_value.chars[2] = value[8]; - radon_value.chars[3] = value[9]; - float radon_day = radon_value.number * convert_to_bwpm3; - - radon_value.chars[0] = value[10]; - radon_value.chars[1] = value[11]; - radon_value.chars[2] = value[12]; - radon_value.chars[3] = value[13]; - float radon_month = radon_value.number * convert_to_bwpm3; - - if (is_valid_radon_value_(radon_month)) { - ESP_LOGV(TAG, "Radon Long Term based on month"); - radon_long_term_sensor_->publish_state(radon_month); - } else if (is_valid_radon_value_(radon_day)) { - ESP_LOGV(TAG, "Radon Long Term based on day"); - radon_long_term_sensor_->publish_state(radon_day); + if (this->radon_sensor_ != nullptr) { + this->radon_sensor_->publish_state(radon_now); } - ESP_LOGV(TAG, " Measurements (Bq/m³) now: %0.03f, day: %0.03f, month: %0.03f", radon_now, radon_day, radon_month); + if (this->radon_long_term_sensor_ != nullptr) { + if (radon_month > 0) { + ESP_LOGV(TAG, "Radon Long Term based on month"); + this->radon_long_term_sensor_->publish_state(radon_month); + } else { + ESP_LOGV(TAG, "Radon Long Term based on day"); + this->radon_long_term_sensor_->publish_state(radon_day); + } + } - ESP_LOGV(TAG, " Measurements (pCi/L) now: %0.03f, day: %0.03f, month: %0.03f", radon_now / convert_to_bwpm3, - radon_day / convert_to_bwpm3, radon_month / convert_to_bwpm3); + ESP_LOGV(TAG, + " Measurements (Bq/m³) now: %0.03f, day: %0.03f, month: %0.03f\n" + " Measurements (pCi/L) now: %0.03f, day: %0.03f, month: %0.03f", + radon_now, radon_day, radon_month, radon_now / convert_to_bwpm3, radon_day / convert_to_bwpm3, + radon_month / convert_to_bwpm3); // This instance must not stay connected // so other clients can connect to it (e.g. the @@ -129,49 +193,23 @@ void RadonEyeRD200::read_sensors_(uint8_t *value, uint16_t value_len) { parent()->set_enabled(false); } -bool RadonEyeRD200::is_valid_radon_value_(float radon) { return radon > 0.0 and radon < 37000; } - void RadonEyeRD200::update() { if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { if (!parent()->enabled) { ESP_LOGW(TAG, "Reconnecting to device"); parent()->set_enabled(true); - parent()->connect(); } else { ESP_LOGW(TAG, "Connection in progress"); } } } -void RadonEyeRD200::write_query_message_() { - ESP_LOGV(TAG, "writing 0x50 to write service"); - int request = 0x50; - auto status = esp_ble_gattc_write_char_descr(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), - this->write_handle_, sizeof(request), (uint8_t *) &request, - ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); - if (status) { - ESP_LOGW(TAG, "Error sending write request for sensor, status=%d", status); - } -} - -void RadonEyeRD200::request_read_values_() { - auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), - this->read_handle_, ESP_GATT_AUTH_REQ_NONE); - if (status) { - ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); - } -} - void RadonEyeRD200::dump_config() { LOG_SENSOR(" ", "Radon", this->radon_sensor_); LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_); } -RadonEyeRD200::RadonEyeRD200() - : PollingComponent(10000), - service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), - sensors_write_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(WRITE_CHARACTERISTIC_UUID)), - sensors_read_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(READ_CHARACTERISTIC_UUID)) {} +RadonEyeRD200::RadonEyeRD200() : PollingComponent(10000) {} } // namespace radon_eye_rd200 } // namespace esphome diff --git a/esphome/components/radon_eye_rd200/radon_eye_rd200.h b/esphome/components/radon_eye_rd200/radon_eye_rd200.h index 7b29be7bd8..f874c815f8 100644 --- a/esphome/components/radon_eye_rd200/radon_eye_rd200.h +++ b/esphome/components/radon_eye_rd200/radon_eye_rd200.h @@ -14,10 +14,6 @@ namespace esphome { namespace radon_eye_rd200 { -static const char *const SERVICE_UUID = "00001523-1212-efde-1523-785feabcd123"; -static const char *const WRITE_CHARACTERISTIC_UUID = "00001524-1212-efde-1523-785feabcd123"; -static const char *const READ_CHARACTERISTIC_UUID = "00001525-1212-efde-1523-785feabcd123"; - class RadonEyeRD200 : public PollingComponent, public ble_client::BLEClientNode { public: RadonEyeRD200(); @@ -32,25 +28,17 @@ class RadonEyeRD200 : public PollingComponent, public ble_client::BLEClientNode void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } protected: - bool is_valid_radon_value_(float radon); - void read_sensors_(uint8_t *value, uint16_t value_len); - void write_query_message_(); - void request_read_values_(); sensor::Sensor *radon_sensor_{nullptr}; sensor::Sensor *radon_long_term_sensor_{nullptr}; + uint8_t write_command_; uint16_t read_handle_; uint16_t write_handle_; esp32_ble_tracker::ESPBTUUID service_uuid_; esp32_ble_tracker::ESPBTUUID sensors_write_characteristic_uuid_; esp32_ble_tracker::ESPBTUUID sensors_read_characteristic_uuid_; - - union RadonValue { - char chars[4]; - float number; - }; }; } // namespace radon_eye_rd200 diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index fa8564f614..8f8740c925 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -12,6 +12,9 @@ static const uint8_t WAIT_I_RQ = 0x30; // RxIRq and IdleIRq static const char *const TAG = "rc522"; +// Max UID size for RFID tags (4, 7, or 10 bytes) +static constexpr size_t RC522_MAX_UID_SIZE = 10; + static const uint8_t RESET_COUNT = 5; void RC522::setup() { @@ -191,8 +194,9 @@ void RC522::loop() { if (status == STATUS_TIMEOUT) { ESP_LOGV(TAG, "STATE_READ_SERIAL_DONE -> TIMEOUT (no tag present) %d", status); } else { + char hex_buf[format_hex_pretty_size(RC522_MAX_UID_SIZE)]; ESP_LOGW(TAG, "Unexpected response. Read status is %d. Read bytes: %d (%s)", status, back_length_, - format_hex_pretty(buffer_, back_length_, '-', false).c_str()); + format_hex_pretty_to(hex_buf, buffer_, back_length_, '-')); } state_ = STATE_DONE; @@ -237,13 +241,18 @@ void RC522::loop() { trigger->process(rfid_uid); if (report) { - ESP_LOGD(TAG, "Found new tag '%s'", format_hex_pretty(rfid_uid, '-', false).c_str()); + char uid_buf[format_hex_pretty_size(RC522_MAX_UID_SIZE)]; + ESP_LOGD(TAG, "Found new tag '%s'", format_hex_pretty_to(uid_buf, rfid_uid.data(), rfid_uid.size(), '-')); } break; } case STATE_DONE: { if (!this->current_uid_.empty()) { - ESP_LOGV(TAG, "Tag '%s' removed", format_hex_pretty(this->current_uid_, '-', false).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char uid_buf[format_hex_pretty_size(RC522_MAX_UID_SIZE)]; + ESP_LOGV(TAG, "Tag '%s' removed", + format_hex_pretty_to(uid_buf, this->current_uid_.data(), this->current_uid_.size(), '-')); +#endif for (auto *trigger : this->triggers_ontagremoved_) trigger->process(this->current_uid_); } @@ -338,7 +347,10 @@ void RC522::pcd_clear_register_bit_mask_(PcdRegister reg, ///< The register to * @return STATUS_OK on success, STATUS_??? otherwise. */ void RC522::pcd_transceive_data_(uint8_t send_len) { - ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_hex_pretty(buffer_, send_len, '-', false).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(RC522_MAX_UID_SIZE)]; + ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_hex_pretty_to(hex_buf, buffer_, send_len, '-')); +#endif delayMicroseconds(1000); // we need 1 ms delay between antenna on and those communication commands send_len_ = send_len; // Prepare values for BitFramingReg @@ -412,8 +424,11 @@ RC522::StatusCode RC522::await_transceive_() { error_reg_value); // TODO: is this always due to collissions? return STATUS_ERROR; } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(RC522_MAX_UID_SIZE)]; ESP_LOGV(TAG, "received %d bytes: %s", back_length_, - format_hex_pretty(buffer_ + send_len_, back_length_, '-', false).c_str()); + format_hex_pretty_to(hex_buf, buffer_ + send_len_, back_length_, '-')); +#endif return STATUS_OK; } diff --git a/esphome/components/rd03d/__init__.py b/esphome/components/rd03d/__init__.py new file mode 100644 index 0000000000..52e9a2c09a --- /dev/null +++ b/esphome/components/rd03d/__init__.py @@ -0,0 +1,50 @@ +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_THROTTLE + +CODEOWNERS = ["@jasstrong"] +DEPENDENCIES = ["uart"] +MULTI_CONF = True + +CONF_RD03D_ID = "rd03d_id" +CONF_TRACKING_MODE = "tracking_mode" + +rd03d_ns = cg.esphome_ns.namespace("rd03d") +RD03DComponent = rd03d_ns.class_("RD03DComponent", cg.Component, uart.UARTDevice) +TrackingMode = rd03d_ns.enum("TrackingMode", is_class=True) + +TRACKING_MODES = { + "single": TrackingMode.SINGLE_TARGET, + "multi": TrackingMode.MULTI_TARGET, +} + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RD03DComponent), + cv.Optional(CONF_TRACKING_MODE): cv.enum(TRACKING_MODES, lower=True), + cv.Optional(CONF_THROTTLE): cv.positive_time_period_milliseconds, + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "rd03d", + require_tx=False, + require_rx=True, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if CONF_TRACKING_MODE in config: + cg.add(var.set_tracking_mode(config[CONF_TRACKING_MODE])) + + if CONF_THROTTLE in config: + cg.add(var.set_throttle(config[CONF_THROTTLE])) diff --git a/esphome/components/rd03d/binary_sensor.py b/esphome/components/rd03d/binary_sensor.py new file mode 100644 index 0000000000..afb7527aa1 --- /dev/null +++ b/esphome/components/rd03d/binary_sensor.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import CONF_TARGET, DEVICE_CLASS_OCCUPANCY + +from . import CONF_RD03D_ID, RD03DComponent + +DEPENDENCIES = ["rd03d"] + +MAX_TARGETS = 3 + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_RD03D_ID): cv.use_id(RD03DComponent), + cv.Optional(CONF_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, + ), + } +).extend( + { + cv.Optional(f"target_{i + 1}"): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, + ) + for i in range(MAX_TARGETS) + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_RD03D_ID]) + + if target_config := config.get(CONF_TARGET): + sens = await binary_sensor.new_binary_sensor(target_config) + cg.add(hub.set_target_binary_sensor(sens)) + + for i in range(MAX_TARGETS): + if target_config := config.get(f"target_{i + 1}"): + sens = await binary_sensor.new_binary_sensor(target_config) + cg.add(hub.set_target_binary_sensor(i, sens)) diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp new file mode 100644 index 0000000000..44e479c153 --- /dev/null +++ b/esphome/components/rd03d/rd03d.cpp @@ -0,0 +1,243 @@ +#include "rd03d.h" +#include "esphome/core/log.h" +#include + +namespace esphome::rd03d { + +static const char *const TAG = "rd03d"; + +// Delay before sending configuration commands to allow radar to initialize +static constexpr uint32_t SETUP_TIMEOUT_MS = 100; + +// Data frame format (radar -> host) +static constexpr uint8_t FRAME_HEADER[] = {0xAA, 0xFF, 0x03, 0x00}; +static constexpr uint8_t FRAME_FOOTER[] = {0x55, 0xCC}; + +// Command frame format (host -> radar) +static constexpr uint8_t CMD_FRAME_HEADER[] = {0xFD, 0xFC, 0xFB, 0xFA}; +static constexpr uint8_t CMD_FRAME_FOOTER[] = {0x04, 0x03, 0x02, 0x01}; + +// RD-03D tracking mode commands +static constexpr uint16_t CMD_SINGLE_TARGET = 0x0080; +static constexpr uint16_t CMD_MULTI_TARGET = 0x0090; + +// Decode coordinate/speed value from RD-03D format +// Per datasheet: MSB=1 means positive, MSB=0 means negative +static constexpr int16_t decode_value(uint8_t low_byte, uint8_t high_byte) { + int16_t value = ((high_byte & 0x7F) << 8) | low_byte; + if ((high_byte & 0x80) == 0) { + value = -value; + } + return value; +} + +void RD03DComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up RD-03D..."); + this->set_timeout(SETUP_TIMEOUT_MS, [this]() { this->apply_config_(); }); +} + +void RD03DComponent::dump_config() { + ESP_LOGCONFIG(TAG, "RD-03D:"); + if (this->tracking_mode_.has_value()) { + ESP_LOGCONFIG(TAG, " Tracking Mode: %s", + *this->tracking_mode_ == TrackingMode::SINGLE_TARGET ? "single" : "multi"); + } + if (this->throttle_ > 0) { + ESP_LOGCONFIG(TAG, " Throttle: %ums", this->throttle_); + } +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Target Count", this->target_count_sensor_); +#endif +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Target", this->target_binary_sensor_); +#endif + for (uint8_t i = 0; i < MAX_TARGETS; i++) { + ESP_LOGCONFIG(TAG, " Target %d:", i + 1); +#ifdef USE_SENSOR + LOG_SENSOR(" ", "X", this->targets_[i].x); + LOG_SENSOR(" ", "Y", this->targets_[i].y); + LOG_SENSOR(" ", "Speed", this->targets_[i].speed); + LOG_SENSOR(" ", "Distance", this->targets_[i].distance); + LOG_SENSOR(" ", "Resolution", this->targets_[i].resolution); + LOG_SENSOR(" ", "Angle", this->targets_[i].angle); +#endif +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Presence", this->target_presence_[i]); +#endif + } +} + +void RD03DComponent::loop() { + while (this->available()) { + uint8_t byte = this->read(); + ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_); + + // Check if we're looking for frame header + if (this->buffer_pos_ < FRAME_HEADER_SIZE) { + if (byte == FRAME_HEADER[this->buffer_pos_]) { + this->buffer_[this->buffer_pos_++] = byte; + } else if (byte == FRAME_HEADER[0]) { + // Start over if we see a potential new header + this->buffer_[0] = byte; + this->buffer_pos_ = 1; + } else { + this->buffer_pos_ = 0; + } + continue; + } + + // Accumulate data bytes + this->buffer_[this->buffer_pos_++] = byte; + + // Check if we have a complete frame + if (this->buffer_pos_ == FRAME_SIZE) { + // Validate footer + if (this->buffer_[FRAME_SIZE - 2] == FRAME_FOOTER[0] && this->buffer_[FRAME_SIZE - 1] == FRAME_FOOTER[1]) { + this->process_frame_(); + } else { + ESP_LOGW(TAG, "Invalid frame footer: 0x%02X 0x%02X (expected 0x55 0xCC)", this->buffer_[FRAME_SIZE - 2], + this->buffer_[FRAME_SIZE - 1]); + } + this->buffer_pos_ = 0; + } + } +} + +void RD03DComponent::process_frame_() { + // Apply throttle if configured + if (this->throttle_ > 0) { + uint32_t now = millis(); + if (now - this->last_publish_time_ < this->throttle_) { + return; + } + this->last_publish_time_ = now; + } + + uint8_t target_count = 0; + + for (uint8_t i = 0; i < MAX_TARGETS; i++) { + // Calculate offset for this target's data + // Header is 4 bytes, each target is 8 bytes + uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE); + + // Extract raw bytes for this target + uint8_t x_low = this->buffer_[offset + 0]; + uint8_t x_high = this->buffer_[offset + 1]; + uint8_t y_low = this->buffer_[offset + 2]; + uint8_t y_high = this->buffer_[offset + 3]; + uint8_t speed_low = this->buffer_[offset + 4]; + uint8_t speed_high = this->buffer_[offset + 5]; + uint8_t res_low = this->buffer_[offset + 6]; + uint8_t res_high = this->buffer_[offset + 7]; + + // Decode values per RD-03D format + int16_t x = decode_value(x_low, x_high); + int16_t y = decode_value(y_low, y_high); + int16_t speed = decode_value(speed_low, speed_high); + uint16_t resolution = (res_high << 8) | res_low; + + // Check if target is present (non-zero coordinates) + bool target_present = (x != 0 || y != 0); + if (target_present) { + target_count++; + } + +#ifdef USE_SENSOR + this->publish_target_(i, x, y, speed, resolution); +#endif + +#ifdef USE_BINARY_SENSOR + if (this->target_presence_[i] != nullptr) { + this->target_presence_[i]->publish_state(target_present); + } +#endif + } + +#ifdef USE_SENSOR + if (this->target_count_sensor_ != nullptr) { + this->target_count_sensor_->publish_state(target_count); + } +#endif + +#ifdef USE_BINARY_SENSOR + if (this->target_binary_sensor_ != nullptr) { + this->target_binary_sensor_->publish_state(target_count > 0); + } +#endif +} + +#ifdef USE_SENSOR +void RD03DComponent::publish_target_(uint8_t target_num, int16_t x, int16_t y, int16_t speed, uint16_t resolution) { + TargetSensor &target = this->targets_[target_num]; + + // Publish X coordinate (mm) + if (target.x != nullptr) { + target.x->publish_state(x); + } + + // Publish Y coordinate (mm) + if (target.y != nullptr) { + target.y->publish_state(y); + } + + // Publish speed (convert from cm/s to mm/s) + if (target.speed != nullptr) { + target.speed->publish_state(static_cast(speed) * 10.0f); + } + + // Publish resolution (mm) + if (target.resolution != nullptr) { + target.resolution->publish_state(resolution); + } + + // Calculate and publish distance (mm) + if (target.distance != nullptr) { + float distance = std::hypot(static_cast(x), static_cast(y)); + target.distance->publish_state(distance); + } + + // Calculate and publish angle (degrees) + // Angle is measured from the Y axis (radar forward direction) + if (target.angle != nullptr) { + if (x == 0 && y == 0) { + target.angle->publish_state(0); + } else { + float angle = std::atan2(static_cast(x), static_cast(y)) * 180.0f / M_PI; + target.angle->publish_state(angle); + } + } +} +#endif + +void RD03DComponent::send_command_(uint16_t command, const uint8_t *data, uint8_t data_len) { + // Send header + this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER)); + + // Send length (command word + data) + uint16_t len = 2 + data_len; + this->write_byte(len & 0xFF); + this->write_byte((len >> 8) & 0xFF); + + // Send command word (little-endian) + this->write_byte(command & 0xFF); + this->write_byte((command >> 8) & 0xFF); + + // Send data if any + if (data != nullptr && data_len > 0) { + this->write_array(data, data_len); + } + + // Send footer + this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)); + + ESP_LOGD(TAG, "Sent command 0x%04X with %d bytes of data", command, data_len); +} + +void RD03DComponent::apply_config_() { + if (this->tracking_mode_.has_value()) { + uint16_t mode_cmd = (*this->tracking_mode_ == TrackingMode::SINGLE_TARGET) ? CMD_SINGLE_TARGET : CMD_MULTI_TARGET; + this->send_command_(mode_cmd); + } +} + +} // namespace esphome::rd03d diff --git a/esphome/components/rd03d/rd03d.h b/esphome/components/rd03d/rd03d.h new file mode 100644 index 0000000000..7413fe38f2 --- /dev/null +++ b/esphome/components/rd03d/rd03d.h @@ -0,0 +1,93 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +#include + +namespace esphome::rd03d { + +static constexpr uint8_t MAX_TARGETS = 3; +static constexpr uint8_t FRAME_HEADER_SIZE = 4; +static constexpr uint8_t FRAME_FOOTER_SIZE = 2; +static constexpr uint8_t TARGET_DATA_SIZE = 8; +static constexpr uint8_t FRAME_SIZE = + FRAME_HEADER_SIZE + (MAX_TARGETS * TARGET_DATA_SIZE) + FRAME_FOOTER_SIZE; // 30 bytes + +enum class TrackingMode : uint8_t { + SINGLE_TARGET = 0, + MULTI_TARGET = 1, +}; + +#ifdef USE_SENSOR +struct TargetSensor { + sensor::Sensor *x{nullptr}; + sensor::Sensor *y{nullptr}; + sensor::Sensor *speed{nullptr}; + sensor::Sensor *distance{nullptr}; + sensor::Sensor *resolution{nullptr}; + sensor::Sensor *angle{nullptr}; +}; +#endif + +class RD03DComponent : public Component, public uart::UARTDevice { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + +#ifdef USE_SENSOR + void set_target_count_sensor(sensor::Sensor *sensor) { this->target_count_sensor_ = sensor; } + void set_x_sensor(uint8_t target, sensor::Sensor *sensor) { this->targets_[target].x = sensor; } + void set_y_sensor(uint8_t target, sensor::Sensor *sensor) { this->targets_[target].y = sensor; } + void set_speed_sensor(uint8_t target, sensor::Sensor *sensor) { this->targets_[target].speed = sensor; } + void set_distance_sensor(uint8_t target, sensor::Sensor *sensor) { this->targets_[target].distance = sensor; } + void set_resolution_sensor(uint8_t target, sensor::Sensor *sensor) { this->targets_[target].resolution = sensor; } + void set_angle_sensor(uint8_t target, sensor::Sensor *sensor) { this->targets_[target].angle = sensor; } +#endif +#ifdef USE_BINARY_SENSOR + void set_target_binary_sensor(binary_sensor::BinarySensor *sensor) { this->target_binary_sensor_ = sensor; } + void set_target_binary_sensor(uint8_t target, binary_sensor::BinarySensor *sensor) { + this->target_presence_[target] = sensor; + } +#endif + + // Configuration setters (called from code generation) + void set_tracking_mode(TrackingMode mode) { this->tracking_mode_ = mode; } + void set_throttle(uint32_t throttle) { this->throttle_ = throttle; } + + protected: + void apply_config_(); + void send_command_(uint16_t command, const uint8_t *data = nullptr, uint8_t data_len = 0); + void process_frame_(); +#ifdef USE_SENSOR + void publish_target_(uint8_t target_num, int16_t x, int16_t y, int16_t speed, uint16_t resolution); +#endif + +#ifdef USE_SENSOR + std::array targets_{}; + sensor::Sensor *target_count_sensor_{nullptr}; +#endif +#ifdef USE_BINARY_SENSOR + std::array target_presence_{}; + binary_sensor::BinarySensor *target_binary_sensor_{nullptr}; +#endif + + // Configuration (only sent if explicitly set) + optional tracking_mode_{}; + uint32_t throttle_{0}; + uint32_t last_publish_time_{0}; + + std::array buffer_{}; + uint8_t buffer_pos_{0}; +}; + +} // namespace esphome::rd03d diff --git a/esphome/components/rd03d/sensor.py b/esphome/components/rd03d/sensor.py new file mode 100644 index 0000000000..4b4fcfd4e4 --- /dev/null +++ b/esphome/components/rd03d/sensor.py @@ -0,0 +1,105 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ANGLE, + CONF_DISTANCE, + CONF_RESOLUTION, + CONF_SPEED, + CONF_X, + CONF_Y, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_SPEED, + STATE_CLASS_MEASUREMENT, + UNIT_DEGREES, + UNIT_MILLIMETER, +) + +from . import CONF_RD03D_ID, RD03DComponent + +DEPENDENCIES = ["rd03d"] + +CONF_TARGET_COUNT = "target_count" + +MAX_TARGETS = 3 + +UNIT_MILLIMETER_PER_SECOND = "mm/s" + +TARGET_SCHEMA = cv.Schema( + { + cv.Optional(CONF_X): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_Y): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_SPEED): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETER_PER_SECOND, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SPEED, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DISTANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_RESOLUTION): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ANGLE): sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_RD03D_ID): cv.use_id(RD03DComponent), + cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema( + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +).extend({cv.Optional(f"target_{i + 1}"): TARGET_SCHEMA for i in range(MAX_TARGETS)}) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_RD03D_ID]) + + if target_count_config := config.get(CONF_TARGET_COUNT): + sens = await sensor.new_sensor(target_count_config) + cg.add(hub.set_target_count_sensor(sens)) + + for i in range(MAX_TARGETS): + if target_config := config.get(f"target_{i + 1}"): + if x_config := target_config.get(CONF_X): + sens = await sensor.new_sensor(x_config) + cg.add(hub.set_x_sensor(i, sens)) + if y_config := target_config.get(CONF_Y): + sens = await sensor.new_sensor(y_config) + cg.add(hub.set_y_sensor(i, sens)) + if speed_config := target_config.get(CONF_SPEED): + sens = await sensor.new_sensor(speed_config) + cg.add(hub.set_speed_sensor(i, sens)) + if distance_config := target_config.get(CONF_DISTANCE): + sens = await sensor.new_sensor(distance_config) + cg.add(hub.set_distance_sensor(i, sens)) + if resolution_config := target_config.get(CONF_RESOLUTION): + sens = await sensor.new_sensor(resolution_config) + cg.add(hub.set_resolution_sensor(i, sens)) + if angle_config := target_config.get(CONF_ANGLE): + sens = await sensor.new_sensor(angle_config) + cg.add(hub.set_angle_sensor(i, sens)) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index d24d24b000..9d3e655c57 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -108,9 +108,6 @@ def register_trigger(name, type, data_type): validator = automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(type), - cv.Optional(CONF_RECEIVER_ID): cv.invalid( - "This has been removed in ESPHome 2022.3.0 and the trigger attaches directly to the parent receiver." - ), } ) registerer = TRIGGER_REGISTRY.register(f"on_{name}", validator) @@ -207,13 +204,7 @@ validate_binary_sensor = cv.validate_registry_entry( "remote receiver", BINARY_SENSOR_REGISTRY ) TRIGGER_REGISTRY = SimpleRegistry() -DUMPER_REGISTRY = Registry( - { - cv.Optional(CONF_RECEIVER_ID): cv.invalid( - "This has been removed in ESPHome 1.20.0 and the dumper attaches directly to the parent receiver." - ), - } -) +DUMPER_REGISTRY = Registry() def validate_dumpers(value): @@ -480,10 +471,6 @@ COOLIX_BASE_SCHEMA = cv.Schema( { cv.Required(CONF_FIRST): cv.hex_int_range(0, 16777215), cv.Optional(CONF_SECOND, default=0): cv.hex_int_range(0, 16777215), - cv.Optional(CONF_DATA): cv.invalid( - "'data' option has been removed in ESPHome 2023.8. " - "Use the 'first' and 'second' options instead." - ), } ) diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index 4b922eb2f1..b8d9293c11 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -232,10 +232,10 @@ template class ABBWelcomeAction : public RemoteTransmitterAction data.set_message_id(this->message_id_.value(x...)); data.auto_message_id = this->auto_message_id_.value(x...); std::vector data_vec; - if (this->len_ >= 0) { + if (this->len_ > 0) { // Static mode: copy from flash to vector data_vec.assign(this->data_.data, this->data_.data + this->len_); - } else { + } else if (this->len_ < 0) { // Template mode: call function data_vec = this->data_.func(x...); } @@ -245,7 +245,7 @@ template class ABBWelcomeAction : public RemoteTransmitterAction } protected: - ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + ssize_t len_{0}; // <0 = template mode, >=0 = static mode with length union Data { std::vector (*func)(Ts...); // Function pointer (stateless lambdas) const uint8_t *data; // Pointer to static data in flash diff --git a/esphome/components/remote_base/haier_protocol.cpp b/esphome/components/remote_base/haier_protocol.cpp index ec5cb5775c..734f3c7789 100644 --- a/esphome/components/remote_base/haier_protocol.cpp +++ b/esphome/components/remote_base/haier_protocol.cpp @@ -1,4 +1,5 @@ #include "haier_protocol.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -12,6 +13,8 @@ constexpr uint32_t BIT_MARK_US = 540; constexpr uint32_t BIT_ONE_SPACE_US = 1650; constexpr uint32_t BIT_ZERO_SPACE_US = 580; constexpr unsigned int HAIER_IR_PACKET_BIT_SIZE = 112; +// Max data bytes in packet (excluding checksum) +constexpr size_t HAIER_MAX_DATA_BYTES = (HAIER_IR_PACKET_BIT_SIZE / 8); void HaierProtocol::encode_byte_(RemoteTransmitData *dst, uint8_t item) { for (uint8_t mask = 1 << 7; mask != 0; mask >>= 1) { @@ -77,7 +80,8 @@ optional HaierProtocol::decode(RemoteReceiveData src) { } void HaierProtocol::dump(const HaierData &data) { - ESP_LOGI(TAG, "Received Haier: %s", format_hex_pretty(data.data).c_str()); + char hex_buf[format_hex_pretty_size(HAIER_MAX_DATA_BYTES)]; + ESP_LOGI(TAG, "Received Haier: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); } } // namespace remote_base diff --git a/esphome/components/remote_base/midea_protocol.cpp b/esphome/components/remote_base/midea_protocol.cpp index 8006fe4048..4fa717cf08 100644 --- a/esphome/components/remote_base/midea_protocol.cpp +++ b/esphome/components/remote_base/midea_protocol.cpp @@ -70,7 +70,10 @@ optional MideaProtocol::decode(RemoteReceiveData src) { return {}; } -void MideaProtocol::dump(const MideaData &data) { ESP_LOGI(TAG, "Received Midea: %s", data.to_string().c_str()); } +void MideaProtocol::dump(const MideaData &data) { + char buf[MideaData::TO_STR_BUFFER_SIZE]; + ESP_LOGI(TAG, "Received Midea: %s", data.to_str(buf)); +} } // namespace remote_base } // namespace esphome diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index 94fb6f3d94..0a5de8e9df 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -30,6 +30,13 @@ class MideaData { void finalize() { this->data_[OFFSET_CS] = this->calc_cs_(); } bool is_compliment(const MideaData &rhs) const; std::string to_string() const { return format_hex_pretty(this->data_.data(), this->data_.size()); } + /// Buffer size for to_str(): 6 bytes = "AA.BB.CC.DD.EE.FF\0" + static constexpr size_t TO_STR_BUFFER_SIZE = format_hex_pretty_size(6); + /// Format to buffer, returns pointer to buffer + const char *to_str(char *buffer) const { + format_hex_pretty_to(buffer, TO_STR_BUFFER_SIZE, this->data_.data(), this->data_.size(), '.'); + return buffer; + } // compare only 40-bits bool operator==(const MideaData &rhs) const { return std::equal(this->data_.begin(), this->data_.begin() + OFFSET_CS, rhs.data_.begin()); diff --git a/esphome/components/remote_base/mirage_protocol.cpp b/esphome/components/remote_base/mirage_protocol.cpp index 10d644a1cd..2ae877f193 100644 --- a/esphome/components/remote_base/mirage_protocol.cpp +++ b/esphome/components/remote_base/mirage_protocol.cpp @@ -1,4 +1,5 @@ #include "mirage_protocol.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -13,9 +14,12 @@ constexpr uint32_t BIT_ONE_SPACE_US = 1592; constexpr uint32_t BIT_ZERO_SPACE_US = 545; constexpr unsigned int MIRAGE_IR_PACKET_BIT_SIZE = 120; +// Max data bytes in packet (excluding checksum) +constexpr size_t MIRAGE_MAX_DATA_BYTES = (MIRAGE_IR_PACKET_BIT_SIZE / 8); void MirageProtocol::encode(RemoteTransmitData *dst, const MirageData &data) { - ESP_LOGI(TAG, "Transive Mirage: %s", format_hex_pretty(data.data).c_str()); + char hex_buf[format_hex_pretty_size(MIRAGE_MAX_DATA_BYTES)]; + ESP_LOGI(TAG, "Transmit Mirage: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); dst->set_carrier_frequency(38000); dst->reserve(5 + ((data.data.size() + 1) * 2)); dst->mark(HEADER_MARK_US); @@ -77,7 +81,8 @@ optional MirageProtocol::decode(RemoteReceiveData src) { } void MirageProtocol::dump(const MirageData &data) { - ESP_LOGI(TAG, "Received Mirage: %s", format_hex_pretty(data.data).c_str()); + char hex_buf[format_hex_pretty_size(MIRAGE_MAX_DATA_BYTES)]; + ESP_LOGI(TAG, "Received Mirage: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size())); } } // namespace remote_base diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 9fbc9e85ba..401a0976b2 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -104,8 +104,10 @@ void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::vectorbuffer_write; + arg->buffer[buffer_write++] = (int32_t) delta * multiplier; + if (buffer_write >= arg->buffer_size) { + buffer_write = 0; + } + + // detect overflow and reset the write pointer + if (buffer_write == arg->buffer_read) { + buffer_write = arg->buffer_start; + arg->overflow = true; + } + + // detect idle and start a new sequence unless there is only idle in + // which case reset the write pointer instead + if (delta >= arg->idle_us) { + if (arg->buffer_write == arg->buffer_start) { + buffer_write = arg->buffer_start; + } else { + arg->buffer_start = buffer_write; + } + } + arg->buffer_write = buffer_write; +} + +static void IRAM_ATTR HOT commit_value(RemoteReceiverComponentStore *arg, uint32_t micros, bool level) { + // commit value if the level is different from the last commit level + if (level != arg->commit_level) { + write_value(arg, micros - arg->commit_micros, level); + arg->commit_micros = micros; + arg->commit_level = level; + } +} + void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) { - const uint32_t now = micros(); - // If the lhs is 1 (rising edge) we should write to an uneven index and vice versa - const uint32_t next = (arg->buffer_write_at + 1) % arg->buffer_size; - const bool level = arg->pin.digital_read(); - if (level != next % 2) - return; + // invert the level so it matches the level of the signal before the edge + const bool curr_level = !arg->pin.digital_read(); + const uint32_t curr_micros = micros(); + const bool prev_level = arg->prev_level; + const uint32_t prev_micros = arg->prev_micros; - // If next is buffer_read, we have hit an overflow - if (next == arg->buffer_read_at) - return; - - const uint32_t last_change = arg->buffer[arg->buffer_write_at]; - const uint32_t time_since_change = now - last_change; - if (time_since_change <= arg->filter_us) - return; - - arg->buffer[arg->buffer_write_at = next] = now; // NOLINT(clang-diagnostic-deprecated-volatile) + // commit the previous value if the pulse is not filtered and the level is different + if (curr_micros - prev_micros >= arg->filter_us && prev_level != curr_level) { + commit_value(arg, prev_micros, prev_level); + } + arg->prev_micros = curr_micros; + arg->prev_level = curr_level; } void RemoteReceiverComponent::setup() { this->pin_->setup(); - auto &s = this->store_; - s.filter_us = this->filter_us_; - s.pin = this->pin_->to_isr(); - s.buffer_size = this->buffer_size_; - - this->high_freq_.start(); - if (s.buffer_size % 2 != 0) { - // Make sure divisible by two. This way, we know that every 0bxxx0 index is a space and every 0bxxx1 index is a mark - s.buffer_size++; - } - - s.buffer = new uint32_t[s.buffer_size]; - void *buf = (void *) s.buffer; - memset(buf, 0, s.buffer_size * sizeof(uint32_t)); - - // First index is a space. - if (this->pin_->digital_read()) { - s.buffer_write_at = s.buffer_read_at = 1; - } else { - s.buffer_write_at = s.buffer_read_at = 0; - } + this->store_.idle_us = this->idle_us_; + this->store_.filter_us = this->filter_us_; + this->store_.pin = this->pin_->to_isr(); + this->store_.buffer = new int32_t[this->buffer_size_]; + this->store_.buffer_size = this->buffer_size_; + this->store_.prev_micros = micros(); + this->store_.commit_micros = this->store_.prev_micros; + this->store_.prev_level = this->pin_->digital_read(); + this->store_.commit_level = this->store_.prev_level; this->pin_->attach_interrupt(RemoteReceiverComponentStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); + this->high_freq_.start(); } + void RemoteReceiverComponent::dump_config() { - ESP_LOGCONFIG(TAG, "Remote Receiver:"); - LOG_PIN(" Pin: ", this->pin_); - if (this->pin_->digital_read()) { - ESP_LOGW(TAG, "Remote Receiver Signal starts with a HIGH value. Usually this means you have to " - "invert the signal using 'inverted: True' in the pin schema!"); - } ESP_LOGCONFIG(TAG, + "Remote Receiver:\n" " Buffer Size: %u\n" " Tolerance: %u%s\n" " Filter out pulses shorter than: %u us\n" @@ -70,56 +85,58 @@ void RemoteReceiverComponent::dump_config() { this->buffer_size_, this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_, this->idle_us_); + LOG_PIN(" Pin: ", this->pin_); } void RemoteReceiverComponent::loop() { + // check for overflow auto &s = this->store_; - - // copy write at to local variables, as it's volatile - const uint32_t write_at = s.buffer_write_at; - const uint32_t dist = (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size; - // signals must at least one rising and one leading edge - if (dist <= 1) - return; - const uint32_t now = micros(); - if (now - s.buffer[write_at] < this->idle_us_) { - // The last change was fewer than the configured idle time ago. - return; + if (s.overflow) { + ESP_LOGW(TAG, "Buffer overflow"); + s.overflow = false; } - ESP_LOGVV(TAG, "read_at=%u write_at=%u dist=%u now=%u end=%u", s.buffer_read_at, write_at, dist, now, - s.buffer[write_at]); - - // Skip first value, it's from the previous idle level - s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; - uint32_t prev = s.buffer_read_at; - s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; - const uint32_t reserve_size = 1 + (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size; - this->temp_.clear(); - this->temp_.reserve(reserve_size); - int32_t multiplier = s.buffer_read_at % 2 == 0 ? 1 : -1; - - for (uint32_t i = 0; prev != write_at; i++) { - int32_t delta = s.buffer[s.buffer_read_at] - s.buffer[prev]; - if (uint32_t(delta) >= this->idle_us_) { - // already found a space longer than idle. There must have been two pulses - break; + // if no data is available check for uncommitted data stuck in the buffer and commit + // the previous value if needed + uint32_t last_index = s.buffer_start; + if (last_index == s.buffer_read) { + InterruptLock lock; + if (s.buffer_read == s.buffer_start && s.buffer_write != s.buffer_start && + micros() - s.prev_micros >= this->idle_us_) { + commit_value(&s, s.prev_micros, s.prev_level); + write_value(&s, s.idle_us, !s.commit_level); + last_index = s.buffer_start; } - - ESP_LOGVV(TAG, " i=%u buffer[%u]=%u - buffer[%u]=%u -> %d", i, s.buffer_read_at, s.buffer[s.buffer_read_at], prev, - s.buffer[prev], multiplier * delta); - this->temp_.push_back(multiplier * delta); - prev = s.buffer_read_at; - s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size; - multiplier *= -1; } - s.buffer_read_at = (s.buffer_size + s.buffer_read_at - 1) % s.buffer_size; - this->temp_.push_back(this->idle_us_ * multiplier); + if (last_index == s.buffer_read) { + return; + } + // find the size of the packet and reserve the memory + uint32_t temp_read = s.buffer_read; + uint32_t reserve_size = 0; + while (temp_read != last_index && (uint32_t) std::abs(s.buffer[temp_read]) < this->idle_us_) { + reserve_size++; + temp_read++; + if (temp_read >= s.buffer_size) { + temp_read = 0; + } + } + this->temp_.clear(); + this->temp_.reserve(reserve_size + 1); + + // read the buffer + for (uint32_t i = 0; i < reserve_size + 1; i++) { + this->temp_.push_back((int32_t) s.buffer[s.buffer_read++]); + if (s.buffer_read >= s.buffer_size) { + s.buffer_read = 0; + } + } + + // call the listeners and dumpers this->call_listeners_dumpers_(); } -} // namespace remote_receiver -} // namespace esphome +} // namespace esphome::remote_receiver #endif diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 3ddcf353c7..3d9199a904 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -9,25 +9,31 @@ #include #endif -namespace esphome { -namespace remote_receiver { +namespace esphome::remote_receiver { -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) struct RemoteReceiverComponentStore { static void gpio_intr(RemoteReceiverComponentStore *arg); - /// Stores the time (in micros) that the leading/falling edge happened at - /// * An even index means a falling edge appeared at the time stored at the index - /// * An uneven index means a rising edge appeared at the time stored at the index - volatile uint32_t *buffer{nullptr}; + /// Stores pulse durations in microseconds as signed integers + /// * Positive values indicate high pulses (marks) + /// * Negative values indicate low pulses (spaces) + volatile int32_t *buffer{nullptr}; /// The position last written to - volatile uint32_t buffer_write_at; + volatile uint32_t buffer_write{0}; + /// The start position of the last sequence + volatile uint32_t buffer_start{0}; /// The position last read from - uint32_t buffer_read_at{0}; - bool overflow{false}; + uint32_t buffer_read{0}; + volatile uint32_t commit_micros{0}; + volatile uint32_t prev_micros{0}; uint32_t buffer_size{1000}; uint32_t filter_us{10}; + uint32_t idle_us{10000}; ISRInternalGPIOPin pin; + volatile bool commit_level{false}; + volatile bool prev_level{false}; + volatile bool overflow{false}; }; #elif defined(USE_ESP32) struct RemoteReceiverComponentStore { @@ -84,8 +90,11 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, std::string error_string_{""}; #endif -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_ESP32) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) || defined(USE_ESP32) RemoteReceiverComponentStore store_; +#endif + +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) HighFrequencyLoopRequester high_freq_; #endif @@ -94,5 +103,4 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, uint32_t idle_us_{10000}; }; -} // namespace remote_receiver -} // namespace esphome +} // namespace esphome::remote_receiver diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index 49358eef3f..eda8365169 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -4,8 +4,7 @@ #ifdef USE_ESP32 #include -namespace esphome { -namespace remote_receiver { +namespace esphome::remote_receiver { static const char *const TAG = "remote_receiver.esp32"; #ifdef USE_ESP32_VARIANT_ESP32H2 @@ -118,9 +117,8 @@ void RemoteReceiverComponent::setup() { } void RemoteReceiverComponent::dump_config() { - ESP_LOGCONFIG(TAG, "Remote Receiver:"); - LOG_PIN(" Pin: ", this->pin_); ESP_LOGCONFIG(TAG, + "Remote Receiver:\n" " Clock resolution: %" PRIu32 " hz\n" " RMT symbols: %" PRIu32 "\n" " Filter symbols: %" PRIu32 "\n" @@ -133,6 +131,7 @@ void RemoteReceiverComponent::dump_config() { this->clock_resolution_, this->rmt_symbols_, this->filter_symbols_, this->receive_symbols_, this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_); + LOG_PIN(" Pin: ", this->pin_); if (this->is_failed()) { ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_), this->error_string_.c_str()); @@ -248,7 +247,6 @@ void RemoteReceiverComponent::decode_rmt_(rmt_symbol_word_t *item, size_t item_c } } -} // namespace remote_receiver -} // namespace esphome +} // namespace esphome::remote_receiver #endif diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index faa6c827f7..f182a1ec0d 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -55,20 +55,20 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_EOT_LEVEL): cv.All(cv.only_on_esp32, cv.boolean), cv.Optional(CONF_USE_DMA): cv.All( esp32.only_on_variant( - supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4] + supported=[esp32.VARIANT_ESP32P4, esp32.VARIANT_ESP32S3] ), cv.boolean, ), cv.SplitDefault( CONF_RMT_SYMBOLS, esp32=64, - esp32_s2=64, - esp32_s3=48, - esp32_p4=48, esp32_c3=48, esp32_c5=48, esp32_c6=48, esp32_h2=48, + esp32_p4=48, + esp32_s2=64, + esp32_s3=48, ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), @@ -156,6 +156,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, + PlatformFramework.RP2040_ARDUINO, }, } ) diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index 347e9d9d33..576143bcbc 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -#if defined(USE_LIBRETINY) || defined(USE_ESP8266) +#if defined(USE_LIBRETINY) || defined(USE_ESP8266) || defined(USE_RP2040) namespace esphome { namespace remote_transmitter { @@ -40,8 +40,8 @@ void RemoteTransmitterComponent::await_target_time_() { if (this->target_time_ == 0) { this->target_time_ = current_time; } else if ((int32_t) (this->target_time_ - current_time) > 0) { -#if defined(USE_LIBRETINY) - // busy loop for libretiny is required (see the comment inside micros() in wiring.c) +#if defined(USE_LIBRETINY) || defined(USE_RP2040) + // busy loop is required for libretiny and rp2040 as interrupts are disabled while ((int32_t) (this->target_time_ - micros()) > 0) ; #else diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index cc3b82ad61..dd6a849e4c 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -62,7 +62,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) +#if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2040) void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period); void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); diff --git a/esphome/components/resampler/speaker/__init__.py b/esphome/components/resampler/speaker/__init__.py index def62547b2..7036862d14 100644 --- a/esphome/components/resampler/speaker/__init__.py +++ b/esphome/components/resampler/speaker/__init__.py @@ -63,9 +63,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_BUFFER_DURATION, default="100ms" ): cv.positive_time_period_milliseconds, - cv.SplitDefault(CONF_TASK_STACK_IN_PSRAM, esp32_idf=False): cv.All( - cv.boolean, cv.only_with_esp_idf - ), + cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean, cv.Optional(CONF_FILTERS, default=16): cv.int_range(min=2, max=1024), cv.Optional(CONF_TAPS, default=16): _validate_taps, } diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index 5e5615cbb9..ad61aca084 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -66,17 +66,17 @@ void ResamplerSpeaker::loop() { } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) { - this->status_set_error("Resampler task failed to allocate the internal buffers"); + this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM); this->state_ = speaker::STATE_STOPPING; } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) { - this->status_set_error("Cannot resample due to an unsupported audio stream"); + this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); this->state_ = speaker::STATE_STOPPING; } if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) { - this->status_set_error("Resampler task failed"); + this->status_set_error(LOG_STR("Resampler task failed")); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL); this->state_ = speaker::STATE_STOPPING; } @@ -106,12 +106,12 @@ void ResamplerSpeaker::loop() { } else { switch (err) { case ESP_ERR_INVALID_STATE: - this->status_set_error("Failed to start resampler: resampler task failed to start"); + this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start")); break; case ESP_ERR_NO_MEM: - this->status_set_error("Failed to start resampler: not enough memory for task stack"); + this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack")); default: - this->status_set_error("Failed to start resampler"); + this->status_set_error(LOG_STR("Failed to start resampler")); break; } diff --git a/esphome/components/rp2040/gpio.cpp b/esphome/components/rp2040/gpio.cpp index 3927815e46..2b1699f888 100644 --- a/esphome/components/rp2040/gpio.cpp +++ b/esphome/components/rp2040/gpio.cpp @@ -64,10 +64,8 @@ void RP2040GPIOPin::pin_mode(gpio::Flags flags) { pinMode(pin_, flags_to_mode(flags, pin_)); // NOLINT } -std::string RP2040GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); - return buffer; +size_t RP2040GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "GPIO%u", this->pin_); } bool RP2040GPIOPin::digital_read() { diff --git a/esphome/components/rp2040/gpio.h b/esphome/components/rp2040/gpio.h index 47a6fe17f2..a98e1dab14 100644 --- a/esphome/components/rp2040/gpio.h +++ b/esphome/components/rp2040/gpio.h @@ -18,7 +18,7 @@ class RP2040GPIOPin : public InternalGPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return pin_; } diff --git a/esphome/components/rp2040_pwm/output.py b/esphome/components/rp2040_pwm/output.py index ac1892fa29..441a52de7f 100644 --- a/esphome/components/rp2040_pwm/output.py +++ b/esphome/components/rp2040_pwm/output.py @@ -11,7 +11,7 @@ DEPENDENCIES = ["rp2040"] rp2040_pwm_ns = cg.esphome_ns.namespace("rp2040_pwm") RP2040PWM = rp2040_pwm_ns.class_("RP2040PWM", output.FloatOutput, cg.Component) SetFrequencyAction = rp2040_pwm_ns.class_("SetFrequencyAction", automation.Action) -validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6)) +validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6)) CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.cpp b/esphome/components/rp2040_pwm/rp2040_pwm.cpp index ec164b3c05..90a507b14f 100644 --- a/esphome/components/rp2040_pwm/rp2040_pwm.cpp +++ b/esphome/components/rp2040_pwm/rp2040_pwm.cpp @@ -36,9 +36,11 @@ void RP2040PWM::setup_pwm_() { } void RP2040PWM::dump_config() { - ESP_LOGCONFIG(TAG, "RP2040 PWM:"); + ESP_LOGCONFIG(TAG, + "RP2040 PWM:\n" + " Frequency: %.1f Hz", + this->frequency_); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); LOG_FLOAT_OUTPUT(this); } void HOT RP2040PWM::write_state(float state) { diff --git a/esphome/components/rpi_dpi_rgb/display.py b/esphome/components/rpi_dpi_rgb/display.py index 513ed8eb58..e92eee7c0c 100644 --- a/esphome/components/rpi_dpi_rgb/display.py +++ b/esphome/components/rpi_dpi_rgb/display.py @@ -1,7 +1,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import display -from esphome.components.esp32 import const, only_on_variant +from esphome.components.esp32 import VARIANT_ESP32S3, only_on_variant from esphome.components.mipi import ( CONF_DE_PIN, CONF_HSYNC_BACK_PORCH, @@ -121,8 +121,7 @@ CONFIG_SCHEMA = cv.All( } ) ), - only_on_variant(supported=[const.VARIANT_ESP32S3]), - cv.only_with_esp_idf, + only_on_variant(supported=[VARIANT_ESP32S3]), ) diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp index 042b8877e6..a81bb17dfc 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp @@ -126,8 +126,10 @@ void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) { void RpiDpiRgb::dump_config() { ESP_LOGCONFIG("", "RPI_DPI_RGB LCD"); - ESP_LOGCONFIG(TAG, " Height: %u", this->height_); - ESP_LOGCONFIG(TAG, " Width: %u", this->width_); + ESP_LOGCONFIG(TAG, + " Height: %u\n" + " Width: %u", + this->height_, this->width_); LOG_PIN(" DE Pin: ", this->de_pin_); LOG_PIN(" Enable Pin: ", this->enable_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); diff --git a/esphome/components/rtl87xx/__init__.py b/esphome/components/rtl87xx/__init__.py index 109c986f75..8f27544108 100644 --- a/esphome/components/rtl87xx/__init__.py +++ b/esphome/components/rtl87xx/__init__.py @@ -6,10 +6,13 @@ # in schema.py file in this directory. from esphome import pins +import esphome.codegen as cg from esphome.components import libretiny from esphome.components.libretiny.const import ( COMPONENT_RTL87XX, + FAMILY_RTL8710B, KEY_COMPONENT_DATA, + KEY_FAMILY, KEY_LIBRETINY, LibreTinyComponent, ) @@ -45,6 +48,11 @@ CONFIG_SCHEMA.prepend_extra(_set_core_data) async def to_code(config): + # Use FreeRTOS 8.2.3+ for xTaskNotifyGive/ulTaskNotifyTake required by AsyncTCP 3.4.3+ + # https://github.com/esphome/esphome/issues/10220 + # Only for RTL8710B (ambz) - RTL8720C (ambz2) requires FreeRTOS 10.x + if CORE.data[KEY_LIBRETINY][KEY_FAMILY] == FAMILY_RTL8710B: + cg.add_platformio_option("custom_versions.freertos", "8.2.3") return await libretiny.component_to_code(config) diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index f95be5291f..7e837a18e8 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -27,8 +27,10 @@ void RuntimeStatsCollector::record_component_time(Component *component, uint32_t } void RuntimeStatsCollector::log_stats_() { - ESP_LOGI(TAG, "Component Runtime Statistics"); - ESP_LOGI(TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_); + ESP_LOGI(TAG, + "Component Runtime Statistics\n" + "Period stats (last %" PRIu32 "ms):", + this->log_interval_); // First collect stats we want to display std::vector stats_to_display; diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp index bdd012cf5c..1b126bdef0 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.cpp +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -99,7 +99,8 @@ bool RuuviListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (!res.has_value()) return false; - ESP_LOGD(TAG, "Got RuuviTag (%s):", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGD(TAG, "Got RuuviTag (%s):", device.address_str_to(addr_buf)); if (res->humidity.has_value()) { ESP_LOGD(TAG, " Humidity: %.2f%%", *res->humidity); diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 62bbca4fb1..c7bd8748f5 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -9,6 +9,10 @@ #include #include +#ifdef USE_OTA_ROLLBACK +#include +#endif + namespace esphome { namespace safe_mode { @@ -32,6 +36,16 @@ void SafeModeComponent::dump_config() { ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); } } + +#ifdef USE_OTA_ROLLBACK + const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition(); + if (last_invalid != nullptr) { + ESP_LOGW(TAG, + "OTA rollback detected! Rolled back from partition '%s'\n" + "The device reset before the boot was marked successful", + last_invalid->label); + } +#endif } float SafeModeComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; } @@ -42,6 +56,10 @@ void SafeModeComponent::loop() { ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); this->clean_rtc(); this->boot_successful_ = true; +#ifdef USE_OTA_ROLLBACK + // Mark OTA partition as valid to prevent rollback + esp_ota_mark_app_valid_cancel_rollback(); +#endif // Disable loop since we no longer need to check this->disable_loop(); } @@ -125,7 +143,14 @@ uint32_t SafeModeComponent::read_rtc_() { return val; } -void SafeModeComponent::clean_rtc() { this->write_rtc_(0); } +void SafeModeComponent::clean_rtc() { + // Save without sync - preferences will be written at shutdown or by IntervalSyncer. + // This avoids blocking the loop for 50+ ms on flash write. If the device crashes + // before sync, the boot wasn't really successful anyway and the counter should + // remain incremented. + uint32_t val = 0; + this->rtc_.save(&val); +} void SafeModeComponent::on_safe_shutdown() { if (this->read_rtc_() != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 51cece01e4..cd1a084f16 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -1,8 +1,8 @@ #pragma once +#include #include #include -#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -46,14 +46,14 @@ template class Script : public ScriptLogger, public Trigger &tuple) { - this->execute_tuple_(tuple, typename gens::type()); + this->execute_tuple_(tuple, std::make_index_sequence{}); } // Internal function to give scripts readable names. void set_name(const LogString *name) { name_ = name; } protected: - template void execute_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void execute_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->execute(std::get(tuple)...); } @@ -157,7 +157,7 @@ template class QueueingScript : public Script, public Com const size_t queue_capacity = static_cast(this->max_runs_ - 1); auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]); this->queue_front_ = (this->queue_front_ + 1) % queue_capacity; - this->trigger_tuple_(*tuple_ptr, typename gens::type()); + this->trigger_tuple_(*tuple_ptr, std::make_index_sequence{}); } } @@ -174,7 +174,7 @@ template class QueueingScript : public Script, public Com } } - template void trigger_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void trigger_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->trigger(std::get(tuple)...); } @@ -278,7 +278,12 @@ template class ScriptWaitAction : public Action, void setup() override { // Start with loop disabled - only enable when there's work to do - this->disable_loop(); + // IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already + // called before our setup() (e.g., from on_boot trigger at same priority level) + // and we must not undo its enable_loop() call + if (this->num_running_ == 0) { + this->disable_loop(); + } } void play_complex(const Ts &...x) override { @@ -290,10 +295,10 @@ template class ScriptWaitAction : public Action, } // Store parameters for later execution - this->param_queue_.emplace_front(x...); - // Enable loop now that we have work to do + this->param_queue_.emplace_back(x...); + // Enable loop now that we have work to do - don't call loop() synchronously! + // Let the event loop call it to avoid reentrancy issues this->enable_loop(); - this->loop(); } void loop() override { @@ -303,13 +308,17 @@ template class ScriptWaitAction : public Action, if (this->script_->is_running()) return; - while (!this->param_queue_.empty()) { + // Only process ONE queued item per loop iteration + // Processing all items in a while loop causes infinite loops because + // play_next_() can trigger more items to be queued + if (!this->param_queue_.empty()) { auto ¶ms = this->param_queue_.front(); - this->play_next_tuple_(params, typename gens::type()); + this->play_next_tuple_(params, std::make_index_sequence{}); this->param_queue_.pop_front(); + } else { + // Queue is now empty - disable loop until next play_complex + this->disable_loop(); } - // Queue is now empty - disable loop until next play_complex - this->disable_loop(); } void play(const Ts &...x) override { /* ignore - see play_complex */ @@ -321,12 +330,12 @@ template class ScriptWaitAction : public Action, } protected: - template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_next_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play_next_(std::get(tuple)...); } C *script_; - std::forward_list> param_queue_; + std::list> param_queue_; }; } // namespace script diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index c815c98419..b9ce1f9151 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -10,6 +10,9 @@ namespace seeed_mr60bha2 { static const char *const TAG = "seeed_mr60bha2"; +// Maximum bytes to log in verbose hex output +static constexpr size_t MR60BHA2_MAX_LOG_BYTES = 64; + // Prints the component's configuration data. dump_config() prints all of the component's configuration // items in an easy-to-read format, including the configuration key-value pairs. void MR60BHA2Component::dump_config() { @@ -110,7 +113,10 @@ bool MR60BHA2Component::validate_message_() { if (at == 7) { if (!validate_checksum(data, 7, header_checksum)) { ESP_LOGE(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", header_checksum); - ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty(data, 8).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MR60BHA2_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data, 8)); return false; } return true; @@ -125,14 +131,22 @@ bool MR60BHA2Component::validate_message_() { if (at == 8 + length) { if (!validate_checksum(data + 8, length, data_checksum)) { ESP_LOGE(TAG, "DATA_CKSUM_FRAME ERROR: 0x%02x", data_checksum); - ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty(data, 8 + length).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MR60BHA2_MAX_LOG_BYTES)]; +#endif + ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data, 8 + length)); return false; } } const uint8_t *frame_data = data + 8; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf1[format_hex_pretty_size(MR60BHA2_MAX_LOG_BYTES)]; + char hex_buf2[format_hex_pretty_size(MR60BHA2_MAX_LOG_BYTES)]; +#endif ESP_LOGV(TAG, "Received Frame: ID: 0x%04x, Type: 0x%04x, Data: [%s] Raw Data: [%s]", frame_id, frame_type, - format_hex_pretty(frame_data, length).c_str(), format_hex_pretty(this->rx_message_).c_str()); + format_hex_pretty_to(hex_buf1, sizeof(hex_buf1), frame_data, length), + format_hex_pretty_to(hex_buf2, sizeof(hex_buf2), this->rx_message_.data(), this->rx_message_.size())); this->process_frame_(frame_id, frame_type, data + 8, length); // Return false to reset rx buffer diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index 7f8bd6a43c..b5b5b4d05a 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -10,6 +10,9 @@ namespace seeed_mr60fda2 { static const char *const TAG = "seeed_mr60fda2"; +// Maximum bytes to log in verbose hex output +static constexpr size_t MR60FDA2_MAX_LOG_BYTES = 64; + // Prints the component's configuration data. dump_config() prints all of the component's configuration // items in an easy-to-read format, including the configuration key-value pairs. void MR60FDA2Component::dump_config() { @@ -202,9 +205,13 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { this->current_frame_locate_++; } else { ESP_LOGD(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", buffer); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char frame_buf[format_hex_pretty_size(MR60FDA2_MAX_LOG_BYTES)]; + char byte_buf[format_hex_pretty_size(1)]; +#endif ESP_LOGV(TAG, "CURRENT_FRAME: %s %s", - format_hex_pretty(this->current_frame_buf_, this->current_frame_len_).c_str(), - format_hex_pretty(&buffer, 1).c_str()); + format_hex_pretty_to(frame_buf, this->current_frame_buf_, this->current_frame_len_), + format_hex_pretty_to(byte_buf, &buffer, 1)); this->current_frame_locate_ = LOCATE_FRAME_HEADER; } break; @@ -228,9 +235,13 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { this->process_frame_(); } else { ESP_LOGD(TAG, "DATA_CKSUM_FRAME ERROR: 0x%02x", buffer); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char frame_buf[format_hex_pretty_size(MR60FDA2_MAX_LOG_BYTES)]; + char byte_buf[format_hex_pretty_size(1)]; +#endif ESP_LOGV(TAG, "GET CURRENT_FRAME: %s %s", - format_hex_pretty(this->current_frame_buf_, this->current_frame_len_).c_str(), - format_hex_pretty(&buffer, 1).c_str()); + format_hex_pretty_to(frame_buf, this->current_frame_buf_, this->current_frame_len_), + format_hex_pretty_to(byte_buf, &buffer, 1)); this->current_frame_locate_ = LOCATE_FRAME_HEADER; } @@ -328,7 +339,10 @@ void MR60FDA2Component::set_install_height(uint8_t index) { float_to_bytes(INSTALL_HEIGHT[index], &send_data[8]); send_data[12] = calculate_checksum(send_data + 8, 4); this->write_array(send_data, 13); - ESP_LOGV(TAG, "SEND INSTALL HEIGHT FRAME: %s", format_hex_pretty(send_data, 13).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(13)]; +#endif + ESP_LOGV(TAG, "SEND INSTALL HEIGHT FRAME: %s", format_hex_pretty_to(hex_buf, send_data, 13)); } void MR60FDA2Component::set_height_threshold(uint8_t index) { @@ -336,7 +350,10 @@ void MR60FDA2Component::set_height_threshold(uint8_t index) { float_to_bytes(HEIGHT_THRESHOLD[index], &send_data[8]); send_data[12] = calculate_checksum(send_data + 8, 4); this->write_array(send_data, 13); - ESP_LOGV(TAG, "SEND HEIGHT THRESHOLD: %s", format_hex_pretty(send_data, 13).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(13)]; +#endif + ESP_LOGV(TAG, "SEND HEIGHT THRESHOLD: %s", format_hex_pretty_to(hex_buf, send_data, 13)); } void MR60FDA2Component::set_sensitivity(uint8_t index) { @@ -346,19 +363,28 @@ void MR60FDA2Component::set_sensitivity(uint8_t index) { send_data[12] = calculate_checksum(send_data + 8, 4); this->write_array(send_data, 13); - ESP_LOGV(TAG, "SEND SET SENSITIVITY: %s", format_hex_pretty(send_data, 13).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(13)]; +#endif + ESP_LOGV(TAG, "SEND SET SENSITIVITY: %s", format_hex_pretty_to(hex_buf, send_data, 13)); } void MR60FDA2Component::get_radar_parameters() { uint8_t send_data[8] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x06, 0xF6}; this->write_array(send_data, 8); - ESP_LOGV(TAG, "SEND GET PARAMETERS: %s", format_hex_pretty(send_data, 8).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(8)]; +#endif + ESP_LOGV(TAG, "SEND GET PARAMETERS: %s", format_hex_pretty_to(hex_buf, send_data, 8)); } void MR60FDA2Component::factory_reset() { uint8_t send_data[8] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x21, 0x10, 0xCF}; this->write_array(send_data, 8); - ESP_LOGV(TAG, "SEND RESET: %s", format_hex_pretty(send_data, 8).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(8)]; +#endif + ESP_LOGV(TAG, "SEND RESET: %s", format_hex_pretty_to(hex_buf, send_data, 8)); this->get_radar_parameters(); } diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h index 3e42eaf98a..dda5403557 100644 --- a/esphome/components/select/automation.h +++ b/esphome/components/select/automation.h @@ -4,14 +4,17 @@ #include "esphome/core/component.h" #include "select.h" -namespace esphome { -namespace select { +namespace esphome::select { class SelectStateTrigger : public Trigger { public: - explicit SelectStateTrigger(Select *parent) { - parent->add_on_state_callback([this](const std::string &value, size_t index) { this->trigger(value, index); }); + explicit SelectStateTrigger(Select *parent) : parent_(parent) { + parent->add_on_state_callback( + [this](size_t index) { this->trigger(std::string(this->parent_->option_at(index)), index); }); } + + protected: + Select *parent_; }; template class SelectSetAction : public Action { @@ -63,5 +66,4 @@ template class SelectOperationAction : public Action { Select *select_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 9fe7a52422..28d7eb07d4 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace select { +namespace esphome::select { static const char *const TAG = "select"; @@ -33,8 +32,7 @@ void Select::publish_state(size_t index) { this->state = option; // Update deprecated member for backward compatibility #pragma GCC diagnostic pop ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); - // Callback signature requires std::string, create temporary for compatibility - this->state_callback_.call(std::string(option), index); + this->state_callback_.call(index); #if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_select_update(this); #endif @@ -42,7 +40,7 @@ void Select::publish_state(size_t index) { const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; } -void Select::add_on_state_callback(std::function &&callback) { +void Select::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } @@ -57,12 +55,10 @@ size_t Select::size() const { return options.size(); } -optional Select::index_of(const std::string &option) const { return this->index_of(option.c_str()); } - -optional Select::index_of(const char *option) const { +optional Select::index_of(const char *option, size_t len) const { const auto &options = traits.get_options(); for (size_t i = 0; i < options.size(); i++) { - if (strcmp(options[i], option) == 0) { + if (strncmp(options[i], option, len) == 0 && options[i][len] == '\0') { return i; } } @@ -86,5 +82,4 @@ optional Select::at(size_t index) const { const char *Select::option_at(size_t index) const { return traits.get_options().at(index); } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 7459c9d146..330d18ce6f 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -6,8 +6,7 @@ #include "select_call.h" #include "select_traits.h" -namespace esphome { -namespace select { +namespace esphome::select { #define LOG_SELECT(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -63,8 +62,9 @@ class Select : public EntityBase { size_t size() const; /// Find the (optional) index offset of the provided option value. - optional index_of(const std::string &option) const; - optional index_of(const char *option) const; + optional index_of(const char *option, size_t len) const; + optional index_of(const std::string &option) const { return this->index_of(option.data(), option.size()); } + optional index_of(const char *option) const { return this->index_of(option, strlen(option)); } /// Return the (optional) index offset of the currently active option. optional active_index() const; @@ -75,7 +75,7 @@ class Select : public EntityBase { /// Return the option value at the provided index offset (as const char* from flash). const char *option_at(size_t index) const; - void add_on_state_callback(std::function &&callback); + void add_on_state_callback(std::function &&callback); protected: friend class SelectCall; @@ -111,8 +111,7 @@ class Select : public EntityBase { } } - CallbackManager state_callback_; + LazyCallbackManager state_callback_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index aa7559e24e..2ff99c961d 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -2,14 +2,11 @@ #include "select.h" #include "esphome/core/log.h" -namespace esphome { -namespace select { +namespace esphome::select { static const char *const TAG = "select"; -SelectCall &SelectCall::set_option(const std::string &option) { return this->with_option(option); } - -SelectCall &SelectCall::set_option(const char *option) { return this->with_option(option); } +SelectCall &SelectCall::set_option(const char *option, size_t len) { return this->with_option(option, len); } SelectCall &SelectCall::set_index(size_t index) { return this->with_index(index); } @@ -33,12 +30,10 @@ SelectCall &SelectCall::with_cycle(bool cycle) { return *this; } -SelectCall &SelectCall::with_option(const std::string &option) { return this->with_option(option.c_str()); } - -SelectCall &SelectCall::with_option(const char *option) { +SelectCall &SelectCall::with_option(const char *option, size_t len) { this->operation_ = SELECT_OP_SET; // Find the option index - this validates the option exists - this->index_ = this->parent_->index_of(option); + this->index_ = this->parent_->index_of(option, len); return *this; } @@ -125,5 +120,4 @@ void SelectCall::perform() { parent->control(idx); } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index eae7d3de1d..c9abbc69a0 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -2,8 +2,7 @@ #include "esphome/core/helpers.h" -namespace esphome { -namespace select { +namespace esphome::select { class Select; @@ -21,8 +20,9 @@ class SelectCall { explicit SelectCall(Select *parent) : parent_(parent) {} void perform(); - SelectCall &set_option(const std::string &option); - SelectCall &set_option(const char *option); + SelectCall &set_option(const char *option, size_t len); + SelectCall &set_option(const std::string &option) { return this->set_option(option.data(), option.size()); } + SelectCall &set_option(const char *option) { return this->set_option(option, strlen(option)); } SelectCall &set_index(size_t index); SelectCall &select_next(bool cycle); @@ -32,8 +32,9 @@ class SelectCall { SelectCall &with_operation(SelectOperation operation); SelectCall &with_cycle(bool cycle); - SelectCall &with_option(const std::string &option); - SelectCall &with_option(const char *option); + SelectCall &with_option(const char *option, size_t len); + SelectCall &with_option(const std::string &option) { return this->with_option(option.data(), option.size()); } + SelectCall &with_option(const char *option) { return this->with_option(option, strlen(option)); } SelectCall &with_index(size_t index); protected: @@ -45,5 +46,4 @@ class SelectCall { bool cycle_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index e5e12bdc7a..ff52c0d85b 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -1,7 +1,6 @@ #include "select_traits.h" -namespace esphome { -namespace select { +namespace esphome::select { void SelectTraits::set_options(const std::initializer_list &options) { this->options_ = options; } @@ -14,5 +13,4 @@ void SelectTraits::set_options(const FixedVector &options) { const FixedVector &SelectTraits::get_options() const { return this->options_; } -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h index ee59a030ad..78a83e5944 100644 --- a/esphome/components/select/select_traits.h +++ b/esphome/components/select/select_traits.h @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include -namespace esphome { -namespace select { +namespace esphome::select { class SelectTraits { public: @@ -16,5 +15,4 @@ class SelectTraits { FixedVector options_; }; -} // namespace select -} // namespace esphome +} // namespace esphome::select diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index 3298a5b8db..d5c9dfa3ae 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -1,4 +1,5 @@ #include "sen5x.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -154,10 +155,10 @@ void SEN5XComponent::setup() { if (this->voc_sensor_ && this->store_baseline_) { uint32_t combined_serial = encode_uint24(this->serial_number_[0], this->serial_number_[1], this->serial_number_[2]); - // Hash with compilation time and serial number + // Hash with config hash, version, and serial number // This ensures the baseline storage is cleared after OTA - // Serial numbers are unique to each sensor, so mulitple sensors can be used without conflict - uint32_t hash = fnv1_hash(App.get_compilation_time() + std::to_string(combined_serial)); + // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict + uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), combined_serial); this->pref_ = global_preferences->make_preference(hash, true); if (this->pref_.load(&this->voc_baselines_storage_)) { diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index 9668a253c0..9c3114b9e2 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -4,17 +4,28 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor import esphome.config_validation as cv from esphome.const import ( + CONF_ALGORITHM_TUNING, CONF_GAIN_FACTOR, + CONF_GATING_MAX_DURATION_MINUTES, CONF_HUMIDITY, CONF_ID, + CONF_INDEX_OFFSET, + CONF_LEARNING_TIME_GAIN_HOURS, + CONF_LEARNING_TIME_OFFSET_HOURS, + CONF_NORMALIZED_OFFSET_SLOPE, + CONF_NOX, CONF_OFFSET, CONF_PM_1_0, CONF_PM_2_5, CONF_PM_4_0, CONF_PM_10_0, + CONF_STD_INITIAL, CONF_STORE_BASELINE, CONF_TEMPERATURE, CONF_TEMPERATURE_COMPENSATION, + CONF_TIME_CONSTANT, + CONF_VOC, + CONF_VOC_BASELINE, DEVICE_CLASS_AQI, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PM1, @@ -42,18 +53,7 @@ SEN5XComponent = sen5x_ns.class_( RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode") CONF_ACCELERATION_MODE = "acceleration_mode" -CONF_ALGORITHM_TUNING = "algorithm_tuning" CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval" -CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes" -CONF_INDEX_OFFSET = "index_offset" -CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours" -CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours" -CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope" -CONF_NOX = "nox" -CONF_STD_INITIAL = "std_initial" -CONF_TIME_CONSTANT = "time_constant" -CONF_VOC = "voc" -CONF_VOC_BASELINE = "voc_baseline" # Actions diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index e8fec222a1..2ac45a55ac 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -3,7 +3,7 @@ import math from esphome import automation import esphome.codegen as cg -from esphome.components import mqtt, web_server +from esphome.components import mqtt, web_server, zigbee import esphome.config_validation as cv from esphome.const import ( CONF_ABOVE, @@ -182,6 +182,7 @@ STATE_CLASSES = { "measurement": StateClasses.STATE_CLASS_MEASUREMENT, "total_increasing": StateClasses.STATE_CLASS_TOTAL_INCREASING, "total": StateClasses.STATE_CLASS_TOTAL, + "measurement_angle": StateClasses.STATE_CLASS_MEASUREMENT_ANGLE, } validate_state_class = cv.enum(STATE_CLASSES, lower=True, space="_") @@ -270,7 +271,9 @@ ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) ThrottleWithPriorityFilter = sensor_ns.class_( "ThrottleWithPriorityFilter", ValueListFilter ) -TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) +TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component) +TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase) +TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) DeltaFilter = sensor_ns.class_("DeltaFilter", Filter) @@ -292,6 +295,7 @@ validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") _SENSOR_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMPONENT_SCHEMA) + .extend(zigbee.SENSOR_SCHEMA) .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSensorComponent), @@ -301,9 +305,6 @@ _SENSOR_SCHEMA = ( cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_STATE_CLASS): validate_state_class, cv.Optional(CONF_ENTITY_CATEGORY): sensor_entity_category, - cv.Optional("last_reset_type"): cv.invalid( - "last_reset_type has been removed since 2021.9.0. state_class: total_increasing should be used for total values." - ), cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, cv.Optional(CONF_EXPIRE_AFTER): cv.All( cv.requires_component("mqtt"), @@ -335,6 +336,7 @@ _SENSOR_SCHEMA = ( ) _SENSOR_SCHEMA.add_extra(entity_duplicate_validator("sensor")) +_SENSOR_SCHEMA.add_extra(zigbee.validate_sensor) def sensor_schema( @@ -681,11 +683,16 @@ TIMEOUT_SCHEMA = cv.maybe_simple_value( ) -@FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA) +@FILTER_REGISTRY.register("timeout", TimeoutFilterBase, TIMEOUT_SCHEMA) async def timeout_filter_to_code(config, filter_id): + filter_id = filter_id.copy() if config[CONF_VALUE] == "last": + # Use TimeoutFilterLast for "last" mode (smaller, more common - LD2450, LD2412, etc.) + filter_id.type = TimeoutFilterLast var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT]) else: + # Use TimeoutFilterConfigured for configured value mode + filter_id.type = TimeoutFilterConfigured template_ = await cg.templatable(config[CONF_VALUE], [], float) var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) await cg.register_component(var, {}) @@ -913,6 +920,8 @@ async def setup_sensor_core_(var, config): if web_server_config := config.get(CONF_WEB_SERVER): await web_server.add_entity_config(var, web_server_config) + await zigbee.setup_sensor(var, config) + async def register_sensor(var, config): if not CORE.has_id(config[CONF_ID]): diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 65d8dea31c..c8c6540112 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -339,20 +339,43 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { this->phi_.initialize(parent, nullptr); } -// TimeoutFilter -optional TimeoutFilter::new_value(float value) { - if (this->value_.has_value()) { - this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value().value()); }); - } else { - this->set_timeout("timeout", this->time_period_, [this, value]() { this->output(value); }); +// TimeoutFilterBase - shared loop logic +void TimeoutFilterBase::loop() { + // Check if timeout period has elapsed + // Use cached loop start time to avoid repeated millis() calls + const uint32_t now = App.get_loop_component_start_time(); + if (now - this->timeout_start_time_ >= this->time_period_) { + // Timeout fired - get output value from derived class and output it + this->output(this->get_output_value()); + + // Disable loop until next value arrives + this->disable_loop(); } +} + +float TimeoutFilterBase::get_setup_priority() const { return setup_priority::HARDWARE; } + +// TimeoutFilterLast - "last" mode implementation +optional TimeoutFilterLast::new_value(float value) { + // Store the value to output when timeout fires + this->pending_value_ = value; + + // Record when timeout started and enable loop + this->timeout_start_time_ = millis(); + this->enable_loop(); + return value; } -TimeoutFilter::TimeoutFilter(uint32_t time_period) : time_period_(time_period) {} -TimeoutFilter::TimeoutFilter(uint32_t time_period, const TemplatableValue &new_value) - : time_period_(time_period), value_(new_value) {} -float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } +// TimeoutFilterConfigured - configured value mode implementation +optional TimeoutFilterConfigured::new_value(float value) { + // Record when timeout started and enable loop + // Note: we don't store the incoming value since we have a configured value + this->timeout_start_time_ = millis(); + this->enable_loop(); + + return value; +} // DebounceFilter optional DebounceFilter::new_value(float value) { diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 75e28a1efe..92a9184c18 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -380,18 +380,46 @@ class ThrottleWithPriorityFilter : public ValueListFilter { uint32_t min_time_between_inputs_; }; -class TimeoutFilter : public Filter, public Component { +// Base class for timeout filters - contains common loop logic +class TimeoutFilterBase : public Filter, public Component { public: - explicit TimeoutFilter(uint32_t time_period); - explicit TimeoutFilter(uint32_t time_period, const TemplatableValue &new_value); - - optional new_value(float value) override; - + void loop() override; float get_setup_priority() const override; protected: - uint32_t time_period_; - optional> value_; + explicit TimeoutFilterBase(uint32_t time_period) : time_period_(time_period) { this->disable_loop(); } + virtual float get_output_value() = 0; + + uint32_t time_period_; // 4 bytes (timeout duration in ms) + uint32_t timeout_start_time_{0}; // 4 bytes (when the timeout was started) + // Total base: 8 bytes +}; + +// Timeout filter for "last" mode - outputs the last received value after timeout +class TimeoutFilterLast : public TimeoutFilterBase { + public: + explicit TimeoutFilterLast(uint32_t time_period) : TimeoutFilterBase(time_period) {} + + optional new_value(float value) override; + + protected: + float get_output_value() override { return this->pending_value_; } + float pending_value_{0}; // 4 bytes (value to output when timeout fires) + // Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead +}; + +// Timeout filter with configured value - evaluates TemplatableValue after timeout +class TimeoutFilterConfigured : public TimeoutFilterBase { + public: + explicit TimeoutFilterConfigured(uint32_t time_period, const TemplatableValue &new_value) + : TimeoutFilterBase(time_period), value_(new_value) {} + + optional new_value(float value) override; + + protected: + float get_output_value() override { return this->value_.value(); } + TemplatableValue value_; // 16 bytes (configured output value, can be lambda) + // Total: 8 (base) + 16 = 24 bytes + vtable ptr + Component overhead }; class DebounceFilter : public Filter, public Component { diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index df6bd644e8..c1d28bf260 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -44,6 +44,8 @@ const LogString *state_class_to_string(StateClass state_class) { return LOG_STR("total_increasing"); case STATE_CLASS_TOTAL: return LOG_STR("total"); + case STATE_CLASS_MEASUREMENT_ANGLE: + return LOG_STR("measurement_angle"); case STATE_CLASS_NONE: default: return LOG_STR(""); @@ -74,9 +76,7 @@ StateClass Sensor::get_state_class() { void Sensor::publish_state(float state) { this->raw_state = state; - if (this->raw_callback_) { - this->raw_callback_->call(state); - } + this->raw_callback_.call(state); ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); @@ -89,10 +89,7 @@ void Sensor::publish_state(float state) { void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } void Sensor::add_on_raw_state_callback(std::function &&callback) { - if (!this->raw_callback_) { - this->raw_callback_ = make_unique>(); - } - this->raw_callback_->add(std::move(callback)); + this->raw_callback_.add(std::move(callback)); } void Sensor::add_filter(Filter *filter) { diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index a4210e5e6c..a792c0d3fd 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -31,6 +31,7 @@ enum StateClass : uint8_t { STATE_CLASS_MEASUREMENT = 1, STATE_CLASS_TOTAL_INCREASING = 2, STATE_CLASS_TOTAL = 3, + STATE_CLASS_MEASUREMENT_ANGLE = 4 }; const LogString *state_class_to_string(StateClass state_class); @@ -124,8 +125,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa void internal_send_state_to_frontend(float state); protected: - std::unique_ptr> raw_callback_; ///< Storage for raw state callbacks (lazy allocated). - CallbackManager callback_; ///< Storage for filtered state callbacks. + LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. + LazyCallbackManager callback_; ///< Storage for filtered state callbacks. Filter *filter_list_{nullptr}; ///< Store all active filters. diff --git a/esphome/components/sfa30/sfa30.cpp b/esphome/components/sfa30/sfa30.cpp index 99709d5fbb..bbe3bcd7d2 100644 --- a/esphome/components/sfa30/sfa30.cpp +++ b/esphome/components/sfa30/sfa30.cpp @@ -73,17 +73,17 @@ void SFA30Component::update() { } if (this->formaldehyde_sensor_ != nullptr) { - const float formaldehyde = raw_data[0] / 5.0f; + const float formaldehyde = static_cast(raw_data[0]) / 5.0f; this->formaldehyde_sensor_->publish_state(formaldehyde); } if (this->humidity_sensor_ != nullptr) { - const float humidity = raw_data[1] / 100.0f; + const float humidity = static_cast(raw_data[1]) / 100.0f; this->humidity_sensor_->publish_state(humidity); } if (this->temperature_sensor_ != nullptr) { - const float temperature = raw_data[2] / 200.0f; + const float temperature = static_cast(raw_data[2]) / 200.0f; this->temperature_sensor_->publish_state(temperature); } diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 9e8d6b332c..18814405d4 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -72,10 +72,10 @@ void SGP30Component::setup() { return; } - // Hash with compilation time and serial number + // Hash with config hash, version, and serial number // This ensures the baseline storage is cleared after OTA - // Serial numbers are unique to each sensor, so mulitple sensors can be used without conflict - uint32_t hash = fnv1_hash(App.get_compilation_time() + std::to_string(this->serial_number_)); + // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict + uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), this->serial_number_); this->pref_ = global_preferences->make_preference(hash, true); if (this->store_baseline_ && this->pref_.load(&this->baselines_storage_)) { diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py index ad9de6fe24..b16151ec1f 100644 --- a/esphome/components/sgp40/sensor.py +++ b/esphome/components/sgp40/sensor.py @@ -4,5 +4,5 @@ CODEOWNERS = ["@SenexCrenshaw"] CONFIG_SCHEMA = cv.invalid( "SGP40 is deprecated.\nPlease use the SGP4x platform instead.\nSGP4x supports both SPG40 and SGP41.\n" - " See https://esphome.io/components/sensor/sgp4x.html" + " See https://esphome.io/components/sensor/sgp4x/" ) diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py index 7c6fe580b2..ab78ab59d9 100644 --- a/esphome/components/sgp4x/sensor.py +++ b/esphome/components/sgp4x/sensor.py @@ -2,11 +2,20 @@ import esphome.codegen as cg from esphome.components import i2c, sensirion_common, sensor import esphome.config_validation as cv from esphome.const import ( + CONF_ALGORITHM_TUNING, CONF_COMPENSATION, CONF_GAIN_FACTOR, + CONF_GATING_MAX_DURATION_MINUTES, CONF_ID, + CONF_INDEX_OFFSET, + CONF_LEARNING_TIME_GAIN_HOURS, + CONF_LEARNING_TIME_OFFSET_HOURS, + CONF_NOX, + CONF_STD_INITIAL, CONF_STORE_BASELINE, CONF_TEMPERATURE_SOURCE, + CONF_VOC, + CONF_VOC_BASELINE, DEVICE_CLASS_AQI, ICON_RADIATOR, STATE_CLASS_MEASUREMENT, @@ -24,16 +33,7 @@ SGP4xComponent = sgp4x_ns.class_( sensirion_common.SensirionI2CDevice, ) -CONF_ALGORITHM_TUNING = "algorithm_tuning" -CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes" CONF_HUMIDITY_SOURCE = "humidity_source" -CONF_INDEX_OFFSET = "index_offset" -CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours" -CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours" -CONF_NOX = "nox" -CONF_STD_INITIAL = "std_initial" -CONF_VOC = "voc" -CONF_VOC_BASELINE = "voc_baseline" def validate_sensors(config): diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index 99d88006f7..23589265ca 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -1,4 +1,5 @@ #include "sgp4x.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" #include @@ -56,10 +57,10 @@ void SGP4xComponent::setup() { ESP_LOGD(TAG, "Version 0x%0X", featureset); if (this->store_baseline_) { - // Hash with compilation time and serial number + // Hash with config hash, version, and serial number // This ensures the baseline storage is cleared after OTA - // Serial numbers are unique to each sensor, so mulitple sensors can be used without conflict - uint32_t hash = fnv1_hash(App.get_compilation_time() + std::to_string(this->serial_number_)); + // Serial numbers are unique to each sensor, so multiple sensors can be used without conflict + uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), this->serial_number_); this->pref_ = global_preferences->make_preference(hash, true); if (this->pref_.load(&this->voc_baselines_storage_)) { diff --git a/esphome/components/sha256/__init__.py b/esphome/components/sha256/__init__.py index f07157416d..5db0e77b76 100644 --- a/esphome/components/sha256/__init__.py +++ b/esphome/components/sha256/__init__.py @@ -12,6 +12,8 @@ CONFIG_SCHEMA = cv.Schema({}) async def to_code(config: ConfigType) -> None: + cg.add_define("USE_SHA256") + # Add OpenSSL library for host platform if not CORE.is_host: return diff --git a/esphome/components/sha256/sha256.cpp b/esphome/components/sha256/sha256.cpp index 32abbd739d..48559d7c73 100644 --- a/esphome/components/sha256/sha256.cpp +++ b/esphome/components/sha256/sha256.cpp @@ -10,23 +10,26 @@ namespace esphome::sha256 { #if defined(USE_ESP32) || defined(USE_LIBRETINY) -// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS: +// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS (IDF 5.5.x): // // The ESP32-S3 uses hardware DMA for SHA acceleration. The mbedtls_sha256_context structure contains -// internal state that the DMA engine references. This imposes two critical constraints: +// internal state that the DMA engine references. This imposes three critical constraints: // -// 1. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to +// 1. ALIGNMENT: The SHA256 object MUST be declared with `alignas(32)` for proper DMA alignment. +// Without this, the DMA engine may crash with an abort in sha_hal_read_digest(). +// +// 2. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to // write to incorrect memory locations. This results in null pointer dereferences and crashes. // ALWAYS use fixed-size arrays (e.g., char buf[65], not char buf[size+1]). // -// 2. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same +// 3. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same // function. NEVER pass the SHA256 object or HashBase pointer to another function. When the stack // frame changes (function call/return), the DMA references become invalid and will produce // truncated hash output (20 bytes instead of 32) or corrupt memory. // // CORRECT USAGE: // void my_function() { -// sha256::SHA256 hasher; // Created locally +// alignas(32) sha256::SHA256 hasher; // Created locally with proper alignment // hasher.init(); // hasher.add(data, len); // Any size, no chunking needed // hasher.calculate(); @@ -36,7 +39,7 @@ namespace esphome::sha256 { // // INCORRECT USAGE (WILL FAIL ON ESP32-S3): // void my_function() { -// sha256::SHA256 hasher; +// sha256::SHA256 hasher; // WRONG: Missing alignas(32) // helper(&hasher); // WRONG: Passed to different stack frame // } // void helper(HashBase *h) { diff --git a/esphome/components/sha256/sha256.h b/esphome/components/sha256/sha256.h index a2b62799e1..17d80636f1 100644 --- a/esphome/components/sha256/sha256.h +++ b/esphome/components/sha256/sha256.h @@ -22,6 +22,18 @@ namespace esphome::sha256 { +/// SHA256 hash implementation. +/// +/// CRITICAL for ESP32-S3 with IDF 5.5.x hardware SHA acceleration: +/// 1. SHA256 objects MUST be declared with `alignas(32)` for proper DMA alignment +/// 2. The object MUST stay in the same stack frame (no passing to other functions) +/// 3. NO Variable Length Arrays (VLAs) in the same function +/// +/// Example usage: +/// alignas(32) sha256::SHA256 hasher; +/// hasher.init(); +/// hasher.add(data, len); +/// hasher.calculate(); class SHA256 : public esphome::HashBase { public: SHA256() = default; @@ -39,10 +51,8 @@ class SHA256 : public esphome::HashBase { protected: #if defined(USE_ESP32) || defined(USE_LIBRETINY) - // CRITICAL: The mbedtls context MUST be stack-allocated (not a pointer) for ESP32-S3 hardware SHA acceleration. - // The ESP32-S3 DMA engine references this structure's memory addresses. If the context is passed to another - // function (crossing stack frames) or if VLAs are present, the DMA operations will corrupt memory and produce - // truncated/incorrect hash results. + // The mbedtls context for ESP32-S3 hardware SHA requires proper alignment and stack frame constraints. + // See class documentation above for critical requirements. mbedtls_sha256_context ctx_{}; #elif defined(USE_ESP8266) || defined(USE_RP2040) br_sha256_context ctx_{}; diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index b336bbcb65..bdb33d31af 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -113,26 +113,20 @@ void ShellyDimmer::setup() { void ShellyDimmer::update() { this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); } void ShellyDimmer::dump_config() { - ESP_LOGCONFIG(TAG, "ShellyDimmer:"); - LOG_PIN(" NRST Pin: ", this->pin_nrst_); - LOG_PIN(" BOOT0 Pin: ", this->pin_boot0_); - ESP_LOGCONFIG(TAG, + "ShellyDimmer:\n" " Leading Edge: %s\n" " Warmup Brightness: %d\n" " Minimum Brightness: %d\n" - " Maximum Brightness: %d", - YESNO(this->leading_edge_), this->warmup_brightness_, this->min_brightness_, this->max_brightness_); - // ESP_LOGCONFIG(TAG, " Warmup Time: %d", this->warmup_time_); - // ESP_LOGCONFIG(TAG, " Fade Rate: %d", this->fade_rate_); - - LOG_UPDATE_INTERVAL(this); - - ESP_LOGCONFIG(TAG, - " STM32 current firmware version: %d.%d \n" + " Maximum Brightness: %d\n" + " STM32 current firmware version: %d.%d\n" " STM32 required firmware version: %d.%d", + YESNO(this->leading_edge_), this->warmup_brightness_, this->min_brightness_, this->max_brightness_, this->version_major_, this->version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION, USE_SHD_FIRMWARE_MINOR_VERSION); + LOG_PIN(" NRST Pin: ", this->pin_nrst_); + LOG_PIN(" BOOT0 Pin: ", this->pin_boot0_); + LOG_UPDATE_INTERVAL(this); if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION || this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) { @@ -270,7 +264,10 @@ void ShellyDimmer::send_settings_() { } bool ShellyDimmer::send_command_(uint8_t cmd, const uint8_t *const payload, uint8_t len) { - ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, format_hex(payload, len).c_str()); + // Buffer for hex formatting: max frame size * 2 + null (covers any payload) + char hex_buf[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE * 2 + 1]; + ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, + format_hex_to(hex_buf, sizeof(hex_buf), payload, len)); // Prepare a command frame. uint8_t frame[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE]; @@ -436,13 +433,15 @@ bool ShellyDimmer::handle_frame_() { current = CURRENT_SCALING_FACTOR / static_cast(current_raw); } - ESP_LOGI(TAG, "Got dimmer data:"); - ESP_LOGI(TAG, " HW version: %d", hw_version); - ESP_LOGI(TAG, " Brightness: %d", brightness); - ESP_LOGI(TAG, " Fade rate: %d", fade_rate); - ESP_LOGI(TAG, " Power: %f W", power); - ESP_LOGI(TAG, " Voltage: %f V", voltage); - ESP_LOGI(TAG, " Current: %f A", current); + ESP_LOGI(TAG, + "Got dimmer data:\n" + " HW version: %d\n" + " Brightness: %d\n" + " Fade rate: %d\n" + " Power: %f W\n" + " Voltage: %f V\n" + " Current: %f A", + hw_version, brightness, fade_rate, power, voltage, current); // Update sensors. if (this->power_sensor_ != nullptr) { diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index 79f1674020..d473df43c7 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -60,8 +60,10 @@ void SHT3XDComponent::dump_config() { ESP_LOGE(TAG, " Communication with SHT3xD failed!"); return; } - ESP_LOGD(TAG, " Serial Number: 0x%08" PRIX32, this->serial_number_); - ESP_LOGD(TAG, " Heater Enabled: %s", this->heater_enabled_ ? "true" : "false"); + ESP_LOGD(TAG, + " Serial Number: 0x%08" PRIX32 "\n" + " Heater Enabled: %s", + this->serial_number_, TRUEFALSE(this->heater_enabled_)); LOG_I2C_DEVICE(this); LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 62b8717ded..9d29746f0b 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -7,16 +7,28 @@ namespace sht4x { static const char *const TAG = "sht4x"; static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; +static const uint8_t SERIAL_NUMBER_COMMAND = 0x89; void SHT4XComponent::start_heater_() { uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]}; ESP_LOGD(TAG, "Heater turning on"); if (this->write(cmd, 1) != i2c::ERROR_OK) { - this->status_set_error("Failed to turn on heater"); + this->status_set_error(LOG_STR("Failed to turn on heater")); } } +void SHT4XComponent::read_serial_number_() { + uint16_t buffer[2]; + if (!this->get_8bit_register(SERIAL_NUMBER_COMMAND, buffer, 2, 1)) { + ESP_LOGE(TAG, "Get serial number failed"); + this->serial_number_ = 0; + return; + } + this->serial_number_ = (uint32_t(buffer[0]) << 16) | (uint32_t(buffer[1])); + ESP_LOGD(TAG, "Serial number: %08" PRIx32, this->serial_number_); +} + void SHT4XComponent::setup() { auto err = this->write(nullptr, 0); if (err != i2c::ERROR_OK) { @@ -24,6 +36,8 @@ void SHT4XComponent::setup() { return; } + this->read_serial_number_(); + if (std::isfinite(this->duty_cycle_) && this->duty_cycle_ > 0.0f) { uint32_t heater_interval = static_cast(static_cast(this->heater_time_) / this->duty_cycle_); ESP_LOGD(TAG, "Heater interval: %" PRIu32, heater_interval); @@ -54,11 +68,18 @@ void SHT4XComponent::setup() { } void SHT4XComponent::dump_config() { - ESP_LOGCONFIG(TAG, "SHT4x:"); + ESP_LOGCONFIG(TAG, + "SHT4x:\n" + " Serial number: %08" PRIx32, + this->serial_number_); + LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } + if (this->serial_number_ == 0) { + ESP_LOGW(TAG, "Get serial number failed"); + } } void SHT4XComponent::update() { diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index accc7323be..aec0f3d7f8 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -36,7 +36,9 @@ class SHT4XComponent : public PollingComponent, public sensirion_common::Sensiri float duty_cycle_; void start_heater_(); + void read_serial_number_(); uint8_t heater_command_; + uint32_t serial_number_; sensor::Sensor *temp_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index d532bd7f44..933dd9bde9 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -49,8 +49,10 @@ void SHTCXComponent::setup() { } void SHTCXComponent::dump_config() { - ESP_LOGCONFIG(TAG, "SHTCx:"); - ESP_LOGCONFIG(TAG, " Model: %s (%04x)", to_string(this->type_), this->sensor_id_); + ESP_LOGCONFIG(TAG, + "SHTCx:\n" + " Model: %s (%04x)", + to_string(this->type_), this->sensor_id_); LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 55cadcf182..e3edda0e72 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -323,8 +323,10 @@ void Sim800LComponent::parse_cmd_(std::string message) { kick ESPHome callback now */ if (ok || message.compare(0, 6, "+CMGL:") == 0) { - ESP_LOGD(TAG, "Received SMS from: %s", this->sender_.c_str()); - ESP_LOGD(TAG, "%s", this->message_.c_str()); + ESP_LOGD(TAG, + "Received SMS from: %s\n" + "%s", + this->sender_.c_str(), this->message_.c_str()); this->sms_received_callback_.call(this->message_, this->sender_); this->state_ = STATE_RECEIVED_SMS; } else { diff --git a/esphome/components/sm16716/sm16716.cpp b/esphome/components/sm16716/sm16716.cpp index aa33b7b679..b8e293929b 100644 --- a/esphome/components/sm16716/sm16716.cpp +++ b/esphome/components/sm16716/sm16716.cpp @@ -14,11 +14,13 @@ void SM16716::setup() { this->pwm_amounts_.resize(this->num_channels_, 0); } void SM16716::dump_config() { - ESP_LOGCONFIG(TAG, "SM16716:"); + ESP_LOGCONFIG(TAG, + "SM16716:\n" + " Total number of channels: %u\n" + " Number of chips: %u", + this->num_channels_, this->num_chips_); LOG_PIN(" Data Pin: ", this->data_pin_); LOG_PIN(" Clock Pin: ", this->clock_pin_); - ESP_LOGCONFIG(TAG, " Total number of channels: %u", this->num_channels_); - ESP_LOGCONFIG(TAG, " Number of chips: %u", this->num_chips_); } void SM16716::loop() { if (!this->update_) diff --git a/esphome/components/sm2135/sm2135.cpp b/esphome/components/sm2135/sm2135.cpp index e55f836929..1293c3f321 100644 --- a/esphome/components/sm2135/sm2135.cpp +++ b/esphome/components/sm2135/sm2135.cpp @@ -34,11 +34,13 @@ void SM2135::setup() { } void SM2135::dump_config() { - ESP_LOGCONFIG(TAG, "SM2135:"); + ESP_LOGCONFIG(TAG, + "SM2135:\n" + " CW Current: %dmA\n" + " RGB Current: %dmA", + 10 + (this->cw_current_ * 5), 10 + (this->rgb_current_ * 5)); LOG_PIN(" Data Pin: ", this->data_pin_); LOG_PIN(" Clock Pin: ", this->clock_pin_); - ESP_LOGCONFIG(TAG, " CW Current: %dmA", 10 + (this->cw_current_ * 5)); - ESP_LOGCONFIG(TAG, " RGB Current: %dmA", 10 + (this->rgb_current_ * 5)); } void SM2135::write_byte_(uint8_t data) { diff --git a/esphome/components/sm2235/sm2235.cpp b/esphome/components/sm2235/sm2235.cpp index 820fcb521a..4476862318 100644 --- a/esphome/components/sm2235/sm2235.cpp +++ b/esphome/components/sm2235/sm2235.cpp @@ -15,13 +15,13 @@ void SM2235::setup() { } void SM2235::dump_config() { - ESP_LOGCONFIG(TAG, "sm2235:"); - LOG_PIN(" Data Pin: ", this->data_pin_); - LOG_PIN(" Clock Pin: ", this->clock_pin_); ESP_LOGCONFIG(TAG, + "SM2235:\n" " Color Channels Max Power: %u\n" " White Channels Max Power: %u", this->max_power_color_channels_, this->max_power_white_channels_); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); } } // namespace sm2235 diff --git a/esphome/components/sm2335/sm2335.cpp b/esphome/components/sm2335/sm2335.cpp index 0580a782f5..f860517021 100644 --- a/esphome/components/sm2335/sm2335.cpp +++ b/esphome/components/sm2335/sm2335.cpp @@ -15,13 +15,13 @@ void SM2335::setup() { } void SM2335::dump_config() { - ESP_LOGCONFIG(TAG, "sm2335:"); - LOG_PIN(" Data Pin: ", this->data_pin_); - LOG_PIN(" Clock Pin: ", this->clock_pin_); ESP_LOGCONFIG(TAG, + "sm2335:\n" " Color Channels Max Power: %u\n" " White Channels Max Power: %u", this->max_power_color_channels_, this->max_power_white_channels_); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); } } // namespace sm2335 diff --git a/esphome/components/sml/__init__.py b/esphome/components/sml/__init__.py index 936efd8561..eaeddce390 100644 --- a/esphome/components/sml/__init__.py +++ b/esphome/components/sml/__init__.py @@ -4,7 +4,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_DATA, CONF_TRIGGER_ID CODEOWNERS = ["@alengwenus"] @@ -17,7 +17,6 @@ MULTI_CONF = True CONF_SML_ID = "sml_id" CONF_OBIS_CODE = "obis_code" CONF_SERVER_ID = "server_id" -CONF_ON_DATA = "on_data" sml_ns = cg.esphome_ns.namespace("sml") diff --git a/esphome/components/sml/text_sensor/sml_text_sensor.cpp b/esphome/components/sml/text_sensor/sml_text_sensor.cpp index 64f10698f0..6ceff26fe5 100644 --- a/esphome/components/sml/text_sensor/sml_text_sensor.cpp +++ b/esphome/components/sml/text_sensor/sml_text_sensor.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "sml_text_sensor.h" #include "../sml_parser.h" +#include namespace esphome { namespace sml { @@ -21,22 +22,33 @@ void SmlTextSensor::publish_val(const ObisInfo &obis_info) { switch (value_type) { case SML_HEX: { - publish_state("0x" + bytes_repr(obis_info.value)); + // Buffer for "0x" + up to 32 bytes as hex + null + char buf[67]; + buf[0] = '0'; + buf[1] = 'x'; + // Max 32 bytes of data fit in remaining buffer ((65-1)/2) + size_t hex_bytes = std::min(obis_info.value.size(), size_t(32)); + format_hex_to(buf + 2, sizeof(buf) - 2, obis_info.value.begin(), hex_bytes); + publish_state(buf, 2 + hex_bytes * 2); break; } case SML_INT: { - publish_state(to_string(bytes_to_int(obis_info.value))); + char buf[21]; // Enough for int64_t (-9223372036854775808) + int len = snprintf(buf, sizeof(buf), "%" PRId64, bytes_to_int(obis_info.value)); + publish_state(buf, static_cast(len)); break; } case SML_BOOL: publish_state(bytes_to_uint(obis_info.value) ? "True" : "False"); break; case SML_UINT: { - publish_state(to_string(bytes_to_uint(obis_info.value))); + char buf[21]; // Enough for uint64_t (18446744073709551615) + int len = snprintf(buf, sizeof(buf), "%" PRIu64, bytes_to_uint(obis_info.value)); + publish_state(buf, static_cast(len)); break; } case SML_OCTET: { - publish_state(std::string(obis_info.value.begin(), obis_info.value.end())); + publish_state(reinterpret_cast(obis_info.value.begin()), obis_info.value.size()); break; } } diff --git a/esphome/components/sn74hc165/sn74hc165.cpp b/esphome/components/sn74hc165/sn74hc165.cpp index 416d9db293..718e0b86ed 100644 --- a/esphome/components/sn74hc165/sn74hc165.cpp +++ b/esphome/components/sn74hc165/sn74hc165.cpp @@ -64,7 +64,9 @@ float SN74HC165Component::get_setup_priority() const { return setup_priority::IO bool SN74HC165GPIOPin::digital_read() { return this->parent_->digital_read_(this->pin_) != this->inverted_; } -std::string SN74HC165GPIOPin::dump_summary() const { return str_snprintf("%u via SN74HC165", 18, pin_); } +size_t SN74HC165GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via SN74HC165", this->pin_); +} } // namespace sn74hc165 } // namespace esphome diff --git a/esphome/components/sn74hc165/sn74hc165.h b/esphome/components/sn74hc165/sn74hc165.h index 4684844687..5a3f3fe8ef 100644 --- a/esphome/components/sn74hc165/sn74hc165.h +++ b/esphome/components/sn74hc165/sn74hc165.h @@ -47,7 +47,7 @@ class SN74HC165GPIOPin : public GPIOPin, public Parented { void pin_mode(gpio::Flags flags) override {} bool digital_read() override; void digital_write(bool value) override{}; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_pin(uint16_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index fc47a6dc5e..6b5c5d9fc4 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -70,7 +70,7 @@ void SN74HC595GPIOComponent::write_gpio() { void SN74HC595SPIComponent::write_gpio() { for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) { this->enable(); - this->transfer_byte(output_byte); + this->write_byte(output_byte); this->disable(); } SN74HC595Component::write_gpio(); @@ -93,7 +93,9 @@ float SN74HC595Component::get_setup_priority() const { return setup_priority::IO void SN74HC595GPIOPin::digital_write(bool value) { this->parent_->digital_write_(this->pin_, value != this->inverted_); } -std::string SN74HC595GPIOPin::dump_summary() const { return str_snprintf("%u via SN74HC595", 18, pin_); } +size_t SN74HC595GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via SN74HC595", this->pin_); +} } // namespace sn74hc595 } // namespace esphome diff --git a/esphome/components/sn74hc595/sn74hc595.h b/esphome/components/sn74hc595/sn74hc595.h index 181015b1e6..1cf70c86b5 100644 --- a/esphome/components/sn74hc595/sn74hc595.h +++ b/esphome/components/sn74hc595/sn74hc595.h @@ -54,7 +54,7 @@ class SN74HC595GPIOPin : public GPIOPin, public Parented { void pin_mode(gpio::Flags flags) override {} bool digital_read() override { return false; } void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_pin(uint16_t pin) { pin_ = pin; } void set_inverted(bool inverted) { inverted_ = inverted; } diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 49e074a6ee..e364da78f8 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -47,6 +47,8 @@ def require_wake_loop_threadsafe() -> None: This enables the shared UDP loopback socket mechanism (~208 bytes RAM). The socket is shared across all components that use this feature. + This call is a no-op if networking is not enabled in the configuration. + IMPORTANT: This is for background thread context only, NOT ISR context. Socket operations are not safe to call from ISR handlers. @@ -56,8 +58,11 @@ def require_wake_loop_threadsafe() -> None: async def to_code(config): socket.require_wake_loop_threadsafe() """ + # Only set up once (idempotent - multiple components can call this) - if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False): + if CORE.has_networking and not CORE.data.get( + KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False + ): CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True cg.add_define("USE_WAKE_LOOP_THREADSAFE") # Consume 1 socket for the shared wake notification socket diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index c7cca62027..73be025376 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -12,43 +12,18 @@ #include #endif -namespace esphome { -namespace socket { +namespace esphome::socket { -std::string format_sockaddr(const struct sockaddr_storage &storage) { - if (storage.ss_family == AF_INET) { - const struct sockaddr_in *addr = reinterpret_cast(&storage); - char buf[INET_ADDRSTRLEN]; - if (inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)) != nullptr) - return std::string{buf}; - } -#if LWIP_IPV6 - else if (storage.ss_family == AF_INET6) { - const struct sockaddr_in6 *addr = reinterpret_cast(&storage); - char buf[INET6_ADDRSTRLEN]; - // Format IPv4-mapped IPv6 addresses as regular IPv4 addresses - if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && - addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && - inet_ntop(AF_INET, &addr->sin6_addr.un.u32_addr[3], buf, sizeof(buf)) != nullptr) { - return std::string{buf}; - } - if (inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)) != nullptr) - return std::string{buf}; - } -#endif - return {}; -} - -class BSDSocketImpl : public Socket { +class BSDSocketImpl final : public Socket { public: BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { #ifdef USE_SOCKET_SELECT_SUPPORT // Register new socket with the application for select() if monitoring requested - if (monitor_loop && fd_ >= 0) { + if (monitor_loop && this->fd_ >= 0) { // Only set loop_monitored_ to true if registration succeeds - loop_monitored_ = App.register_socket_fd(fd_); + this->loop_monitored_ = App.register_socket_fd(this->fd_); } else { - loop_monitored_ = false; + this->loop_monitored_ = false; } #else // Without select support, ignore monitor_loop parameter @@ -56,70 +31,55 @@ class BSDSocketImpl : public Socket { #endif } ~BSDSocketImpl() override { - if (!closed_) { - close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) + if (!this->closed_) { + this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) } } - int connect(const struct sockaddr *addr, socklen_t addrlen) override { return ::connect(fd_, addr, addrlen); } + int connect(const struct sockaddr *addr, socklen_t addrlen) override { return ::connect(this->fd_, addr, addrlen); } std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - return accept_impl_(addr, addrlen, false); - } - std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override { - return accept_impl_(addr, addrlen, true); - } - - private: - std::unique_ptr accept_impl_(struct sockaddr *addr, socklen_t *addrlen, bool loop_monitored) { - int fd = ::accept(fd_, addr, addrlen); + int fd = ::accept(this->fd_, addr, addrlen); if (fd == -1) return {}; - return make_unique(fd, loop_monitored); + return make_unique(fd, false); + } + std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override { + int fd = ::accept(this->fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd, true); } - public: - int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(fd_, addr, addrlen); } + int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); } int close() override { - if (!closed_) { + if (!this->closed_) { #ifdef USE_SOCKET_SELECT_SUPPORT // Unregister from select() before closing if monitored - if (loop_monitored_) { - App.unregister_socket_fd(fd_); + if (this->loop_monitored_) { + App.unregister_socket_fd(this->fd_); } #endif - int ret = ::close(fd_); - closed_ = true; + int ret = ::close(this->fd_); + this->closed_ = true; return ret; } return 0; } - int shutdown(int how) override { return ::shutdown(fd_, how); } + int shutdown(int how) override { return ::shutdown(this->fd_, how); } - int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return ::getpeername(fd_, addr, addrlen); } - std::string getpeername() override { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - int err = this->getpeername((struct sockaddr *) &storage, &len); - if (err != 0) - return {}; - return format_sockaddr(storage); + int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { + return ::getpeername(this->fd_, addr, addrlen); } - int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return ::getsockname(fd_, addr, addrlen); } - std::string getsockname() override { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - int err = this->getsockname((struct sockaddr *) &storage, &len); - if (err != 0) - return {}; - return format_sockaddr(storage); + int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { + return ::getsockname(this->fd_, addr, addrlen); } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { - return ::getsockopt(fd_, level, optname, optval, optlen); + return ::getsockopt(this->fd_, level, optname, optval, optlen); } int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { - return ::setsockopt(fd_, level, optname, optval, optlen); + return ::setsockopt(this->fd_, level, optname, optval, optlen); } - int listen(int backlog) override { return ::listen(fd_, backlog); } - ssize_t read(void *buf, size_t len) override { return ::read(fd_, buf, len); } + int listen(int backlog) override { return ::listen(this->fd_, backlog); } + ssize_t read(void *buf, size_t len) override { return ::read(this->fd_, buf, len); } ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { #if defined(USE_ESP32) || defined(USE_HOST) return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len); @@ -129,41 +89,52 @@ class BSDSocketImpl : public Socket { } ssize_t readv(const struct iovec *iov, int iovcnt) override { #if defined(USE_ESP32) - return ::lwip_readv(fd_, iov, iovcnt); + return ::lwip_readv(this->fd_, iov, iovcnt); #else - return ::readv(fd_, iov, iovcnt); + return ::readv(this->fd_, iov, iovcnt); #endif } - ssize_t write(const void *buf, size_t len) override { return ::write(fd_, buf, len); } - ssize_t send(void *buf, size_t len, int flags) { return ::send(fd_, buf, len, flags); } + ssize_t write(const void *buf, size_t len) override { return ::write(this->fd_, buf, len); } + ssize_t send(void *buf, size_t len, int flags) { return ::send(this->fd_, buf, len, flags); } ssize_t writev(const struct iovec *iov, int iovcnt) override { #if defined(USE_ESP32) - return ::lwip_writev(fd_, iov, iovcnt); + return ::lwip_writev(this->fd_, iov, iovcnt); #else - return ::writev(fd_, iov, iovcnt); + return ::writev(this->fd_, iov, iovcnt); #endif } ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { - return ::sendto(fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument) + return ::sendto(this->fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument) } int setblocking(bool blocking) override { - int fl = ::fcntl(fd_, F_GETFL, 0); + int fl = ::fcntl(this->fd_, F_GETFL, 0); if (blocking) { fl &= ~O_NONBLOCK; } else { fl |= O_NONBLOCK; } - ::fcntl(fd_, F_SETFL, fl); + ::fcntl(this->fd_, F_SETFL, fl); return 0; } - int get_fd() const override { return fd_; } + int get_fd() const override { return this->fd_; } + +#ifdef USE_SOCKET_SELECT_SUPPORT + bool ready() const override { + if (!this->loop_monitored_) + return true; + return App.is_socket_ready(this->fd_); + } +#endif protected: int fd_; - bool closed_ = false; + bool closed_{false}; +#ifdef USE_SOCKET_SELECT_SUPPORT + bool loop_monitored_{false}; +#endif }; // Helper to create a socket with optional monitoring @@ -182,7 +153,6 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return create_socket(domain, type, protocol, true); } -} // namespace socket -} // namespace esphome +} // namespace esphome::socket #endif // USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index e0d93d8e2f..429f59ceca 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -14,13 +14,35 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace socket { +#ifdef USE_ESP8266 +#include // For esp_schedule() +#endif + +namespace esphome::socket { + +#ifdef USE_ESP8266 +// Flag to signal socket activity - checked by socket_delay() to exit early +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +static volatile bool s_socket_woke = false; + +void socket_delay(uint32_t ms) { + // Use esp_delay with a callback that checks if socket data arrived. + // This allows the delay to exit early when socket_wake() is called by + // lwip recv_fn/accept_fn callbacks, reducing socket latency. + s_socket_woke = false; + esp_delay(ms, []() { return !s_socket_woke; }); +} + +void socket_wake() { + s_socket_woke = true; + esp_schedule(); +} +#endif static const char *const TAG = "socket.lwip"; // set to 1 to enable verbose lwip logging -#if 0 +#if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if) #define LWIP_LOG(msg, ...) ESP_LOGVV(TAG, "socket %p: " msg, this, ##__VA_ARGS__) #else #define LWIP_LOG(msg, ...) @@ -49,7 +71,7 @@ class LWIPRawImpl : public Socket { errno = EINVAL; return nullptr; } - int bind(const struct sockaddr *name, socklen_t addrlen) override { + int bind(const struct sockaddr *name, socklen_t addrlen) final { if (pcb_ == nullptr) { errno = EBADF; return -1; @@ -113,7 +135,7 @@ class LWIPRawImpl : public Socket { } return 0; } - int close() override { + int close() final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -130,7 +152,7 @@ class LWIPRawImpl : public Socket { pcb_ = nullptr; return 0; } - int shutdown(int how) override { + int shutdown(int how) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -156,7 +178,18 @@ class LWIPRawImpl : public Socket { return 0; } - int getpeername(struct sockaddr *name, socklen_t *addrlen) override { + int getpeername(struct sockaddr *name, socklen_t *addrlen) final { + if (pcb_ == nullptr) { + errno = ECONNRESET; + return -1; + } + if (name == nullptr || addrlen == nullptr) { + errno = EINVAL; + return -1; + } + return this->ip2sockaddr_(&pcb_->remote_ip, pcb_->remote_port, name, addrlen); + } + int getsockname(struct sockaddr *name, socklen_t *addrlen) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -167,32 +200,7 @@ class LWIPRawImpl : public Socket { } return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen); } - std::string getpeername() override { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return ""; - } - return this->format_ip_address_(pcb_->remote_ip); - } - int getsockname(struct sockaddr *name, socklen_t *addrlen) override { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return -1; - } - if (name == nullptr || addrlen == nullptr) { - errno = EINVAL; - return -1; - } - return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen); - } - std::string getsockname() override { - if (pcb_ == nullptr) { - errno = ECONNRESET; - return ""; - } - return this->format_ip_address_(pcb_->local_ip); - } - int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { + int getsockopt(int level, int optname, void *optval, socklen_t *optlen) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -226,7 +234,7 @@ class LWIPRawImpl : public Socket { errno = EINVAL; return -1; } - int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { + int setsockopt(int level, int optname, const void *optval, socklen_t optlen) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -260,7 +268,7 @@ class LWIPRawImpl : public Socket { errno = EOPNOTSUPP; return -1; } - ssize_t read(void *buf, size_t len) override { + ssize_t read(void *buf, size_t len) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -318,14 +326,15 @@ class LWIPRawImpl : public Socket { return read; } - ssize_t readv(const struct iovec *iov, int iovcnt) override { + ssize_t readv(const struct iovec *iov, int iovcnt) final { ssize_t ret = 0; for (int i = 0; i < iovcnt; i++) { ssize_t err = read(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); if (err == -1) { - if (ret != 0) + if (ret != 0) { // if we already read some don't return an error break; + } return err; } ret += err; @@ -334,6 +343,12 @@ class LWIPRawImpl : public Socket { } return ret; } + + ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) final { + errno = ENOTSUP; + return -1; + } + ssize_t internal_write(const void *buf, size_t len) { if (pcb_ == nullptr) { errno = ECONNRESET; @@ -383,13 +398,14 @@ class LWIPRawImpl : public Socket { } return 0; } - ssize_t write(const void *buf, size_t len) override { + ssize_t write(const void *buf, size_t len) final { ssize_t written = internal_write(buf, len); if (written == -1) return -1; - if (written == 0) + if (written == 0) { // no need to output if nothing written return 0; + } if (nodelay_) { int err = internal_output(); if (err == -1) @@ -397,23 +413,25 @@ class LWIPRawImpl : public Socket { } return written; } - ssize_t writev(const struct iovec *iov, int iovcnt) override { + ssize_t writev(const struct iovec *iov, int iovcnt) final { ssize_t written = 0; for (int i = 0; i < iovcnt; i++) { ssize_t err = internal_write(reinterpret_cast(iov[i].iov_base), iov[i].iov_len); if (err == -1) { - if (written != 0) + if (written != 0) { // if we already read some don't return an error break; + } return err; } written += err; if ((size_t) err != iov[i].iov_len) break; } - if (written == 0) + if (written == 0) { // no need to output if nothing written return 0; + } if (nodelay_) { int err = internal_output(); if (err == -1) @@ -421,12 +439,12 @@ class LWIPRawImpl : public Socket { } return written; } - ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { + ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) final { // return ::sendto(fd_, buf, len, flags, to, tolen); errno = ENOSYS; return -1; } - int setblocking(bool blocking) override { + int setblocking(bool blocking) final { if (pcb_ == nullptr) { errno = ECONNRESET; return -1; @@ -467,6 +485,10 @@ class LWIPRawImpl : public Socket { } else { pbuf_cat(rx_buf_, pb); } +#ifdef USE_ESP8266 + // Wake the main loop immediately so it can process the received data. + socket_wake(); +#endif return ERR_OK; } @@ -481,19 +503,6 @@ class LWIPRawImpl : public Socket { } protected: - std::string format_ip_address_(const ip_addr_t &ip) { - char buffer[50] = {}; - if (IP_IS_V4_VAL(ip)) { - inet_ntoa_r(ip, buffer, sizeof(buffer)); - } -#if LWIP_IPV6 - else if (IP_IS_V6_VAL(ip)) { - inet6_ntoa_r(ip, buffer, sizeof(buffer)); - } -#endif - return std::string(buffer); - } - int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { if (family_ == AF_INET) { if (*addrlen < sizeof(struct sockaddr_in)) { @@ -548,7 +557,7 @@ class LWIPRawImpl : public Socket { // Listening socket class - only allocates accept queue when needed (for bind+listen sockets) // This saves 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) for regular connected sockets on ESP8266/RP2040 -class LWIPRawListenImpl : public LWIPRawImpl { +class LWIPRawListenImpl final : public LWIPRawImpl { public: LWIPRawListenImpl(sa_family_t family, struct tcp_pcb *pcb) : LWIPRawImpl(family, pcb) {} @@ -606,7 +615,7 @@ class LWIPRawListenImpl : public LWIPRawImpl { } private: - err_t accept_fn(struct tcp_pcb *newpcb, err_t err) { + err_t accept_fn_(struct tcp_pcb *newpcb, err_t err) { LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err); if (err != ERR_OK || newpcb == nullptr) { // "An error code if there has been an error accepting. Only return ERR_ABRT if you have @@ -627,12 +636,16 @@ class LWIPRawListenImpl : public LWIPRawImpl { sock->init(); accepted_sockets_[accepted_socket_count_++] = std::move(sock); LWIP_LOG("Accepted connection, queue size: %d", accepted_socket_count_); +#ifdef USE_ESP8266 + // Wake the main loop immediately so it can accept the new connection. + socket_wake(); +#endif return ERR_OK; } static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) { LWIPRawListenImpl *arg_this = reinterpret_cast(arg); - return arg_this->accept_fn(newpcb, err); + return arg_this->accept_fn_(newpcb, err); } // Accept queue - holds incoming connections briefly until the event loop calls accept() @@ -670,7 +683,6 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return socket(domain, type, protocol); } -} // namespace socket -} // namespace esphome +} // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_TCP diff --git a/esphome/components/socket/lwip_sockets_impl.cpp b/esphome/components/socket/lwip_sockets_impl.cpp index f8a1cbc046..a885f243f3 100644 --- a/esphome/components/socket/lwip_sockets_impl.cpp +++ b/esphome/components/socket/lwip_sockets_impl.cpp @@ -7,41 +7,18 @@ #include #include "esphome/core/application.h" -namespace esphome { -namespace socket { +namespace esphome::socket { -std::string format_sockaddr(const struct sockaddr_storage &storage) { - if (storage.ss_family == AF_INET) { - const struct sockaddr_in *addr = reinterpret_cast(&storage); - char buf[INET_ADDRSTRLEN]; - const char *ret = lwip_inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)); - if (ret == nullptr) - return {}; - return std::string{buf}; - } -#if LWIP_IPV6 - else if (storage.ss_family == AF_INET6) { - const struct sockaddr_in6 *addr = reinterpret_cast(&storage); - char buf[INET6_ADDRSTRLEN]; - const char *ret = lwip_inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)); - if (ret == nullptr) - return {}; - return std::string{buf}; - } -#endif - return {}; -} - -class LwIPSocketImpl : public Socket { +class LwIPSocketImpl final : public Socket { public: LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { #ifdef USE_SOCKET_SELECT_SUPPORT // Register new socket with the application for select() if monitoring requested - if (monitor_loop && fd_ >= 0) { + if (monitor_loop && this->fd_ >= 0) { // Only set loop_monitored_ to true if registration succeeds - loop_monitored_ = App.register_socket_fd(fd_); + this->loop_monitored_ = App.register_socket_fd(this->fd_); } else { - loop_monitored_ = false; + this->loop_monitored_ = false; } #else // Without select support, ignore monitor_loop parameter @@ -49,93 +26,94 @@ class LwIPSocketImpl : public Socket { #endif } ~LwIPSocketImpl() override { - if (!closed_) { - close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) + if (!this->closed_) { + this->close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall) } } - int connect(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_connect(fd_, addr, addrlen); } + int connect(const struct sockaddr *addr, socklen_t addrlen) override { + return lwip_connect(this->fd_, addr, addrlen); + } std::unique_ptr accept(struct sockaddr *addr, socklen_t *addrlen) override { - return accept_impl_(addr, addrlen, false); - } - std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override { - return accept_impl_(addr, addrlen, true); - } - - private: - std::unique_ptr accept_impl_(struct sockaddr *addr, socklen_t *addrlen, bool loop_monitored) { - int fd = lwip_accept(fd_, addr, addrlen); + int fd = lwip_accept(this->fd_, addr, addrlen); if (fd == -1) return {}; - return make_unique(fd, loop_monitored); + return make_unique(fd, false); + } + std::unique_ptr accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override { + int fd = lwip_accept(this->fd_, addr, addrlen); + if (fd == -1) + return {}; + return make_unique(fd, true); } - public: - int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(fd_, addr, addrlen); } + int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); } int close() override { - if (!closed_) { + if (!this->closed_) { #ifdef USE_SOCKET_SELECT_SUPPORT // Unregister from select() before closing if monitored - if (loop_monitored_) { - App.unregister_socket_fd(fd_); + if (this->loop_monitored_) { + App.unregister_socket_fd(this->fd_); } #endif - int ret = lwip_close(fd_); - closed_ = true; + int ret = lwip_close(this->fd_); + this->closed_ = true; return ret; } return 0; } - int shutdown(int how) override { return lwip_shutdown(fd_, how); } + int shutdown(int how) override { return lwip_shutdown(this->fd_, how); } - int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return lwip_getpeername(fd_, addr, addrlen); } - std::string getpeername() override { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - int err = this->getpeername((struct sockaddr *) &storage, &len); - if (err != 0) - return {}; - return format_sockaddr(storage); + int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { + return lwip_getpeername(this->fd_, addr, addrlen); } - int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return lwip_getsockname(fd_, addr, addrlen); } - std::string getsockname() override { - struct sockaddr_storage storage; - socklen_t len = sizeof(storage); - int err = this->getsockname((struct sockaddr *) &storage, &len); - if (err != 0) - return {}; - return format_sockaddr(storage); + int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { + return lwip_getsockname(this->fd_, addr, addrlen); } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { - return lwip_getsockopt(fd_, level, optname, optval, optlen); + return lwip_getsockopt(this->fd_, level, optname, optval, optlen); } int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override { - return lwip_setsockopt(fd_, level, optname, optval, optlen); + return lwip_setsockopt(this->fd_, level, optname, optval, optlen); } - int listen(int backlog) override { return lwip_listen(fd_, backlog); } - ssize_t read(void *buf, size_t len) override { return lwip_read(fd_, buf, len); } - ssize_t readv(const struct iovec *iov, int iovcnt) override { return lwip_readv(fd_, iov, iovcnt); } - ssize_t write(const void *buf, size_t len) override { return lwip_write(fd_, buf, len); } - ssize_t send(void *buf, size_t len, int flags) { return lwip_send(fd_, buf, len, flags); } - ssize_t writev(const struct iovec *iov, int iovcnt) override { return lwip_writev(fd_, iov, iovcnt); } + int listen(int backlog) override { return lwip_listen(this->fd_, backlog); } + ssize_t read(void *buf, size_t len) override { return lwip_read(this->fd_, buf, len); } + ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { + return lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); + } + ssize_t readv(const struct iovec *iov, int iovcnt) override { return lwip_readv(this->fd_, iov, iovcnt); } + ssize_t write(const void *buf, size_t len) override { return lwip_write(this->fd_, buf, len); } + ssize_t send(void *buf, size_t len, int flags) { return lwip_send(this->fd_, buf, len, flags); } + ssize_t writev(const struct iovec *iov, int iovcnt) override { return lwip_writev(this->fd_, iov, iovcnt); } ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override { - return lwip_sendto(fd_, buf, len, flags, to, tolen); + return lwip_sendto(this->fd_, buf, len, flags, to, tolen); } int setblocking(bool blocking) override { - int fl = lwip_fcntl(fd_, F_GETFL, 0); + int fl = lwip_fcntl(this->fd_, F_GETFL, 0); if (blocking) { fl &= ~O_NONBLOCK; } else { fl |= O_NONBLOCK; } - lwip_fcntl(fd_, F_SETFL, fl); + lwip_fcntl(this->fd_, F_SETFL, fl); return 0; } - int get_fd() const override { return fd_; } + int get_fd() const override { return this->fd_; } + +#ifdef USE_SOCKET_SELECT_SUPPORT + bool ready() const override { + if (!this->loop_monitored_) + return true; + return App.is_socket_ready(this->fd_); + } +#endif protected: int fd_; - bool closed_ = false; + bool closed_{false}; +#ifdef USE_SOCKET_SELECT_SUPPORT + bool loop_monitored_{false}; +#endif }; // Helper to create a socket with optional monitoring @@ -154,7 +132,6 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol return create_socket(domain, type, protocol, true); } -} // namespace socket -} // namespace esphome +} // namespace esphome::socket #endif // USE_SOCKET_IMPL_LWIP_SOCKETS diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index 1c8e72b8fd..c92e33393b 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -6,31 +6,89 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace socket { +namespace esphome::socket { Socket::~Socket() {} -bool Socket::ready() const { -#ifdef USE_SOCKET_SELECT_SUPPORT - if (!loop_monitored_) { - // Non-monitored sockets always return true (assume data may be available) - return true; - } - - // For loop-monitored sockets, check with the Application's select() results - int fd = this->get_fd(); - if (fd < 0) { - // No valid file descriptor, assume ready (fallback behavior) - return true; - } - - return App.is_socket_ready(fd); -#else - // Without select() support, we can't monitor sockets in the loop - // Always return true (assume data may be available) - return true; +// Platform-specific inet_ntop wrappers +#if defined(USE_SOCKET_IMPL_LWIP_TCP) +// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value +static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) { + inet_ntoa_r(*reinterpret_cast(addr), buf, size); + return buf; +} +#if USE_NETWORK_IPV6 +static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) { + inet6_ntoa_r(*reinterpret_cast(addr), buf, size); + return buf; +} #endif +#elif defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +// LWIP sockets (LibreTiny, ESP32 Arduino) +static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) { + return lwip_inet_ntop(AF_INET, addr, buf, size); +} +#if USE_NETWORK_IPV6 +static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) { + return lwip_inet_ntop(AF_INET6, addr, buf, size); +} +#endif +#else +// BSD sockets (host, ESP32-IDF) +static inline const char *esphome_inet_ntop4(const void *addr, char *buf, size_t size) { + return inet_ntop(AF_INET, addr, buf, size); +} +#if USE_NETWORK_IPV6 +static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t size) { + return inet_ntop(AF_INET6, addr, buf, size); +} +#endif +#endif + +// Format sockaddr into caller-provided buffer, returns length written (excluding null) +static size_t format_sockaddr_to(const struct sockaddr_storage &storage, std::span buf) { + if (storage.ss_family == AF_INET) { + const auto *addr = reinterpret_cast(&storage); + if (esphome_inet_ntop4(&addr->sin_addr, buf.data(), buf.size()) != nullptr) + return strlen(buf.data()); + } +#if USE_NETWORK_IPV6 + else if (storage.ss_family == AF_INET6) { + const auto *addr = reinterpret_cast(&storage); +#ifndef USE_SOCKET_IMPL_LWIP_TCP + // Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP) + if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 && + addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) && + esphome_inet_ntop4(&addr->sin6_addr.un.u32_addr[3], buf.data(), buf.size()) != nullptr) { + return strlen(buf.data()); + } +#endif + if (esphome_inet_ntop6(&addr->sin6_addr, buf.data(), buf.size()) != nullptr) + return strlen(buf.data()); + } +#endif + buf[0] = '\0'; + return 0; +} + +size_t Socket::getpeername_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getpeername(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; + return 0; + } + return format_sockaddr_to(storage, buf); +} + +size_t Socket::getsockname_to(std::span buf) { + struct sockaddr_storage storage; + socklen_t len = sizeof(storage); + if (this->getsockname(reinterpret_cast(&storage), &len) != 0) { + buf[0] = '\0'; + return 0; + } + return format_sockaddr_to(storage, buf); } std::unique_ptr socket_ip(int type, int protocol) { @@ -61,9 +119,18 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri server->sin6_family = AF_INET6; server->sin6_port = htons(port); +#ifdef USE_SOCKET_IMPL_BSD_SOCKETS + // Use standard inet_pton for BSD sockets + if (inet_pton(AF_INET6, ip_address.c_str(), &server->sin6_addr) != 1) { + errno = EINVAL; + return 0; + } +#else + // Use LWIP-specific functions ip6_addr_t ip6; inet6_aton(ip_address.c_str(), &ip6); memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); +#endif return sizeof(sockaddr_in6); } #endif /* USE_NETWORK_IPV6 */ @@ -104,6 +171,5 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po return sizeof(sockaddr_in); #endif /* USE_NETWORK_IPV6 */ } -} // namespace socket -} // namespace esphome +} // namespace esphome::socket #endif diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 8f0d28362e..9f9f61de85 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -1,13 +1,22 @@ #pragma once #include +#include #include #include "esphome/core/optional.h" #include "headers.h" #if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) -namespace esphome { -namespace socket { +namespace esphome::socket { + +// Maximum length for formatted socket address string (IP address without port) +// IPv4: "255.255.255.255" = 15 chars + null = 16 +// IPv6: full address = 45 chars + null = 46 +#if USE_NETWORK_IPV6 +static constexpr size_t SOCKADDR_STR_LEN = 46; // INET6_ADDRSTRLEN +#else +static constexpr size_t SOCKADDR_STR_LEN = 16; // INET_ADDRSTRLEN +#endif class Socket { public: @@ -32,16 +41,20 @@ class Socket { virtual int shutdown(int how) = 0; virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; - virtual std::string getpeername() = 0; virtual int getsockname(struct sockaddr *addr, socklen_t *addrlen) = 0; - virtual std::string getsockname() = 0; + + /// Format peer address into a fixed-size buffer (no heap allocation) + /// Non-virtual wrapper around getpeername() - can be optimized away if unused + /// Returns number of characters written (excluding null terminator), or 0 on error + size_t getpeername_to(std::span buf); + /// Format local address into a fixed-size buffer (no heap allocation) + /// Non-virtual wrapper around getsockname() - can be optimized away if unused + size_t getsockname_to(std::span buf); virtual int getsockopt(int level, int optname, void *optval, socklen_t *optlen) = 0; virtual int setsockopt(int level, int optname, const void *optval, socklen_t optlen) = 0; virtual int listen(int backlog) = 0; virtual ssize_t read(void *buf, size_t len) = 0; -#ifdef USE_SOCKET_IMPL_BSD_SOCKETS virtual ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) = 0; -#endif virtual ssize_t readv(const struct iovec *iov, int iovcnt) = 0; virtual ssize_t write(const void *buf, size_t len) = 0; virtual ssize_t writev(const struct iovec *iov, int iovcnt) = 0; @@ -56,12 +69,7 @@ class Socket { /// Check if socket has data ready to read /// For loop-monitored sockets, checks with the Application's select() results /// For non-monitored sockets, always returns true (assumes data may be available) - bool ready() const; - - protected: -#ifdef USE_SOCKET_SELECT_SUPPORT - bool loop_monitored_{false}; ///< Whether this socket is monitored by the event loop -#endif + virtual bool ready() const { return true; } }; /// Create a socket of the given domain, type and protocol. @@ -84,6 +92,14 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri /// Set a sockaddr to the any address and specified port for the IP version used by socket_ip(). socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port); -} // namespace socket -} // namespace esphome +#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +/// Delay that can be woken early by socket activity. +/// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay. +void socket_delay(uint32_t ms); + +/// Called by lwip callbacks to signal socket activity and wake delay. +void socket_wake(); +#endif + +} // namespace esphome::socket #endif diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index cd09f31dd7..7b99086546 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -42,12 +42,17 @@ * M 6C - CRC over bytes 2 to F (Addition) \*********************************************************************************************/ #include "sonoff_d1.h" +#include "esphome/core/helpers.h" namespace esphome { namespace sonoff_d1 { static const char *const TAG = "sonoff_d1"; +// Protocol constants +static constexpr size_t SONOFF_D1_ACK_SIZE = 7; +static constexpr size_t SONOFF_D1_MAX_CMD_SIZE = 17; + uint8_t SonoffD1Output::calc_checksum_(const uint8_t *cmd, const size_t len) { uint8_t crc = 0; for (size_t i = 2; i < len - 1; i++) { @@ -86,8 +91,13 @@ bool SonoffD1Output::read_command_(uint8_t *cmd, size_t &len) { // Read a minimal packet if (this->read_array(cmd, 6)) { - ESP_LOGV(TAG, "[%04d] Reading from dimmer:", this->write_count_); - ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(cmd, 6).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(6)]; + ESP_LOGV(TAG, + "[%04d] Reading from dimmer:\n" + "[%04d] %s", + this->write_count_, this->write_count_, format_hex_pretty_to(hex_buf, cmd, 6)); +#endif if (cmd[0] != 0xAA || cmd[1] != 0x55) { ESP_LOGW(TAG, "[%04d] RX: wrong header (%x%x, must be AA55)", this->write_count_, cmd[0], cmd[1]); @@ -101,7 +111,10 @@ bool SonoffD1Output::read_command_(uint8_t *cmd, size_t &len) { return false; } if (this->read_array(&cmd[6], cmd[5] + 1 /*checksum suffix*/)) { - ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(&cmd[6], cmd[5] + 1).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf2[format_hex_pretty_size(SONOFF_D1_MAX_CMD_SIZE)]; + ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty_to(hex_buf2, &cmd[6], cmd[5] + 1)); +#endif // Check the checksum uint8_t valid_checksum = this->calc_checksum_(cmd, cmd[5] + 7); @@ -145,9 +158,10 @@ bool SonoffD1Output::read_ack_(const uint8_t *cmd, const size_t len) { ESP_LOGD(TAG, "[%04d] Acknowledge received", this->write_count_); return true; } else { + char hex_buf[format_hex_pretty_size(SONOFF_D1_ACK_SIZE)]; ESP_LOGW(TAG, "[%04d] Unexpected acknowledge received (possible clash of RF/HA commands), expected ack was:", this->write_count_); - ESP_LOGW(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(ref_buffer, sizeof(ref_buffer)).c_str()); + ESP_LOGW(TAG, "[%04d] %s", this->write_count_, format_hex_pretty_to(hex_buf, ref_buffer, sizeof(ref_buffer))); } return false; } @@ -174,8 +188,13 @@ bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_a // 2. UART command initiated by this component can clash with a command initiated by RF uint32_t retries = 10; do { - ESP_LOGV(TAG, "[%04d] Writing to the dimmer:", this->write_count_); - ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(cmd, len).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(SONOFF_D1_MAX_CMD_SIZE)]; + ESP_LOGV(TAG, + "[%04d] Writing to the dimmer:\n" + "[%04d] %s", + this->write_count_, this->write_count_, format_hex_pretty_to(hex_buf, cmd, len)); +#endif this->write_array(cmd, len); this->write_count_++; if (!needs_ack) diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp index db6b168bbc..2719172409 100644 --- a/esphome/components/sound_level/sound_level.cpp +++ b/esphome/components/sound_level/sound_level.cpp @@ -167,7 +167,7 @@ bool SoundLevelComponent::start_() { this->audio_buffer_ = audio::AudioSourceTransferBuffer::create( this->microphone_source_->get_audio_stream_info().ms_to_bytes(AUDIO_BUFFER_DURATION_MS)); if (this->audio_buffer_ == nullptr) { - this->status_momentary_error("Failed to allocate transfer buffer", 15000); + this->status_momentary_error("transfer_buffer", 15000); return false; } @@ -176,7 +176,7 @@ bool SoundLevelComponent::start_() { std::shared_ptr temp_ring_buffer = RingBuffer::create(this->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS)); if (temp_ring_buffer.use_count() == 0) { - this->status_momentary_error("Failed to allocate ring buffer", 15000); + this->status_momentary_error("ring_buffer", 15000); this->stop_(); return false; } else { diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 062bff92f8..370b4576a7 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -6,7 +6,7 @@ from pathlib import Path from esphome import automation, external_files import esphome.codegen as cg -from esphome.components import audio, esp32, media_player, network, psram, speaker +from esphome.components import audio, esp32, media_player, network, ota, psram, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -315,7 +315,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ON_VOLUME): automation.validate_automation(single=True), } ), - cv.only_with_esp_idf, + cv.only_on_esp32, _validate_repeated_speaker, _request_high_performance_networking, ) @@ -342,7 +342,7 @@ async def to_code(config): var = await media_player.new_media_player(config) await cg.register_component(var, config) - cg.add_define("USE_OTA_STATE_CALLBACK") + ota.request_ota_state_listeners() cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index dc8572ae43..8be37d740a 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -1,6 +1,6 @@ #include "audio_pipeline.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/defines.h" #include "esphome/core/hal.h" diff --git a/esphome/components/speaker/media_player/audio_pipeline.h b/esphome/components/speaker/media_player/audio_pipeline.h index 98f43fda6e..6fffde6c20 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.h +++ b/esphome/components/speaker/media_player/audio_pipeline.h @@ -1,6 +1,6 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/components/audio/audio.h" #include "esphome/components/audio/audio_reader.h" diff --git a/esphome/components/speaker/media_player/automation.h b/esphome/components/speaker/media_player/automation.h index fdf3db07f9..6270da7bd4 100644 --- a/esphome/components/speaker/media_player/automation.h +++ b/esphome/components/speaker/media_player/automation.h @@ -2,7 +2,7 @@ #include "speaker_media_player.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/components/audio/audio.h" #include "esphome/core/automation.h" diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index b45a78010a..9a3a47bac8 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -1,6 +1,6 @@ #include "speaker_media_player.h" -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "esphome/core/log.h" @@ -66,25 +66,8 @@ void SpeakerMediaPlayer::setup() { this->set_mute_state_(false); } -#ifdef USE_OTA - ota::get_global_ota_callback()->add_on_state_callback( - [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { - if (state == ota::OTA_STARTED) { - if (this->media_pipeline_ != nullptr) { - this->media_pipeline_->suspend_tasks(); - } - if (this->announcement_pipeline_ != nullptr) { - this->announcement_pipeline_->suspend_tasks(); - } - } else if (state == ota::OTA_ERROR) { - if (this->media_pipeline_ != nullptr) { - this->media_pipeline_->resume_tasks(); - } - if (this->announcement_pipeline_ != nullptr) { - this->announcement_pipeline_->resume_tasks(); - } - } - }); +#ifdef USE_OTA_STATE_LISTENER + ota::get_global_ota_callback()->add_global_state_listener(this); #endif this->announcement_pipeline_ = @@ -300,6 +283,27 @@ void SpeakerMediaPlayer::watch_media_commands_() { } } +#ifdef USE_OTA_STATE_LISTENER +void SpeakerMediaPlayer::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, + ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + if (this->media_pipeline_ != nullptr) { + this->media_pipeline_->suspend_tasks(); + } + if (this->announcement_pipeline_ != nullptr) { + this->announcement_pipeline_->suspend_tasks(); + } + } else if (state == ota::OTA_ERROR) { + if (this->media_pipeline_ != nullptr) { + this->media_pipeline_->resume_tasks(); + } + if (this->announcement_pipeline_ != nullptr) { + this->announcement_pipeline_->resume_tasks(); + } + } +} +#endif + void SpeakerMediaPlayer::loop() { this->watch_media_commands_(); diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index 967772d1a5..065926d0cf 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -1,18 +1,22 @@ #pragma once -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #include "audio_pipeline.h" #include "esphome/components/audio/audio.h" - #include "esphome/components/media_player/media_player.h" #include "esphome/components/speaker/speaker.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/preferences.h" +#ifdef USE_OTA_STATE_LISTENER +#include "esphome/components/ota/ota_backend.h" +#endif + #include #include #include @@ -39,12 +43,22 @@ struct VolumeRestoreState { bool is_muted; }; -class SpeakerMediaPlayer : public Component, public media_player::MediaPlayer { +class SpeakerMediaPlayer : public Component, + public media_player::MediaPlayer +#ifdef USE_OTA_STATE_LISTENER + , + public ota::OTAGlobalStateListener +#endif +{ public: float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } void setup() override; void loop() override; +#ifdef USE_OTA_STATE_LISTENER + void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; +#endif + // MediaPlayer implementations media_player::MediaPlayerTraits get_traits() override; bool is_muted() const override { return this->is_muted_; } diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index d803ee66dc..e890567abf 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -3,16 +3,18 @@ from typing import Any from esphome import pins import esphome.codegen as cg -from esphome.components.esp32 import only_on_variant -from esphome.components.esp32.const import ( +from esphome.components.esp32 import ( KEY_ESP32, VARIANT_ESP32C2, VARIANT_ESP32C3, + VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + only_on_variant, ) from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv @@ -47,21 +49,60 @@ SPIDevice = spi_ns.class_("SPIDevice") SPIDataRate = spi_ns.enum("SPIDataRate") SPIMode = spi_ns.enum("SPIMode") -SPI_DATA_RATE_OPTIONS = { - 80e6: SPIDataRate.DATA_RATE_80MHZ, - 40e6: SPIDataRate.DATA_RATE_40MHZ, - 20e6: SPIDataRate.DATA_RATE_20MHZ, - 10e6: SPIDataRate.DATA_RATE_10MHZ, - 8e6: SPIDataRate.DATA_RATE_8MHZ, - 5e6: SPIDataRate.DATA_RATE_5MHZ, - 4e6: SPIDataRate.DATA_RATE_4MHZ, - 2e6: SPIDataRate.DATA_RATE_2MHZ, - 1e6: SPIDataRate.DATA_RATE_1MHZ, - 2e5: SPIDataRate.DATA_RATE_200KHZ, - 75e3: SPIDataRate.DATA_RATE_75KHZ, - 1e3: SPIDataRate.DATA_RATE_1KHZ, +PLATFORM_SPI_CLOCKS = { + PLATFORM_ESP8266: 40e6, + PLATFORM_ESP32: 80e6, + PLATFORM_RP2040: 62.5e6, } -SPI_DATA_RATE_SCHEMA = cv.All(cv.frequency, cv.enum(SPI_DATA_RATE_OPTIONS)) + +MAX_DATA_RATE_ERROR = 0.05 # Max allowable actual data rate difference from requested + + +def _render_hz(value: float) -> str: + """Render a frequency in Hz as a human-readable string using Hz, KHz or MHz. + + Examples: + 500 -> "500 Hz" + 1500 -> "1.5 kHz" + 2000000 -> "2 MHz" + """ + if value >= 1e6: + unit = "MHz" + num = value / 1e6 + elif value >= 1e3: + unit = "kHz" + num = value / 1e3 + else: + unit = "Hz" + num = value + + # Format with up to 2 decimal places, then strip unnecessary trailing zeros and dot + formatted = f"{int(num)}" if unit == "Hz" else f"{num:.2f}".rstrip("0").rstrip(".") + return formatted + unit + + +def _frequency_validator(value): + platform = get_target_platform() + frequency = PLATFORM_SPI_CLOCKS[platform] + value = cv.frequency(value) + if value > frequency: + raise cv.Invalid( + f"The configured SPI data rate ({_render_hz(value)}) exceeds the maximum for this platform ({_render_hz(frequency)})" + ) + if value < 1000: + raise cv.Invalid("The configured SPI data rate must be at least 1000Hz") + divisor = round(frequency / value) + actual = frequency / divisor + error = abs(actual - value) / value + if error > MAX_DATA_RATE_ERROR: + raise cv.Invalid( + f"The configured SPI data rate ({_render_hz(value)}) is not available for this chip - closest is {_render_hz(actual)}" + ) + return value + + +SPI_DATA_RATE_SCHEMA = _frequency_validator + SPI_MODE_OPTIONS = { "MODE0": SPIMode.MODE0, @@ -128,7 +169,9 @@ def get_hw_interface_list(): if get_target_variant() in [ VARIANT_ESP32C2, VARIANT_ESP32C3, + VARIANT_ESP32C5, VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32H2, ]: return [["spi", "spi2"]] @@ -268,10 +311,11 @@ def validate_spi_config(config): # Given an SPI index, convert to a string that represents the C++ object for it. def get_spi_interface(index): - if CORE.using_esp_idf: + platform = get_target_platform() + if platform == PLATFORM_ESP32: + # ESP32 uses ESP-IDF SPI driver for both Arduino and IDF frameworks return ["SPI2_HOST", "SPI3_HOST"][index] # Arduino code follows - platform = get_target_platform() if platform == PLATFORM_RP2040: return ["&SPI", "&SPI1"][index] if index == 0: @@ -306,11 +350,11 @@ def spi_mode_schema(mode): if mode == TYPE_SINGLE: return SPI_SINGLE_SCHEMA pin_count = 4 if mode == TYPE_QUAD else 8 - onlys = [cv.only_on([PLATFORM_ESP32]), cv.only_with_esp_idf] + onlys = [cv.only_on([PLATFORM_ESP32])] if pin_count == 8: onlys.append( only_on_variant( - supported=[VARIANT_ESP32S3, VARIANT_ESP32S2, VARIANT_ESP32P4] + supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3] ) ) return cv.All( @@ -352,7 +396,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(configs): cg.add_define("USE_SPI") cg.add_global(spi_ns.using) - if CORE.using_arduino: + if CORE.using_arduino and not CORE.is_esp32: cg.add_library("SPI", None) for spi in configs: var = cg.new_Pvariable(spi[CONF_ID]) @@ -388,19 +432,20 @@ def spi_device_schema( :param mode Choose single, quad or octal mode. :return: The SPI device schema, `extend` this in your config schema. """ - schema = { - cv.GenerateID(CONF_SPI_ID): cv.use_id(TYPE_CLASS[mode]), - cv.Optional(CONF_DATA_RATE, default=default_data_rate): SPI_DATA_RATE_SCHEMA, - cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum( - SPI_MODE_OPTIONS, upper=True - ), - cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_with_esp_idf), - } - if cs_pin_required: - schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema - else: - schema[cv.Optional(CONF_CS_PIN)] = pins.gpio_output_pin_schema - return cv.Schema(schema) + cs_pin_option = cv.Required if cs_pin_required else cv.Optional + return cv.Schema( + { + cv.GenerateID(CONF_SPI_ID): cv.use_id(TYPE_CLASS[mode]), + cv.Optional( + CONF_DATA_RATE, default=default_data_rate + ): SPI_DATA_RATE_SCHEMA, + cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum( + SPI_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_on_esp32), + cs_pin_option(CONF_CS_PIN): pins.gpio_output_pin_schema, + } + ) async def register_spi_device(var, config): @@ -443,13 +488,15 @@ def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso: FILTER_SOURCE_FILES = filter_source_files_from_platform( { "spi_arduino.cpp": { - PlatformFramework.ESP32_ARDUINO, PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2040_ARDUINO, PlatformFramework.BK72XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.LN882X_ARDUINO, }, - "spi_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "spi_esp_idf.cpp": { + PlatformFramework.ESP32_ARDUINO, + PlatformFramework.ESP32_IDF, + }, } ) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 00e9845a03..36344a6d38 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace spi { +namespace esphome::spi { const char *const TAG = "spi"; @@ -65,9 +64,9 @@ void SPIComponent::setup() { void SPIComponent::dump_config() { ESP_LOGCONFIG(TAG, "SPI bus:"); - LOG_PIN(" CLK Pin: ", this->clk_pin_) - LOG_PIN(" SDI Pin: ", this->sdi_pin_) - LOG_PIN(" SDO Pin: ", this->sdo_pin_) + LOG_PIN(" CLK Pin: ", this->clk_pin_); + LOG_PIN(" SDI Pin: ", this->sdi_pin_); + LOG_PIN(" SDO Pin: ", this->sdo_pin_); for (size_t i = 0; i != this->data_pins_.size(); i++) { ESP_LOGCONFIG(TAG, " Data pin %u: GPIO%d", i, this->data_pins_[i]); } @@ -119,5 +118,4 @@ uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) { return out_data; } -} // namespace spi -} // namespace esphome +} // namespace esphome::spi diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 5bc80350da..e237cf44f4 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -7,7 +7,13 @@ #include #include -#ifdef USE_ARDUINO +#ifdef USE_ESP32 + +#include "driver/spi_master.h" + +using SPIInterface = spi_host_device_t; + +#elif defined(USE_ARDUINO) #include @@ -17,26 +23,16 @@ using SPIInterface = SPIClassRP2040 *; using SPIInterface = SPIClass *; #endif -#endif +#elif defined(CLANG_TIDY) -#ifdef USE_ESP_IDF +using SPIInterface = void *; // Stub for platforms without SPI (e.g., Zephyr) -#include "driver/spi_master.h" - -using SPIInterface = spi_host_device_t; - -#endif // USE_ESP_IDF - -#ifdef USE_ZEPHYR -// TODO supprse clang-tidy. Remove after SPI driver for nrf52 is added. -using SPIInterface = void *; -#endif +#endif // USE_ESP32 / USE_ARDUINO /** * Implementation of SPI Controller mode. */ -namespace esphome { -namespace spi { +namespace esphome::spi { /// The bit-order for SPI devices. This defines how the data read from and written to the device is interpreted. enum SPIBitOrder { @@ -124,7 +120,11 @@ class NullPin : public GPIOPin { void digital_write(bool value) override {} - std::string dump_summary() const override { return std::string(); } + size_t dump_summary(char *buffer, size_t len) const override { + if (len > 0) + buffer[0] = '\0'; + return 0; + } protected: static GPIOPin *const NULL_PIN; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -509,5 +509,4 @@ class SPIDevice : public SPIClient { template void transfer_array(std::array &data) { this->transfer_array(data.data(), N); } }; -} // namespace spi -} // namespace esphome +} // namespace esphome::spi diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp index a34e3c3c82..4267fe63ce 100644 --- a/esphome/components/spi/spi_arduino.cpp +++ b/esphome/components/spi/spi_arduino.cpp @@ -1,9 +1,8 @@ #include "spi.h" #include -namespace esphome { -namespace spi { -#ifdef USE_ARDUINO +namespace esphome::spi { +#if defined(USE_ARDUINO) && !defined(USE_ESP32) static const char *const TAG = "spi-esp-arduino"; class SPIDelegateHw : public SPIDelegate { @@ -101,6 +100,5 @@ SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo return new SPIBusHw(clk, sdo, sdi, interface); } -#endif // USE_ARDUINO -} // namespace spi -} // namespace esphome +#endif // USE_ARDUINO && !USE_ESP32 +} // namespace esphome::spi diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp index 549f516eb1..a1837fa58d 100644 --- a/esphome/components/spi/spi_esp_idf.cpp +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -1,10 +1,9 @@ #include "spi.h" #include -namespace esphome { -namespace spi { +namespace esphome::spi { -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 static const char *const TAG = "spi-esp-idf"; static const size_t MAX_TRANSFER_SIZE = 4092; // dictated by ESP-IDF API. @@ -266,6 +265,5 @@ SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo return new SPIBusHw(clk, sdo, sdi, interface, data_pins); } -#endif -} // namespace spi -} // namespace esphome +#endif // USE_ESP32 +} // namespace esphome::spi diff --git a/esphome/components/spi_device/spi_device.cpp b/esphome/components/spi_device/spi_device.cpp index dbfbc9eccb..4cc7286ba9 100644 --- a/esphome/components/spi_device/spi_device.cpp +++ b/esphome/components/spi_device/spi_device.cpp @@ -11,9 +11,11 @@ static const char *const TAG = "spi_device"; void SPIDeviceComponent::setup() { this->spi_setup(); } void SPIDeviceComponent::dump_config() { - ESP_LOGCONFIG(TAG, "SPIDevice"); + ESP_LOGCONFIG(TAG, + "SPIDevice\n" + " Mode: %d", + this->mode_); LOG_PIN(" CS pin: ", this->cs_); - ESP_LOGCONFIG(TAG, " Mode: %d", this->mode_); if (this->data_rate_ < 1000000) { ESP_LOGCONFIG(TAG, " Data rate: %" PRId32 "kHz", this->data_rate_ / 1000); } else { diff --git a/esphome/components/spi_led_strip/spi_led_strip.cpp b/esphome/components/spi_led_strip/spi_led_strip.cpp index 85c10ee87d..afb51afe3a 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.cpp +++ b/esphome/components/spi_led_strip/spi_led_strip.cpp @@ -34,8 +34,10 @@ light::LightTraits SpiLedStrip::get_traits() { return traits; } void SpiLedStrip::dump_config() { - esph_log_config(TAG, "SPI LED Strip:"); - esph_log_config(TAG, " LEDs: %d", this->num_leds_); + esph_log_config(TAG, + "SPI LED Strip:\n" + " LEDs: %d", + this->num_leds_); if (this->data_rate_ >= spi::DATA_RATE_1MHZ) { esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000)); } else { diff --git a/esphome/components/sprinkler/__init__.py b/esphome/components/sprinkler/__init__.py index 2dccb6896a..50c69f9496 100644 --- a/esphome/components/sprinkler/__init__.py +++ b/esphome/components/sprinkler/__init__.py @@ -19,6 +19,7 @@ from esphome.const import ( UNIT_MINUTE, UNIT_SECOND, ) +from esphome.helpers import docs_url AUTO_LOAD = ["number", "switch"] CODEOWNERS = ["@kbx81"] @@ -162,55 +163,9 @@ def validate_sprinkler(config): raise cv.Invalid( f"{CONF_RUN_DURATION} must be greater than {CONF_VALVE_OPEN_DELAY}" ) - if ( - CONF_PUMP_OFF_SWITCH_ID in valve and CONF_PUMP_ON_SWITCH_ID not in valve - ) or ( - CONF_PUMP_ON_SWITCH_ID in valve and CONF_PUMP_OFF_SWITCH_ID not in valve - ): + if CONF_VALVE_SWITCH_ID not in valve: raise cv.Invalid( - f"Both {CONF_PUMP_OFF_SWITCH_ID} and {CONF_PUMP_ON_SWITCH_ID} must be specified for latching pump configuration" - ) - if CONF_PUMP_SWITCH_ID in valve and ( - CONF_PUMP_OFF_SWITCH_ID in valve or CONF_PUMP_ON_SWITCH_ID in valve - ): - raise cv.Invalid( - f"Do not specify {CONF_PUMP_OFF_SWITCH_ID} or {CONF_PUMP_ON_SWITCH_ID} when using {CONF_PUMP_SWITCH_ID}" - ) - if CONF_PUMP_PULSE_DURATION not in sprinkler_controller and ( - CONF_PUMP_OFF_SWITCH_ID in valve or CONF_PUMP_ON_SWITCH_ID in valve - ): - raise cv.Invalid( - f"{CONF_PUMP_PULSE_DURATION} must be specified when using {CONF_PUMP_OFF_SWITCH_ID} and {CONF_PUMP_ON_SWITCH_ID}" - ) - if ( - CONF_VALVE_OFF_SWITCH_ID in valve - and CONF_VALVE_ON_SWITCH_ID not in valve - ) or ( - CONF_VALVE_ON_SWITCH_ID in valve - and CONF_VALVE_OFF_SWITCH_ID not in valve - ): - raise cv.Invalid( - f"Both {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID} must be specified for latching valve configuration" - ) - if CONF_VALVE_SWITCH_ID in valve and ( - CONF_VALVE_OFF_SWITCH_ID in valve or CONF_VALVE_ON_SWITCH_ID in valve - ): - raise cv.Invalid( - f"Do not specify {CONF_VALVE_OFF_SWITCH_ID} or {CONF_VALVE_ON_SWITCH_ID} when using {CONF_VALVE_SWITCH_ID}" - ) - if CONF_VALVE_PULSE_DURATION not in sprinkler_controller and ( - CONF_VALVE_OFF_SWITCH_ID in valve or CONF_VALVE_ON_SWITCH_ID in valve - ): - raise cv.Invalid( - f"{CONF_VALVE_PULSE_DURATION} must be specified when using {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID}" - ) - if ( - CONF_VALVE_SWITCH_ID not in valve - and CONF_VALVE_OFF_SWITCH_ID not in valve - and CONF_VALVE_ON_SWITCH_ID not in valve - ): - raise cv.Invalid( - f"Either {CONF_VALVE_SWITCH_ID} or {CONF_VALVE_OFF_SWITCH_ID} and {CONF_VALVE_ON_SWITCH_ID} must be specified in valve configuration" + f"{CONF_VALVE_SWITCH_ID} must be specified in valve configuration" ) if CONF_RUN_DURATION not in valve and CONF_RUN_DURATION_NUMBER not in valve: raise cv.Invalid( @@ -290,8 +245,15 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema( ), key=CONF_NAME, ), - cv.Optional(CONF_PUMP_OFF_SWITCH_ID): cv.use_id(switch.Switch), - cv.Optional(CONF_PUMP_ON_SWITCH_ID): cv.use_id(switch.Switch), + # Removed latching pump keys - accepted for validation error reporting + cv.Optional(CONF_PUMP_OFF_SWITCH_ID): cv.invalid( + f"This option was removed in 2026.1.0; for latching pumps, use {CONF_PUMP_SWITCH_ID} with an H-Bridge switch. " + f"See {docs_url('components/switch/h_bridge')} for more information" + ), + cv.Optional(CONF_PUMP_ON_SWITCH_ID): cv.invalid( + f"This option was removed in 2026.1.0; for latching pumps, use {CONF_PUMP_SWITCH_ID} with an H-Bridge switch. " + f"See {docs_url('components/switch/h_bridge')} for more information" + ), cv.Optional(CONF_PUMP_SWITCH_ID): cv.use_id(switch.Switch), cv.Optional(CONF_RUN_DURATION): cv.positive_time_period_seconds, cv.Optional(CONF_RUN_DURATION_NUMBER): cv.maybe_simple_value( @@ -321,8 +283,15 @@ SPRINKLER_VALVE_SCHEMA = cv.Schema( switch.switch_schema(SprinklerControllerSwitch), key=CONF_NAME, ), - cv.Optional(CONF_VALVE_OFF_SWITCH_ID): cv.use_id(switch.Switch), - cv.Optional(CONF_VALVE_ON_SWITCH_ID): cv.use_id(switch.Switch), + # Removed latching valve keys - accepted for validation error reporting + cv.Optional(CONF_VALVE_OFF_SWITCH_ID): cv.invalid( + f"This option was removed in 2026.1.0; for latching valves, use {CONF_VALVE_SWITCH_ID} with an H-Bridge switch. " + f"See {docs_url('components/switch/h_bridge')} for more information" + ), + cv.Optional(CONF_VALVE_ON_SWITCH_ID): cv.invalid( + f"This option was removed in 2026.1.0; for latching valves, use {CONF_VALVE_SWITCH_ID} with an H-Bridge switch. " + f"See {docs_url('components/switch/h_bridge')} for more information" + ), cv.Optional(CONF_VALVE_SWITCH_ID): cv.use_id(switch.Switch), } ) @@ -410,8 +379,15 @@ SPRINKLER_CONTROLLER_SCHEMA = cv.Schema( validate_min_max, key=CONF_NAME, ), - cv.Optional(CONF_PUMP_PULSE_DURATION): cv.positive_time_period_milliseconds, - cv.Optional(CONF_VALVE_PULSE_DURATION): cv.positive_time_period_milliseconds, + # Removed latching valve keys - accepted for validation error reporting + cv.Optional(CONF_PUMP_PULSE_DURATION): cv.invalid( + f"This option was removed in 2026.1.0; for latching pumps, use {CONF_PUMP_SWITCH_ID} with an H-Bridge switch. " + f"See {docs_url('components/switch/h_bridge')} for more information" + ), + cv.Optional(CONF_VALVE_PULSE_DURATION): cv.invalid( + f"This option was removed in 2026.1.0; for latching valves, use {CONF_VALVE_SWITCH_ID} with an H-Bridge switch. " + f"See {docs_url('components/switch/h_bridge')} for more information" + ), cv.Exclusive( CONF_PUMP_START_PUMP_DELAY, "pump_start_xxxx_delay" ): cv.positive_time_period_seconds, @@ -765,35 +741,10 @@ async def to_code(config): valve_index, valve_switch, valve[CONF_RUN_DURATION] ) ) - elif CONF_VALVE_OFF_SWITCH_ID in valve and CONF_VALVE_ON_SWITCH_ID in valve: - valve_switch_off = await cg.get_variable( - valve[CONF_VALVE_OFF_SWITCH_ID] - ) - valve_switch_on = await cg.get_variable(valve[CONF_VALVE_ON_SWITCH_ID]) - cg.add( - var.configure_valve_switch_pulsed( - valve_index, - valve_switch_off, - valve_switch_on, - sprinkler_controller[CONF_VALVE_PULSE_DURATION], - valve[CONF_RUN_DURATION], - ) - ) if CONF_PUMP_SWITCH_ID in valve: pump = await cg.get_variable(valve[CONF_PUMP_SWITCH_ID]) cg.add(var.configure_valve_pump_switch(valve_index, pump)) - elif CONF_PUMP_OFF_SWITCH_ID in valve and CONF_PUMP_ON_SWITCH_ID in valve: - pump_off = await cg.get_variable(valve[CONF_PUMP_OFF_SWITCH_ID]) - pump_on = await cg.get_variable(valve[CONF_PUMP_ON_SWITCH_ID]) - cg.add( - var.configure_valve_pump_switch_pulsed( - valve_index, - pump_off, - pump_on, - sprinkler_controller[CONF_PUMP_PULSE_DURATION], - ) - ) if CONF_RUN_DURATION_NUMBER in valve: num_rd_var = await number.new_number( diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h index d6c877ae90..b3f030805d 100644 --- a/esphome/components/sprinkler/automation.h +++ b/esphome/components/sprinkler/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/components/sprinkler/sprinkler.h" -namespace esphome { -namespace sprinkler { +namespace esphome::sprinkler { template class SetDividerAction : public Action { public: @@ -181,5 +180,4 @@ template class ResumeOrStartAction : public Action { Sprinkler *sprinkler_; }; -} // namespace sprinkler -} // namespace esphome +} // namespace esphome::sprinkler diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 8edb240a41..ca9f85abd8 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -7,75 +7,10 @@ #include #include -namespace esphome { -namespace sprinkler { +namespace esphome::sprinkler { static const char *const TAG = "sprinkler"; -SprinklerSwitch::SprinklerSwitch() {} -SprinklerSwitch::SprinklerSwitch(switch_::Switch *sprinkler_switch) : on_switch_(sprinkler_switch) {} -SprinklerSwitch::SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *on_switch, uint32_t pulse_duration) - : pulse_duration_(pulse_duration), off_switch_(off_switch), on_switch_(on_switch) {} - -bool SprinklerSwitch::is_latching_valve() { return (this->off_switch_ != nullptr) && (this->on_switch_ != nullptr); } - -void SprinklerSwitch::loop() { - if ((this->pinned_millis_) && (App.get_loop_component_start_time() > this->pinned_millis_ + this->pulse_duration_)) { - this->pinned_millis_ = 0; // reset tracker - if (this->off_switch_->state) { - this->off_switch_->turn_off(); - } - if (this->on_switch_->state) { - this->on_switch_->turn_off(); - } - } -} - -void SprinklerSwitch::turn_off() { - if (!this->state()) { // do nothing if we're already in the requested state - return; - } - if (this->off_switch_ != nullptr) { // latching valve, start a pulse - if (!this->off_switch_->state) { - this->off_switch_->turn_on(); - } - this->pinned_millis_ = millis(); - } else if (this->on_switch_ != nullptr) { // non-latching valve - this->on_switch_->turn_off(); - } - this->state_ = false; -} - -void SprinklerSwitch::turn_on() { - if (this->state()) { // do nothing if we're already in the requested state - return; - } - if (this->off_switch_ != nullptr) { // latching valve, start a pulse - if (!this->on_switch_->state) { - this->on_switch_->turn_on(); - } - this->pinned_millis_ = millis(); - } else if (this->on_switch_ != nullptr) { // non-latching valve - this->on_switch_->turn_on(); - } - this->state_ = true; -} - -bool SprinklerSwitch::state() { - if ((this->off_switch_ == nullptr) && (this->on_switch_ != nullptr)) { // latching valve is not configured... - return this->on_switch_->state; // ...so just return the pump switch state - } - return this->state_; -} - -void SprinklerSwitch::sync_valve_state(bool latch_state) { - if (this->is_latching_valve()) { - this->state_ = latch_state; - } else if (this->on_switch_ != nullptr) { - this->state_ = this->on_switch_->state; - } -} - void SprinklerControllerNumber::setup() { float value; if (!this->restore_value_) { @@ -220,8 +155,8 @@ void SprinklerValveOperator::start() { this->state_ = STARTING; // STARTING state requires both a pump and a start_delay_ if (this->start_delay_is_valve_delay_) { this->pump_on_(); - } else if (!this->pump_switch()->state()) { // if the pump is already on, wait to switch on the valve - this->valve_on_(); // to ensure consistent run time + } else if (!this->pump_switch()->state) { // if the pump is already on, wait to switch on the valve + this->valve_on_(); // to ensure consistent run time } } else { this->run_(); // there is no start_delay_, so just start the pump and valve @@ -241,8 +176,8 @@ void SprinklerValveOperator::stop() { } else { this->valve_off_(); } - if (this->pump_switch()->state()) { // if the pump is still on at this point, it may be in use... - this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time + if (this->pump_switch()->state) { // if the pump is still on at this point, it may be in use... + this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time } } else { this->kill_(); // there is no stop_delay_, so just stop the pump and valve @@ -275,7 +210,7 @@ uint32_t SprinklerValveOperator::time_remaining() { SprinklerState SprinklerValveOperator::state() { return this->state_; } -SprinklerSwitch *SprinklerValveOperator::pump_switch() { +switch_::Switch *SprinklerValveOperator::pump_switch() { if ((this->controller_ == nullptr) || (this->valve_ == nullptr)) { return nullptr; } @@ -286,48 +221,50 @@ SprinklerSwitch *SprinklerValveOperator::pump_switch() { } void SprinklerValveOperator::pump_off_() { - if ((this->valve_ == nullptr) || (this->pump_switch() == nullptr)) { // safety first! + auto *pump = this->pump_switch(); + if ((this->valve_ == nullptr) || (pump == nullptr)) { // safety first! return; } if (this->controller_ == nullptr) { // safety first! - this->pump_switch()->turn_off(); // if no controller was set, just switch off the pump + pump->turn_off(); // if no controller was set, just switch off the pump } else { // ...otherwise, do it "safely" auto state = this->state_; // this is silly, but... this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does - this->controller_->set_pump_state(this->pump_switch(), false); + this->controller_->set_pump_state(pump, false); this->state_ = state; } } void SprinklerValveOperator::pump_on_() { - if ((this->valve_ == nullptr) || (this->pump_switch() == nullptr)) { // safety first! + auto *pump = this->pump_switch(); + if ((this->valve_ == nullptr) || (pump == nullptr)) { // safety first! return; } if (this->controller_ == nullptr) { // safety first! - this->pump_switch()->turn_on(); // if no controller was set, just switch on the pump + pump->turn_on(); // if no controller was set, just switch on the pump } else { // ...otherwise, do it "safely" auto state = this->state_; // this is silly, but... this->state_ = BYPASS; // ...exclude me from the pump-in-use check that set_pump_state() does - this->controller_->set_pump_state(this->pump_switch(), true); + this->controller_->set_pump_state(pump, true); this->state_ = state; } } void SprinklerValveOperator::valve_off_() { - if (this->valve_ == nullptr) { // safety first! + if ((this->valve_ == nullptr) || (this->valve_->valve_switch == nullptr)) { // safety first! return; } - if (this->valve_->valve_switch.state()) { - this->valve_->valve_switch.turn_off(); + if (this->valve_->valve_switch->state) { + this->valve_->valve_switch->turn_off(); } } void SprinklerValveOperator::valve_on_() { - if (this->valve_ == nullptr) { // safety first! + if ((this->valve_ == nullptr) || (this->valve_->valve_switch == nullptr)) { // safety first! return; } - if (!this->valve_->valve_switch.state()) { - this->valve_->valve_switch.turn_on(); + if (!this->valve_->valve_switch->state) { + this->valve_->valve_switch->turn_on(); } } @@ -402,16 +339,11 @@ Sprinkler::Sprinkler(const std::string &name) { void Sprinkler::setup() { this->all_valves_off_(true); } void Sprinkler::loop() { - for (auto &p : this->pump_) { - p.loop(); - } - for (auto &v : this->valve_) { - v.valve_switch.loop(); - } for (auto &vo : this->valve_op_) { vo.loop(); } - if (this->prev_req_.has_request() && this->prev_req_.valve_operator()->state() == IDLE) { + if (this->prev_req_.has_request() && this->prev_req_.has_valve_operator() && + this->prev_req_.valve_operator()->state() == IDLE) { this->prev_req_.reset(); } } @@ -423,10 +355,15 @@ void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControll new_valve->controller_switch = valve_sw; new_valve->controller_switch->set_state_lambda([this, new_valve_number]() -> optional { - if (this->valve_pump_switch(new_valve_number) != nullptr) { - return this->valve_switch(new_valve_number)->state() && this->valve_pump_switch(new_valve_number)->state(); + auto *valve = this->valve_switch(new_valve_number); + auto *pump = this->valve_pump_switch(new_valve_number); + if (valve == nullptr) { + return false; } - return this->valve_switch(new_valve_number)->state(); + if (pump != nullptr) { + return valve->state && pump->state; + } + return valve->state; }); new_valve->valve_turn_off_automation = @@ -496,18 +433,7 @@ void Sprinkler::set_controller_repeat_number(SprinklerControllerNumber *repeat_n void Sprinkler::configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration) { if (this->is_a_valid_valve(valve_number)) { - this->valve_[valve_number].valve_switch.set_on_switch(valve_switch); - this->valve_[valve_number].run_duration = run_duration; - } -} - -void Sprinkler::configure_valve_switch_pulsed(size_t valve_number, switch_::Switch *valve_switch_off, - switch_::Switch *valve_switch_on, uint32_t pulse_duration, - uint32_t run_duration) { - if (this->is_a_valid_valve(valve_number)) { - this->valve_[valve_number].valve_switch.set_off_switch(valve_switch_off); - this->valve_[valve_number].valve_switch.set_on_switch(valve_switch_on); - this->valve_[valve_number].valve_switch.set_pulse_duration(pulse_duration); + this->valve_[valve_number].valve_switch = valve_switch; this->valve_[valve_number].run_duration = run_duration; } } @@ -515,31 +441,12 @@ void Sprinkler::configure_valve_switch_pulsed(size_t valve_number, switch_::Swit void Sprinkler::configure_valve_pump_switch(size_t valve_number, switch_::Switch *pump_switch) { if (this->is_a_valid_valve(valve_number)) { for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump - if (this->pump_[i].on_switch() == pump_switch) { // if the "new" pump matches one we already have... - this->valve_[valve_number].pump_switch_index = i; // ...save its index in the SprinklerSwitch vector pump_... + if (this->pump_[i] == pump_switch) { // if the "new" pump matches one we already have... + this->valve_[valve_number].pump_switch_index = i; // ...save its index in the pump vector... return; // ...and we are done } - } // if we end up here, no pumps matched, so add a new one and set the valve's SprinklerSwitch at it - this->pump_.resize(this->pump_.size() + 1); - this->pump_.back().set_on_switch(pump_switch); - this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump - } -} - -void Sprinkler::configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off, - switch_::Switch *pump_switch_on, uint32_t pulse_duration) { - if (this->is_a_valid_valve(valve_number)) { - for (size_t i = 0; i < this->pump_.size(); i++) { // check each existing registered pump - if ((this->pump_[i].off_switch() == pump_switch_off) && - (this->pump_[i].on_switch() == pump_switch_on)) { // if the "new" pump matches one we already have... - this->valve_[valve_number].pump_switch_index = i; // ...save its index in the SprinklerSwitch vector pump_... - return; // ...and we are done - } - } // if we end up here, no pumps matched, so add a new one and set the valve's SprinklerSwitch at it - this->pump_.resize(this->pump_.size() + 1); - this->pump_.back().set_off_switch(pump_switch_off); - this->pump_.back().set_on_switch(pump_switch_on); - this->pump_.back().set_pulse_duration(pulse_duration); + } // if we end up here, no pumps matched, so add a new one + this->pump_.push_back(pump_switch); this->valve_[valve_number].pump_switch_index = this->pump_.size() - 1; // save the index to the new pump } } @@ -808,11 +715,11 @@ bool Sprinkler::standby() { void Sprinkler::start_from_queue() { if (this->standby()) { - ESP_LOGD(TAG, "start_from_queue called but standby is enabled; no action taken"); + this->log_standby_warning_(LOG_STR("start_from_queue")); return; } if (this->multiplier() == 0) { - ESP_LOGD(TAG, "start_from_queue called but multiplier is set to zero; no action taken"); + this->log_multiplier_zero_warning_(LOG_STR("start_from_queue")); return; } if (this->queued_valves_.empty()) { @@ -832,11 +739,11 @@ void Sprinkler::start_from_queue() { void Sprinkler::start_full_cycle() { if (this->standby()) { - ESP_LOGD(TAG, "start_full_cycle called but standby is enabled; no action taken"); + this->log_standby_warning_(LOG_STR("start_full_cycle")); return; } if (this->multiplier() == 0) { - ESP_LOGD(TAG, "start_full_cycle called but multiplier is set to zero; no action taken"); + this->log_multiplier_zero_warning_(LOG_STR("start_full_cycle")); return; } if (this->auto_advance() && this->active_valve().has_value()) { @@ -855,11 +762,11 @@ void Sprinkler::start_full_cycle() { void Sprinkler::start_single_valve(const optional valve_number, optional run_duration) { if (this->standby()) { - ESP_LOGD(TAG, "start_single_valve called but standby is enabled; no action taken"); + this->log_standby_warning_(LOG_STR("start_single_valve")); return; } if (this->multiplier() == 0) { - ESP_LOGD(TAG, "start_single_valve called but multiplier is set to zero; no action taken"); + this->log_multiplier_zero_warning_(LOG_STR("start_single_valve")); return; } if (!valve_number.has_value() || (valve_number == this->active_valve())) { @@ -891,6 +798,11 @@ void Sprinkler::clear_queued_valves() { } void Sprinkler::next_valve() { + if (this->standby()) { + this->log_standby_warning_(LOG_STR("next_valve")); + return; + } + if (this->state_ == IDLE) { this->reset_cycle_states_(); // just in case auto-advance is switched on later } @@ -914,6 +826,11 @@ void Sprinkler::next_valve() { } void Sprinkler::previous_valve() { + if (this->standby()) { + this->log_standby_warning_(LOG_STR("previous_valve")); + return; + } + if (this->state_ == IDLE) { this->reset_cycle_states_(); // just in case auto-advance is switched on later } @@ -964,7 +881,7 @@ void Sprinkler::pause() { void Sprinkler::resume() { if (this->standby()) { - ESP_LOGD(TAG, "resume called but standby is enabled; no action taken"); + this->log_standby_warning_(LOG_STR("resume")); return; } @@ -1009,7 +926,7 @@ optional Sprinkler::active_valve_request_is_from } optional Sprinkler::active_valve() { - if (!this->valve_overlap_ && this->prev_req_.has_request() && + if (!this->valve_overlap_ && this->prev_req_.has_request() && this->prev_req_.has_valve_operator() && (this->prev_req_.valve_operator()->state() == STARTING || this->prev_req_.valve_operator()->state() == ACTIVE)) { return this->prev_req_.valve_as_opt(); } @@ -1029,11 +946,9 @@ optional Sprinkler::manual_valve() { return this->manual_valve_; } size_t Sprinkler::number_of_valves() { return this->valve_.size(); } -bool Sprinkler::is_a_valid_valve(const size_t valve_number) { - return ((valve_number >= 0) && (valve_number < this->number_of_valves())); -} +bool Sprinkler::is_a_valid_valve(const size_t valve_number) { return (valve_number < this->number_of_valves()); } -bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) { +bool Sprinkler::pump_in_use(switch_::Switch *pump_switch) { if (pump_switch == nullptr) { return false; // we can't do anything if there's nothing to check } @@ -1046,8 +961,7 @@ bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) { for (auto &vo : this->valve_op_) { // first, check if any SprinklerValveOperator has a valve dependent on this pump if ((vo.state() != BYPASS) && (vo.pump_switch() != nullptr)) { // the SprinklerValveOperator is configured with a pump; now check if it is the pump of interest - if ((vo.pump_switch()->off_switch() == pump_switch->off_switch()) && - (vo.pump_switch()->on_switch() == pump_switch->on_switch())) { + if (vo.pump_switch() == pump_switch) { // now if the SprinklerValveOperator has a pump and it is either ACTIVE, is STARTING with a valve delay or // is STOPPING with a valve delay, its pump can be considered "in use", so just return indicating this now if ((vo.state() == ACTIVE) || @@ -1062,13 +976,16 @@ bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) { this->active_req_.has_request() && (this->state_ != STOPPING)) { // ...the controller is configured to keep the pump on during a valve open delay, so just return // whether or not the next valve shares the same pump - return (pump_switch->off_switch() == this->valve_pump_switch(this->active_req_.valve())->off_switch()) && - (pump_switch->on_switch() == this->valve_pump_switch(this->active_req_.valve())->on_switch()); + auto *valve_pump = this->valve_pump_switch(this->active_req_.valve()); + if (valve_pump == nullptr) { + return false; // valve has no pump, so this pump isn't in use by it + } + return pump_switch == valve_pump; } return false; } -void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) { +void Sprinkler::set_pump_state(switch_::Switch *pump_switch, bool state) { if (pump_switch == nullptr) { return; // we can't do anything if there's nothing to check } @@ -1079,15 +996,10 @@ void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) { if (controller != this) { // dummy check if (controller->pump_in_use(pump_switch)) { hold_pump_on = true; // if another controller says it's using this pump, keep it on - // at this point we know if there exists another SprinklerSwitch that is "on" with its - // off_switch_ and on_switch_ pointers pointing to the same pair of switch objects } } } if (hold_pump_on) { - // at this point we know if there exists another SprinklerSwitch that is "on" with its - // off_switch_ and on_switch_ pointers pointing to the same pair of switch objects... - pump_switch->sync_valve_state(true); // ...so ensure our state is consistent ESP_LOGD(TAG, "Leaving pump on because another controller instance is using it"); } @@ -1095,8 +1007,6 @@ void Sprinkler::set_pump_state(SprinklerSwitch *pump_switch, bool state) { pump_switch->turn_on(); } else if (!hold_pump_on && !this->pump_in_use(pump_switch)) { pump_switch->turn_off(); - } else if (hold_pump_on) { // we must assume the other controller will switch off the pump when done... - pump_switch->sync_valve_state(false); // ...this only impacts latching valves } } @@ -1262,23 +1172,23 @@ SprinklerControllerSwitch *Sprinkler::enable_switch(size_t valve_number) { return nullptr; } -SprinklerSwitch *Sprinkler::valve_switch(const size_t valve_number) { +switch_::Switch *Sprinkler::valve_switch(const size_t valve_number) { if (this->is_a_valid_valve(valve_number)) { - return &this->valve_[valve_number].valve_switch; + return this->valve_[valve_number].valve_switch; } return nullptr; } -SprinklerSwitch *Sprinkler::valve_pump_switch(const size_t valve_number) { +switch_::Switch *Sprinkler::valve_pump_switch(const size_t valve_number) { if (this->is_a_valid_valve(valve_number) && this->valve_[valve_number].pump_switch_index.has_value()) { - return &this->pump_[this->valve_[valve_number].pump_switch_index.value()]; + return this->pump_[this->valve_[valve_number].pump_switch_index.value()]; } return nullptr; } -SprinklerSwitch *Sprinkler::valve_pump_switch_by_pump_index(size_t pump_index) { +switch_::Switch *Sprinkler::valve_pump_switch_by_pump_index(size_t pump_index) { if (pump_index < this->pump_.size()) { - return &this->pump_[pump_index]; + return this->pump_[pump_index]; } return nullptr; } @@ -1426,8 +1336,8 @@ void Sprinkler::start_valve_(SprinklerValveRunRequest *req) { if (vo.state() == IDLE) { auto run_duration = req->run_duration() ? req->run_duration() : this->valve_run_duration_adjusted(req->valve()); ESP_LOGD(TAG, "%s is starting valve %zu for %" PRIu32 " seconds, cycle %" PRIu32 " of %" PRIu32, - this->req_as_str_(req->request_is_from()).c_str(), req->valve(), run_duration, this->repeat_count_ + 1, - this->repeat().value_or(0) + 1); + LOG_STR_ARG(this->req_as_str_(req->request_is_from())), req->valve(), run_duration, + this->repeat_count_ + 1, this->repeat().value_or(0) + 1); req->set_valve_operator(&vo); vo.set_controller(this); vo.set_valve(&this->valve_[req->valve()]); @@ -1442,8 +1352,9 @@ void Sprinkler::start_valve_(SprinklerValveRunRequest *req) { void Sprinkler::all_valves_off_(const bool include_pump) { for (size_t valve_index = 0; valve_index < this->number_of_valves(); valve_index++) { - if (this->valve_[valve_index].valve_switch.state()) { - this->valve_[valve_index].valve_switch.turn_off(); + auto *valve_sw = this->valve_[valve_index].valve_switch; + if ((valve_sw != nullptr) && valve_sw->state) { + valve_sw->turn_off(); } if (include_pump) { this->set_pump_state(this->valve_pump_switch(valve_index), false); @@ -1488,7 +1399,7 @@ void Sprinkler::fsm_kick_() { } void Sprinkler::fsm_transition_() { - ESP_LOGVV(TAG, "fsm_transition_ called; state is %s", this->state_as_str_(this->state_).c_str()); + ESP_LOGVV(TAG, "fsm_transition_ called; state is %s", LOG_STR_ARG(this->state_as_str_(this->state_))); switch (this->state_) { case IDLE: // the system was off -> start it up // advances to ACTIVE @@ -1502,8 +1413,11 @@ void Sprinkler::fsm_transition_() { case STARTING: { // follows valve open delay interval - this->set_timer_duration_(sprinkler::TIMER_SM, - this->active_req_.run_duration() - this->switching_delay_.value_or(0)); + uint32_t timer_duration = this->active_req_.run_duration(); + if (timer_duration > this->switching_delay_.value_or(0)) { + timer_duration -= this->switching_delay_.value_or(0); + } + this->set_timer_duration_(sprinkler::TIMER_SM, timer_duration); this->start_timer_(sprinkler::TIMER_SM); this->start_valve_(&this->active_req_); this->state_ = ACTIVE; @@ -1531,7 +1445,7 @@ void Sprinkler::fsm_transition_() { this->set_timer_duration_(sprinkler::TIMER_SM, this->manual_selection_delay_.value_or(1)); this->start_timer_(sprinkler::TIMER_SM); } - ESP_LOGVV(TAG, "fsm_transition_ complete; new state is %s", this->state_as_str_(this->state_).c_str()); + ESP_LOGVV(TAG, "fsm_transition_ complete; new state is %s", LOG_STR_ARG(this->state_as_str_(this->state_))); } void Sprinkler::fsm_transition_from_shutdown_() { @@ -1543,8 +1457,11 @@ void Sprinkler::fsm_transition_from_shutdown_() { this->active_req_.set_run_duration(this->next_req_.run_duration()); this->next_req_.reset(); - this->set_timer_duration_(sprinkler::TIMER_SM, - this->active_req_.run_duration() - this->switching_delay_.value_or(0)); + uint32_t timer_duration = this->active_req_.run_duration(); + if (timer_duration > this->switching_delay_.value_or(0)) { + timer_duration -= this->switching_delay_.value_or(0); + } + this->set_timer_duration_(sprinkler::TIMER_SM, timer_duration); this->start_timer_(sprinkler::TIMER_SM); this->start_valve_(&this->active_req_); this->state_ = ACTIVE; @@ -1571,8 +1488,9 @@ void Sprinkler::fsm_transition_from_valve_run_() { this->load_next_valve_run_request_(this->active_req_.valve()); if (this->next_req_.has_request()) { // there is another valve to run... - bool same_pump = - this->valve_pump_switch(this->active_req_.valve()) == this->valve_pump_switch(this->next_req_.valve()); + auto *active_pump = this->valve_pump_switch(this->active_req_.valve()); + auto *next_pump = this->valve_pump_switch(this->next_req_.valve()); + bool same_pump = (active_pump != nullptr) && (next_pump != nullptr) && (active_pump == next_pump); this->active_req_.set_valve(this->next_req_.valve()); this->active_req_.set_request_from(this->next_req_.request_is_from()); @@ -1581,8 +1499,11 @@ void Sprinkler::fsm_transition_from_valve_run_() { // this->state_ = ACTIVE; // state isn't changing if (this->valve_overlap_ || !this->switching_delay_.has_value()) { - this->set_timer_duration_(sprinkler::TIMER_SM, - this->active_req_.run_duration() - this->switching_delay_.value_or(0)); + uint32_t timer_duration = this->active_req_.run_duration(); + if (timer_duration > this->switching_delay_.value_or(0)) { + timer_duration -= this->switching_delay_.value_or(0); + } + this->set_timer_duration_(sprinkler::TIMER_SM, timer_duration); this->start_timer_(sprinkler::TIMER_SM); this->start_valve_(&this->active_req_); } else { @@ -1605,41 +1526,49 @@ void Sprinkler::fsm_transition_to_shutdown_() { this->start_timer_(sprinkler::TIMER_SM); } -std::string Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) { +void Sprinkler::log_standby_warning_(const LogString *method_name) { + ESP_LOGW(TAG, "%s called but standby is enabled; no action taken", LOG_STR_ARG(method_name)); +} + +void Sprinkler::log_multiplier_zero_warning_(const LogString *method_name) { + ESP_LOGW(TAG, "%s called but multiplier is set to zero; no action taken", LOG_STR_ARG(method_name)); +} + +const LogString *Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) { switch (origin) { case USER: - return "USER"; + return LOG_STR("USER"); case CYCLE: - return "CYCLE"; + return LOG_STR("CYCLE"); case QUEUE: - return "QUEUE"; + return LOG_STR("QUEUE"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } -std::string Sprinkler::state_as_str_(SprinklerState state) { +const LogString *Sprinkler::state_as_str_(SprinklerState state) { switch (state) { case IDLE: - return "IDLE"; + return LOG_STR("IDLE"); case STARTING: - return "STARTING"; + return LOG_STR("STARTING"); case ACTIVE: - return "ACTIVE"; + return LOG_STR("ACTIVE"); case STOPPING: - return "STOPPING"; + return LOG_STR("STOPPING"); case BYPASS: - return "BYPASS"; + return LOG_STR("BYPASS"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } @@ -1724,10 +1653,6 @@ void Sprinkler::dump_config() { " Name: %s\n" " Run Duration: %" PRIu32 " seconds", valve_number, this->valve_name(valve_number), this->valve_run_duration(valve_number)); - if (this->valve_[valve_number].valve_switch.pulse_duration()) { - ESP_LOGCONFIG(TAG, " Pulse Duration: %" PRIu32 " milliseconds", - this->valve_[valve_number].valve_switch.pulse_duration()); - } } if (!this->pump_.empty()) { ESP_LOGCONFIG(TAG, " Total number of pumps: %zu", this->pump_.size()); @@ -1737,5 +1662,4 @@ void Sprinkler::dump_config() { } } -} // namespace sprinkler -} // namespace esphome +} // namespace esphome::sprinkler diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index c4a8b8aeb8..25e2d42446 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -8,8 +8,7 @@ #include -namespace esphome { -namespace sprinkler { +namespace esphome::sprinkler { const std::string MIN_STR = "min"; @@ -36,7 +35,6 @@ enum SprinklerValveRunRequestOrigin : uint8_t { class Sprinkler; // this component class SprinklerControllerNumber; // number components that appear in the front end; based on number core class SprinklerControllerSwitch; // switches that appear in the front end; based on switch core -class SprinklerSwitch; // switches representing any valve or pump; provides abstraction for latching valves class SprinklerValveOperator; // manages all switching on/off of valves and associated pumps class SprinklerValveRunRequest; // tells the sprinkler controller what valve to run and for how long as well as what // SprinklerValveOperator is handling it @@ -44,34 +42,6 @@ template class StartSingleValveAction; template class ShutdownAction; template class ResumeOrStartAction; -class SprinklerSwitch { - public: - SprinklerSwitch(); - SprinklerSwitch(switch_::Switch *sprinkler_switch); - SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *on_switch, uint32_t pulse_duration); - - bool is_latching_valve(); // returns true if configured as a latching valve - void loop(); // called as a part of loop(), used for latching valve pulses - uint32_t pulse_duration() { return this->pulse_duration_; } - bool state(); // returns the switch's current state - void set_off_switch(switch_::Switch *off_switch) { this->off_switch_ = off_switch; } - void set_on_switch(switch_::Switch *on_switch) { this->on_switch_ = on_switch; } - void set_pulse_duration(uint32_t pulse_duration) { this->pulse_duration_ = pulse_duration; } - void sync_valve_state( - bool latch_state); // syncs internal state to switch; if latching valve, sets state to latch_state - void turn_off(); // sets internal flag and actuates the switch - void turn_on(); // sets internal flag and actuates the switch - switch_::Switch *off_switch() { return this->off_switch_; } - switch_::Switch *on_switch() { return this->on_switch_; } - - protected: - bool state_{false}; - uint32_t pulse_duration_{0}; - uint64_t pinned_millis_{0}; - switch_::Switch *off_switch_{nullptr}; // only used for latching valves - switch_::Switch *on_switch_{nullptr}; // used for both latching and non-latching valves -}; - struct SprinklerQueueItem { size_t valve_number; uint32_t run_duration; @@ -89,7 +59,7 @@ struct SprinklerValve { SprinklerControllerNumber *run_duration_number; SprinklerControllerSwitch *controller_switch; SprinklerControllerSwitch *enable_switch; - SprinklerSwitch valve_switch; + switch_::Switch *valve_switch; uint32_t run_duration; optional pump_switch_index; bool valve_cycle_complete; @@ -156,7 +126,7 @@ class SprinklerValveOperator { uint32_t run_duration(); // returns the desired run duration in seconds uint32_t time_remaining(); // returns seconds remaining (does not include stop_delay_) SprinklerState state(); // returns the valve's state/status - SprinklerSwitch *pump_switch(); // returns this SprinklerValveOperator's pump's SprinklerSwitch + switch_::Switch *pump_switch(); // returns this SprinklerValveOperator's pump switch protected: void pump_off_(); @@ -229,13 +199,9 @@ class Sprinkler : public Component { /// configure a valve's switch object and run duration. run_duration is time in seconds. void configure_valve_switch(size_t valve_number, switch_::Switch *valve_switch, uint32_t run_duration); - void configure_valve_switch_pulsed(size_t valve_number, switch_::Switch *valve_switch_off, - switch_::Switch *valve_switch_on, uint32_t pulse_duration, uint32_t run_duration); /// configure a valve's associated pump switch object void configure_valve_pump_switch(size_t valve_number, switch_::Switch *pump_switch); - void configure_valve_pump_switch_pulsed(size_t valve_number, switch_::Switch *pump_switch_off, - switch_::Switch *pump_switch_on, uint32_t pulse_duration); /// configure a valve's run duration number component void configure_valve_run_duration_number(size_t valve_number, SprinklerControllerNumber *run_duration_number); @@ -384,10 +350,10 @@ class Sprinkler : public Component { bool is_a_valid_valve(size_t valve_number); /// returns true if the pump the pointer points to is in use - bool pump_in_use(SprinklerSwitch *pump_switch); + bool pump_in_use(switch_::Switch *pump_switch); /// switches on/off a pump "safely" by checking that the new state will not conflict with another controller - void set_pump_state(SprinklerSwitch *pump_switch, bool state); + void set_pump_state(switch_::Switch *pump_switch, bool state); /// returns the amount of time in seconds required for all valves uint32_t total_cycle_time_all_valves(); @@ -420,13 +386,13 @@ class Sprinkler : public Component { SprinklerControllerSwitch *enable_switch(size_t valve_number); /// returns a pointer to a valve's switch object - SprinklerSwitch *valve_switch(size_t valve_number); + switch_::Switch *valve_switch(size_t valve_number); /// returns a pointer to a valve's pump switch object - SprinklerSwitch *valve_pump_switch(size_t valve_number); + switch_::Switch *valve_pump_switch(size_t valve_number); /// returns a pointer to a valve's pump switch object - SprinklerSwitch *valve_pump_switch_by_pump_index(size_t pump_index); + switch_::Switch *valve_pump_switch_by_pump_index(size_t pump_index); protected: /// returns true if valve number is enabled @@ -490,11 +456,17 @@ class Sprinkler : public Component { /// starts up the system from IDLE state void fsm_transition_to_shutdown_(); + /// log error message when a method is called but standby is enabled + void log_standby_warning_(const LogString *method_name); + + /// log error message when a method is called but multiplier is zero + void log_multiplier_zero_warning_(const LogString *method_name); + /// return the specified SprinklerValveRunRequestOrigin as a string - std::string req_as_str_(SprinklerValveRunRequestOrigin origin); + const LogString *req_as_str_(SprinklerValveRunRequestOrigin origin); /// return the specified SprinklerState state as a string - std::string state_as_str_(SprinklerState state); + const LogString *state_as_str_(SprinklerState state); /// Start/cancel/get status of valve timers void start_timer_(SprinklerTimerIndex timer_index); @@ -572,8 +544,8 @@ class Sprinkler : public Component { /// Queue of valves to activate next, regardless of auto-advance std::vector queued_valves_; - /// Sprinkler valve pump objects - std::vector pump_; + /// Sprinkler valve pump switches + std::vector pump_; /// Sprinkler valve objects std::vector valve_; @@ -607,5 +579,4 @@ class Sprinkler : public Component { std::unique_ptr> sprinkler_standby_turn_on_automation_; }; -} // namespace sprinkler -} // namespace esphome +} // namespace esphome::sprinkler diff --git a/esphome/components/sps30/automation.h b/esphome/components/sps30/automation.h index 67af813687..5eafc1b6c2 100644 --- a/esphome/components/sps30/automation.h +++ b/esphome/components/sps30/automation.h @@ -1,20 +1,25 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/helpers.h" #include "sps30.h" namespace esphome { namespace sps30 { -template class StartFanAction : public Action { +template class StartFanAction : public Action, public Parented { public: - explicit StartFanAction(SPS30Component *sps30) : sps30_(sps30) {} + void play(const Ts &...x) override { this->parent_->start_fan_cleaning(); } +}; - void play(const Ts &...x) override { this->sps30_->start_fan_cleaning(); } +template class StartMeasurementAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->start_measurement(); } +}; - protected: - SPS30Component *sps30_; +template class StopMeasurementAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->stop_measurement(); } }; } // namespace sps30 diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index d4f91b4188..3c967fc01b 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -38,8 +38,11 @@ SPS30Component = sps30_ns.class_( # Actions StartFanAction = sps30_ns.class_("StartFanAction", automation.Action) +StartMeasurementAction = sps30_ns.class_("StartMeasurementAction", automation.Action) +StopMeasurementAction = sps30_ns.class_("StopMeasurementAction", automation.Action) CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval" +CONF_IDLE_INTERVAL = "idle_interval" CONFIG_SCHEMA = ( cv.Schema( @@ -109,6 +112,7 @@ CONFIG_SCHEMA = ( state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.update_interval, + cv.Optional(CONF_IDLE_INTERVAL): cv.update_interval, } ) .extend(cv.polling_component_schema("60s")) @@ -164,6 +168,9 @@ async def to_code(config): if CONF_AUTO_CLEANING_INTERVAL in config: cg.add(var.set_auto_cleaning_interval(config[CONF_AUTO_CLEANING_INTERVAL])) + if CONF_IDLE_INTERVAL in config: + cg.add(var.set_idle_interval(config[CONF_IDLE_INTERVAL])) + SPS30_ACTION_SCHEMA = maybe_simple_id( { @@ -175,6 +182,13 @@ SPS30_ACTION_SCHEMA = maybe_simple_id( @automation.register_action( "sps30.start_fan_autoclean", StartFanAction, SPS30_ACTION_SCHEMA ) -async def sps30_fan_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) +@automation.register_action( + "sps30.start_measurement", StartMeasurementAction, SPS30_ACTION_SCHEMA +) +@automation.register_action( + "sps30.stop_measurement", StopMeasurementAction, SPS30_ACTION_SCHEMA +) +async def sps30_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index 21a782e49a..dbb44743d2 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -20,6 +20,7 @@ static const uint16_t SPS30_CMD_START_FAN_CLEANING = 0x5607; static const uint16_t SPS30_CMD_SOFT_RESET = 0xD304; static const size_t SERIAL_NUMBER_LENGTH = 8; static const uint8_t MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR = 5; +static const uint32_t SPS30_WARM_UP_SEC = 30; void SPS30Component::setup() { this->write_command(SPS30_CMD_SOFT_RESET); @@ -63,6 +64,8 @@ void SPS30Component::setup() { this->status_clear_warning(); this->skipped_data_read_cycles_ = 0; this->start_continuous_measurement_(); + this->next_state_ms_ = millis() + SPS30_WARM_UP_SEC * 1000; + this->next_state_ = READ; this->setup_complete_ = true; }); }); @@ -101,6 +104,9 @@ void SPS30Component::dump_config() { " Serial number: %s\n" " Firmware version v%0d.%0d", this->serial_number_, this->raw_firmware_version_ >> 8, this->raw_firmware_version_ & 0xFF); + if (this->idle_interval_.has_value()) { + ESP_LOGCONFIG(TAG, " Idle interval: %us", this->idle_interval_.value() / 1000); + } LOG_SENSOR(" ", "PM1.0 Weight Concentration", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM2.5 Weight Concentration", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM4 Weight Concentration", this->pm_4_0_sensor_); @@ -132,6 +138,26 @@ void SPS30Component::update() { } return; } + + // If its not time to take an action, do nothing. + const uint32_t update_start_ms = millis(); + if (this->next_state_ != NONE && (int32_t) (this->next_state_ms_ - update_start_ms) > 0) { + ESP_LOGD(TAG, "Sensor waiting for %ums before transitioning to state %d.", (this->next_state_ms_ - update_start_ms), + this->next_state_); + return; + } + + switch (this->next_state_) { + case WAKE: + this->start_measurement(); + return; + case NONE: + return; + case READ: + // Read logic continues below + break; + } + /// Check if measurement is ready before reading the value if (!this->write_command(SPS30_CMD_GET_DATA_READY_STATUS)) { this->status_set_warning(); @@ -211,6 +237,16 @@ void SPS30Component::update() { this->status_clear_warning(); this->skipped_data_read_cycles_ = 0; + + // Stop measurements and wait if we have an idle interval. If not using idle mode, let the next state just execute + // on next update. + if (this->idle_interval_.has_value()) { + this->stop_measurement(); + this->next_state_ms_ = millis() + this->idle_interval_.value(); + this->next_state_ = WAKE; + } else { + this->next_state_ms_ = millis(); + } }); } @@ -219,6 +255,26 @@ bool SPS30Component::start_continuous_measurement_() { ESP_LOGE(TAG, "Error initiating measurements"); return false; } + ESP_LOGD(TAG, "Started measurements"); + + // Notify the state machine to wait the warm up interval before reading + this->next_state_ms_ = millis() + SPS30_WARM_UP_SEC * 1000; + this->next_state_ = READ; + return true; +} + +bool SPS30Component::start_measurement() { return start_continuous_measurement_(); } + +bool SPS30Component::stop_measurement() { + if (!write_command(SPS30_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Error stopping measurements"); + return false; + } else { + ESP_LOGD(TAG, "Stopped measurements"); + // Exit the state machine if measurement is stopped. + this->next_state_ms_ = 0; + this->next_state_ = NONE; + } return true; } diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index 18847e16d9..4e9b90ba7e 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -23,17 +23,23 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri void set_pm_size_sensor(sensor::Sensor *pm_size) { pm_size_sensor_ = pm_size; } void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { fan_interval_ = auto_cleaning_interval; } + void set_idle_interval(uint32_t idle_interval) { idle_interval_ = idle_interval; } void setup() override; void update() override; void dump_config() override; bool start_fan_cleaning(); + bool stop_measurement(); + bool start_measurement(); protected: bool setup_complete_{false}; uint16_t raw_firmware_version_; char serial_number_[17] = {0}; /// Terminating NULL character uint8_t skipped_data_read_cycles_ = 0; + uint32_t next_state_ms_ = 0; + + enum NextState : uint8_t { WAKE, READ, NONE } next_state_{NONE}; bool start_continuous_measurement_(); @@ -58,6 +64,7 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri sensor::Sensor *pmc_10_0_sensor_{nullptr}; sensor::Sensor *pm_size_sensor_{nullptr}; optional fan_interval_; + optional idle_interval_; }; } // namespace sps30 diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 00425b853f..b0c39033e3 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -329,6 +329,12 @@ void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, Color color) { } } void SSD1306::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + uint8_t fill = color.is_on() ? 0xFF : 0x00; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; diff --git a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp index 8e490834bc..ab6fee7b02 100644 --- a/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp +++ b/esphome/components/ssd1306_i2c/ssd1306_i2c.cpp @@ -20,18 +20,18 @@ void I2CSSD1306::setup() { } void I2CSSD1306::dump_config() { LOG_DISPLAY("", "I2C SSD1306", this); - LOG_I2C_DEVICE(this); - ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); - LOG_PIN(" Reset Pin: ", this->reset_pin_); ESP_LOGCONFIG(TAG, + " Model: %s\n" " External VCC: %s\n" " Flip X: %s\n" " Flip Y: %s\n" " Offset X: %d\n" " Offset Y: %d\n" " Inverted Color: %s", - YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), this->offset_x_, - this->offset_y_, YESNO(this->invert_)); + this->model_str_(), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), + this->offset_x_, this->offset_y_, YESNO(this->invert_)); + LOG_I2C_DEVICE(this); + LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_UPDATE_INTERVAL(this); if (this->error_code_ == COMMUNICATION_FAILED) { diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index d93742c0e5..db28dfc564 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -16,19 +16,19 @@ void SPISSD1306::setup() { } void SPISSD1306::dump_config() { LOG_DISPLAY("", "SPI SSD1306", this); - ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); - LOG_PIN(" CS Pin: ", this->cs_); - LOG_PIN(" DC Pin: ", this->dc_pin_); - LOG_PIN(" Reset Pin: ", this->reset_pin_); ESP_LOGCONFIG(TAG, + " Model: %s\n" " External VCC: %s\n" " Flip X: %s\n" " Flip Y: %s\n" " Offset X: %d\n" " Offset Y: %d\n" " Inverted Color: %s", - YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), this->offset_x_, - this->offset_y_, YESNO(this->invert_)); + this->model_str_(), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_), + this->offset_x_, this->offset_y_, YESNO(this->invert_)); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_UPDATE_INTERVAL(this); } void SPISSD1306::command(uint8_t value) { diff --git a/esphome/components/ssd1322_base/ssd1322_base.cpp b/esphome/components/ssd1322_base/ssd1322_base.cpp index eb8d87998f..23576e7b2c 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.cpp +++ b/esphome/components/ssd1322_base/ssd1322_base.cpp @@ -174,6 +174,12 @@ void HOT SSD1322::draw_absolute_pixel_internal(int x, int y, Color color) { this->buffer_[pos] |= color4; } void SSD1322::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + const uint32_t color4 = display::ColorUtil::color_to_grayscale4(color); uint8_t fill = (color4 & SSD1322_COLORMASK) | ((color4 & SSD1322_COLORMASK) << SSD1322_COLORSHIFT); for (uint32_t i = 0; i < this->get_buffer_length_(); i++) diff --git a/esphome/components/ssd1322_spi/ssd1322_spi.cpp b/esphome/components/ssd1322_spi/ssd1322_spi.cpp index 6a8918353b..bc7d298922 100644 --- a/esphome/components/ssd1322_spi/ssd1322_spi.cpp +++ b/esphome/components/ssd1322_spi/ssd1322_spi.cpp @@ -19,11 +19,13 @@ void SPISSD1322::setup() { } void SPISSD1322::dump_config() { LOG_DISPLAY("", "SPI SSD1322", this); - ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + ESP_LOGCONFIG(TAG, + " Model: %s\n" + " Initial Brightness: %.2f", + this->model_str_(), this->brightness_); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); - ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); LOG_UPDATE_INTERVAL(this); } void SPISSD1322::command(uint8_t value) { diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp index 3c9dfd3324..07a5119d8f 100644 --- a/esphome/components/ssd1325_spi/ssd1325_spi.cpp +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -19,12 +19,14 @@ void SPISSD1325::setup() { } void SPISSD1325::dump_config() { LOG_DISPLAY("", "SPI SSD1325", this); - ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + ESP_LOGCONFIG(TAG, + " Model: %s\n" + " Initial Brightness: %.2f\n" + " External VCC: %s", + this->model_str_(), this->brightness_, YESNO(this->external_vcc_)); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); - ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); - ESP_LOGCONFIG(TAG, " External VCC: %s", YESNO(this->external_vcc_)); LOG_UPDATE_INTERVAL(this); } void SPISSD1325::command(uint8_t value) { diff --git a/esphome/components/ssd1327_base/ssd1327_base.cpp b/esphome/components/ssd1327_base/ssd1327_base.cpp index 6b83ec5f9d..2498bfcd67 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.cpp +++ b/esphome/components/ssd1327_base/ssd1327_base.cpp @@ -150,6 +150,12 @@ void HOT SSD1327::draw_absolute_pixel_internal(int x, int y, Color color) { this->buffer_[pos] |= color4; } void SSD1327::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + const uint32_t color4 = display::ColorUtil::color_to_grayscale4(color); uint8_t fill = (color4 & SSD1327_COLORMASK) | ((color4 & SSD1327_COLORMASK) << SSD1327_COLORSHIFT); for (uint32_t i = 0; i < this->get_buffer_length_(); i++) diff --git a/esphome/components/ssd1327_spi/ssd1327_spi.cpp b/esphome/components/ssd1327_spi/ssd1327_spi.cpp index c26238ae19..54d1a51100 100644 --- a/esphome/components/ssd1327_spi/ssd1327_spi.cpp +++ b/esphome/components/ssd1327_spi/ssd1327_spi.cpp @@ -19,11 +19,13 @@ void SPISSD1327::setup() { } void SPISSD1327::dump_config() { LOG_DISPLAY("", "SPI SSD1327", this); - ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + ESP_LOGCONFIG(TAG, + " Model: %s\n" + " Initial Brightness: %.2f", + this->model_str_(), this->brightness_); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); - ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); LOG_UPDATE_INTERVAL(this); } void SPISSD1327::command(uint8_t value) { diff --git a/esphome/components/ssd1331_base/ssd1331_base.cpp b/esphome/components/ssd1331_base/ssd1331_base.cpp index 8ee12387e4..a2993edef3 100644 --- a/esphome/components/ssd1331_base/ssd1331_base.cpp +++ b/esphome/components/ssd1331_base/ssd1331_base.cpp @@ -128,6 +128,12 @@ void HOT SSD1331::draw_absolute_pixel_internal(int x, int y, Color color) { this->buffer_[pos] = color565 & 0xff; } void SSD1331::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + const uint32_t color565 = display::ColorUtil::color_to_565(color); for (uint32_t i = 0; i < this->get_buffer_length_(); i++) { if (i & 1) { diff --git a/esphome/components/ssd1351_base/ssd1351_base.cpp b/esphome/components/ssd1351_base/ssd1351_base.cpp index 09530e8a27..69bf67f476 100644 --- a/esphome/components/ssd1351_base/ssd1351_base.cpp +++ b/esphome/components/ssd1351_base/ssd1351_base.cpp @@ -160,6 +160,12 @@ void HOT SSD1351::draw_absolute_pixel_internal(int x, int y, Color color) { this->buffer_[pos] = color565 & 0xff; } void SSD1351::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + const uint32_t color565 = display::ColorUtil::color_to_565(color); for (uint32_t i = 0; i < this->get_buffer_length_(); i++) { if (i & 1) { diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.cpp b/esphome/components/ssd1351_spi/ssd1351_spi.cpp index ffac07b82b..b046f0adcb 100644 --- a/esphome/components/ssd1351_spi/ssd1351_spi.cpp +++ b/esphome/components/ssd1351_spi/ssd1351_spi.cpp @@ -19,11 +19,13 @@ void SPISSD1351::setup() { } void SPISSD1351::dump_config() { LOG_DISPLAY("", "SPI SSD1351", this); - ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + ESP_LOGCONFIG(TAG, + " Model: %s\n" + " Initial Brightness: %.2f", + this->model_str_(), this->brightness_); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); - ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); LOG_UPDATE_INTERVAL(this); } void SPISSD1351::command(uint8_t value) { diff --git a/esphome/components/st7567_base/st7567_base.cpp b/esphome/components/st7567_base/st7567_base.cpp index 0afd2a70ba..8c47094b26 100644 --- a/esphome/components/st7567_base/st7567_base.cpp +++ b/esphome/components/st7567_base/st7567_base.cpp @@ -131,7 +131,16 @@ void HOT ST7567::draw_absolute_pixel_internal(int x, int y, Color color) { } } -void ST7567::fill(Color color) { memset(buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); } +void ST7567::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + + uint8_t fill = color.is_on() ? 0xFF : 0x00; + memset(buffer_, fill, this->get_buffer_length_()); +} void ST7567::init_reset_() { if (this->reset_pin_ != nullptr) { diff --git a/esphome/components/st7567_i2c/st7567_i2c.cpp b/esphome/components/st7567_i2c/st7567_i2c.cpp index 14c21d5148..3214339571 100644 --- a/esphome/components/st7567_i2c/st7567_i2c.cpp +++ b/esphome/components/st7567_i2c/st7567_i2c.cpp @@ -20,14 +20,14 @@ void I2CST7567::setup() { void I2CST7567::dump_config() { LOG_DISPLAY("", "I2CST7567", this); - LOG_I2C_DEVICE(this); - ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); - LOG_PIN(" Reset Pin: ", this->reset_pin_); ESP_LOGCONFIG(TAG, + " Model: %s\n" " Mirror X: %s\n" " Mirror Y: %s\n" " Invert Colors: %s", - YESNO(this->mirror_x_), YESNO(this->mirror_y_), YESNO(this->invert_colors_)); + this->model_str_(), YESNO(this->mirror_x_), YESNO(this->mirror_y_), YESNO(this->invert_colors_)); + LOG_I2C_DEVICE(this); + LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_UPDATE_INTERVAL(this); if (this->error_code_ == COMMUNICATION_FAILED) { diff --git a/esphome/components/st7567_spi/st7567_spi.cpp b/esphome/components/st7567_spi/st7567_spi.cpp index 813afcf682..7476fd7c8d 100644 --- a/esphome/components/st7567_spi/st7567_spi.cpp +++ b/esphome/components/st7567_spi/st7567_spi.cpp @@ -18,13 +18,15 @@ void SPIST7567::setup() { void SPIST7567::dump_config() { LOG_DISPLAY("", "SPI ST7567", this); - ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + ESP_LOGCONFIG(TAG, + " Model: %s\n" + " Mirror X: %s\n" + " Mirror Y: %s\n" + " Invert Colors: %s", + this->model_str_(), YESNO(this->mirror_x_), YESNO(this->mirror_y_), YESNO(this->invert_colors_)); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); - ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->mirror_x_)); - ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->mirror_y_)); - ESP_LOGCONFIG(TAG, " Invert Colors: %s", YESNO(this->invert_colors_)); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py index 497740b8d2..3078158d25 100644 --- a/esphome/components/st7701s/display.py +++ b/esphome/components/st7701s/display.py @@ -1,7 +1,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import display, spi -from esphome.components.esp32 import const, only_on_variant +from esphome.components.esp32 import VARIANT_ESP32S3, only_on_variant from esphome.components.mipi import ( CONF_DE_PIN, CONF_HSYNC_BACK_PORCH, @@ -161,8 +161,8 @@ CONFIG_SCHEMA = cv.All( } ).extend(spi.spi_device_schema(cs_pin_required=False, default_data_rate=1e6)) ), - only_on_variant(supported=[const.VARIANT_ESP32S3]), - cv.only_with_esp_idf, + cv.only_on_esp32, + only_on_variant(supported=[VARIANT_ESP32S3]), ) FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index 160ba151f7..1a74b5ce1e 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -373,15 +373,18 @@ void ST7735::display_init_(const uint8_t *addr) { void ST7735::dump_config() { LOG_DISPLAY("", "ST7735", this); - ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); - ESP_LOGD(TAG, " Buffer Size: %zu", this->get_buffer_length()); - ESP_LOGD(TAG, " Height: %d", this->height_); - ESP_LOGD(TAG, " Width: %d", this->width_); - ESP_LOGD(TAG, " ColStart: %d", this->colstart_); - ESP_LOGD(TAG, " RowStart: %d", this->rowstart_); + ESP_LOGCONFIG(TAG, + " Model: %s\n" + " Buffer Size: %zu\n" + " Height: %d\n" + " Width: %d\n" + " ColStart: %d\n" + " RowStart: %d", + this->model_str_(), this->get_buffer_length(), this->height_, this->width_, this->colstart_, + this->rowstart_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index ade9c1126f..cd0b6cabc3 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -127,15 +127,15 @@ void ST7789V::dump_config() { " Width: %u\n" " Height Offset: %u\n" " Width Offset: %u\n" - " 8-bit color mode: %s", + " 8-bit color mode: %s\n" + " Data rate: %dMHz", this->model_str_, this->height_, this->width_, this->offset_height_, this->offset_width_, - YESNO(this->eightbitcolor_)); + YESNO(this->eightbitcolor_), (unsigned) (this->data_rate_ / 1000000)); LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" B/L Pin: ", this->backlight_pin_); LOG_UPDATE_INTERVAL(this); - ESP_LOGCONFIG(TAG, " Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000)); #ifdef USE_POWER_SUPPLY ESP_LOGCONFIG(TAG, " Power Supply Configured: yes"); #endif diff --git a/esphome/components/st7920/st7920.cpp b/esphome/components/st7920/st7920.cpp index c7ce7140e3..afd7cd61bd 100644 --- a/esphome/components/st7920/st7920.cpp +++ b/esphome/components/st7920/st7920.cpp @@ -89,7 +89,16 @@ void HOT ST7920::write_display_data() { } } -void ST7920::fill(Color color) { memset(this->buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); } +void ST7920::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + + uint8_t fill = color.is_on() ? 0xFF : 0x00; + memset(this->buffer_, fill, this->get_buffer_length_()); +} void ST7920::dump_config() { LOG_DISPLAY("", "ST7920", this); diff --git a/esphome/components/stts22h/__init__.py b/esphome/components/stts22h/__init__.py new file mode 100644 index 0000000000..a33c0b554b --- /dev/null +++ b/esphome/components/stts22h/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@B48D81EFCC"] diff --git a/esphome/components/stts22h/sensor.py b/esphome/components/stts22h/sensor.py new file mode 100644 index 0000000000..094c233361 --- /dev/null +++ b/esphome/components/stts22h/sensor.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["i2c"] + +sensor_ns = cg.esphome_ns.namespace("stts22h") +stts22h = sensor_ns.class_( + "STTS22HComponent", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + stts22h, + accuracy_decimals=2, + unit_of_measurement=UNIT_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x3C)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/stts22h/stts22h.cpp b/esphome/components/stts22h/stts22h.cpp new file mode 100644 index 0000000000..2b2559c843 --- /dev/null +++ b/esphome/components/stts22h/stts22h.cpp @@ -0,0 +1,101 @@ +#include "esphome/core/log.h" +#include "stts22h.h" + +namespace esphome::stts22h { + +static const char *const TAG = "stts22h"; + +static const uint8_t WHOAMI_REG = 0x01; +static const uint8_t CTRL_REG = 0x04; +static const uint8_t TEMPERATURE_REG = 0x06; + +// CTRL_REG flags +static const uint8_t LOW_ODR_CTRL_ENABLE_FLAG = 0x80; // Flag to enable low ODR mode in CTRL_REG +static const uint8_t FREERUN_CTRL_ENABLE_FLAG = 0x04; // Flag to enable FREERUN mode in CTRL_REG +static const uint8_t ADD_INC_ENABLE_FLAG = 0x08; // Flag to enable ADD_INC (IF_ADD_INC) mode in CTRL_REG + +static const uint8_t WHOAMI_STTS22H_IDENTIFICATION = 0xA0; // ID value of STTS22H in WHOAMI_REG + +static const float SENSOR_SCALE = 0.01f; // Sensor resolution in degrees Celsius + +void STTS22HComponent::setup() { + // Check if device is a STTS22H + if (!this->is_stts22h_sensor_()) { + this->mark_failed(LOG_STR("Device is not a STTS22H sensor")); + return; + } + + this->initialize_sensor_(); +} + +void STTS22HComponent::update() { + if (this->is_failed()) { + return; + } + + this->publish_state(this->read_temperature_()); +} + +void STTS22HComponent::dump_config() { + LOG_SENSOR("", "STTS22H", this); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +float STTS22HComponent::read_temperature_() { + uint8_t temp_reg_value[2]; + if (this->read_register(TEMPERATURE_REG, temp_reg_value, 2) != i2c::NO_ERROR) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + return NAN; + } + + // Combine the two bytes into a single 16-bit signed integer + // The STTS22H temperature data is in two's complement format + int16_t temp_raw_value = static_cast(encode_uint16(temp_reg_value[1], temp_reg_value[0])); + return temp_raw_value * SENSOR_SCALE; // Apply sensor resolution +} + +bool STTS22HComponent::is_stts22h_sensor_() { + uint8_t whoami_value; + if (this->read_register(WHOAMI_REG, &whoami_value, 1) != i2c::NO_ERROR) { + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return false; + } + + if (whoami_value != WHOAMI_STTS22H_IDENTIFICATION) { + this->mark_failed(LOG_STR("Unexpected WHOAMI identifier. Sensor is not a STTS22H")); + return false; + } + + return true; +} + +void STTS22HComponent::initialize_sensor_() { + // Read current CTRL_REG configuration + uint8_t ctrl_value; + if (this->read_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Enable low ODR mode and enable ADD_INC + // Before low ODR mode can be used, + // FREERUN bit must be cleared (see sensor documentation) + ctrl_value &= ~FREERUN_CTRL_ENABLE_FLAG; // Clear FREERUN bit + if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + + // Enable LOW ODR mode and ADD_INC + ctrl_value |= LOW_ODR_CTRL_ENABLE_FLAG | ADD_INC_ENABLE_FLAG; // Set LOW ODR bit and ADD_INC bit + if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) { + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } +} + +} // namespace esphome::stts22h diff --git a/esphome/components/stts22h/stts22h.h b/esphome/components/stts22h/stts22h.h new file mode 100644 index 0000000000..442a263e49 --- /dev/null +++ b/esphome/components/stts22h/stts22h.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::stts22h { + +class STTS22HComponent : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + protected: + void initialize_sensor_(); + bool is_stts22h_sensor_(); + float read_temperature_(); +}; + +} // namespace esphome::stts22h diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index df7030461b..e8fc4e44d1 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -172,21 +172,25 @@ struct SunAtTime { void debug() const { // debug output like in example 25.a, p. 165 - ESP_LOGV(TAG, "jde: %f", jde); - ESP_LOGV(TAG, "T: %f", t); - ESP_LOGV(TAG, "L_0: %f", mean_longitude()); - ESP_LOGV(TAG, "M: %f", mean_anomaly()); - ESP_LOGV(TAG, "e: %f", eccentricity()); - ESP_LOGV(TAG, "C: %f", equation_of_center()); - ESP_LOGV(TAG, "Odot: %f", true_longitude()); - ESP_LOGV(TAG, "Omega: %f", omega()); - ESP_LOGV(TAG, "lambda: %f", apparent_longitude()); - ESP_LOGV(TAG, "epsilon_0: %f", mean_obliquity()); - ESP_LOGV(TAG, "epsilon: %f", true_obliquity()); - ESP_LOGV(TAG, "v: %f", true_anomaly()); auto eq = equatorial_coordinate(); - ESP_LOGV(TAG, "right_ascension: %f", eq.right_ascension); - ESP_LOGV(TAG, "declination: %f", eq.declination); + ESP_LOGV(TAG, + "jde: %f\n" + "T: %f\n" + "L_0: %f\n" + "M: %f\n" + "e: %f\n" + "C: %f\n" + "Odot: %f\n" + "Omega: %f\n" + "lambda: %f\n" + "epsilon_0: %f\n" + "epsilon: %f\n" + "v: %f\n" + "right_ascension: %f\n" + "declination: %f", + jde, t, mean_longitude(), mean_anomaly(), eccentricity(), equation_of_center(), true_longitude(), omega(), + apparent_longitude(), mean_obliquity(), true_obliquity(), true_anomaly(), eq.right_ascension, + eq.declination); } }; diff --git a/esphome/components/sun/text_sensor/sun_text_sensor.h b/esphome/components/sun/text_sensor/sun_text_sensor.h index ce7d21fb86..9345a32223 100644 --- a/esphome/components/sun/text_sensor/sun_text_sensor.h +++ b/esphome/components/sun/text_sensor/sun_text_sensor.h @@ -28,7 +28,9 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent { return; } - this->publish_state(res->strftime(this->format_)); + char buf[ESPTime::STRFTIME_BUFFER_SIZE]; + size_t len = res->strftime_to(buf, this->format_.c_str()); + this->publish_state(buf, len); } void dump_config() override; diff --git a/esphome/components/sun_gtil2/sun_gtil2.cpp b/esphome/components/sun_gtil2/sun_gtil2.cpp index 46b4902654..d416d9a636 100644 --- a/esphome/components/sun_gtil2/sun_gtil2.cpp +++ b/esphome/components/sun_gtil2/sun_gtil2.cpp @@ -47,14 +47,15 @@ void SunGTIL2::loop() { } } -std::string SunGTIL2::state_to_string_(uint8_t state) { +const char *SunGTIL2::state_to_string_(uint8_t state, std::span buffer) { switch (state) { case 0x02: return "Starting voltage too low"; case 0x07: return "Working"; default: - return str_sprintf("Unknown (0x%02x)", state); + snprintf(buffer.data(), buffer.size(), "Unknown (0x%02x)", state); + return buffer.data(); } } @@ -106,12 +107,11 @@ void SunGTIL2::handle_char_(uint8_t c) { #endif #ifdef USE_TEXT_SENSOR if (this->state_ != nullptr) { - this->state_->publish_state(this->state_to_string_(msg.state)); + char state_buffer[STATE_BUFFER_SIZE]; + this->state_->publish_state(this->state_to_string_(msg.state, state_buffer)); } if (this->serial_number_ != nullptr) { - std::string serial_number; - serial_number.assign(msg.serial_number, 10); - this->serial_number_->publish_state(serial_number); + this->serial_number_->publish_state(msg.serial_number, 10); } #endif } diff --git a/esphome/components/sun_gtil2/sun_gtil2.h b/esphome/components/sun_gtil2/sun_gtil2.h index 0c29ae695d..3e28527cf7 100644 --- a/esphome/components/sun_gtil2/sun_gtil2.h +++ b/esphome/components/sun_gtil2/sun_gtil2.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/core/defines.h" @@ -34,8 +36,10 @@ class SunGTIL2 : public Component, public uart::UARTDevice { void set_serial_number(text_sensor::TextSensor *text_sensor) { serial_number_ = text_sensor; } #endif + static constexpr size_t STATE_BUFFER_SIZE = 32; + protected: - std::string state_to_string_(uint8_t state); + const char *state_to_string_(uint8_t state, std::span buffer); #ifdef USE_SENSOR sensor::Sensor *ac_voltage_{nullptr}; sensor::Sensor *dc_voltage_{nullptr}; diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index 6371e35292..9319adf9ed 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -134,8 +134,8 @@ class Switch : public EntityBase, public EntityBase_DeviceClass { // Pointer first (4 bytes) ESPPreferenceObject rtc_; - // CallbackManager (12 bytes on 32-bit - contains vector) - CallbackManager state_callback_{}; + // LazyCallbackManager (4 bytes on 32-bit - nullptr when empty) + LazyCallbackManager state_callback_{}; // Small types grouped together Deduplicator publish_dedup_; // 2 bytes (bool has_value_ + bool last_value_) diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 1eb83b7a33..ed878ed0d4 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -1,6 +1,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import spi +from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET import esphome.config_validation as cv from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID from esphome.core import ID, TimePeriod @@ -14,7 +15,6 @@ CONF_SX126X_ID = "sx126x_id" CONF_BANDWIDTH = "bandwidth" CONF_BITRATE = "bitrate" CONF_CODING_RATE = "coding_rate" -CONF_CRC_ENABLE = "crc_enable" CONF_CRC_INVERTED = "crc_inverted" CONF_CRC_SIZE = "crc_size" CONF_CRC_POLYNOMIAL = "crc_polynomial" @@ -23,7 +23,6 @@ CONF_DEVIATION = "deviation" CONF_DIO1_PIN = "dio1_pin" CONF_HW_VERSION = "hw_version" CONF_MODULATION = "modulation" -CONF_ON_PACKET = "on_packet" CONF_PA_POWER = "pa_power" CONF_PA_RAMP = "pa_ramp" CONF_PAYLOAD_LENGTH = "payload_length" @@ -200,9 +199,13 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_CRC_INITIAL, default=0x1D0F): cv.All( cv.hex_int, cv.Range(min=0, max=0xFFFF) ), - cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), + cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All( + cv.frequency, cv.float_range(min=0, max=100000) + ), cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema, - cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), + cv.Required(CONF_FREQUENCY): cv.All( + cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6) + ), cv.Required(CONF_HW_VERSION): cv.one_of( "sx1261", "sx1262", "sx1268", "llcc68", lower=True ), diff --git a/esphome/components/sx126x/packet_transport/sx126x_transport.cpp b/esphome/components/sx126x/packet_transport/sx126x_transport.cpp index 2cfc4b700e..59d80bd297 100644 --- a/esphome/components/sx126x/packet_transport/sx126x_transport.cpp +++ b/esphome/components/sx126x/packet_transport/sx126x_transport.cpp @@ -12,12 +12,6 @@ void SX126xTransport::setup() { this->parent_->register_listener(this); } -void SX126xTransport::update() { - PacketTransport::update(); - this->updated_ = true; - this->resend_data_ = true; -} - void SX126xTransport::send_packet(const std::vector &buf) const { this->parent_->transmit_packet(buf); } void SX126xTransport::on_packet(const std::vector &packet, float rssi, float snr) { this->process_(packet); } diff --git a/esphome/components/sx126x/packet_transport/sx126x_transport.h b/esphome/components/sx126x/packet_transport/sx126x_transport.h index 755d30417d..640c6a76f9 100644 --- a/esphome/components/sx126x/packet_transport/sx126x_transport.h +++ b/esphome/components/sx126x/packet_transport/sx126x_transport.h @@ -11,7 +11,6 @@ namespace sx126x { class SX126xTransport : public packet_transport::PacketTransport, public Parented, public SX126xListener { public: void setup() override; - void update() override; void on_packet(const std::vector &packet, float rssi, float snr) override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index bb59f26b79..707d6f1fbf 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -527,7 +527,9 @@ void SX126x::dump_config() { this->spreading_factor_, cr, this->preamble_size_); } if (!this->sync_value_.empty()) { - ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str()); + char hex_buf[17]; // 8 bytes max = 16 hex chars + null + ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", + format_hex_to(hex_buf, this->sync_value_.data(), this->sync_value_.size())); } if (this->is_failed()) { ESP_LOGE(TAG, "Configuring SX126x failed"); diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index 77cb61f7f8..f3a9cca93f 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -1,6 +1,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import spi +from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID from esphome.core import ID @@ -16,11 +17,9 @@ CONF_BANDWIDTH = "bandwidth" CONF_BITRATE = "bitrate" CONF_BITSYNC = "bitsync" CONF_CODING_RATE = "coding_rate" -CONF_CRC_ENABLE = "crc_enable" CONF_DEVIATION = "deviation" CONF_DIO0_PIN = "dio0_pin" CONF_MODULATION = "modulation" -CONF_ON_PACKET = "on_packet" CONF_PA_PIN = "pa_pin" CONF_PA_POWER = "pa_power" CONF_PA_RAMP = "pa_ramp" @@ -197,9 +196,13 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_BITSYNC): cv.boolean, cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, - cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), + cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All( + cv.frequency, cv.float_range(min=0, max=100000) + ), cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema, - cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), + cv.Required(CONF_FREQUENCY): cv.All( + cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6) + ), cv.Required(CONF_MODULATION): cv.enum(MOD), cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), cv.Optional(CONF_PA_PIN, default="BOOST"): cv.enum(PA_PIN), diff --git a/esphome/components/sx127x/packet_transport/sx127x_transport.cpp b/esphome/components/sx127x/packet_transport/sx127x_transport.cpp index b1d014bb96..893726e816 100644 --- a/esphome/components/sx127x/packet_transport/sx127x_transport.cpp +++ b/esphome/components/sx127x/packet_transport/sx127x_transport.cpp @@ -12,12 +12,6 @@ void SX127xTransport::setup() { this->parent_->register_listener(this); } -void SX127xTransport::update() { - PacketTransport::update(); - this->updated_ = true; - this->resend_data_ = true; -} - void SX127xTransport::send_packet(const std::vector &buf) const { this->parent_->transmit_packet(buf); } void SX127xTransport::on_packet(const std::vector &packet, float rssi, float snr) { this->process_(packet); } diff --git a/esphome/components/sx127x/packet_transport/sx127x_transport.h b/esphome/components/sx127x/packet_transport/sx127x_transport.h index e27b7f8d57..6208372971 100644 --- a/esphome/components/sx127x/packet_transport/sx127x_transport.h +++ b/esphome/components/sx127x/packet_transport/sx127x_transport.h @@ -11,7 +11,6 @@ namespace sx127x { class SX127xTransport : public packet_transport::PacketTransport, public Parented, public SX127xListener { public: void setup() override; - void update() override; void on_packet(const std::vector &packet, float rssi, float snr) override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 8e6db5dc9e..3185574b1a 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -476,7 +476,9 @@ void SX127x::dump_config() { ESP_LOGCONFIG(TAG, " Payload Length: %" PRIu32, this->payload_length_); } if (!this->sync_value_.empty()) { - ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str()); + char hex_buf[17]; // 8 bytes max = 16 hex chars + null + ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", + format_hex_to(hex_buf, this->sync_value_.data(), this->sync_value_.size())); } if (this->preamble_size_ > 0 || this->preamble_detect_ > 0) { ESP_LOGCONFIG(TAG, diff --git a/esphome/components/sx1509/output/sx1509_float_output.cpp b/esphome/components/sx1509/output/sx1509_float_output.cpp index 1d2541bb46..4a24d78478 100644 --- a/esphome/components/sx1509/output/sx1509_float_output.cpp +++ b/esphome/components/sx1509/output/sx1509_float_output.cpp @@ -22,8 +22,10 @@ void SX1509FloatOutputChannel::setup() { } void SX1509FloatOutputChannel::dump_config() { - ESP_LOGCONFIG(TAG, "SX1509 PWM:"); - ESP_LOGCONFIG(TAG, " sx1509 pin: %d", this->pin_); + ESP_LOGCONFIG(TAG, + "SX1509 PWM:\n" + " sx1509 pin: %d", + this->pin_); LOG_FLOAT_OUTPUT(this); } diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h index 2afd0d0e4e..f98fc0a44f 100644 --- a/esphome/components/sx1509/sx1509.h +++ b/esphome/components/sx1509/sx1509.h @@ -40,7 +40,7 @@ class SX1509Component : public Component, void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::HARDWARE; } + float get_setup_priority() const override { return setup_priority::IO; } void loop() override; uint16_t read_key_data(); diff --git a/esphome/components/sx1509/sx1509_gpio_pin.cpp b/esphome/components/sx1509/sx1509_gpio_pin.cpp index a74c8b60b8..41a99eba4b 100644 --- a/esphome/components/sx1509/sx1509_gpio_pin.cpp +++ b/esphome/components/sx1509/sx1509_gpio_pin.cpp @@ -12,10 +12,8 @@ void SX1509GPIOPin::setup() { pin_mode(flags_); } void SX1509GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool SX1509GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void SX1509GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string SX1509GPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via sx1509", this->pin_); - return buffer; +size_t SX1509GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via sx1509", this->pin_); } } // namespace sx1509 diff --git a/esphome/components/sx1509/sx1509_gpio_pin.h b/esphome/components/sx1509/sx1509_gpio_pin.h index eb9207e882..5903af9d12 100644 --- a/esphome/components/sx1509/sx1509_gpio_pin.h +++ b/esphome/components/sx1509/sx1509_gpio_pin.h @@ -13,7 +13,7 @@ class SX1509GPIOPin : public GPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_parent(SX1509Component *parent) { this->parent_ = parent; } void set_pin(uint8_t pin) { this->pin_ = pin; } diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index 71468fa932..83ad6b2720 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -4,8 +4,7 @@ #include "esphome/core/application.h" #include "esphome/core/time.h" -namespace esphome { -namespace syslog { +namespace esphome::syslog { // Map log levels to syslog severity using an array, indexed by ESPHome log level (1-7) constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = { @@ -19,11 +18,10 @@ constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = { 7 // VERY_VERBOSE }; -void Syslog::setup() { - logger::global_logger->add_on_log_callback( - [this](int level, const char *tag, const char *message, size_t message_len) { - this->log_(level, tag, message, message_len); - }); +void Syslog::setup() { logger::global_logger->add_log_listener(this); } + +void Syslog::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + this->log_(level, tag, message, message_len); } void Syslog::log_(const int level, const char *tag, const char *message, size_t message_len) const { @@ -35,7 +33,7 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t severity = LOG_LEVEL_TO_SYSLOG_SEVERITY[level]; } int pri = this->facility_ * 8 + severity; - auto timestamp = this->time_->now().strftime("%b %e %H:%M:%S"); + size_t len = message_len; // remove color formatting if (this->strip_ && message[0] == 0x1B && len > 11) { @@ -43,9 +41,40 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t len -= 11; } - auto data = str_sprintf("<%d>%s %s %s: %.*s", pri, timestamp.c_str(), App.get_name().c_str(), tag, len, message); - this->parent_->send_packet((const uint8_t *) data.data(), data.size()); + // Build syslog packet on stack (508 bytes chosen as practical limit for syslog over UDP) + char packet[508]; + size_t offset = 0; + size_t remaining = sizeof(packet); + + // Write PRI - abort if this fails as packet would be malformed + int ret = snprintf(packet, remaining, "<%d>", pri); + if (ret <= 0 || static_cast(ret) >= remaining) { + return; + } + offset = ret; + remaining -= ret; + + // Write timestamp directly into packet (RFC 5424: use "-" if time not valid or strftime fails) + auto now = this->time_->now(); + size_t ts_written = now.is_valid() ? now.strftime(packet + offset, remaining, "%b %e %H:%M:%S") : 0; + if (ts_written > 0) { + offset += ts_written; + remaining -= ts_written; + } else if (remaining > 0) { + packet[offset++] = '-'; + remaining--; + } + + // Write hostname, tag, and message + ret = snprintf(packet + offset, remaining, " %s %s: %.*s", App.get_name().c_str(), tag, (int) len, message); + if (ret > 0) { + // snprintf returns chars that would be written; clamp to actual buffer space + offset += std::min(static_cast(ret), remaining > 0 ? remaining - 1 : 0); + } + + if (offset > 0) { + this->parent_->send_packet(reinterpret_cast(packet), offset); + } } -} // namespace syslog -} // namespace esphome +} // namespace esphome::syslog diff --git a/esphome/components/syslog/esphome_syslog.h b/esphome/components/syslog/esphome_syslog.h index e3b2f7dae5..bde6ab5ed4 100644 --- a/esphome/components/syslog/esphome_syslog.h +++ b/esphome/components/syslog/esphome_syslog.h @@ -2,16 +2,17 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/components/logger/logger.h" #include "esphome/components/udp/udp_component.h" #include "esphome/components/time/real_time_clock.h" #ifdef USE_NETWORK -namespace esphome { -namespace syslog { -class Syslog : public Component, public Parented { +namespace esphome::syslog { +class Syslog : public Component, public Parented, public logger::LogListener { public: Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {} void setup() override; + void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override; void set_strip(bool strip) { this->strip_ = strip; } void set_facility(int facility) { this->facility_ = facility; } @@ -22,6 +23,5 @@ class Syslog : public Component, public Parented { bool strip_{true}; int facility_{16}; }; -} // namespace syslog -} // namespace esphome +} // namespace esphome::syslog #endif diff --git a/esphome/components/tca9548a/__init__.py b/esphome/components/tca9548a/__init__.py index cef779de2e..72973a54ad 100644 --- a/esphome/components/tca9548a/__init__.py +++ b/esphome/components/tca9548a/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv -from esphome.const import CONF_CHANNEL, CONF_CHANNELS, CONF_ID, CONF_SCAN +from esphome.const import CONF_CHANNEL, CONF_CHANNELS, CONF_ID CODEOWNERS = ["@andreashergert1984"] @@ -18,7 +18,6 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(TCA9548AComponent), - cv.Optional(CONF_SCAN): cv.invalid("This option has been removed"), cv.Optional(CONF_CHANNELS, default=[]): cv.ensure_list( { cv.Required(CONF_BUS_ID): cv.declare_id(TCA9548AChannel), diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp index c3449ce254..376de6a370 100644 --- a/esphome/components/tca9555/tca9555.cpp +++ b/esphome/components/tca9555/tca9555.cpp @@ -138,7 +138,9 @@ void TCA9555GPIOPin::setup() { this->pin_mode(this->flags_); } void TCA9555GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool TCA9555GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void TCA9555GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } -std::string TCA9555GPIOPin::dump_summary() const { return str_sprintf("%u via TCA9555", this->pin_); } +size_t TCA9555GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via TCA9555", this->pin_); +} } // namespace tca9555 } // namespace esphome diff --git a/esphome/components/tca9555/tca9555.h b/esphome/components/tca9555/tca9555.h index 0c236ae4e3..9f7273b1e7 100644 --- a/esphome/components/tca9555/tca9555.h +++ b/esphome/components/tca9555/tca9555.h @@ -48,7 +48,7 @@ class TCA9555GPIOPin : public GPIOPin, public Parented { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void set_pin(uint8_t pin) { this->pin_ = pin; } void set_inverted(bool inverted) { this->inverted_ = inverted; } diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp index d6513dbbe0..06481b628b 100644 --- a/esphome/components/tee501/tee501.cpp +++ b/esphome/components/tee501/tee501.cpp @@ -7,6 +7,8 @@ namespace tee501 { static const char *const TAG = "tee501"; +static constexpr size_t TEE501_SERIAL_NUMBER_SIZE = 7; + void TEE501Component::setup() { uint8_t address[] = {0x70, 0x29}; uint8_t identification[9]; @@ -17,7 +19,10 @@ void TEE501Component::setup() { this->mark_failed(); return; } - ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(identification + 0, 7).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char serial_hex[format_hex_size(TEE501_SERIAL_NUMBER_SIZE)]; +#endif + ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex_to(serial_hex, identification, TEE501_SERIAL_NUMBER_SIZE)); } void TEE501Component::dump_config() { diff --git a/esphome/components/template/alarm_control_panel/__init__.py b/esphome/components/template/alarm_control_panel/__init__.py index 5d2421fcbc..256c7f276a 100644 --- a/esphome/components/template/alarm_control_panel/__init__.py +++ b/esphome/components/template/alarm_control_panel/__init__.py @@ -137,7 +137,11 @@ async def to_code(config): cg.add(var.set_arming_night_time(config[CONF_ARMING_NIGHT_TIME])) supports_arm_night = True - for sensor in config.get(CONF_BINARY_SENSORS, []): + if sensors := config.get(CONF_BINARY_SENSORS, []): + # Initialize FixedVector with the exact number of sensors + cg.add(var.init_sensors(len(sensors))) + + for sensor in sensors: bs = await cg.get_variable(sensor[CONF_INPUT]) flags = BinarySensorFlags[FLAG_NORMAL] diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index af662a05a0..50e43da8d5 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { using namespace esphome::alarm_control_panel; @@ -20,10 +19,13 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, // Save the flags and type. Assign a store index for the per sensor data type. SensorDataStore sd; sd.last_chime_state = false; - this->sensor_map_[sensor].flags = flags; - this->sensor_map_[sensor].type = type; + AlarmSensor alarm_sensor; + alarm_sensor.sensor = sensor; + alarm_sensor.info.flags = flags; + alarm_sensor.info.type = type; + alarm_sensor.info.store_index = this->next_store_index_++; + this->sensors_.push_back(alarm_sensor); this->sensor_data_.push_back(sd); - this->sensor_map_[sensor].store_index = this->next_store_index_++; }; static const LogString *sensor_type_to_string(AlarmSensorType type) { @@ -45,7 +47,7 @@ void TemplateAlarmControlPanel::dump_config() { ESP_LOGCONFIG(TAG, "TemplateAlarmControlPanel:\n" " Current State: %s\n" - " Number of Codes: %u\n" + " Number of Codes: %zu\n" " Requires Code To Arm: %s\n" " Arming Away Time: %" PRIu32 "s\n" " Arming Home Time: %" PRIu32 "s\n" @@ -58,7 +60,8 @@ void TemplateAlarmControlPanel::dump_config() { (this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR - for (auto const &[sensor, info] : this->sensor_map_) { + for (const auto &alarm_sensor : this->sensors_) { + const uint16_t flags = alarm_sensor.info.flags; ESP_LOGCONFIG(TAG, " Binary Sensor:\n" " Name: %s\n" @@ -67,11 +70,10 @@ void TemplateAlarmControlPanel::dump_config() { " Armed night bypass: %s\n" " Auto bypass: %s\n" " Chime mode: %s", - sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME)); + alarm_sensor.sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(alarm_sensor.info.type)), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_AUTO), TRUEFALSE(flags & BINARY_SENSOR_MODE_CHIME)); } #endif } @@ -121,7 +123,9 @@ void TemplateAlarmControlPanel::loop() { #ifdef USE_BINARY_SENSOR // Test all of the sensors regardless of the alarm panel state - for (auto const &[sensor, info] : this->sensor_map_) { + for (const auto &alarm_sensor : this->sensors_) { + const auto &info = alarm_sensor.info; + auto *sensor = alarm_sensor.sensor; // Check for chime zones if (info.flags & BINARY_SENSOR_MODE_CHIME) { // Look for the transition from closed to open @@ -242,11 +246,11 @@ void TemplateAlarmControlPanel::arm_(optional code, alarm_control_p void TemplateAlarmControlPanel::bypass_before_arming() { #ifdef USE_BINARY_SENSOR - for (auto const &[sensor, info] : this->sensor_map_) { + for (const auto &alarm_sensor : this->sensors_) { // Check for faulted bypass_auto sensors and remove them from monitoring - if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) { - ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str()); - this->bypassed_sensor_indicies_.push_back(info.store_index); + if ((alarm_sensor.info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (alarm_sensor.sensor->state)) { + ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", alarm_sensor.sensor->get_name().c_str()); + this->bypassed_sensor_indicies_.push_back(alarm_sensor.info.store_index); } } #endif @@ -281,5 +285,4 @@ void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { } } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 202dc7c13f..2038d8f1b0 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -1,11 +1,12 @@ #pragma once #include -#include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/components/alarm_control_panel/alarm_control_panel.h" @@ -13,8 +14,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #endif -namespace esphome { -namespace template_ { +namespace esphome::template_ { #ifdef USE_BINARY_SENSOR enum BinarySensorFlags : uint16_t { @@ -39,6 +39,7 @@ enum TemplateAlarmControlPanelRestoreMode { ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, }; +#ifdef USE_BINARY_SENSOR struct SensorDataStore { bool last_chime_state; }; @@ -49,6 +50,12 @@ struct SensorInfo { uint8_t store_index; }; +struct AlarmSensor { + binary_sensor::BinarySensor *sensor; + SensorInfo info; +}; +#endif + class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControlPanel, public Component { public: TemplateAlarmControlPanel(); @@ -63,6 +70,12 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl void bypass_before_arming(); #ifdef USE_BINARY_SENSOR + /** Initialize the sensors vector with the specified capacity. + * + * @param capacity The number of sensors to allocate space for. + */ + void init_sensors(size_t capacity) { this->sensors_.init(capacity); } + /** Add a binary_sensor to the alarm_panel. * * @param sensor The BinarySensor instance. @@ -122,10 +135,13 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl protected: void control(const alarm_control_panel::AlarmControlPanelCall &call) override; #ifdef USE_BINARY_SENSOR - // This maps a binary sensor to its alarm specific info - std::map sensor_map_; + // List of binary sensors with their alarm-specific info + FixedVector sensors_; // a list of automatically bypassed sensors std::vector bypassed_sensor_indicies_; + // Per sensor data store + std::vector sensor_data_; + uint8_t next_store_index_ = 0; #endif TemplateAlarmControlPanelRestoreMode restore_mode_{}; @@ -141,19 +157,15 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl uint32_t trigger_time_; // a list of codes std::vector codes_; - // Per sensor data store - std::vector sensor_data_; // requires a code to arm bool requires_code_to_arm_ = false; bool supports_arm_home_ = false; bool supports_arm_night_ = false; bool sensors_ready_ = false; - uint8_t next_store_index_ = 0; // check if the code is valid bool is_code_valid_(optional code); void arm_(optional code, alarm_control_panel::AlarmControlPanelState state, uint32_t delay); }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index 806aed49b1..b63121d2db 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -1,8 +1,7 @@ #include "template_binary_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { static const char *const TAG = "template.binary_sensor"; @@ -23,5 +22,4 @@ void TemplateBinarySensor::loop() { void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 0af709b097..c78a95e0e3 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -4,8 +4,7 @@ #include "esphome/core/template_lambda.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateBinarySensor final : public Component, public binary_sensor::BinarySensor { public: @@ -21,5 +20,4 @@ class TemplateBinarySensor final : public Component, public binary_sensor::Binar TemplateLambda f_; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/button/template_button.h b/esphome/components/template/button/template_button.h index 5bda82c58f..f64a85eef0 100644 --- a/esphome/components/template/button/template_button.h +++ b/esphome/components/template/button/template_button.h @@ -2,8 +2,7 @@ #include "esphome/components/button/button.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateButton final : public button::Button { public: @@ -11,5 +10,4 @@ class TemplateButton final : public button::Button { void press_action() override{}; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index a87f28ccec..9c8a8fc9bc 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -1,8 +1,7 @@ #include "template_cover.h" #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { using namespace esphome::cover; @@ -133,5 +132,4 @@ void TemplateCover::stop_prev_trigger_() { } } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 125c67bb86..9c4a787283 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -5,8 +5,7 @@ #include "esphome/core/template_lambda.h" #include "esphome/components/cover/cover.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { enum TemplateCoverRestoreMode { COVER_NO_RESTORE, @@ -63,5 +62,4 @@ class TemplateCover final : public cover::Cover, public Component { bool has_tilt_{false}; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index 3f6626e847..303d5ae2b0 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { static const char *const TAG = "template.date"; @@ -104,7 +103,6 @@ void TemplateDate::dump_config() { LOG_UPDATE_INTERVAL(this); } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ #endif // USE_DATETIME_DATE diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index fe64b0ba14..0379a9bc67 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -11,8 +11,7 @@ #include "esphome/core/time.h" #include "esphome/core/template_lambda.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateDate final : public datetime::DateEntity, public PollingComponent { public: @@ -41,7 +40,6 @@ class TemplateDate final : public datetime::DateEntity, public PollingComponent ESPPreferenceObject pref_; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ #endif // USE_DATETIME_DATE diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index 62f842a7ad..81a823f53e 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { static const char *const TAG = "template.datetime"; @@ -143,7 +142,6 @@ void TemplateDateTime::dump_config() { LOG_UPDATE_INTERVAL(this); } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ #endif // USE_DATETIME_DATETIME diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index c44bd85265..b7eb490933 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -11,8 +11,7 @@ #include "esphome/core/time.h" #include "esphome/core/template_lambda.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateDateTime final : public datetime::DateTimeEntity, public PollingComponent { public: @@ -41,7 +40,6 @@ class TemplateDateTime final : public datetime::DateTimeEntity, public PollingCo ESPPreferenceObject pref_; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ #endif // USE_DATETIME_DATETIME diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index dab28d01cc..21f843dcc7 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -4,8 +4,7 @@ #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { static const char *const TAG = "template.time"; @@ -104,7 +103,6 @@ void TemplateTime::dump_config() { LOG_UPDATE_INTERVAL(this); } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ #endif // USE_DATETIME_TIME diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index 0c95330d27..cb83b1b3e5 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -11,8 +11,7 @@ #include "esphome/core/time.h" #include "esphome/core/template_lambda.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateTime final : public datetime::TimeEntity, public PollingComponent { public: @@ -41,7 +40,6 @@ class TemplateTime final : public datetime::TimeEntity, public PollingComponent ESPPreferenceObject pref_; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ #endif // USE_DATETIME_TIME diff --git a/esphome/components/template/event/template_event.h b/esphome/components/template/event/template_event.h index 5467a64141..fe83dc9f34 100644 --- a/esphome/components/template/event/template_event.h +++ b/esphome/components/template/event/template_event.h @@ -3,10 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/event/event.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateEvent final : public Component, public event::Event {}; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index eba4c673b5..384e6b0ca1 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -1,8 +1,7 @@ #include "template_fan.h" #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { static const char *const TAG = "template.fan"; @@ -34,5 +33,4 @@ void TemplateFan::control(const fan::FanCall &call) { this->publish_state(); } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 052b385b93..b7e1d4ab5a 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -3,8 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/fan/fan.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateFan final : public Component, public fan::Fan { public: @@ -27,5 +26,4 @@ class TemplateFan final : public Component, public fan::Fan { std::vector preset_modes_{}; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/lock/automation.h b/esphome/components/template/lock/automation.h index bd110b7b0c..42a2a826e2 100644 --- a/esphome/components/template/lock/automation.h +++ b/esphome/components/template/lock/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { template class TemplateLockPublishAction : public Action, public Parented { public: @@ -14,5 +13,4 @@ template class TemplateLockPublishAction : public Action, void play(const Ts &...x) override { this->parent_->publish_state(this->state_.value(x...)); } }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index 8ed87b9736..de8f9b762c 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -1,8 +1,7 @@ #include "template_lock.h" #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { using namespace esphome::lock; @@ -56,5 +55,4 @@ void TemplateLock::dump_config() { ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index ac10794e4d..f4396c2c5d 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -5,8 +5,7 @@ #include "esphome/core/template_lambda.h" #include "esphome/components/lock/lock.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateLock final : public lock::Lock, public Component { public: @@ -36,5 +35,4 @@ class TemplateLock final : public lock::Lock, public Component { Trigger<> *prev_trigger_{nullptr}; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index 145a89a2f7..76fef82225 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -1,8 +1,7 @@ #include "template_number.h" #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { static const char *const TAG = "template.number"; @@ -51,5 +50,4 @@ void TemplateNumber::dump_config() { LOG_UPDATE_INTERVAL(this); } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 876ec96b3b..42c27fc3ca 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -6,8 +6,7 @@ #include "esphome/core/preferences.h" #include "esphome/core/template_lambda.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateNumber final : public number::Number, public PollingComponent { public: @@ -34,5 +33,4 @@ class TemplateNumber final : public number::Number, public PollingComponent { ESPPreferenceObject pref_; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/output/template_output.h b/esphome/components/template/output/template_output.h index 9ecfc446b9..e536660b02 100644 --- a/esphome/components/template/output/template_output.h +++ b/esphome/components/template/output/template_output.h @@ -4,8 +4,7 @@ #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateBinaryOutput final : public output::BinaryOutput { public: @@ -27,5 +26,4 @@ class TemplateFloatOutput final : public output::FloatOutput { Trigger *trigger_ = new Trigger(); }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 112f24e919..9d2df0956b 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -1,8 +1,7 @@ #include "template_select.h" #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { static const char *const TAG = "template.select"; @@ -63,5 +62,4 @@ void TemplateSelect::dump_config() { YESNO(this->optimistic_), this->option_at(this->initial_option_index_), YESNO(this->restore_value_)); } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index cb5b546976..2757c51405 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -6,8 +6,7 @@ #include "esphome/core/preferences.h" #include "esphome/core/template_lambda.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateSelect final : public select::Select, public PollingComponent { public: @@ -34,5 +33,4 @@ class TemplateSelect final : public select::Select, public PollingComponent { ESPPreferenceObject pref_; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index 1558ea9b15..313a163e38 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace template_ { +namespace esphome::template_ { static const char *const TAG = "template.sensor"; @@ -24,5 +23,4 @@ void TemplateSensor::dump_config() { LOG_UPDATE_INTERVAL(this); } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 3ca965dde3..825a2b4ffa 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -4,8 +4,7 @@ #include "esphome/core/template_lambda.h" #include "esphome/components/sensor/sensor.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateSensor final : public sensor::Sensor, public PollingComponent { public: @@ -21,5 +20,4 @@ class TemplateSensor final : public sensor::Sensor, public PollingComponent { TemplateLambda f_; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index e86657510f..8ae5a07dc3 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC, - CONF_RESTORE_STATE, CONF_STATE, CONF_TURN_OFF_ACTION, CONF_TURN_ON_ACTION, @@ -44,9 +43,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_TURN_ON_ACTION): automation.validate_automation( single=True ), - cv.Optional(CONF_RESTORE_STATE): cv.invalid( - "The restore_state option has been removed in 2023.7.0. Use the restore_mode option instead" - ), } ) .extend(cv.COMPONENT_SCHEMA), diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 95e8692da5..cfa8798e75 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -1,8 +1,7 @@ #include "template_switch.h" #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { static const char *const TAG = "template.switch"; @@ -57,5 +56,4 @@ void TemplateSwitch::dump_config() { } void TemplateSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 35c18af448..91b7b396f6 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -5,8 +5,7 @@ #include "esphome/core/template_lambda.h" #include "esphome/components/switch/switch.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateSwitch final : public switch_::Switch, public Component { public: @@ -37,5 +36,4 @@ class TemplateSwitch final : public switch_::Switch, public Component { Trigger<> *prev_trigger_{nullptr}; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/text/template_text.cpp b/esphome/components/template/text/template_text.cpp index a917c72a14..32ed8f047b 100644 --- a/esphome/components/template/text/template_text.cpp +++ b/esphome/components/template/text/template_text.cpp @@ -1,8 +1,7 @@ #include "template_text.h" #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { static const char *const TAG = "template.text"; @@ -16,7 +15,7 @@ void TemplateText::setup() { uint32_t key = this->get_preference_hash(); key += this->traits.get_min_length() << 2; key += this->traits.get_max_length() << 4; - key += fnv1_hash(this->traits.get_pattern()) << 6; + key += fnv1_hash(this->traits.get_pattern_c_str()) << 6; this->pref_->setup(key, value); } if (!value.empty()) @@ -51,5 +50,4 @@ void TemplateText::dump_config() { LOG_UPDATE_INTERVAL(this); } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index 1a0a66ed5b..178b410ed2 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -6,8 +6,7 @@ #include "esphome/core/preferences.h" #include "esphome/core/template_lambda.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { // We keep this separate so we don't have to template and duplicate // the text input for each different size flash allocation. @@ -84,5 +83,4 @@ class TemplateText final : public text::Text, public PollingComponent { TemplateTextSaverBase *pref_ = nullptr; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 024d0093a2..89a15b6081 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -1,8 +1,7 @@ #include "template_text_sensor.h" #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { static const char *const TAG = "template.text_sensor"; @@ -20,5 +19,4 @@ float TemplateTextSensor::get_setup_priority() const { return setup_priority::HA void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index da5c518c7f..0538a7ec21 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -5,8 +5,7 @@ #include "esphome/core/template_lambda.h" #include "esphome/components/text_sensor/text_sensor.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { class TemplateTextSensor final : public text_sensor::TextSensor, public PollingComponent { public: @@ -22,5 +21,4 @@ class TemplateTextSensor final : public text_sensor::TextSensor, public PollingC TemplateLambda f_{}; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/valve/automation.h b/esphome/components/template/valve/automation.h index e3f394ac7c..a27e98b25c 100644 --- a/esphome/components/template/valve/automation.h +++ b/esphome/components/template/valve/automation.h @@ -4,8 +4,7 @@ #include "esphome/core/automation.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { template class TemplateValvePublishAction : public Action, public Parented { TEMPLATABLE_VALUE(float, position) @@ -20,5 +19,4 @@ template class TemplateValvePublishAction : public Action } }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index b91b32473e..4e772f9253 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -1,8 +1,7 @@ #include "template_valve.h" #include "esphome/core/log.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { using namespace esphome::valve; @@ -127,5 +126,4 @@ void TemplateValve::stop_prev_trigger_() { } } -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index c452648193..4205682a2a 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -5,8 +5,7 @@ #include "esphome/core/template_lambda.h" #include "esphome/components/valve/valve.h" -namespace esphome { -namespace template_ { +namespace esphome::template_ { enum TemplateValveRestoreMode { VALVE_NO_RESTORE, @@ -57,5 +56,4 @@ class TemplateValve final : public valve::Valve, public Component { bool has_position_{false}; }; -} // namespace template_ -} // namespace esphome +} // namespace esphome::template_ diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py new file mode 100644 index 0000000000..716289035a --- /dev/null +++ b/esphome/components/template/water_heater/__init__.py @@ -0,0 +1,123 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import water_heater +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MODE, + CONF_OPTIMISTIC, + CONF_RESTORE_MODE, + CONF_SET_ACTION, + CONF_SUPPORTED_MODES, + CONF_TARGET_TEMPERATURE, +) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType + +from .. import template_ns + +CONF_CURRENT_TEMPERATURE = "current_temperature" + +TemplateWaterHeater = template_ns.class_( + "TemplateWaterHeater", water_heater.WaterHeater +) + +TemplateWaterHeaterPublishAction = template_ns.class_( + "TemplateWaterHeaterPublishAction", + automation.Action, + cg.Parented.template(TemplateWaterHeater), +) + +TemplateWaterHeaterRestoreMode = template_ns.enum("TemplateWaterHeaterRestoreMode") +RESTORE_MODES = { + "NO_RESTORE": TemplateWaterHeaterRestoreMode.WATER_HEATER_NO_RESTORE, + "RESTORE": TemplateWaterHeaterRestoreMode.WATER_HEATER_RESTORE, + "RESTORE_AND_CALL": TemplateWaterHeaterRestoreMode.WATER_HEATER_RESTORE_AND_CALL, +} + +CONFIG_SCHEMA = water_heater.water_heater_schema(TemplateWaterHeater).extend( + { + cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean, + cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum( + RESTORE_MODES, upper=True + ), + cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda, + cv.Optional(CONF_MODE): cv.returning_lambda, + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( + water_heater.validate_water_heater_mode + ), + } +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await water_heater.register_water_heater(var, config) + + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + + if CONF_SET_ACTION in config: + await automation.build_automation( + var.get_set_trigger(), [], config[CONF_SET_ACTION] + ) + + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) + + if CONF_CURRENT_TEMPERATURE in config: + template_ = await cg.process_lambda( + config[CONF_CURRENT_TEMPERATURE], + [], + return_type=cg.optional.template(cg.float_), + ) + cg.add(var.set_current_temperature_lambda(template_)) + + if CONF_MODE in config: + template_ = await cg.process_lambda( + config[CONF_MODE], + [], + return_type=cg.optional.template(water_heater.WaterHeaterMode), + ) + cg.add(var.set_mode_lambda(template_)) + + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) + + +@automation.register_action( + "water_heater.template.publish", + TemplateWaterHeaterPublishAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(TemplateWaterHeater), + cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_MODE): cv.templatable( + water_heater.validate_water_heater_mode + ), + } + ), +) +async def water_heater_template_publish_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + if current_temp := config.get(CONF_CURRENT_TEMPERATURE): + template_ = await cg.templatable(current_temp, args, float) + cg.add(var.set_current_temperature(template_)) + + if target_temp := config.get(CONF_TARGET_TEMPERATURE): + template_ = await cg.templatable(target_temp, args, float) + cg.add(var.set_target_temperature(template_)) + + if mode := config.get(CONF_MODE): + template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode) + cg.add(var.set_mode(template_)) + + return var diff --git a/esphome/components/template/water_heater/automation.h b/esphome/components/template/water_heater/automation.h new file mode 100644 index 0000000000..3dad2b85ae --- /dev/null +++ b/esphome/components/template/water_heater/automation.h @@ -0,0 +1,35 @@ +#pragma once + +#include "template_water_heater.h" +#include "esphome/core/automation.h" + +namespace esphome::template_ { + +template +class TemplateWaterHeaterPublishAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, current_temperature) + TEMPLATABLE_VALUE(float, target_temperature) + TEMPLATABLE_VALUE(water_heater::WaterHeaterMode, mode) + + void play(const Ts &...x) override { + if (this->current_temperature_.has_value()) { + this->parent_->set_current_temperature(this->current_temperature_.value(x...)); + } + bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value(); + if (needs_call) { + auto call = this->parent_->make_call(); + if (this->target_temperature_.has_value()) { + call.set_target_temperature(this->target_temperature_.value(x...)); + } + if (this->mode_.has_value()) { + call.set_mode(this->mode_.value(x...)); + } + call.perform(); + } else { + this->parent_->publish_state(); + } + } +}; + +} // namespace esphome::template_ diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp new file mode 100644 index 0000000000..5ae5c30f36 --- /dev/null +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -0,0 +1,88 @@ +#include "template_water_heater.h" +#include "esphome/core/log.h" + +namespace esphome::template_ { + +static const char *const TAG = "template.water_heater"; + +TemplateWaterHeater::TemplateWaterHeater() : set_trigger_(new Trigger<>()) {} + +void TemplateWaterHeater::setup() { + if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE || + this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE_AND_CALL) { + auto restore = this->restore_state(); + + if (restore.has_value()) { + restore->perform(); + } + } + if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value()) + this->disable_loop(); +} + +water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { + water_heater::WaterHeaterTraits traits; + + if (!this->supported_modes_.empty()) { + traits.set_supported_modes(this->supported_modes_); + } + + traits.set_supports_current_temperature(true); + return traits; +} + +void TemplateWaterHeater::loop() { + bool changed = false; + + auto curr_temp = this->current_temperature_f_.call(); + if (curr_temp.has_value()) { + if (*curr_temp != this->current_temperature_) { + this->current_temperature_ = *curr_temp; + changed = true; + } + } + + auto new_mode = this->mode_f_.call(); + if (new_mode.has_value()) { + if (*new_mode != this->mode_) { + this->mode_ = *new_mode; + changed = true; + } + } + + if (changed) { + this->publish_state(); + } +} + +void TemplateWaterHeater::dump_config() { + LOG_WATER_HEATER("", "Template Water Heater", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); +} + +float TemplateWaterHeater::get_setup_priority() const { return setup_priority::HARDWARE; } + +water_heater::WaterHeaterCallInternal TemplateWaterHeater::make_call() { + return water_heater::WaterHeaterCallInternal(this); +} + +void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) { + if (call.get_mode().has_value()) { + if (this->optimistic_) { + this->mode_ = *call.get_mode(); + } + } + if (!std::isnan(call.get_target_temperature())) { + if (this->optimistic_) { + this->target_temperature_ = call.get_target_temperature(); + } + } + + this->set_trigger_->trigger(); + + if (this->optimistic_) { + this->publish_state(); + } +} + +} // namespace esphome::template_ diff --git a/esphome/components/template/water_heater/template_water_heater.h b/esphome/components/template/water_heater/template_water_heater.h new file mode 100644 index 0000000000..e5f51b72dc --- /dev/null +++ b/esphome/components/template/water_heater/template_water_heater.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" +#include "esphome/components/water_heater/water_heater.h" + +namespace esphome::template_ { + +enum TemplateWaterHeaterRestoreMode { + WATER_HEATER_NO_RESTORE, + WATER_HEATER_RESTORE, + WATER_HEATER_RESTORE_AND_CALL, +}; + +class TemplateWaterHeater : public water_heater::WaterHeater { + public: + TemplateWaterHeater(); + + template void set_current_temperature_lambda(F &&f) { + this->current_temperature_f_.set(std::forward(f)); + } + template void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward(f)); } + + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + void set_supported_modes(const std::initializer_list &modes) { + this->supported_modes_ = modes; + } + + Trigger<> *get_set_trigger() const { return this->set_trigger_; } + + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + + water_heater::WaterHeaterCallInternal make_call() override; + + protected: + void control(const water_heater::WaterHeaterCall &call) override; + water_heater::WaterHeaterTraits traits() override; + + // Ordered to minimize padding on 32-bit: 4-byte members first, then smaller + Trigger<> *set_trigger_; + TemplateLambda current_temperature_f_; + TemplateLambda mode_f_; + TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; + water_heater::WaterHeaterModeMask supported_modes_; + bool optimistic_{true}; +}; + +} // namespace esphome::template_ diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 933d82c85c..c2ade56f69 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -2,28 +2,35 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include namespace esphome { namespace text { static const char *const TAG = "text"; -void Text::publish_state(const std::string &state) { - this->set_has_state(true); - this->state = state; - if (this->traits.get_mode() == TEXT_MODE_PASSWORD) { - ESP_LOGD(TAG, "'%s': Sending state " LOG_SECRET("'%s'"), this->get_name().c_str(), state.c_str()); +void Text::publish_state(const std::string &state) { this->publish_state(state.data(), state.size()); } - } else { - ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); +void Text::publish_state(const char *state) { this->publish_state(state, strlen(state)); } + +void Text::publish_state(const char *state, size_t len) { + this->set_has_state(true); + // Only assign if changed to avoid heap allocation + if (len != this->state.size() || memcmp(state, this->state.data(), len) != 0) { + this->state.assign(state, len); } - this->state_callback_.call(state); + if (this->traits.get_mode() == TEXT_MODE_PASSWORD) { + ESP_LOGD(TAG, "'%s': Sending state " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str()); + } else { + ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), this->state.c_str()); + } + this->state_callback_.call(this->state); #if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_text_update(this); #endif } -void Text::add_on_state_callback(std::function &&callback) { +void Text::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index 74d08eda8a..e4ad64334b 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -27,11 +27,13 @@ class Text : public EntityBase { TextTraits traits; void publish_state(const std::string &state); + void publish_state(const char *state); + void publish_state(const char *state, size_t len); /// Instantiate a TextCall object to modify this text component's state. TextCall make_call() { return TextCall(this); } - void add_on_state_callback(std::function &&callback); + void add_on_state_callback(std::function &&callback); protected: friend class TextCall; @@ -44,7 +46,7 @@ class Text : public EntityBase { */ virtual void control(const std::string &value) = 0; - CallbackManager state_callback_; + LazyCallbackManager state_callback_; }; } // namespace text diff --git a/esphome/components/text/text_traits.h b/esphome/components/text/text_traits.h index ceaba2dead..473daafb8e 100644 --- a/esphome/components/text/text_traits.h +++ b/esphome/components/text/text_traits.h @@ -1,8 +1,7 @@ #pragma once -#include +#include -#include "esphome/core/helpers.h" #include "esphome/core/string_ref.h" namespace esphome { @@ -22,8 +21,9 @@ class TextTraits { int get_max_length() const { return this->max_length_; } // Set/get the pattern. - void set_pattern(std::string pattern) { this->pattern_ = std::move(pattern); } - std::string get_pattern() const { return this->pattern_; } + void set_pattern(const char *pattern) { this->pattern_ = pattern; } + std::string get_pattern() const { return std::string(this->pattern_); } + const char *get_pattern_c_str() const { return this->pattern_; } StringRef get_pattern_ref() const { return StringRef(this->pattern_); } // Set/get the frontend mode. @@ -33,7 +33,7 @@ class TextTraits { protected: int min_length_; int max_length_; - std::string pattern_; + const char *pattern_{""}; TextMode mode_{TEXT_MODE_TEXT}; }; diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index a242b43b1c..4cace372ae 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -56,20 +56,33 @@ optional ToLowerFilter::new_value(std::string value) { } // Append -optional AppendFilter::new_value(std::string value) { return value + this->suffix_; } +optional AppendFilter::new_value(std::string value) { + value.append(this->suffix_); + return value; +} // Prepend -optional PrependFilter::new_value(std::string value) { return this->prefix_ + value; } +optional PrependFilter::new_value(std::string value) { + value.insert(0, this->prefix_); + return value; +} // Substitute SubstituteFilter::SubstituteFilter(const std::initializer_list &substitutions) : substitutions_(substitutions) {} optional SubstituteFilter::new_value(std::string value) { - std::size_t pos; for (const auto &sub : this->substitutions_) { - while ((pos = value.find(sub.from)) != std::string::npos) - value.replace(pos, sub.from.size(), sub.to); + // Compute lengths once per substitution (strlen is fast, called infrequently) + const size_t from_len = strlen(sub.from); + const size_t to_len = strlen(sub.to); + std::size_t pos = 0; + while ((pos = value.find(sub.from, pos, from_len)) != std::string::npos) { + value.replace(pos, from_len, sub.to, to_len); + // Advance past the replacement to avoid infinite loop when + // the replacement contains the search pattern (e.g., f -> foo) + pos += to_len; + } } return value; } @@ -79,8 +92,10 @@ MapFilter::MapFilter(const std::initializer_list &mappings) : mapp optional MapFilter::new_value(std::string value) { for (const auto &mapping : this->mappings_) { - if (mapping.from == value) - return mapping.to; + if (value == mapping.from) { + value.assign(mapping.to); + return value; + } } return value; // Pass through if no match } diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 85acac5c8d..0f66b753b4 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -92,26 +92,26 @@ class ToLowerFilter : public Filter { /// A simple filter that adds a string to the end of another string class AppendFilter : public Filter { public: - AppendFilter(std::string suffix) : suffix_(std::move(suffix)) {} + explicit AppendFilter(const char *suffix) : suffix_(suffix) {} optional new_value(std::string value) override; protected: - std::string suffix_; + const char *suffix_; }; /// A simple filter that adds a string to the start of another string class PrependFilter : public Filter { public: - PrependFilter(std::string prefix) : prefix_(std::move(prefix)) {} + explicit PrependFilter(const char *prefix) : prefix_(prefix) {} optional new_value(std::string value) override; protected: - std::string prefix_; + const char *prefix_; }; struct Substitution { - std::string from; - std::string to; + const char *from; + const char *to; }; /// A simple filter that replaces a substring with another substring diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index a7bcf19967..66301564a4 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include namespace esphome { namespace text_sensor { @@ -24,18 +25,32 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text } } -void TextSensor::publish_state(const std::string &state) { - this->raw_state = state; - if (this->raw_callback_) { - this->raw_callback_->call(state); - } +void TextSensor::publish_state(const std::string &state) { this->publish_state(state.data(), state.size()); } - ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str()); +void TextSensor::publish_state(const char *state) { this->publish_state(state, strlen(state)); } +void TextSensor::publish_state(const char *state, size_t len) { if (this->filter_list_ == nullptr) { - this->internal_send_state_to_frontend(state); + // No filters: raw_state == state, store once and use for both callbacks + // Only assign if changed to avoid heap allocation + if (len != this->state.size() || memcmp(state, this->state.data(), len) != 0) { + this->state.assign(state, len); + } + this->raw_callback_.call(this->state); + ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), this->state.c_str()); + this->notify_frontend_(); } else { - this->filter_list_->input(state); + // Has filters: need separate raw storage +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + // Only assign if changed to avoid heap allocation + if (len != this->raw_state.size() || memcmp(state, this->raw_state.data(), len) != 0) { + this->raw_state.assign(state, len); + } + this->raw_callback_.call(this->raw_state); + ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), this->raw_state.c_str()); + this->filter_list_->input(this->raw_state); +#pragma GCC diagnostic pop } } @@ -69,23 +84,40 @@ void TextSensor::clear_filters() { this->filter_list_ = nullptr; } -void TextSensor::add_on_state_callback(std::function callback) { +void TextSensor::add_on_state_callback(std::function callback) { this->callback_.add(std::move(callback)); } -void TextSensor::add_on_raw_state_callback(std::function callback) { - if (!this->raw_callback_) { - this->raw_callback_ = make_unique>(); - } - this->raw_callback_->add(std::move(callback)); +void TextSensor::add_on_raw_state_callback(std::function callback) { + this->raw_callback_.add(std::move(callback)); } -std::string TextSensor::get_state() const { return this->state; } -std::string TextSensor::get_raw_state() const { return this->raw_state; } +const std::string &TextSensor::get_state() const { return this->state; } +const std::string &TextSensor::get_raw_state() const { + if (this->filter_list_ == nullptr) { + return this->state; // No filters, raw == filtered + } +// Suppress deprecation warning - get_raw_state() is the replacement API +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + return this->raw_state; +#pragma GCC diagnostic pop +} void TextSensor::internal_send_state_to_frontend(const std::string &state) { - this->state = state; + this->internal_send_state_to_frontend(state.data(), state.size()); +} + +void TextSensor::internal_send_state_to_frontend(const char *state, size_t len) { + // Only assign if changed to avoid heap allocation + if (len != this->state.size() || memcmp(state, this->state.data(), len) != 0) { + this->state.assign(state, len); + } + this->notify_frontend_(); +} + +void TextSensor::notify_frontend_() { this->set_has_state(true); - ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); - this->callback_.call(state); + ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), this->state.c_str()); + this->callback_.call(this->state); #if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_text_sensor_update(this); #endif diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index db2e857ae3..1352a8c1e4 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -24,14 +24,26 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text class TextSensor : public EntityBase, public EntityBase_DeviceClass { public: + std::string state; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + /// @deprecated Use get_raw_state() instead. This member will be removed in ESPHome 2026.6.0. + ESPDEPRECATED("Use get_raw_state() instead of .raw_state. Will be removed in 2026.6.0", "2025.12.0") + std::string raw_state; + TextSensor() = default; + ~TextSensor() = default; +#pragma GCC diagnostic pop /// Getter-syntax for .state. - std::string get_state() const; + const std::string &get_state() const; /// Getter-syntax for .raw_state - std::string get_raw_state() const; + const std::string &get_raw_state() const; void publish_state(const std::string &state); + void publish_state(const char *state); + void publish_state(const char *state, size_t len); /// Add a filter to the filter chain. Will be appended to the back. void add_filter(Filter *filter); @@ -45,22 +57,21 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { /// Clear the entire filter chain. void clear_filters(); - void add_on_state_callback(std::function callback); + void add_on_state_callback(std::function callback); /// Add a callback that will be called every time the sensor sends a raw value. - void add_on_raw_state_callback(std::function callback); - - std::string state; - std::string raw_state; + void add_on_raw_state_callback(std::function callback); // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) void internal_send_state_to_frontend(const std::string &state); + void internal_send_state_to_frontend(const char *state, size_t len); protected: - std::unique_ptr> - raw_callback_; ///< Storage for raw state callbacks (lazy allocated). - CallbackManager callback_; ///< Storage for filtered state callbacks. + /// Notify frontend that state has changed (assumes this->state is already set) + void notify_frontend_(); + LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. + LazyCallbackManager callback_; ///< Storage for filtered state callbacks. Filter *filter_list_{nullptr}; ///< Store all active filters. }; diff --git a/esphome/components/thermopro_ble/__init__.py b/esphome/components/thermopro_ble/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/thermopro_ble/sensor.py b/esphome/components/thermopro_ble/sensor.py new file mode 100644 index 0000000000..de63229621 --- /dev/null +++ b/esphome/components/thermopro_ble/sensor.py @@ -0,0 +1,97 @@ +import esphome.codegen as cg +from esphome.components import esp32_ble_tracker, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_EXTERNAL_TEMPERATURE, + CONF_HUMIDITY, + CONF_ID, + CONF_MAC_ADDRESS, + CONF_SIGNAL_STRENGTH, + CONF_TEMPERATURE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_DECIBEL_MILLIWATT, + UNIT_PERCENT, +) + +CODEOWNERS = ["@sittner"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +thermopro_ble_ns = cg.esphome_ns.namespace("thermopro_ble") +ThermoProBLE = thermopro_ble_ns.class_( + "ThermoProBLE", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ThermoProBLE), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): + sens = await sensor.new_sensor(external_temperature_config) + cg.add(var.set_external_temperature(sens)) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) + if battery_level_config := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(battery_level_config) + cg.add(var.set_battery_level(sens)) + if signal_strength_config := config.get(CONF_SIGNAL_STRENGTH): + sens = await sensor.new_sensor(signal_strength_config) + cg.add(var.set_signal_strength(sens)) diff --git a/esphome/components/thermopro_ble/thermopro_ble.cpp b/esphome/components/thermopro_ble/thermopro_ble.cpp new file mode 100644 index 0000000000..2c90ee23f8 --- /dev/null +++ b/esphome/components/thermopro_ble/thermopro_ble.cpp @@ -0,0 +1,205 @@ +#include "thermopro_ble.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome::thermopro_ble { + +// this size must be large enough to hold the largest data frame +// of all supported devices +static constexpr std::size_t MAX_DATA_SIZE = 24; + +struct DeviceParserMapping { + const char *prefix; + DeviceParser parser; +}; + +static float tp96_battery(uint16_t voltage); + +static optional parse_tp972(const uint8_t *data, std::size_t data_size); +static optional parse_tp96(const uint8_t *data, std::size_t data_size); +static optional parse_tp3(const uint8_t *data, std::size_t data_size); + +static const char *const TAG = "thermopro_ble"; + +static const struct DeviceParserMapping DEVICE_PARSER_MAP[] = { + {"TP972", parse_tp972}, {"TP970", parse_tp96}, {"TP96", parse_tp96}, {"TP3", parse_tp3}}; + +void ThermoProBLE::dump_config() { + ESP_LOGCONFIG(TAG, "ThermoPro BLE"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "External temperature", this->external_temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool ThermoProBLE::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + // check for matching mac address + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + + // check for valid device type + update_device_type_(device.get_name()); + if (this->device_parser_ == nullptr) { + ESP_LOGVV(TAG, "parse_device(): invalid device type."); + return false; + } + + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str_to(addr_buf)); + + // publish signal strength + float signal_strength = float(device.get_rssi()); + if (this->signal_strength_ != nullptr) + this->signal_strength_->publish_state(signal_strength); + + bool success = false; + for (auto &service_data : device.get_manufacturer_datas()) { + // check maximum data size + std::size_t data_size = service_data.data.size() + 2; + if (data_size > MAX_DATA_SIZE) { + ESP_LOGVV(TAG, "parse_device(): maximum data size exceeded!"); + continue; + } + + // reconstruct whole record from 2 byte uuid and data + esp_bt_uuid_t uuid = service_data.uuid.get_uuid(); + uint8_t data[MAX_DATA_SIZE] = {static_cast(uuid.uuid.uuid16), static_cast(uuid.uuid.uuid16 >> 8)}; + std::copy(service_data.data.begin(), service_data.data.end(), std::begin(data) + 2); + + // dispatch data to parser + optional result = this->device_parser_(data, data_size); + if (!result.has_value()) { + continue; + } + + // publish sensor values + if (result->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*result->temperature); + if (result->external_temperature.has_value() && this->external_temperature_ != nullptr) + this->external_temperature_->publish_state(*result->external_temperature); + if (result->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*result->humidity); + if (result->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*result->battery_level); + + success = true; + } + + return success; +} + +void ThermoProBLE::update_device_type_(const std::string &device_name) { + // check for changed device name (should only happen on initial call) + if (this->device_name_ == device_name) { + return; + } + + // remember device name + this->device_name_ = device_name; + + // try to find device parser + for (const auto &mapping : DEVICE_PARSER_MAP) { + if (device_name.starts_with(mapping.prefix)) { + this->device_parser_ = mapping.parser; + return; + } + } + + // device type unknown + this->device_parser_ = nullptr; + ESP_LOGVV(TAG, "update_device_type_(): unknown device type %s.", device_name.c_str()); +} + +static inline uint16_t read_uint16(const uint8_t *data, std::size_t offset) { + return static_cast(data[offset + 0]) | (static_cast(data[offset + 1]) << 8); +} + +static inline int16_t read_int16(const uint8_t *data, std::size_t offset) { + return static_cast(read_uint16(data, offset)); +} + +static inline uint32_t read_uint32(const uint8_t *data, std::size_t offset) { + return static_cast(data[offset + 0]) | (static_cast(data[offset + 1]) << 8) | + (static_cast(data[offset + 2]) << 16) | (static_cast(data[offset + 3]) << 24); +} + +// Battery calculation used with permission from: +// https://github.com/Bluetooth-Devices/thermopro-ble/blob/main/src/thermopro_ble/parser.py +// +// TP96x battery values appear to be a voltage reading, probably in millivolts. +// This means that calculating battery life from it is a non-linear function. +// Examining the curve, it looked fairly close to a curve from the tanh function. +// So, I created a script to use Tensorflow to optimize an equation in the format +// A*tanh(B*x+C)+D +// Where A,B,C,D are the variables to optimize for. This yielded the below function +static float tp96_battery(uint16_t voltage) { + float level = 52.317286f * tanh(static_cast(voltage) / 273.624277936f - 8.76485439394f) + 51.06925f; + return std::max(0.0f, std::min(level, 100.0f)); +} + +static optional parse_tp972(const uint8_t *data, std::size_t data_size) { + if (data_size != 23) { + ESP_LOGVV(TAG, "parse_tp972(): payload has wrong size of %d (!= 23)!", data_size); + return {}; + } + + ParseResult result; + + // ambient temperature, 2 bytes, 16-bit unsigned integer, -54 °C offset + result.external_temperature = static_cast(read_uint16(data, 1)) - 54.0f; + + // battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage) + result.battery_level = tp96_battery(read_uint16(data, 3)); + + // internal temperature, 4 bytes, float, -54 °C offset + result.temperature = static_cast(read_uint32(data, 9)) - 54.0f; + + return result; +} + +static optional parse_tp96(const uint8_t *data, std::size_t data_size) { + if (data_size != 7) { + ESP_LOGVV(TAG, "parse_tp96(): payload has wrong size of %d (!= 7)!", data_size); + return {}; + } + + ParseResult result; + + // internal temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset + result.temperature = static_cast(read_uint16(data, 1)) - 30.0f; + + // battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage) + result.battery_level = tp96_battery(read_uint16(data, 3)); + + // ambient temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset + result.external_temperature = static_cast(read_uint16(data, 5)) - 30.0f; + + return result; +} + +static optional parse_tp3(const uint8_t *data, std::size_t data_size) { + if (data_size < 6) { + ESP_LOGVV(TAG, "parse_tp3(): payload has wrong size of %d (< 6)!", data_size); + return {}; + } + + ParseResult result; + + // temperature, 2 bytes, 16-bit signed integer, 0.1 °C + result.temperature = static_cast(read_int16(data, 1)) * 0.1f; + + // humidity, 1 byte, 8-bit unsigned integer, 1.0 % + result.humidity = static_cast(data[3]); + + // battery level, 2 bits (0-2) + result.battery_level = static_cast(data[4] & 0x3) * 50.0; + + return result; +} + +} // namespace esphome::thermopro_ble + +#endif diff --git a/esphome/components/thermopro_ble/thermopro_ble.h b/esphome/components/thermopro_ble/thermopro_ble.h new file mode 100644 index 0000000000..38bed82102 --- /dev/null +++ b/esphome/components/thermopro_ble/thermopro_ble.h @@ -0,0 +1,49 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 + +namespace esphome::thermopro_ble { + +struct ParseResult { + optional temperature; + optional external_temperature; + optional humidity; + optional battery_level; +}; + +using DeviceParser = optional (*)(const uint8_t *data, std::size_t data_size); + +class ThermoProBLE : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { this->address_ = address; }; + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; } + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_external_temperature(sensor::Sensor *external_temperature) { + this->external_temperature_ = external_temperature; + } + void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } + + protected: + uint64_t address_; + std::string device_name_; + DeviceParser device_parser_{nullptr}; + sensor::Sensor *signal_strength_{nullptr}; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *external_temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + + void update_device_type_(const std::string &device_name); +}; + +} // namespace esphome::thermopro_ble + +#endif diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 2b51f58f4f..d5fb259dad 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace thermostat { +namespace esphome::thermostat { static const char *const TAG = "thermostat.climate"; @@ -66,10 +65,12 @@ void ThermostatClimate::setup() { } void ThermostatClimate::loop() { - for (auto &timer : this->timer_) { - if (timer.active && (timer.started + timer.time < App.get_loop_component_start_time())) { + uint32_t now = App.get_loop_component_start_time(); + for (uint8_t i = 0; i < THERMOSTAT_TIMER_COUNT; i++) { + auto &timer = this->timer_[i]; + if (timer.active && (now - timer.started >= timer.time)) { timer.active = false; - timer.func(); + this->call_timer_callback_(static_cast(i)); } } } @@ -654,7 +655,7 @@ void ThermostatClimate::trigger_supplemental_action_() { void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction action) { // setup_complete_ helps us ensure an action is called immediately after boot - if ((action == this->humidification_action_) && this->setup_complete_) { + if ((action == this->humidification_action) && this->setup_complete_) { // already in target mode return; } @@ -683,7 +684,7 @@ void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction this->prev_humidity_control_trigger_->stop_action(); this->prev_humidity_control_trigger_ = nullptr; } - this->humidification_action_ = action; + this->humidification_action = action; this->prev_humidity_control_trigger_ = trig; if (trig != nullptr) { trig->trigger(); @@ -916,8 +917,42 @@ uint32_t ThermostatClimate::timer_duration_(ThermostatClimateTimerIndex timer_in return this->timer_[timer_index].time; } -std::function ThermostatClimate::timer_cbf_(ThermostatClimateTimerIndex timer_index) { - return this->timer_[timer_index].func; +void ThermostatClimate::call_timer_callback_(ThermostatClimateTimerIndex timer_index) { + switch (timer_index) { + case THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME: + this->cooling_max_run_time_timer_callback_(); + break; + case THERMOSTAT_TIMER_COOLING_OFF: + this->cooling_off_timer_callback_(); + break; + case THERMOSTAT_TIMER_COOLING_ON: + this->cooling_on_timer_callback_(); + break; + case THERMOSTAT_TIMER_FAN_MODE: + this->fan_mode_timer_callback_(); + break; + case THERMOSTAT_TIMER_FANNING_OFF: + this->fanning_off_timer_callback_(); + break; + case THERMOSTAT_TIMER_FANNING_ON: + this->fanning_on_timer_callback_(); + break; + case THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME: + this->heating_max_run_time_timer_callback_(); + break; + case THERMOSTAT_TIMER_HEATING_OFF: + this->heating_off_timer_callback_(); + break; + case THERMOSTAT_TIMER_HEATING_ON: + this->heating_on_timer_callback_(); + break; + case THERMOSTAT_TIMER_IDLE_ON: + this->idle_on_timer_callback_(); + break; + case THERMOSTAT_TIMER_COUNT: + default: + break; + } } void ThermostatClimate::cooling_max_run_time_timer_callback_() { @@ -1114,7 +1149,7 @@ bool ThermostatClimate::dehumidification_required_() { } // if we get here, the current humidity is between target + hysteresis and target - hysteresis, // so the action should not change - return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY; + return this->humidification_action == THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY; } bool ThermostatClimate::humidification_required_() { @@ -1127,7 +1162,7 @@ bool ThermostatClimate::humidification_required_() { } // if we get here, the current humidity is between target - hysteresis and target + hysteresis, // so the action should not change - return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY; + return this->humidification_action == THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY; } void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config) { @@ -1330,45 +1365,64 @@ void ThermostatClimate::set_heat_deadband(float deadband) { this->heating_deadba void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ = overrun; } void ThermostatClimate::set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; } void ThermostatClimate::set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; } + +void ThermostatClimate::set_timer_duration_in_sec_(ThermostatClimateTimerIndex timer_index, uint32_t time) { + uint32_t new_duration_ms = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); + + if (this->timer_[timer_index].active) { + // Timer is running, calculate elapsed time and adjust if needed + uint32_t current_time = App.get_loop_component_start_time(); + uint32_t elapsed = current_time - this->timer_[timer_index].started; + + if (elapsed >= new_duration_ms) { + // Timer should complete immediately (including when new_duration_ms is 0) + ESP_LOGVV(TAG, "timer %d completing immediately (elapsed %d >= new %d)", timer_index, elapsed, new_duration_ms); + this->timer_[timer_index].active = false; + // Trigger the timer callback immediately + this->call_timer_callback_(timer_index); + return; + } else { + // Adjust timer to run for remaining time - keep original start time + ESP_LOGVV(TAG, "timer %d adjusted: elapsed %d, new total %d, remaining %d", timer_index, elapsed, new_duration_ms, + new_duration_ms - elapsed); + this->timer_[timer_index].time = new_duration_ms; + return; + } + } + + // Original logic for non-running timers + this->timer_[timer_index].time = new_duration_ms; +} + void ThermostatClimate::set_cooling_maximum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME].time = - 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); + this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME, time); } void ThermostatClimate::set_cooling_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_OFF].time = - 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); + this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_COOLING_OFF, time); } void ThermostatClimate::set_cooling_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_ON].time = - 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); + this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_COOLING_ON, time); } void ThermostatClimate::set_fan_mode_minimum_switching_time_in_sec(uint32_t time) { - this->timer_[thermostat::THERMOSTAT_TIMER_FAN_MODE].time = - 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); + this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_FAN_MODE, time); } void ThermostatClimate::set_fanning_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_OFF].time = - 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); + this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_FANNING_OFF, time); } void ThermostatClimate::set_fanning_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_ON].time = - 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); + this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_FANNING_ON, time); } void ThermostatClimate::set_heating_maximum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME].time = - 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); + this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME, time); } void ThermostatClimate::set_heating_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_OFF].time = - 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); + this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_HEATING_OFF, time); } void ThermostatClimate::set_heating_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_ON].time = - 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); + this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_HEATING_ON, time); } void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { - this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time = - 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); + this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_IDLE_ON, time); } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { @@ -1653,5 +1707,4 @@ ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float defau float default_temperature_high) : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} -} // namespace thermostat -} // namespace esphome +} // namespace esphome::thermostat diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 76391f800c..564b6127b3 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -10,8 +10,7 @@ #include #include -namespace esphome { -namespace thermostat { +namespace esphome::thermostat { enum HumidificationAction : uint8_t { THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF = 0, @@ -41,13 +40,11 @@ enum OnBootRestoreFrom : uint8_t { struct ThermostatClimateTimer { ThermostatClimateTimer() = default; - ThermostatClimateTimer(bool active, uint32_t time, uint32_t started, std::function func) - : active(active), time(time), started(started), func(std::move(func)) {} + ThermostatClimateTimer(bool active, uint32_t time, uint32_t started) : active(active), time(time), started(started) {} bool active; uint32_t time; uint32_t started; - std::function func; }; struct ThermostatClimateTargetTempConfig { @@ -207,6 +204,9 @@ class ThermostatClimate : public climate::Climate, public Component { void validate_target_temperature_high(); void validate_target_humidity(); + /// The current humidification action + HumidificationAction humidification_action{THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE}; + protected: /// Override control to change settings of the climate device. void control(const climate::ClimateCall &call) override; @@ -263,7 +263,10 @@ class ThermostatClimate : public climate::Climate, public Component { bool cancel_timer_(ThermostatClimateTimerIndex timer_index); bool timer_active_(ThermostatClimateTimerIndex timer_index); uint32_t timer_duration_(ThermostatClimateTimerIndex timer_index); - std::function timer_cbf_(ThermostatClimateTimerIndex timer_index); + /// Call the appropriate timer callback based on timer index + void call_timer_callback_(ThermostatClimateTimerIndex timer_index); + /// Enhanced timer duration setter with running timer adjustment + void set_timer_duration_in_sec_(ThermostatClimateTimerIndex timer_index, uint32_t time); /// set_timeout() callbacks for various actions (see above) void cooling_max_run_time_timer_callback_(); @@ -301,9 +304,6 @@ class ThermostatClimate : public climate::Climate, public Component { /// The current supplemental action climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; - /// The current humidification action - HumidificationAction humidification_action_{THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE}; - /// Default standard preset to use on start up climate::ClimatePreset default_preset_{}; @@ -532,27 +532,16 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_humidity_control_trigger_{nullptr}; /// Climate action timers - std::array timer_{ - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)), - ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)), - }; + std::array timer_{}; /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) FixedVector preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") FixedVector custom_preset_config_{}; - /// Default custom preset to use on start up (pointer to entry in custom_preset_config_) + private: + /// Default custom preset to use on start up (pointer to entry in custom_preset_config_) const char *default_custom_preset_{nullptr}; }; -} // namespace thermostat -} // namespace esphome +} // namespace esphome::thermostat diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index f7c1916ffe..8bc87878d1 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -4,8 +4,7 @@ #include -namespace esphome { -namespace time { +namespace esphome::time { static const char *const TAG = "automation"; static const int MAX_TIMESTAMP_DRIFT = 900; // how far can the clock drift before we consider @@ -92,5 +91,4 @@ SyncTrigger::SyncTrigger(RealTimeClock *rtc) : rtc_(rtc) { rtc->add_on_time_sync_callback([this]() { this->trigger(); }); } -} // namespace time -} // namespace esphome +} // namespace esphome::time diff --git a/esphome/components/time/automation.h b/esphome/components/time/automation.h index b5c8291533..4ccfc641d6 100644 --- a/esphome/components/time/automation.h +++ b/esphome/components/time/automation.h @@ -8,8 +8,7 @@ #include -namespace esphome { -namespace time { +namespace esphome::time { class CronTrigger : public Trigger<>, public Component { public: @@ -48,5 +47,4 @@ class SyncTrigger : public Trigger<>, public Component { protected: RealTimeClock *rtc_; }; -} // namespace time -} // namespace esphome +} // namespace esphome::time diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 175cee0c1f..639af4457f 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -17,8 +17,7 @@ #include -namespace esphome { -namespace time { +namespace esphome::time { static const char *const TAG = "time"; @@ -78,5 +77,4 @@ void RealTimeClock::apply_timezone_() { } #endif -} // namespace time -} // namespace esphome +} // namespace esphome::time diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 2f17bd86d6..70469e11b0 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -7,8 +7,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/time.h" -namespace esphome { -namespace time { +namespace esphome::time { /// The RealTimeClock class exposes common timekeeping functions via the device's local real-time clock. /// @@ -75,5 +74,4 @@ template class TimeHasTimeCondition : public Condition { RealTimeClock *parent_; }; -} // namespace time -} // namespace esphome +} // namespace esphome::time diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py index 72afc18387..90043e969c 100644 --- a/esphome/components/tinyusb/__init__.py +++ b/esphome/components/tinyusb/__init__.py @@ -1,10 +1,11 @@ import esphome.codegen as cg from esphome.components import esp32 -from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option -from esphome.components.esp32.const import ( +from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + add_idf_component, + add_idf_sdkconfig_option, ) import esphome.config_validation as cv from esphome.const import CONF_ID diff --git a/esphome/components/tinyusb/tinyusb_component.cpp b/esphome/components/tinyusb/tinyusb_component.cpp index a2057c90ce..19bb545c4b 100644 --- a/esphome/components/tinyusb/tinyusb_component.cpp +++ b/esphome/components/tinyusb/tinyusb_component.cpp @@ -41,4 +41,4 @@ void TinyUSB::dump_config() { } } // namespace esphome::tinyusb -#endif +#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/tinyusb/tinyusb_component.h b/esphome/components/tinyusb/tinyusb_component.h index 56c286f455..7d8caade74 100644 --- a/esphome/components/tinyusb/tinyusb_component.h +++ b/esphome/components/tinyusb/tinyusb_component.h @@ -69,4 +69,4 @@ class TinyUSB : public Component { }; } // namespace esphome::tinyusb -#endif +#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/tlc5947/tlc5947.cpp b/esphome/components/tlc5947/tlc5947.cpp index 6d4e099f7a..0a278bbaf6 100644 --- a/esphome/components/tlc5947/tlc5947.cpp +++ b/esphome/components/tlc5947/tlc5947.cpp @@ -21,12 +21,14 @@ void TLC5947::setup() { this->pwm_amounts_.resize(this->num_chips_ * N_CHANNELS_PER_CHIP, 0); } void TLC5947::dump_config() { - ESP_LOGCONFIG(TAG, "TLC5947:"); + ESP_LOGCONFIG(TAG, + "TLC5947:\n" + " Number of chips: %u", + this->num_chips_); LOG_PIN(" Data Pin: ", this->data_pin_); LOG_PIN(" Clock Pin: ", this->clock_pin_); LOG_PIN(" LAT Pin: ", this->lat_pin_); LOG_PIN(" OE Pin: ", this->outenable_pin_); - ESP_LOGCONFIG(TAG, " Number of chips: %u", this->num_chips_); } void TLC5947::loop() { diff --git a/esphome/components/tlc5971/tlc5971.cpp b/esphome/components/tlc5971/tlc5971.cpp index 719ab7c2b3..be17780f8c 100644 --- a/esphome/components/tlc5971/tlc5971.cpp +++ b/esphome/components/tlc5971/tlc5971.cpp @@ -15,10 +15,12 @@ void TLC5971::setup() { this->pwm_amounts_.resize(this->num_chips_ * N_CHANNELS_PER_CHIP, 0); } void TLC5971::dump_config() { - ESP_LOGCONFIG(TAG, "TLC5971:"); + ESP_LOGCONFIG(TAG, + "TLC5971:\n" + " Number of chips: %u", + this->num_chips_); LOG_PIN(" Data Pin: ", this->data_pin_); LOG_PIN(" Clock Pin: ", this->clock_pin_); - ESP_LOGCONFIG(TAG, " Number of chips: %u", this->num_chips_); } void TLC5971::loop() { diff --git a/esphome/components/tmp1075/tmp1075.cpp b/esphome/components/tmp1075/tmp1075.cpp index 1d9b384c66..9eb1e86c75 100644 --- a/esphome/components/tmp1075/tmp1075.cpp +++ b/esphome/components/tmp1075/tmp1075.cpp @@ -73,12 +73,15 @@ void TMP1075Sensor::set_fault_count(const int faults) { } void TMP1075Sensor::log_config_() { - ESP_LOGV(TAG, " oneshot : %d", config_.fields.oneshot); - ESP_LOGV(TAG, " rate : %d", config_.fields.rate); - ESP_LOGV(TAG, " faults : %d", config_.fields.faults); - ESP_LOGV(TAG, " polarity : %d", config_.fields.polarity); - ESP_LOGV(TAG, " alert_mode: %d", config_.fields.alert_mode); - ESP_LOGV(TAG, " shutdown : %d", config_.fields.shutdown); + ESP_LOGV(TAG, + " oneshot : %d\n" + " rate : %d\n" + " faults : %d\n" + " polarity : %d\n" + " alert_mode: %d\n" + " shutdown : %d", + config_.fields.oneshot, config_.fields.rate, config_.fields.faults, config_.fields.polarity, + config_.fields.alert_mode, config_.fields.shutdown); } void TMP1075Sensor::write_config() { diff --git a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp index edfbb2ac60..a63e9c8318 100644 --- a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp +++ b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp @@ -14,8 +14,10 @@ void TuyaBinarySensor::setup() { } void TuyaBinarySensor::dump_config() { - ESP_LOGCONFIG(TAG, "Tuya Binary Sensor:"); - ESP_LOGCONFIG(TAG, " Binary Sensor has datapoint ID %u", this->sensor_id_); + ESP_LOGCONFIG(TAG, + "Tuya Binary Sensor:\n" + " Binary Sensor has datapoint ID %u", + this->sensor_id_); } } // namespace tuya diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index 1d2286e3c7..4d2ccba8b1 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -37,10 +37,6 @@ COLOR_TYPES = { TuyaLight = tuya_ns.class_("TuyaLight", light.LightOutput, cg.Component) -COLOR_CONFIG_ERROR = ( - "This option has been removed, use color_datapoint and color_type instead." -) - CONFIG_SCHEMA = cv.All( light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( { @@ -49,8 +45,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t, cv.Optional(CONF_MIN_VALUE_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, - cv.Optional(CONF_RGB_DATAPOINT): cv.invalid(COLOR_CONFIG_ERROR), - cv.Optional(CONF_HSV_DATAPOINT): cv.invalid(COLOR_CONFIG_ERROR), cv.Inclusive(CONF_COLOR_DATAPOINT, "color"): cv.uint8_t, cv.Inclusive(CONF_COLOR_TYPE, "color"): cv.enum(COLOR_TYPES, upper=True), cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp index fbe511811f..3c492d609d 100644 --- a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp @@ -20,9 +20,10 @@ void TuyaTextSensor::setup() { break; } case TuyaDatapointType::ENUM: { - std::string data = to_string(datapoint.value_enum); - ESP_LOGD(TAG, "MCU reported text sensor %u is: %s", datapoint.id, data.c_str()); - this->publish_state(data); + char buf[4]; // uint8_t max is 3 digits + null + snprintf(buf, sizeof(buf), "%u", datapoint.value_enum); + ESP_LOGD(TAG, "MCU reported text sensor %u is: %s", datapoint.id, buf); + this->publish_state(buf); break; } default: @@ -33,8 +34,10 @@ void TuyaTextSensor::setup() { } void TuyaTextSensor::dump_config() { - ESP_LOGCONFIG(TAG, "Tuya Text Sensor:"); - ESP_LOGCONFIG(TAG, " Text Sensor has datapoint ID %u", this->sensor_id_); + ESP_LOGCONFIG(TAG, + "Tuya Text Sensor:\n" + " Text Sensor has datapoint ID %u", + this->sensor_id_); } } // namespace tuya diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 12b14be9ff..2812fb6ad6 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -20,6 +20,8 @@ static const char *const TAG = "tuya"; static const int COMMAND_DELAY = 10; static const int RECEIVE_TIMEOUT = 300; static const int MAX_RETRIES = 5; +// Max bytes to log for datapoint values (larger values are truncated) +static constexpr size_t MAX_DATAPOINT_LOG_BYTES = 16; void Tuya::setup() { this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); @@ -51,7 +53,9 @@ void Tuya::dump_config() { } for (auto &info : this->datapoints_) { if (info.type == TuyaDatapointType::RAW) { - ESP_LOGCONFIG(TAG, " Datapoint %u: raw (value: %s)", info.id, format_hex_pretty(info.value_raw).c_str()); + char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)]; + ESP_LOGCONFIG(TAG, " Datapoint %u: raw (value: %s)", info.id, + format_hex_pretty_to(hex_buf, info.value_raw.data(), info.value_raw.size())); } else if (info.type == TuyaDatapointType::BOOLEAN) { ESP_LOGCONFIG(TAG, " Datapoint %u: switch (value: %s)", info.id, ONOFF(info.value_bool)); } else if (info.type == TuyaDatapointType::INTEGER) { @@ -122,8 +126,11 @@ bool Tuya::validate_message_() { // valid message const uint8_t *message_data = data + 6; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)]; ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, - format_hex_pretty(message_data, length).c_str(), static_cast(this->init_state_)); + format_hex_pretty_to(hex_buf, message_data, length), static_cast(this->init_state_)); +#endif this->handle_command_(command, version, message_data, length); // return false to reset rx buffer @@ -349,7 +356,11 @@ void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) { switch (datapoint.type) { case TuyaDatapointType::RAW: datapoint.value_raw = std::vector(data, data + data_size); - ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, format_hex_pretty(datapoint.value_raw).c_str()); + { + char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)]; + ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, + format_hex_pretty_to(hex_buf, datapoint.value_raw.data(), datapoint.value_raw.size())); + } break; case TuyaDatapointType::BOOLEAN: if (data_size != 1) { @@ -460,8 +471,12 @@ void Tuya::send_raw_command_(TuyaCommand command) { break; } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)]; ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", static_cast(command.cmd), - version, format_hex_pretty(command.payload).c_str(), static_cast(this->init_state_)); + version, format_hex_pretty_to(hex_buf, command.payload.data(), command.payload.size()), + static_cast(this->init_state_)); +#endif this->write_array({0x55, 0xAA, version, (uint8_t) command.cmd, len_hi, len_lo}); if (!command.payload.empty()) @@ -675,7 +690,8 @@ void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType } void Tuya::set_raw_datapoint_value_(uint8_t datapoint_id, const std::vector &value, bool forced) { - ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, format_hex_pretty(value).c_str()); + char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)]; + ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, format_hex_pretty_to(hex_buf, value.data(), value.size())); optional datapoint = this->get_datapoint_(datapoint_id); if (!datapoint.has_value()) { ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id); diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 7b0d9726b8..31e37a06e0 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from logging import getLogger import math import re @@ -18,7 +19,6 @@ from esphome.const import ( CONF_DUMMY_RECEIVER_ID, CONF_FLOW_CONTROL_PIN, CONF_ID, - CONF_INVERT, CONF_LAMBDA, CONF_NUMBER, CONF_PORT, @@ -32,13 +32,26 @@ from esphome.const import ( PLATFORM_HOST, PlatformFramework, ) -from esphome.core import CORE, ID +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.yaml_util import make_data_base _LOGGER = getLogger(__name__) CODEOWNERS = ["@esphome/core"] +DOMAIN = "uart" + + +def AUTO_LOAD() -> list[str]: + """Ideally, we would only auto-load socket only when wake_loop_on_rx is requested; + however, AUTO_LOAD is examined before wake_loop_on_rx is set, so instead, since ESP32 + always uses socket select support in the main app, we'll just ensure it's loaded here. + """ + if CORE.is_esp32: + return ["socket"] + return [] + + uart_ns = cg.esphome_ns.namespace("uart") UARTComponent = uart_ns.class_("UARTComponent") @@ -52,6 +65,7 @@ LibreTinyUARTComponent = uart_ns.class_( ) HostUartComponent = uart_ns.class_("HostUartComponent", UARTComponent, cg.Component) + NATIVE_UART_CLASSES = ( str(IDFUARTComponent), str(ESP8266UartComponent), @@ -100,6 +114,38 @@ MULTI_CONF = True MULTI_CONF_NO_DEFAULT = True +@dataclass +class UARTData: + """State data for UART component configuration generation.""" + + wake_loop_on_rx: bool = False + + +def _get_data() -> UARTData: + """Get UART component data from CORE.data.""" + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = UARTData() + return CORE.data[DOMAIN] + + +def request_wake_loop_on_rx() -> None: + """Request that the UART wake the main loop when data is received. + + Components that need low-latency notification of incoming UART data + should call this function during their code generation. + This enables the RX event task which wakes the main loop when data arrives. + """ + data = _get_data() + if not data.wake_loop_on_rx: + data.wake_loop_on_rx = True + + # UART RX event task uses wake_loop_threadsafe() to notify the main loop + # Automatically enable the socket wake infrastructure when RX wake is requested + from esphome.components import socket + + socket.require_wake_loop_threadsafe() + + def validate_raw_data(value): if isinstance(value, str): return value.encode("utf-8") @@ -257,9 +303,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PARITY, default="NONE"): cv.enum( UART_PARITY_OPTIONS, upper=True ), - cv.Optional(CONF_INVERT): cv.invalid( - "This option has been removed. Please instead use invert in the tx/rx pin schemas." - ), cv.Optional(CONF_DEBUG): maybe_empty_debug, } ).extend(cv.COMPONENT_SCHEMA), @@ -335,6 +378,30 @@ async def to_code(config): if CONF_DEBUG in config: await debug_to_code(config[CONF_DEBUG], var) + # ESP8266: Enable the Arduino Serial objects that might be used based on pin config + # The C++ code selects hardware serial at runtime based on these pin combinations: + # - Serial (UART0): TX=1 or null, RX=3 or null + # - Serial (UART0 swap): TX=15 or null, RX=13 or null + # - Serial1: TX=2 or null, RX=8 or null + if CORE.is_esp8266: + from esphome.components.esp8266.const import enable_serial, enable_serial1 + + tx_num = config[CONF_TX_PIN][CONF_NUMBER] if CONF_TX_PIN in config else None + rx_num = config[CONF_RX_PIN][CONF_NUMBER] if CONF_RX_PIN in config else None + + # Check if this config could use Serial (UART0 regular or swap) + if (tx_num is None or tx_num in (1, 15)) and ( + rx_num is None or rx_num in (3, 13) + ): + enable_serial() + cg.add_define("USE_ESP8266_UART_SERIAL") + # Check if this config could use Serial1 + if (tx_num is None or tx_num == 2) and (rx_num is None or rx_num == 8): + enable_serial1() + cg.add_define("USE_ESP8266_UART_SERIAL1") + + CORE.add_job(final_step) + # A schema to use for all UART devices, all UART integrations must extend this! UART_DEVICE_SCHEMA = cv.Schema( @@ -437,7 +504,7 @@ def final_validate_device_schema( async def register_uart_device(var, config): """Register a UART device, setting up all the internal values. - This is a coroutine, you need to await it with a 'yield' expression! + This is a coroutine, you need to await it with an 'await' expression! """ parent = await cg.get_variable(config[CONF_UART_ID]) cg.add(var.set_uart_parent(parent)) @@ -472,6 +539,13 @@ async def uart_write_to_code(config, action_id, template_arg, args): return var +@coroutine_with_priority(CoroPriority.FINAL) +async def final_step(): + """Final code generation step to configure optional UART features.""" + if _get_data().wake_loop_on_rx: + cg.add_define("USE_UART_WAKE_LOOP_ON_RX") + + FILTER_SOURCE_FILES = filter_source_files_from_platform( { "uart_component_esp_idf.cpp": { diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index c2eb308eb8..c99caac97b 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -5,8 +5,7 @@ #include -namespace esphome { -namespace uart { +namespace esphome::uart { template class UARTWriteAction : public Action, public Parented { public: @@ -41,5 +40,4 @@ template class UARTWriteAction : public Action, public Pa } code_; }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/button/uart_button.cpp b/esphome/components/uart/button/uart_button.cpp index dd228b9bb7..809ceaabb0 100644 --- a/esphome/components/uart/button/uart_button.cpp +++ b/esphome/components/uart/button/uart_button.cpp @@ -1,8 +1,7 @@ #include "uart_button.h" #include "esphome/core/log.h" -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart.button"; @@ -13,5 +12,4 @@ void UARTButton::press_action() { void UARTButton::dump_config() { LOG_BUTTON("", "UART Button", this); } -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/button/uart_button.h b/esphome/components/uart/button/uart_button.h index 8c7d762a05..2b530d3c4b 100644 --- a/esphome/components/uart/button/uart_button.h +++ b/esphome/components/uart/button/uart_button.h @@ -6,8 +6,7 @@ #include -namespace esphome { -namespace uart { +namespace esphome::uart { class UARTButton : public button::Button, public UARTDevice, public Component { public: @@ -21,5 +20,4 @@ class UARTButton : public button::Button, public UARTDevice, public Component { std::vector data_; }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/event/__init__.py b/esphome/components/uart/event/__init__.py new file mode 100644 index 0000000000..64af318a11 --- /dev/null +++ b/esphome/components/uart/event/__init__.py @@ -0,0 +1,90 @@ +import esphome.codegen as cg +from esphome.components import event, uart +import esphome.config_validation as cv +from esphome.const import CONF_EVENT_TYPES, CONF_ID +from esphome.core import ID +from esphome.types import ConfigType + +from .. import uart_ns + +CODEOWNERS = ["@eoasmxd"] + +DEPENDENCIES = ["uart"] + +UARTEvent = uart_ns.class_("UARTEvent", event.Event, uart.UARTDevice, cg.Component) + + +def validate_event_types(value) -> list[tuple[str, str | list[int]]]: + if not isinstance(value, list): + raise cv.Invalid("Event type must be a list of key-value mappings.") + + processed: list[tuple[str, str | list[int]]] = [] + for item in value: + if not isinstance(item, dict): + raise cv.Invalid(f"Event type item must be a mapping (dictionary): {item}") + if len(item) != 1: + raise cv.Invalid( + f"Event type item must be a single key-value mapping: {item}" + ) + + # Get the single key-value pair + event_name, match_data = next(iter(item.items())) + + if not isinstance(event_name, str): + raise cv.Invalid(f"Event name (key) must be a string: {event_name}") + + try: + # Try to validate as list of hex bytes + match_data_bin = cv.ensure_list(cv.hex_uint8_t)(match_data) + processed.append((event_name, match_data_bin)) + continue + except cv.Invalid: + pass # Not binary, try string + + try: + # Try to validate as string + match_data_str = cv.string_strict(match_data) + processed.append((event_name, match_data_str)) + continue + except cv.Invalid: + pass # Not string either + + # If neither validation passed + raise cv.Invalid( + f"Event match data for '{event_name}' must be a string or a list of hex bytes. Invalid data: {match_data}" + ) + + if not processed: + raise cv.Invalid("event_types must contain at least one event mapping.") + + return processed + + +CONFIG_SCHEMA = ( + event.event_schema(UARTEvent) + .extend( + { + cv.Required(CONF_EVENT_TYPES): validate_event_types, + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config: ConfigType) -> None: + event_names = [item[0] for item in config[CONF_EVENT_TYPES]] + var = await event.new_event(config, event_types=event_names) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + for i, (event_name, match_data) in enumerate(config[CONF_EVENT_TYPES]): + if isinstance(match_data, str): + match_data = [ord(c) for c in match_data] + + match_data_var_id = ID( + f"match_data_{config[CONF_ID]}_{i}", is_declaration=True, type=cg.uint8 + ) + match_data_var = cg.static_const_array( + match_data_var_id, cg.ArrayInitializer(*match_data) + ) + cg.add(var.add_event_matcher(event_name, match_data_var, len(match_data))) diff --git a/esphome/components/uart/event/uart_event.cpp b/esphome/components/uart/event/uart_event.cpp new file mode 100644 index 0000000000..02c5f2e631 --- /dev/null +++ b/esphome/components/uart/event/uart_event.cpp @@ -0,0 +1,48 @@ +#include "uart_event.h" +#include "esphome/core/log.h" +#include + +namespace esphome::uart { + +static const char *const TAG = "uart.event"; + +void UARTEvent::setup() {} + +void UARTEvent::dump_config() { LOG_EVENT("", "UART Event", this); } + +void UARTEvent::loop() { this->read_data_(); } + +void UARTEvent::add_event_matcher(const char *event_name, const uint8_t *match_data, size_t match_data_len) { + this->matchers_.push_back({event_name, match_data, match_data_len}); + if (match_data_len > this->max_matcher_len_) { + this->max_matcher_len_ = match_data_len; + } +} + +void UARTEvent::read_data_() { + while (this->available()) { + uint8_t data; + this->read_byte(&data); + this->buffer_.push_back(data); + + bool match_found = false; + for (const auto &matcher : this->matchers_) { + if (this->buffer_.size() < matcher.data_len) { + continue; + } + + if (std::equal(matcher.data, matcher.data + matcher.data_len, this->buffer_.end() - matcher.data_len)) { + this->trigger(matcher.event_name); + this->buffer_.clear(); + match_found = true; + break; + } + } + + if (!match_found && this->max_matcher_len_ > 0 && this->buffer_.size() > this->max_matcher_len_) { + this->buffer_.erase(this->buffer_.begin()); + } + } +} + +} // namespace esphome::uart diff --git a/esphome/components/uart/event/uart_event.h b/esphome/components/uart/event/uart_event.h new file mode 100644 index 0000000000..8a00b5894b --- /dev/null +++ b/esphome/components/uart/event/uart_event.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/event/event.h" +#include "esphome/components/uart/uart.h" +#include + +namespace esphome::uart { + +class UARTEvent : public event::Event, public UARTDevice, public Component { + public: + void setup() override; + void loop() override; + void dump_config() override; + + void add_event_matcher(const char *event_name, const uint8_t *match_data, size_t match_data_len); + + protected: + struct EventMatcher { + const char *event_name; + const uint8_t *data; + size_t data_len; + }; + + void read_data_(); + std::vector matchers_; + std::vector buffer_; + size_t max_matcher_len_ = 0; +}; + +} // namespace esphome::uart diff --git a/esphome/components/uart/packet_transport/uart_transport.cpp b/esphome/components/uart/packet_transport/uart_transport.cpp index 423b657532..6b8eae611c 100644 --- a/esphome/components/uart/packet_transport/uart_transport.cpp +++ b/esphome/components/uart/packet_transport/uart_transport.cpp @@ -2,8 +2,7 @@ #include "esphome/core/application.h" #include "uart_transport.h" -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart_transport"; @@ -56,12 +55,6 @@ void UARTTransport::loop() { } } -void UARTTransport::update() { - this->updated_ = true; - this->resend_data_ = true; - PacketTransport::update(); -} - /** * Write a byte to the UART bus. If the byte is a flag or control byte, it will be escaped. * @param byte The byte to write. @@ -84,5 +77,5 @@ void UARTTransport::send_packet(const std::vector &buf) const { this->write_byte_(crc >> 8); this->parent_->write_byte(FLAG_BYTE); } -} // namespace uart -} // namespace esphome + +} // namespace esphome::uart diff --git a/esphome/components/uart/packet_transport/uart_transport.h b/esphome/components/uart/packet_transport/uart_transport.h index f1431e948c..1c92af536e 100644 --- a/esphome/components/uart/packet_transport/uart_transport.h +++ b/esphome/components/uart/packet_transport/uart_transport.h @@ -5,8 +5,7 @@ #include #include "../uart.h" -namespace esphome { -namespace uart { +namespace esphome::uart { /** * A transport protocol for sending and receiving packets over a UART connection. @@ -24,7 +23,6 @@ static const uint8_t CONTROL_BYTE = 0x7D; class UARTTransport : public packet_transport::PacketTransport, public UARTDevice { public: void loop() override; - void update() override; float get_setup_priority() const override { return setup_priority::PROCESSOR; } protected: @@ -37,5 +35,4 @@ class UARTTransport : public packet_transport::PacketTransport, public UARTDevic bool rx_control_{}; }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/switch/uart_switch.cpp b/esphome/components/uart/switch/uart_switch.cpp index 4f5ff9fc99..642bd19772 100644 --- a/esphome/components/uart/switch/uart_switch.cpp +++ b/esphome/components/uart/switch/uart_switch.cpp @@ -2,8 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart.switch"; @@ -58,5 +57,4 @@ void UARTSwitch::dump_config() { } } -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/switch/uart_switch.h b/esphome/components/uart/switch/uart_switch.h index 909307d57e..5730fc9b4b 100644 --- a/esphome/components/uart/switch/uart_switch.h +++ b/esphome/components/uart/switch/uart_switch.h @@ -7,8 +7,7 @@ #include #include -namespace esphome { -namespace uart { +namespace esphome::uart { class UARTSwitch : public switch_::Switch, public UARTDevice, public Component { public: @@ -33,5 +32,4 @@ class UARTSwitch : public switch_::Switch, public UARTDevice, public Component { uint32_t last_transmission_; }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index b18454bf9d..6cfd6537a5 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -5,8 +5,7 @@ #include "esphome/core/log.h" #include -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart"; @@ -43,5 +42,4 @@ const LogString *parity_to_str(UARTParityOptions parity) { } } -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index e2912db122..72c282f1c4 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -6,8 +6,7 @@ #include "esphome/core/log.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { class UARTDevice { public: @@ -74,5 +73,4 @@ class UARTDevice { UARTComponent *parent_{nullptr}; }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/uart_component.cpp b/esphome/components/uart/uart_component.cpp index 8f670275d4..30fc208fc9 100644 --- a/esphome/components/uart/uart_component.cpp +++ b/esphome/components/uart/uart_component.cpp @@ -1,7 +1,6 @@ #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart"; @@ -28,5 +27,4 @@ void UARTComponent::set_rx_full_threshold_ms(uint8_t time) { this->set_rx_full_threshold(val); } -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 452688b3e9..ea6e1562f4 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -11,8 +11,7 @@ #include "esphome/core/automation.h" #endif -namespace esphome { -namespace uart { +namespace esphome::uart { enum UARTParityOptions { UART_CONFIG_PARITY_NONE, @@ -190,14 +189,13 @@ class UARTComponent { size_t rx_buffer_size_; size_t rx_full_threshold_{1}; size_t rx_timeout_{0}; - uint32_t baud_rate_; - uint8_t stop_bits_; - uint8_t data_bits_; - UARTParityOptions parity_; + uint32_t baud_rate_{0}; + uint8_t stop_bits_{0}; + uint8_t data_bits_{0}; + UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; #ifdef USE_UART_DEBUGGER CallbackManager debug_callback_{}; #endif }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index c84a877ef4..504d494e2e 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -9,8 +9,7 @@ #include "esphome/components/logger/logger.h" #endif -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart.arduino_esp8266"; bool ESP8266UartComponent::serial0_in_use = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -76,6 +75,7 @@ void ESP8266UartComponent::setup() { // is 1 we still want to use Serial. SerialConfig config = static_cast(get_config()); +#ifdef USE_ESP8266_UART_SERIAL if (!ESP8266UartComponent::serial0_in_use && (tx_pin_ == nullptr || tx_pin_->get_pin() == 1) && (rx_pin_ == nullptr || rx_pin_->get_pin() == 3) #ifdef USE_LOGGER @@ -101,11 +101,16 @@ void ESP8266UartComponent::setup() { this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); this->hw_serial_->swap(); ESP8266UartComponent::serial0_in_use = true; - } else if ((tx_pin_ == nullptr || tx_pin_->get_pin() == 2) && (rx_pin_ == nullptr || rx_pin_->get_pin() == 8)) { + } else +#endif // USE_ESP8266_UART_SERIAL +#ifdef USE_ESP8266_UART_SERIAL1 + if ((tx_pin_ == nullptr || tx_pin_->get_pin() == 2) && (rx_pin_ == nullptr || rx_pin_->get_pin() == 8)) { this->hw_serial_ = &Serial1; this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); - } else { + } else +#endif // USE_ESP8266_UART_SERIAL1 + { this->sw_serial_ = new ESP8266SoftwareSerial(); // NOLINT this->sw_serial_->setup(tx_pin_, rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_, this->parity_, this->rx_buffer_size_); @@ -331,6 +336,5 @@ int ESP8266SoftwareSerial::available() { return avail; } -} // namespace uart -} // namespace esphome +} // namespace esphome::uart #endif // USE_ESP8266 diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index 749dd4c61e..e33dd00644 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -9,8 +9,7 @@ #include "esphome/core/log.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { class ESP8266SoftwareSerial { public: @@ -88,7 +87,5 @@ class ESP8266UartComponent : public UARTComponent, public Component { static bool serial0_in_use; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) }; -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_ESP8266 diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 61ca8c1c0c..90997787aa 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -9,13 +9,14 @@ #include "esphome/core/gpio.h" #include "driver/gpio.h" #include "soc/gpio_num.h" +#include "soc/uart_pins.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif -namespace esphome { -namespace uart { +namespace esphome::uart { + static const char *const TAG = "uart.idf"; uart_config_t IDFUARTComponent::get_config_() { @@ -112,6 +113,12 @@ void IDFUARTComponent::load_settings(bool dump_config) { esp_err_t err; if (uart_is_driver_installed(this->uart_num_)) { +#ifdef USE_UART_WAKE_LOOP_ON_RX + if (this->rx_event_task_handle_ != nullptr) { + vTaskDelete(this->rx_event_task_handle_); + this->rx_event_task_handle_ = nullptr; + } +#endif err = uart_driver_delete(this->uart_num_); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err)); @@ -133,6 +140,22 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } + int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; + int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; + int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; + + // Workaround for ESP-IDF issue: https://github.com/espressif/esp-idf/issues/17459 + // Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks + // UART on default UART0 pins that may have residual state from boot console. + // Reset these pins before configuring UART to ensure they're in a clean state. + if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) { + gpio_reset_pin(static_cast(tx)); + } + if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) { + gpio_reset_pin(static_cast(rx)); + } + + // Setup pins after reset to preserve open drain/pullup/pulldown flags auto setup_pin_if_needed = [](InternalGPIOPin *pin) { if (!pin) { return; @@ -148,10 +171,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } - int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; - int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; - int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; - uint32_t invert = 0; if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { invert |= UART_SIGNAL_TXD_INV; @@ -204,6 +223,11 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } +#ifdef USE_UART_WAKE_LOOP_ON_RX + // Start the RX event task to enable low-latency data notifications + this->start_rx_event_task_(); +#endif // USE_UART_WAKE_LOOP_ON_RX + if (dump_config) { ESP_LOGCONFIG(TAG, "Reloaded UART %u", this->uart_num_); this->dump_config(); @@ -226,7 +250,11 @@ void IDFUARTComponent::dump_config() { " Baud Rate: %" PRIu32 " baud\n" " Data Bits: %u\n" " Parity: %s\n" - " Stop bits: %u", + " Stop bits: %u" +#ifdef USE_UART_WAKE_LOOP_ON_RX + "\n Wake on data RX: ENABLED" +#endif + , this->baud_rate_, this->data_bits_, LOG_STR_ARG(parity_to_str(this->parity_)), this->stop_bits_); this->check_logger_conflict(); } @@ -337,7 +365,62 @@ void IDFUARTComponent::flush() { void IDFUARTComponent::check_logger_conflict() {} -} // namespace uart -} // namespace esphome +#ifdef USE_UART_WAKE_LOOP_ON_RX +void IDFUARTComponent::start_rx_event_task_() { + // Create FreeRTOS task to monitor UART events + BaseType_t result = xTaskCreate(rx_event_task_func, // Task function + "uart_rx_evt", // Task name (max 16 chars) + 2240, // Stack size in bytes (~2.2KB); increase if needed for logging + this, // Task parameter (this pointer) + tskIDLE_PRIORITY + 1, // Priority (low, just above idle) + &this->rx_event_task_handle_ // Task handle + ); + if (result != pdPASS) { + ESP_LOGE(TAG, "Failed to create RX event task"); + return; + } + + ESP_LOGV(TAG, "RX event task started"); +} + +void IDFUARTComponent::rx_event_task_func(void *param) { + auto *self = static_cast(param); + uart_event_t event; + + ESP_LOGV(TAG, "RX event task running"); + + // Run forever - task lifecycle matches component lifecycle + while (true) { + // Wait for UART events (blocks efficiently) + if (xQueueReceive(self->uart_event_queue_, &event, portMAX_DELAY) == pdTRUE) { + switch (event.type) { + case UART_DATA: + // Data available in UART RX buffer - wake the main loop + ESP_LOGVV(TAG, "Data event: %d bytes", event.size); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + break; + + case UART_FIFO_OVF: + case UART_BUFFER_FULL: + ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing"); + uart_flush_input(self->uart_num_); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + break; + + default: + // Ignore other event types + ESP_LOGVV(TAG, "Event type: %d", event.type); + break; + } + } + } +} +#endif // USE_UART_WAKE_LOOP_ON_RX + +} // namespace esphome::uart #endif // USE_ESP32 diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index a2ba2aa968..bd6d0c792e 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -6,8 +6,7 @@ #include "esphome/core/component.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { class IDFUARTComponent : public UARTComponent, public Component { public: @@ -53,9 +52,15 @@ class IDFUARTComponent : public UARTComponent, public Component { bool has_peek_{false}; uint8_t peek_byte_; + +#ifdef USE_UART_WAKE_LOOP_ON_RX + // RX notification support + void start_rx_event_task_(); + static void rx_event_task_func(void *param); + + TaskHandle_t rx_event_task_handle_{nullptr}; +#endif // USE_UART_WAKE_LOOP_ON_RX }; -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_ESP32 diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index adb11266c5..69b24607d1 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -96,8 +96,7 @@ speed_t get_baud(int baud) { } // namespace -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart.host"; @@ -296,7 +295,5 @@ void HostUartComponent::update_error_(const std::string &error) { ESP_LOGE(TAG, "Port error: %s", error.c_str()); } -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_HOST diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index c1f1dd0d2c..a4a6946c0c 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -6,8 +6,7 @@ #include "esphome/core/log.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { class HostUartComponent : public UARTComponent, public Component { public: @@ -32,7 +31,5 @@ class HostUartComponent : public UARTComponent, public Component { uint8_t peek_byte_; }; -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_HOST diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 1e408b169b..863732c88d 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -14,8 +14,7 @@ #include #endif -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart.lt"; @@ -121,8 +120,10 @@ void LibreTinyUARTComponent::setup() { void LibreTinyUARTComponent::dump_config() { bool is_software = this->hardware_idx_ == -1; - ESP_LOGCONFIG(TAG, "UART Bus:"); - ESP_LOGCONFIG(TAG, " Type: %s", UART_TYPE[is_software]); + ESP_LOGCONFIG(TAG, + "UART Bus:\n" + " Type: %s", + UART_TYPE[is_software]); if (!is_software) { ESP_LOGCONFIG(TAG, " Port number: %d", this->hardware_idx_); } @@ -187,7 +188,5 @@ void LibreTinyUARTComponent::check_logger_conflict() { #endif } -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_LIBRETINY diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h index 00982fd297..ec13e7da5a 100644 --- a/esphome/components/uart/uart_component_libretiny.h +++ b/esphome/components/uart/uart_component_libretiny.h @@ -8,8 +8,7 @@ #include "esphome/core/log.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { class LibreTinyUARTComponent : public UARTComponent, public Component { public: @@ -37,7 +36,5 @@ class LibreTinyUARTComponent : public UARTComponent, public Component { int8_t hardware_idx_{-1}; }; -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_LIBRETINY diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index cd3905b5c1..5799d26a54 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -11,8 +11,7 @@ #include "esphome/components/logger/logger.h" #endif -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart.arduino_rp2040"; @@ -193,7 +192,5 @@ void RP2040UartComponent::flush() { this->serial_->flush(); } -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_RP2040 diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2040.h index f26c913cff..d626d11a2e 100644 --- a/esphome/components/uart/uart_component_rp2040.h +++ b/esphome/components/uart/uart_component_rp2040.h @@ -11,8 +11,7 @@ #include "esphome/core/log.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { class RP2040UartComponent : public UARTComponent, public Component { public: @@ -40,7 +39,5 @@ class RP2040UartComponent : public UARTComponent, public Component { HardwareSerial *serial_{nullptr}; }; -} // namespace uart -} // namespace esphome - +} // namespace esphome::uart #endif // USE_RP2040 diff --git a/esphome/components/uart/uart_debugger.cpp b/esphome/components/uart/uart_debugger.cpp index e2d92eac60..b51a57d68e 100644 --- a/esphome/components/uart/uart_debugger.cpp +++ b/esphome/components/uart/uart_debugger.cpp @@ -6,8 +6,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace uart { +namespace esphome::uart { static const char *const TAG = "uart_debug"; @@ -197,6 +196,5 @@ void UARTDebug::log_binary(UARTDirection direction, std::vector bytes, delay(10); } -} // namespace uart -} // namespace esphome +} // namespace esphome::uart #endif diff --git a/esphome/components/uart/uart_debugger.h b/esphome/components/uart/uart_debugger.h index 4f9b6d09df..df87655962 100644 --- a/esphome/components/uart/uart_debugger.h +++ b/esphome/components/uart/uart_debugger.h @@ -8,8 +8,7 @@ #include "uart.h" #include "uart_component.h" -namespace esphome { -namespace uart { +namespace esphome::uart { /// The UARTDebugger class adds debugging support to a UART bus. /// @@ -96,6 +95,5 @@ class UARTDebug { static void log_binary(UARTDirection direction, std::vector bytes, uint8_t separator); }; -} // namespace uart -} // namespace esphome +} // namespace esphome::uart #endif diff --git a/esphome/components/udp/packet_transport/udp_transport.cpp b/esphome/components/udp/packet_transport/udp_transport.cpp index b92e0d64df..f3e33573a5 100644 --- a/esphome/components/udp/packet_transport/udp_transport.cpp +++ b/esphome/components/udp/packet_transport/udp_transport.cpp @@ -8,29 +8,14 @@ namespace udp { static const char *const TAG = "udp_transport"; -bool UDPTransport::should_send() { return this->should_broadcast_ && network::is_connected(); } +bool UDPTransport::should_send() { return network::is_connected(); } void UDPTransport::setup() { PacketTransport::setup(); - this->should_broadcast_ = this->ping_pong_enable_; -#ifdef USE_SENSOR - this->should_broadcast_ |= !this->sensors_.empty(); -#endif -#ifdef USE_BINARY_SENSOR - this->should_broadcast_ |= !this->binary_sensors_.empty(); -#endif - if (this->should_broadcast_) - this->parent_->set_should_broadcast(); if (!this->providers_.empty() || this->is_encrypted_()) { this->parent_->add_listener([this](std::vector &buf) { this->process_(buf); }); } } -void UDPTransport::update() { - PacketTransport::update(); - this->updated_ = true; - this->resend_data_ = this->should_broadcast_; -} - void UDPTransport::send_packet(const std::vector &buf) const { this->parent_->send_packet(buf); } } // namespace udp } // namespace esphome diff --git a/esphome/components/udp/packet_transport/udp_transport.h b/esphome/components/udp/packet_transport/udp_transport.h index c87eb62780..8d01ae0909 100644 --- a/esphome/components/udp/packet_transport/udp_transport.h +++ b/esphome/components/udp/packet_transport/udp_transport.h @@ -12,14 +12,12 @@ namespace udp { class UDPTransport : public packet_transport::PacketTransport, public Parented { public: void setup() override; - void update() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } protected: void send_packet(const std::vector &buf) const override; bool should_send() override; - bool should_broadcast_{false}; size_t get_max_packet_size() override { return MAX_PACKET_SIZE; } }; diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 8a9ce612b4..4474efeb77 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -21,8 +21,8 @@ void UDPComponent::setup() { if (this->should_broadcast_) { this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); - this->status_set_error("Could not create socket"); return; } int enable = 1; @@ -41,15 +41,15 @@ void UDPComponent::setup() { if (this->should_listen_) { this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->listen_socket_ == nullptr) { + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); - this->status_set_error("Could not create socket"); return; } auto err = this->listen_socket_->setblocking(false); if (err < 0) { ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno); + this->status_set_error(LOG_STR("Unable to set nonblocking")); this->mark_failed(); - this->status_set_error("Unable to set nonblocking"); return; } int enable = 1; @@ -65,16 +65,19 @@ void UDPComponent::setup() { server.sin_port = htons(this->listen_port_); if (this->listen_address_.has_value()) { + // Only 16 bytes needed for IPv4, but use standard size for consistency + char addr_buf[network::IP_ADDRESS_BUFFER_SIZE]; + this->listen_address_.value().str_to(addr_buf); struct ip_mreq imreq = {}; imreq.imr_interface.s_addr = ESPHOME_INADDR_ANY; - inet_aton(this->listen_address_.value().str().c_str(), &imreq.imr_multiaddr); + inet_aton(addr_buf, &imreq.imr_multiaddr); server.sin_addr.s_addr = imreq.imr_multiaddr.s_addr; - ESP_LOGD(TAG, "Join multicast %s", this->listen_address_.value().str().c_str()); + ESP_LOGD(TAG, "Join multicast %s", addr_buf); err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq)); if (err < 0) { ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno); + this->status_set_error(LOG_STR("Failed to set IP_ADD_MEMBERSHIP")); this->mark_failed(); - this->status_set_error("Failed to set IP_ADD_MEMBERSHIP"); return; } } @@ -82,8 +85,8 @@ void UDPComponent::setup() { err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); + this->status_set_error(LOG_STR("Unable to bind socket")); this->mark_failed(); - this->status_set_error("Unable to bind socket"); return; } } @@ -130,7 +133,8 @@ void UDPComponent::dump_config() { for (const auto &address : this->addresses_) ESP_LOGCONFIG(TAG, " Address: %s", address.c_str()); if (this->listen_address_.has_value()) { - ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str().c_str()); + char addr_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str_to(addr_buf)); } ESP_LOGCONFIG(TAG, " Broadcasting: %s\n" diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index 0a57ecc67b..3868dc92b7 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -102,16 +102,16 @@ void UFireECComponent::write_data_(uint8_t reg, float data) { } void UFireECComponent::dump_config() { - ESP_LOGCONFIG(TAG, "uFire-EC"); + ESP_LOGCONFIG(TAG, + "uFire-EC:\n" + " Temperature Compensation: %f\n" + " Temperature Coefficient: %f", + this->temperature_compensation_, this->temperature_coefficient_); LOG_I2C_DEVICE(this) LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "EC Sensor", this->ec_sensor_); LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_); LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_); - ESP_LOGCONFIG(TAG, - " Temperature Compensation: %f\n" - " Temperature Coefficient: %f", - this->temperature_compensation_, this->temperature_coefficient_); } } // namespace ufire_ec diff --git a/esphome/components/uln2003/uln2003.cpp b/esphome/components/uln2003/uln2003.cpp index 991fe53487..11e1c3d4c0 100644 --- a/esphome/components/uln2003/uln2003.cpp +++ b/esphome/components/uln2003/uln2003.cpp @@ -34,12 +34,14 @@ void ULN2003::loop() { this->write_step_(this->current_uln_pos_); } void ULN2003::dump_config() { - ESP_LOGCONFIG(TAG, "ULN2003:"); + ESP_LOGCONFIG(TAG, + "ULN2003:\n" + " Sleep when done: %s", + YESNO(this->sleep_when_done_)); LOG_PIN(" Pin A: ", this->pin_a_); LOG_PIN(" Pin B: ", this->pin_b_); LOG_PIN(" Pin C: ", this->pin_c_); LOG_PIN(" Pin D: ", this->pin_d_); - ESP_LOGCONFIG(TAG, " Sleep when done: %s", YESNO(this->sleep_when_done_)); const char *step_mode_s; switch (this->step_mode_) { case ULN2003_STEP_MODE_FULL_STEP: diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index 937d9a5261..4b04ee7578 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -1,3 +1,5 @@ +import logging + from esphome import pins import esphome.codegen as cg from esphome.components import sensor @@ -11,6 +13,8 @@ from esphome.const import ( UNIT_METER, ) +_LOGGER = logging.getLogger(__name__) + CONF_PULSE_TIME = "pulse_time" ultrasonic_ns = cg.esphome_ns.namespace("ultrasonic") @@ -28,9 +32,9 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.Required(CONF_TRIGGER_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_TRIGGER_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema, - cv.Optional(CONF_TIMEOUT, default="2m"): cv.distance, + cv.Optional(CONF_TIMEOUT): cv.distance, cv.Optional( CONF_PULSE_TIME, default="10us" ): cv.positive_time_period_microseconds, @@ -49,5 +53,11 @@ async def to_code(config): echo = await cg.gpio_pin_expression(config[CONF_ECHO_PIN]) cg.add(var.set_echo_pin(echo)) - cg.add(var.set_timeout_us(config[CONF_TIMEOUT] / (0.000343 / 2))) + # Remove before 2026.8.0 + if CONF_TIMEOUT in config: + _LOGGER.warning( + "'timeout' option is deprecated and will be removed in 2026.8.0. " + "The option has no effect and can be safely removed." + ) + cg.add(var.set_pulse_time_us(config[CONF_PULSE_TIME])) diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index e864ea6419..369a10edbd 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -1,64 +1,89 @@ #include "ultrasonic_sensor.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" -namespace esphome { -namespace ultrasonic { +namespace esphome::ultrasonic { static const char *const TAG = "ultrasonic.sensor"; +static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us (noise filtering) +static constexpr uint32_t MEASUREMENT_TIMEOUT_US = 80000; // Maximum time to wait for measurement completion + +void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { + uint32_t now = micros(); + if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) { + arg->echo_start_us = now; + arg->echo_start = true; + } else { + arg->echo_end_us = now; + arg->echo_end = true; + } +} + +void IRAM_ATTR UltrasonicSensorComponent::send_trigger_pulse_() { + InterruptLock lock; + this->store_.echo_start_us = 0; + this->store_.echo_end_us = 0; + this->store_.echo_start = false; + this->store_.echo_end = false; + this->trigger_pin_isr_.digital_write(true); + delayMicroseconds(this->pulse_time_us_); + this->trigger_pin_isr_.digital_write(false); + this->measurement_pending_ = true; + this->measurement_start_us_ = micros(); +} + void UltrasonicSensorComponent::setup() { this->trigger_pin_->setup(); this->trigger_pin_->digital_write(false); + this->trigger_pin_isr_ = this->trigger_pin_->to_isr(); this->echo_pin_->setup(); - // isr is faster to access - echo_isr_ = echo_pin_->to_isr(); + this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } + void UltrasonicSensorComponent::update() { - this->trigger_pin_->digital_write(true); - delayMicroseconds(this->pulse_time_us_); - this->trigger_pin_->digital_write(false); + if (this->measurement_pending_) { + return; + } + this->send_trigger_pulse_(); +} - const uint32_t start = micros(); - while (micros() - start < timeout_us_ && echo_isr_.digital_read()) - ; - while (micros() - start < timeout_us_ && !echo_isr_.digital_read()) - ; - const uint32_t pulse_start = micros(); - while (micros() - start < timeout_us_ && echo_isr_.digital_read()) - ; - const uint32_t pulse_end = micros(); +void UltrasonicSensorComponent::loop() { + if (!this->measurement_pending_) { + return; + } - ESP_LOGV(TAG, "Echo took %" PRIu32 "µs", pulse_end - pulse_start); - - if (pulse_end - start >= timeout_us_) { - ESP_LOGD(TAG, "'%s' - Distance measurement timed out!", this->name_.c_str()); - this->publish_state(NAN); - } else { - float result = UltrasonicSensorComponent::us_to_m(pulse_end - pulse_start); + if (this->store_.echo_end) { + uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; + ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration); + float result = UltrasonicSensorComponent::us_to_m(pulse_duration); ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); this->publish_state(result); + this->measurement_pending_ = false; + return; + } + + uint32_t elapsed = micros() - this->measurement_start_us_; + if (elapsed >= MEASUREMENT_TIMEOUT_US) { + ESP_LOGD(TAG, "'%s' - Measurement timed out after %" PRIu32 "us", this->name_.c_str(), elapsed); + this->publish_state(NAN); + this->measurement_pending_ = false; } } + void UltrasonicSensorComponent::dump_config() { LOG_SENSOR("", "Ultrasonic Sensor", this); LOG_PIN(" Echo Pin: ", this->echo_pin_); LOG_PIN(" Trigger Pin: ", this->trigger_pin_); - ESP_LOGCONFIG(TAG, - " Pulse time: %" PRIu32 " µs\n" - " Timeout: %" PRIu32 " µs", - this->pulse_time_us_, this->timeout_us_); + ESP_LOGCONFIG(TAG, " Pulse time: %" PRIu32 " us", this->pulse_time_us_); LOG_UPDATE_INTERVAL(this); } + float UltrasonicSensorComponent::us_to_m(uint32_t us) { const float speed_sound_m_per_s = 343.0f; const float time_s = us / 1e6f; const float total_dist = time_s * speed_sound_m_per_s; return total_dist / 2.0f; } -float UltrasonicSensorComponent::get_setup_priority() const { return setup_priority::DATA; } -void UltrasonicSensorComponent::set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; } -void UltrasonicSensorComponent::set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; } -} // namespace ultrasonic -} // namespace esphome +} // namespace esphome::ultrasonic diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index 1a255d6122..b0c00e51f0 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -6,41 +6,45 @@ #include -namespace esphome { -namespace ultrasonic { +namespace esphome::ultrasonic { + +struct UltrasonicSensorStore { + static void gpio_intr(UltrasonicSensorStore *arg); + + volatile uint32_t echo_start_us{0}; + volatile uint32_t echo_end_us{0}; + volatile bool echo_start{false}; + volatile bool echo_end{false}; +}; class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent { public: - void set_trigger_pin(GPIOPin *trigger_pin) { trigger_pin_ = trigger_pin; } - void set_echo_pin(InternalGPIOPin *echo_pin) { echo_pin_ = echo_pin; } + void set_trigger_pin(InternalGPIOPin *trigger_pin) { this->trigger_pin_ = trigger_pin; } + void set_echo_pin(InternalGPIOPin *echo_pin) { this->echo_pin_ = echo_pin; } - /// Set the timeout for waiting for the echo in µs. - void set_timeout_us(uint32_t timeout_us); - - // ========== INTERNAL METHODS ========== - // (In most use cases you won't need these) - /// Set up pins and register interval. void setup() override; + void loop() override; void dump_config() override; - void update() override; - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::DATA; } /// Set the time in µs the trigger pin should be enabled for in µs, defaults to 10µs (for HC-SR04) - void set_pulse_time_us(uint32_t pulse_time_us); + void set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; } protected: /// Helper function to convert the specified echo duration in µs to meters. static float us_to_m(uint32_t us); - /// Helper function to convert the specified distance in meters to the echo duration in µs. + void send_trigger_pulse_(); - GPIOPin *trigger_pin_; + InternalGPIOPin *trigger_pin_; + ISRInternalGPIOPin trigger_pin_isr_; InternalGPIOPin *echo_pin_; - ISRInternalGPIOPin echo_isr_; - uint32_t timeout_us_{}; /// 2 meters. + UltrasonicSensorStore store_; uint32_t pulse_time_us_{}; + + uint32_t measurement_start_us_{0}; + bool measurement_pending_{false}; }; -} // namespace ultrasonic -} // namespace esphome +} // namespace esphome::ultrasonic diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 7a381c85a8..e146f7e685 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -29,6 +29,9 @@ UpdateInfo = update_ns.struct("UpdateInfo") PerformAction = update_ns.class_( "PerformAction", automation.Action, cg.Parented.template(UpdateEntity) ) +CheckAction = update_ns.class_( + "CheckAction", automation.Action, cg.Parented.template(UpdateEntity) +) IsAvailableCondition = update_ns.class_( "IsAvailableCondition", automation.Condition, cg.Parented.template(UpdateEntity) ) @@ -143,6 +146,21 @@ async def update_perform_action_to_code(config, action_id, template_arg, args): return var +@automation.register_action( + "update.check", + CheckAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(UpdateEntity), + } + ), +) +async def update_check_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + @automation.register_condition( "update.is_available", IsAvailableCondition, diff --git a/esphome/components/update/automation.h b/esphome/components/update/automation.h index 8563b855fe..af24c838b1 100644 --- a/esphome/components/update/automation.h +++ b/esphome/components/update/automation.h @@ -14,6 +14,11 @@ template class PerformAction : public Action, public Pare void play(const Ts &...x) override { this->parent_->perform(this->force_.value(x...)); } }; +template class CheckAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->check(); } +}; + template class IsAvailableCondition : public Condition, public Parented { public: bool check(const Ts &...x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 567fc9fc8e..6d13341a8a 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -9,8 +9,10 @@ namespace update { static const char *const TAG = "update"; void UpdateEntity::publish_state() { - ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str()); - ESP_LOGD(TAG, " Current Version: %s", this->update_info_.current_version.c_str()); + ESP_LOGD(TAG, + "'%s' - Publishing:\n" + " Current Version: %s", + this->name_.c_str(), this->update_info_.current_version.c_str()); if (!this->update_info_.md5.empty()) { ESP_LOGD(TAG, " Latest Version: %s", this->update_info_.latest_version.c_str()); diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 9424e80b9f..8eba78b44b 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -50,7 +50,7 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { UpdateState state_{UPDATE_STATE_UNKNOWN}; UpdateInfo update_info_; - CallbackManager state_callback_{}; + LazyCallbackManager state_callback_{}; std::unique_ptr> update_available_trigger_{nullptr}; }; diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index 221f07c80e..4c3a4b05df 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -8,6 +8,9 @@ namespace uponor_smatrix { static const char *const TAG = "uponor_smatrix"; +// Maximum bytes to log in verbose hex output +static constexpr size_t UPONOR_MAX_LOG_BYTES = 36; + void UponorSmatrixComponent::setup() { #ifdef USE_TIME if (this->time_id_ != nullptr) { @@ -97,8 +100,11 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { return false; } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_size(UPONOR_MAX_LOG_BYTES)]; +#endif ESP_LOGV(TAG, "Received packet: addr=%08X, data=%s, crc=%04X", device_address, - format_hex(&packet[4], packet_len - 6).c_str(), crc); + format_hex_to(hex_buf, &packet[4], packet_len - 6), crc); // Handle packet size_t data_len = (packet_len - 6) / 3; diff --git a/esphome/components/usb_cdc_acm/__init__.py b/esphome/components/usb_cdc_acm/__init__.py new file mode 100644 index 0000000000..6693d8e75e --- /dev/null +++ b/esphome/components/usb_cdc_acm/__init__.py @@ -0,0 +1,76 @@ +import esphome.codegen as cg +from esphome.components import esp32, uart +from esphome.components.esp32 import ( + VARIANT_ESP32P4, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + add_idf_sdkconfig_option, +) +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_RX_BUFFER_SIZE, CONF_TX_BUFFER_SIZE +from esphome.types import ConfigType + +CODEOWNERS = ["@kbx81"] +AUTO_LOAD = ["uart"] +DEPENDENCIES = ["tinyusb"] + +CONF_INTERFACES = "interfaces" + +usb_cdc_acm_ns = cg.esphome_ns.namespace("usb_cdc_acm") +USBCDCACMComponent = usb_cdc_acm_ns.class_("USBCDCACMComponent", cg.Component) +USBCDCACMInstance = usb_cdc_acm_ns.class_( + "USBCDCACMInstance", uart.UARTComponent, cg.Parented.template(USBCDCACMComponent) +) + + +# Schema for individual CDC ACM interface instances +INTERFACE_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(USBCDCACMInstance), + } +) + +# Main component schema +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(USBCDCACMComponent), + cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.All( + cv.validate_bytes, cv.uint16_t + ), + cv.Optional(CONF_TX_BUFFER_SIZE, default=256): cv.All( + cv.validate_bytes, cv.uint16_t + ), + cv.Optional(CONF_INTERFACES, default=[{}]): cv.All( + cv.ensure_list(INTERFACE_SCHEMA), + cv.Length(min=1, max=2), # At least 1, at most 2 interfaces + ), + } + ).extend(cv.COMPONENT_SCHEMA), + esp32.only_on_variant( + supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3], + ), +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + # Create and register interface instances + for interface_index, interface_conf in enumerate(config[CONF_INTERFACES]): + interface = cg.new_Pvariable(interface_conf[CONF_ID]) + await cg.register_parented(interface, var) + cg.add(interface.set_interface_number(interface_index)) + cg.add(var.add_interface(interface)) + + # Configure TinyUSB with the correct number of CDC interfaces + num_interfaces = len(config[CONF_INTERFACES]) + add_idf_sdkconfig_option("CONFIG_TINYUSB_CDC_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_TINYUSB_CDC_COUNT", num_interfaces) + add_idf_sdkconfig_option( + "CONFIG_TINYUSB_CDC_RX_BUFSIZE", config[CONF_RX_BUFFER_SIZE] + ) + add_idf_sdkconfig_option( + "CONFIG_TINYUSB_CDC_TX_BUFSIZE", config[CONF_TX_BUFFER_SIZE] + ) diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp new file mode 100644 index 0000000000..29120a3d0b --- /dev/null +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.cpp @@ -0,0 +1,505 @@ +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "usb_cdc_acm.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/ringbuf.h" +#include "freertos/task.h" +#include "esp_log.h" + +#include "tusb.h" +#include "tusb_cdc_acm.h" + +namespace esphome::usb_cdc_acm { + +static const char *TAG = "usb_cdc_acm"; + +// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512) +static constexpr size_t USB_CDC_MAX_LOG_BYTES = 168; + +static constexpr size_t USB_TX_TASK_STACK_SIZE = 4096; +static constexpr size_t USB_TX_TASK_STACK_SIZE_VV = 8192; + +// Global component instance for managing USB device +USBCDCACMComponent *global_usb_cdc_component = nullptr; + +static USBCDCACMInstance *get_instance_by_itf(int itf) { + if (global_usb_cdc_component == nullptr) { + return nullptr; + } + return global_usb_cdc_component->get_interface_by_number(itf); +} + +static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) { + USBCDCACMInstance *instance = get_instance_by_itf(itf); + if (instance == nullptr) { + ESP_LOGE(TAG, "RX callback: invalid interface %d", itf); + return; + } + + size_t rx_size = 0; + static uint8_t rx_buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE] = {0}; + + // read from USB + esp_err_t ret = + tinyusb_cdcacm_read(static_cast(itf), rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size); + ESP_LOGV(TAG, "tinyusb_cdc_rx_callback itf=%d (size: %u)", itf, rx_size); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char rx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty_to(rx_hex_buf, rx_buf, rx_size)); + + if (ret == ESP_OK && rx_size > 0) { + RingbufHandle_t rx_ringbuf = instance->get_rx_ringbuf(); + if (rx_ringbuf != nullptr) { + BaseType_t send_res = xRingbufferSend(rx_ringbuf, rx_buf, rx_size, 0); + if (send_res != pdTRUE) { + ESP_LOGE(TAG, "USB RX itf=%d: buffer full, %u bytes lost", itf, rx_size); + } else { + ESP_LOGV(TAG, "USB RX itf=%d: queued %u bytes", itf, rx_size); + } + } + } +} + +static void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event) { + USBCDCACMInstance *instance = get_instance_by_itf(itf); + if (instance == nullptr) { + ESP_LOGE(TAG, "Line state callback: invalid interface %d", itf); + return; + } + + int dtr = event->line_state_changed_data.dtr; + int rts = event->line_state_changed_data.rts; + ESP_LOGV(TAG, "Line state itf=%d: DTR=%d, RTS=%d", itf, dtr, rts); + + // Queue event for processing in main loop + instance->queue_line_state_event(dtr != 0, rts != 0); +} + +static void tinyusb_cdc_line_coding_changed_callback(int itf, cdcacm_event_t *event) { + USBCDCACMInstance *instance = get_instance_by_itf(itf); + if (instance == nullptr) { + ESP_LOGE(TAG, "Line coding callback: invalid interface %d", itf); + return; + } + + uint32_t bit_rate = event->line_coding_changed_data.p_line_coding->bit_rate; + uint8_t stop_bits = event->line_coding_changed_data.p_line_coding->stop_bits; + uint8_t parity = event->line_coding_changed_data.p_line_coding->parity; + uint8_t data_bits = event->line_coding_changed_data.p_line_coding->data_bits; + ESP_LOGV(TAG, "Line coding itf=%d: bit_rate=%" PRIu32 " stop_bits=%u parity=%u data_bits=%u", itf, bit_rate, + stop_bits, parity, data_bits); + + // Queue event for processing in main loop + instance->queue_line_coding_event(bit_rate, stop_bits, parity, data_bits); +} + +static esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size, + TickType_t xTicksToWait) { + size_t read_sz; + uint8_t *buf = static_cast(xRingbufferReceiveUpTo(ring_buf, &read_sz, xTicksToWait, out_buf_sz)); + + if (buf == nullptr) { + return ESP_FAIL; + } + + memcpy(out_buf, buf, read_sz); + vRingbufferReturnItem(ring_buf, (void *) buf); + *rx_data_size = read_sz; + + // Buffer's data can be wrapped, in which case we should perform another read + buf = static_cast(xRingbufferReceiveUpTo(ring_buf, &read_sz, 0, out_buf_sz - *rx_data_size)); + if (buf != nullptr) { + memcpy(out_buf + *rx_data_size, buf, read_sz); + vRingbufferReturnItem(ring_buf, (void *) buf); + *rx_data_size += read_sz; + } + + return ESP_OK; +} + +//============================================================================== +// USBCDCACMInstance Implementation +//============================================================================== + +void USBCDCACMInstance::setup() { + this->usb_tx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_TX_BUFSIZE, RINGBUF_TYPE_BYTEBUF); + if (this->usb_tx_ringbuf_ == nullptr) { + ESP_LOGE(TAG, "USB TX buffer creation error for itf %d", this->itf_); + this->parent_->mark_failed(); + return; + } + + this->usb_rx_ringbuf_ = xRingbufferCreate(CONFIG_TINYUSB_CDC_RX_BUFSIZE, RINGBUF_TYPE_BYTEBUF); + if (this->usb_rx_ringbuf_ == nullptr) { + ESP_LOGE(TAG, "USB RX buffer creation error for itf %d", this->itf_); + this->parent_->mark_failed(); + return; + } + + // Configure this CDC interface + const tinyusb_config_cdcacm_t acm_cfg = { + .usb_dev = TINYUSB_USBDEV_0, + .cdc_port = this->itf_, + .callback_rx = &tinyusb_cdc_rx_callback, + .callback_rx_wanted_char = NULL, + .callback_line_state_changed = &tinyusb_cdc_line_state_changed_callback, + .callback_line_coding_changed = &tinyusb_cdc_line_coding_changed_callback, + }; + + esp_err_t result = tusb_cdc_acm_init(&acm_cfg); + if (result != ESP_OK) { + ESP_LOGE(TAG, "tusb_cdc_acm_init failed: %d", result); + this->parent_->mark_failed(); + return; + } + + // Use a larger stack size for (very) verbose logging + const size_t stack_size = esp_log_level_get(TAG) > ESP_LOG_DEBUG ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE; + + // Create a simple, unique task name per interface + char task_name[] = "usb_tx_0"; + task_name[sizeof(task_name) - 1] = format_hex_char(static_cast(this->itf_)); + xTaskCreate(usb_tx_task_fn, task_name, stack_size, this, 4, &this->usb_tx_task_handle_); + + if (this->usb_tx_task_handle_ == nullptr) { + ESP_LOGE(TAG, "Failed to create USB TX task for itf %d", this->itf_); + this->parent_->mark_failed(); + return; + } +} + +void USBCDCACMInstance::loop() { + // Process events from the lock-free queue + this->process_events_(); +} + +void USBCDCACMInstance::queue_line_state_event(bool dtr, bool rts) { + // Allocate event from pool + CDCEvent *event = this->event_pool_.allocate(); + if (event == nullptr) { + ESP_LOGW(TAG, "Event pool exhausted, line state event dropped (itf=%d)", this->itf_); + return; + } + + event->type = CDC_EVENT_LINE_STATE_CHANGED; + event->data.line_state.dtr = dtr; + event->data.line_state.rts = rts; + + if (!this->event_queue_.push(event)) { + ESP_LOGW(TAG, "Event queue full, line state event dropped (itf=%d)", this->itf_); + // Return event to pool since we couldn't queue it + this->event_pool_.release(event); + } else { + // Wake main loop immediately to process event +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + } +} + +void USBCDCACMInstance::queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, + uint8_t data_bits) { + // Allocate event from pool + CDCEvent *event = this->event_pool_.allocate(); + if (event == nullptr) { + ESP_LOGW(TAG, "Event pool exhausted, line coding event dropped (itf=%d)", this->itf_); + return; + } + + event->type = CDC_EVENT_LINE_CODING_CHANGED; + event->data.line_coding.bit_rate = bit_rate; + event->data.line_coding.stop_bits = stop_bits; + event->data.line_coding.parity = parity; + event->data.line_coding.data_bits = data_bits; + + if (!this->event_queue_.push(event)) { + ESP_LOGW(TAG, "Event queue full, line coding event dropped (itf=%d)", this->itf_); + // Return event to pool since we couldn't queue it + this->event_pool_.release(event); + } else { + // Wake main loop immediately to process event +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif + } +} + +void USBCDCACMInstance::process_events_() { + // Process all pending events from the queue + CDCEvent *event; + while ((event = this->event_queue_.pop()) != nullptr) { + switch (event->type) { + case CDC_EVENT_LINE_STATE_CHANGED: { + bool dtr = event->data.line_state.dtr; + bool rts = event->data.line_state.rts; + + // Invoke user callback in main loop context + if (this->line_state_callback_ != nullptr) { + this->line_state_callback_(dtr, rts); + } + break; + } + case CDC_EVENT_LINE_CODING_CHANGED: { + uint32_t bit_rate = event->data.line_coding.bit_rate; + uint8_t stop_bits = event->data.line_coding.stop_bits; + uint8_t parity = event->data.line_coding.parity; + uint8_t data_bits = event->data.line_coding.data_bits; + + // Update UART configuration based on CDC line coding + this->baud_rate_ = bit_rate; + this->data_bits_ = data_bits; + + // Convert CDC stop bits to UART stop bits format + // CDC: 0=1 stop bit, 1=1.5 stop bits, 2=2 stop bits + this->stop_bits_ = (stop_bits == 0) ? 1 : (stop_bits == 1) ? 1 : 2; + + // Convert CDC parity to UART parity format + // CDC: 0=None, 1=Odd, 2=Even, 3=Mark, 4=Space + switch (parity) { + case 0: + this->parity_ = uart::UART_CONFIG_PARITY_NONE; + break; + case 1: + this->parity_ = uart::UART_CONFIG_PARITY_ODD; + break; + case 2: + this->parity_ = uart::UART_CONFIG_PARITY_EVEN; + break; + default: + // Mark and Space parity are not commonly supported, default to None + this->parity_ = uart::UART_CONFIG_PARITY_NONE; + break; + } + + // Invoke user callback in main loop context + if (this->line_coding_callback_ != nullptr) { + this->line_coding_callback_(bit_rate, stop_bits, parity, data_bits); + } + break; + } + } + // Return event to pool for reuse + this->event_pool_.release(event); + } +} + +void USBCDCACMInstance::usb_tx_task_fn(void *arg) { + auto *instance = static_cast(arg); + instance->usb_tx_task(); +} + +void USBCDCACMInstance::usb_tx_task() { + uint8_t data[CONFIG_TINYUSB_CDC_TX_BUFSIZE] = {0}; + size_t tx_data_size = 0; + + while (1) { + // Wait for a notification from the bridge component + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + // When we do wake up, we can be sure there is data in the ring buffer + esp_err_t ret = ringbuf_read_bytes(this->usb_tx_ringbuf_, data, CONFIG_TINYUSB_CDC_TX_BUFSIZE, &tx_data_size, 0); + + if (ret != ESP_OK) { + ESP_LOGE(TAG, "USB TX itf=%d: RingBuf read failed", this->itf_); + continue; + } else if (tx_data_size == 0) { + ESP_LOGD(TAG, "USB TX itf=%d: RingBuf empty, skipping", this->itf_); + continue; + } + + ESP_LOGV(TAG, "USB TX itf=%d: Read %d bytes from buffer", this->itf_, tx_data_size); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char tx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, "data = %s", format_hex_pretty_to(tx_hex_buf, data, tx_data_size)); + + // Serial data will be split up into 64 byte chunks to be sent over USB so this + // usually will take multiple iterations + uint8_t *data_head = &data[0]; + + while (tx_data_size > 0) { + size_t queued = tinyusb_cdcacm_write_queue(this->itf_, data_head, tx_data_size); + ESP_LOGV(TAG, "USB TX itf=%d: enqueued: size=%d, queued=%u", this->itf_, tx_data_size, queued); + + tx_data_size -= queued; + data_head += queued; + + ESP_LOGV(TAG, "USB TX itf=%d: waiting 10ms for flush", this->itf_); + esp_err_t flush_ret = tinyusb_cdcacm_write_flush(this->itf_, pdMS_TO_TICKS(10)); + + if (flush_ret != ESP_OK) { + ESP_LOGE(TAG, "USB TX itf=%d: flush failed", this->itf_); + tud_cdc_n_write_clear(this->itf_); + break; + } + } + } +} + +//============================================================================== +// UARTComponent Interface Implementation +//============================================================================== + +void USBCDCACMInstance::write_array(const uint8_t *data, size_t len) { + if (len == 0) { + return; + } + + // Write data to TX ring buffer + BaseType_t send_res = xRingbufferSend(this->usb_tx_ringbuf_, data, len, 0); + if (send_res != pdTRUE) { + ESP_LOGW(TAG, "USB TX itf=%d: buffer full, %u bytes dropped", this->itf_, len); + return; + } + + // Notify TX task that data is available + if (this->usb_tx_task_handle_ != nullptr) { + xTaskNotifyGive(this->usb_tx_task_handle_); + } +} + +bool USBCDCACMInstance::peek_byte(uint8_t *data) { + if (this->has_peek_) { + *data = this->peek_buffer_; + return true; + } + + if (this->read_byte(&this->peek_buffer_)) { + *data = this->peek_buffer_; + this->has_peek_ = true; + return true; + } + + return false; +} + +bool USBCDCACMInstance::read_array(uint8_t *data, size_t len) { + if (len == 0) { + return true; + } + + size_t original_len = len; + size_t bytes_read = 0; + + // First, use the peek buffer if available + if (this->has_peek_) { + data[0] = this->peek_buffer_; + this->has_peek_ = false; + bytes_read = 1; + data++; + if (--len == 0) { // Decrement len first, then check it... + return true; // No more to read + } + } + + // Read remaining bytes from RX ring buffer + size_t rx_size = 0; + uint8_t *buf = static_cast(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len)); + if (buf == nullptr) { + return false; + } + + memcpy(data, buf, rx_size); + vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf); + bytes_read += rx_size; + data += rx_size; + len -= rx_size; + if (len == 0) { + return true; // No more to read + } + + // Buffer's data may wrap around, in which case we should perform another read + buf = static_cast(xRingbufferReceiveUpTo(this->usb_rx_ringbuf_, &rx_size, 0, len)); + if (buf == nullptr) { + return false; + } + + memcpy(data, buf, rx_size); + vRingbufferReturnItem(this->usb_rx_ringbuf_, (void *) buf); + bytes_read += rx_size; + + return bytes_read == original_len; +} + +int USBCDCACMInstance::available() { + UBaseType_t waiting = 0; + if (this->usb_rx_ringbuf_ != nullptr) { + vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting); + } + return static_cast(waiting) + (this->has_peek_ ? 1 : 0); +} + +void USBCDCACMInstance::flush() { + // Wait for TX ring buffer to be empty + if (this->usb_tx_ringbuf_ == nullptr) { + return; + } + + UBaseType_t waiting = 1; + while (waiting > 0) { + vRingbufferGetInfo(this->usb_tx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting); + if (waiting > 0) { + vTaskDelay(pdMS_TO_TICKS(1)); + } + } + + // Also wait for USB to finish transmitting + tinyusb_cdcacm_write_flush(this->itf_, pdMS_TO_TICKS(100)); +} + +//============================================================================== +// USBCDCACMComponent Implementation +//============================================================================== + +USBCDCACMComponent::USBCDCACMComponent() { global_usb_cdc_component = this; } + +void USBCDCACMComponent::setup() { + // Setup all registered interfaces + for (auto interface : this->interfaces_) { + if (interface != nullptr) { + interface->setup(); + } + } +} + +void USBCDCACMComponent::loop() { + // Call loop() on all registered interfaces to process events + for (auto interface : this->interfaces_) { + if (interface != nullptr) { + interface->loop(); + } + } +} + +void USBCDCACMComponent::dump_config() { + ESP_LOGCONFIG(TAG, + "USB CDC-ACM:\n" + " Number of Interfaces: %d", + this->interfaces_[MAX_USB_CDC_INSTANCES - 1] != nullptr ? MAX_USB_CDC_INSTANCES : 1); +} + +void USBCDCACMComponent::add_interface(USBCDCACMInstance *interface) { + uint8_t itf_num = static_cast(interface->get_itf()); + if (itf_num < MAX_USB_CDC_INSTANCES) { + this->interfaces_[itf_num] = interface; + } else { + ESP_LOGE(TAG, "Interface number must be less than %u", MAX_USB_CDC_INSTANCES); + } +} + +USBCDCACMInstance *USBCDCACMComponent::get_interface_by_number(uint8_t itf) { + for (auto interface : this->interfaces_) { + if ((interface != nullptr) && (interface->get_itf() == static_cast(itf))) { + return interface; + } + } + return nullptr; +} + +} // namespace esphome::usb_cdc_acm +#endif diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h new file mode 100644 index 0000000000..8c00f5d52f --- /dev/null +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -0,0 +1,135 @@ +#pragma once +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + +#include "esphome/core/component.h" +#include "esphome/core/event_pool.h" +#include "esphome/core/lock_free_queue.h" +#include "esphome/components/uart/uart_component.h" + +#include +#include "freertos/ringbuf.h" +#include "tusb_cdc_acm.h" + +namespace esphome::usb_cdc_acm { + +static const uint8_t EVENT_QUEUE_SIZE = 12; +static const uint8_t MAX_USB_CDC_INSTANCES = 2; + +// Callback types for line coding and line state changes +using LineCodingCallback = std::function; +using LineStateCallback = std::function; + +// Event types +enum CDCEventType : uint8_t { + CDC_EVENT_LINE_STATE_CHANGED, + CDC_EVENT_LINE_CODING_CHANGED, +}; + +// Event structure for the queue +struct CDCEvent { + CDCEventType type; + union { + struct { + bool dtr; + bool rts; + } line_state; + struct { + uint32_t bit_rate; + uint8_t stop_bits; + uint8_t parity; + uint8_t data_bits; + } line_coding; + } data; + + // Required by EventPool - called before returning to pool + void release() { + // No dynamic memory to clean up, data is stored inline + } +}; + +// Forward declaration +class USBCDCACMComponent; + +/// Represents a single CDC ACM interface instance +class USBCDCACMInstance : public uart::UARTComponent, public Parented { + public: + void set_interface_number(uint8_t itf) { this->itf_ = static_cast(itf); } + + void setup(); + void loop(); + + // Get the CDC port number for this instance + tinyusb_cdcacm_itf_t get_itf() const { return this->itf_; } + + // Ring buffer accessors for bridge components + RingbufHandle_t get_tx_ringbuf() const { return this->usb_tx_ringbuf_; } + RingbufHandle_t get_rx_ringbuf() const { return this->usb_rx_ringbuf_; } + + // Task handle accessor for notifying TX task + TaskHandle_t get_tx_task_handle() const { return this->usb_tx_task_handle_; } + + // Callback registration for line coding and line state changes + void set_line_coding_callback(LineCodingCallback callback) { this->line_coding_callback_ = std::move(callback); } + void set_line_state_callback(LineStateCallback callback) { this->line_state_callback_ = std::move(callback); } + + // Called from TinyUSB task context (SPSC producer) - queues event for processing in main loop + void queue_line_coding_event(uint32_t bit_rate, uint8_t stop_bits, uint8_t parity, uint8_t data_bits); + void queue_line_state_event(bool dtr, bool rts); + + static void usb_tx_task_fn(void *arg); + void usb_tx_task(); + + // UARTComponent interface implementation + void write_array(const uint8_t *data, size_t len) override; + bool peek_byte(uint8_t *data) override; + bool read_array(uint8_t *data, size_t len) override; + int available() override; + void flush() override; + + protected: + void check_logger_conflict() override {} + + // Process queued events and invoke callbacks (called from main loop) + void process_events_(); + + TaskHandle_t usb_tx_task_handle_{nullptr}; + tinyusb_cdcacm_itf_t itf_{TINYUSB_CDC_ACM_0}; + + RingbufHandle_t usb_tx_ringbuf_{nullptr}; + RingbufHandle_t usb_rx_ringbuf_{nullptr}; + + // User-registered callbacks (called from main loop) + LineCodingCallback line_coding_callback_{nullptr}; + LineStateCallback line_state_callback_{nullptr}; + + // Lock-free queue and event pool for cross-task event passing + EventPool event_pool_; + LockFreeQueue event_queue_; + + // RX buffer for peek functionality + uint8_t peek_buffer_{0}; + bool has_peek_{false}; +}; + +/// Main USB CDC ACM component that manages the USB device and all CDC interfaces +class USBCDCACMComponent : public Component { + public: + USBCDCACMComponent(); + + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::IO; } + + // Interface management + void add_interface(USBCDCACMInstance *interface); + USBCDCACMInstance *get_interface_by_number(uint8_t itf); + + protected: + std::array interfaces_{nullptr, nullptr}; +}; + +extern USBCDCACMComponent *global_usb_cdc_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::usb_cdc_acm +#endif diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index cccabcf646..e4c11be489 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -53,8 +53,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()), } ), - cv.only_with_esp_idf, - only_on_variant(supported=[VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4]), + only_on_variant(supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3]), ) diff --git a/esphome/components/usb_host/usb_host.h b/esphome/components/usb_host/usb_host.h index 31bdde2df8..d11a148a0f 100644 --- a/esphome/components/usb_host/usb_host.h +++ b/esphome/components/usb_host/usb_host.h @@ -1,7 +1,7 @@ #pragma once // Should not be needed, but it's required to pass CI clang-tidy checks -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include "esphome/core/defines.h" #include "esphome/core/component.h" #include @@ -188,4 +188,4 @@ class USBHost : public Component { } // namespace usb_host } // namespace esphome -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_host/usb_host_client.cpp b/esphome/components/usb_host/usb_host_client.cpp index 4c09cf8a49..09da6e3b73 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -1,5 +1,5 @@ // Should not be needed, but it's required to pass CI clang-tidy checks -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include "usb_host.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" @@ -39,37 +39,46 @@ static void print_ep_desc(const usb_ep_desc_t *ep_desc) { break; } - ESP_LOGV(TAG, "\t\t*** Endpoint descriptor ***"); - ESP_LOGV(TAG, "\t\tbLength %d", ep_desc->bLength); - ESP_LOGV(TAG, "\t\tbDescriptorType %d", ep_desc->bDescriptorType); - ESP_LOGV(TAG, "\t\tbEndpointAddress 0x%x\tEP %d %s", ep_desc->bEndpointAddress, USB_EP_DESC_GET_EP_NUM(ep_desc), - USB_EP_DESC_GET_EP_DIR(ep_desc) ? "IN" : "OUT"); - ESP_LOGV(TAG, "\t\tbmAttributes 0x%x\t%s", ep_desc->bmAttributes, ep_type_str); - ESP_LOGV(TAG, "\t\twMaxPacketSize %d", ep_desc->wMaxPacketSize); - ESP_LOGV(TAG, "\t\tbInterval %d", ep_desc->bInterval); + ESP_LOGV(TAG, + "\t\t*** Endpoint descriptor ***\n" + "\t\tbLength %d\n" + "\t\tbDescriptorType %d\n" + "\t\tbEndpointAddress 0x%x\tEP %d %s\n" + "\t\tbmAttributes 0x%x\t%s\n" + "\t\twMaxPacketSize %d\n" + "\t\tbInterval %d", + ep_desc->bLength, ep_desc->bDescriptorType, ep_desc->bEndpointAddress, USB_EP_DESC_GET_EP_NUM(ep_desc), + USB_EP_DESC_GET_EP_DIR(ep_desc) ? "IN" : "OUT", ep_desc->bmAttributes, ep_type_str, ep_desc->wMaxPacketSize, + ep_desc->bInterval); } static void usbh_print_intf_desc(const usb_intf_desc_t *intf_desc) { - ESP_LOGV(TAG, "\t*** Interface descriptor ***"); - ESP_LOGV(TAG, "\tbLength %d", intf_desc->bLength); - ESP_LOGV(TAG, "\tbDescriptorType %d", intf_desc->bDescriptorType); - ESP_LOGV(TAG, "\tbInterfaceNumber %d", intf_desc->bInterfaceNumber); - ESP_LOGV(TAG, "\tbAlternateSetting %d", intf_desc->bAlternateSetting); - ESP_LOGV(TAG, "\tbNumEndpoints %d", intf_desc->bNumEndpoints); - ESP_LOGV(TAG, "\tbInterfaceClass 0x%x", intf_desc->bInterfaceProtocol); - ESP_LOGV(TAG, "\tiInterface %d", intf_desc->iInterface); + ESP_LOGV(TAG, + "\t*** Interface descriptor ***\n" + "\tbLength %d\n" + "\tbDescriptorType %d\n" + "\tbInterfaceNumber %d\n" + "\tbAlternateSetting %d\n" + "\tbNumEndpoints %d\n" + "\tbInterfaceClass 0x%x\n" + "\tiInterface %d", + intf_desc->bLength, intf_desc->bDescriptorType, intf_desc->bInterfaceNumber, intf_desc->bAlternateSetting, + intf_desc->bNumEndpoints, intf_desc->bInterfaceProtocol, intf_desc->iInterface); } static void usbh_print_cfg_desc(const usb_config_desc_t *cfg_desc) { - ESP_LOGV(TAG, "*** Configuration descriptor ***"); - ESP_LOGV(TAG, "bLength %d", cfg_desc->bLength); - ESP_LOGV(TAG, "bDescriptorType %d", cfg_desc->bDescriptorType); - ESP_LOGV(TAG, "wTotalLength %d", cfg_desc->wTotalLength); - ESP_LOGV(TAG, "bNumInterfaces %d", cfg_desc->bNumInterfaces); - ESP_LOGV(TAG, "bConfigurationValue %d", cfg_desc->bConfigurationValue); - ESP_LOGV(TAG, "iConfiguration %d", cfg_desc->iConfiguration); - ESP_LOGV(TAG, "bmAttributes 0x%x", cfg_desc->bmAttributes); - ESP_LOGV(TAG, "bMaxPower %dmA", cfg_desc->bMaxPower * 2); + ESP_LOGV(TAG, + "*** Configuration descriptor ***\n" + "bLength %d\n" + "bDescriptorType %d\n" + "wTotalLength %d\n" + "bNumInterfaces %d\n" + "bConfigurationValue %d\n" + "iConfiguration %d\n" + "bmAttributes 0x%x\n" + "bMaxPower %dmA", + cfg_desc->bLength, cfg_desc->bDescriptorType, cfg_desc->wTotalLength, cfg_desc->bNumInterfaces, + cfg_desc->bConfigurationValue, cfg_desc->iConfiguration, cfg_desc->bmAttributes, cfg_desc->bMaxPower * 2); } static void usb_client_print_device_descriptor(const usb_device_desc_t *devc_desc) { @@ -77,21 +86,27 @@ static void usb_client_print_device_descriptor(const usb_device_desc_t *devc_des return; } - ESP_LOGV(TAG, "*** Device descriptor ***"); - ESP_LOGV(TAG, "bLength %d", devc_desc->bLength); - ESP_LOGV(TAG, "bDescriptorType %d", devc_desc->bDescriptorType); - ESP_LOGV(TAG, "bcdUSB %d.%d0", ((devc_desc->bcdUSB >> 8) & 0xF), ((devc_desc->bcdUSB >> 4) & 0xF)); - ESP_LOGV(TAG, "bDeviceClass 0x%x", devc_desc->bDeviceClass); - ESP_LOGV(TAG, "bDeviceSubClass 0x%x", devc_desc->bDeviceSubClass); - ESP_LOGV(TAG, "bDeviceProtocol 0x%x", devc_desc->bDeviceProtocol); - ESP_LOGV(TAG, "bMaxPacketSize0 %d", devc_desc->bMaxPacketSize0); - ESP_LOGV(TAG, "idVendor 0x%x", devc_desc->idVendor); - ESP_LOGV(TAG, "idProduct 0x%x", devc_desc->idProduct); - ESP_LOGV(TAG, "bcdDevice %d.%d0", ((devc_desc->bcdDevice >> 8) & 0xF), ((devc_desc->bcdDevice >> 4) & 0xF)); - ESP_LOGV(TAG, "iManufacturer %d", devc_desc->iManufacturer); - ESP_LOGV(TAG, "iProduct %d", devc_desc->iProduct); - ESP_LOGV(TAG, "iSerialNumber %d", devc_desc->iSerialNumber); - ESP_LOGV(TAG, "bNumConfigurations %d", devc_desc->bNumConfigurations); + ESP_LOGV(TAG, + "*** Device descriptor ***\n" + "bLength %d\n" + "bDescriptorType %d\n" + "bcdUSB %d.%d0\n" + "bDeviceClass 0x%x\n" + "bDeviceSubClass 0x%x\n" + "bDeviceProtocol 0x%x\n" + "bMaxPacketSize0 %d\n" + "idVendor 0x%x\n" + "idProduct 0x%x\n" + "bcdDevice %d.%d0\n" + "iManufacturer %d\n" + "iProduct %d\n" + "iSerialNumber %d\n" + "bNumConfigurations %d", + devc_desc->bLength, devc_desc->bDescriptorType, ((devc_desc->bcdUSB >> 8) & 0xF), + ((devc_desc->bcdUSB >> 4) & 0xF), devc_desc->bDeviceClass, devc_desc->bDeviceSubClass, + devc_desc->bDeviceProtocol, devc_desc->bMaxPacketSize0, devc_desc->idVendor, devc_desc->idProduct, + ((devc_desc->bcdDevice >> 8) & 0xF), ((devc_desc->bcdDevice >> 4) & 0xF), devc_desc->iManufacturer, + devc_desc->iProduct, devc_desc->iSerialNumber, devc_desc->bNumConfigurations); } static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc, @@ -188,7 +203,7 @@ void USBClient::setup() { auto err = usb_host_client_register(&config, &this->handle_); if (err != ESP_OK) { ESP_LOGE(TAG, "client register failed: %s", esp_err_to_name(err)); - this->status_set_error("Client register failed"); + this->status_set_error(LOG_STR("Client register failed")); this->mark_failed(); return; } @@ -531,4 +546,4 @@ void USBClient::release_trq(TransferRequest *trq) { } // namespace usb_host } // namespace esphome -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_host/usb_host_component.cpp b/esphome/components/usb_host/usb_host_component.cpp index fb19239c73..790fe6713b 100644 --- a/esphome/components/usb_host/usb_host_component.cpp +++ b/esphome/components/usb_host/usb_host_component.cpp @@ -1,5 +1,5 @@ // Should not be needed, but it's required to pass CI clang-tidy checks -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include "usb_host.h" #include #include "esphome/core/log.h" @@ -11,7 +11,7 @@ void USBHost::setup() { usb_host_config_t config{}; if (usb_host_install(&config) != ESP_OK) { - this->status_set_error("usb_host_install failed"); + this->status_set_error(LOG_STR("usb_host_install failed")); this->mark_failed(); return; } @@ -31,4 +31,4 @@ void USBHost::loop() { } // namespace usb_host } // namespace esphome -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index a852e1f78b..d9bb58ae3a 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.components import socket from esphome.components.uart import ( CONF_DATA_BITS, CONF_PARITY, @@ -17,7 +18,7 @@ from esphome.const import ( ) from esphome.cpp_types import Component -AUTO_LOAD = ["uart", "usb_host", "bytebuffer"] +AUTO_LOAD = ["uart", "usb_host", "bytebuffer", "socket"] CODEOWNERS = ["@clydebarrow"] usb_uart_ns = cg.esphome_ns.namespace("usb_uart") @@ -116,6 +117,10 @@ CONFIG_SCHEMA = cv.ensure_list( async def to_code(config): + # Enable wake_loop_threadsafe for low-latency USB data processing + # The USB task queues data events that need immediate processing + socket.require_wake_loop_threadsafe() + for device in config: var = await register_usb_client(device) for index, channel in enumerate(device[CONF_CHANNELS]): diff --git a/esphome/components/usb_uart/ch34x.cpp b/esphome/components/usb_uart/ch34x.cpp index 889366b579..caa4b65657 100644 --- a/esphome/components/usb_uart/ch34x.cpp +++ b/esphome/components/usb_uart/ch34x.cpp @@ -1,4 +1,4 @@ -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include "usb_uart.h" #include "usb/usb_host.h" #include "esphome/core/log.h" @@ -78,4 +78,4 @@ void USBUartTypeCH34X::enable_channels() { } } // namespace usb_uart } // namespace esphome -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/cp210x.cpp b/esphome/components/usb_uart/cp210x.cpp index 5fec0bed02..483286560a 100644 --- a/esphome/components/usb_uart/cp210x.cpp +++ b/esphome/components/usb_uart/cp210x.cpp @@ -1,4 +1,4 @@ -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include "usb_uart.h" #include "usb/usb_host.h" #include "esphome/core/log.h" @@ -58,8 +58,10 @@ std::vector USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev ESP_LOGE(TAG, "get_active_config_descriptor failed"); return {}; } - ESP_LOGD(TAG, "bDeviceClass: %u, bDeviceSubClass: %u", device_desc->bDeviceClass, device_desc->bDeviceSubClass); - ESP_LOGD(TAG, "bNumInterfaces: %u", config_desc->bNumInterfaces); + ESP_LOGD(TAG, + "bDeviceClass: %u, bDeviceSubClass: %u\n" + "bNumInterfaces: %u", + device_desc->bDeviceClass, device_desc->bDeviceSubClass, config_desc->bNumInterfaces); if (device_desc->bDeviceClass != 0) { ESP_LOGE(TAG, "bDeviceClass != 0"); return {}; @@ -123,4 +125,4 @@ void USBUartTypeCP210X::enable_channels() { } } // namespace usb_uart } // namespace esphome -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index c24fffb11d..edd01c26c6 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -1,7 +1,8 @@ // Should not be needed, but it's required to pass CI clang-tidy checks -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include "usb_uart.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #include "esphome/components/uart/uart_debugger.h" #include @@ -262,6 +263,11 @@ void USBUartComponent::start_input(USBUartChannel *channel) { // Push to lock-free queue for main loop processing // Push always succeeds because pool size == queue size this->usb_data_queue_.push(chunk); + + // Wake main loop immediately to process USB data instead of waiting for select() timeout +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); +#endif } // On success, restart input immediately from USB task for performance @@ -320,7 +326,7 @@ static void fix_mps(const usb_ep_desc_t *ep) { void USBUartTypeCdcAcm::on_connected() { auto cdc_devs = this->parse_descriptors(this->device_handle_); if (cdc_devs.empty()) { - this->status_set_error("No CDC-ACM device found"); + this->status_set_error(LOG_STR("No CDC-ACM device found")); this->disconnect(); return; } @@ -341,7 +347,7 @@ void USBUartTypeCdcAcm::on_connected() { if (err != ESP_OK) { ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_, channel->cdc_dev_.bulk_interface_number); - this->status_set_error("usb_host_interface_claim failed"); + this->status_set_error(LOG_STR("usb_host_interface_claim failed")); this->disconnect(); return; } @@ -386,4 +392,4 @@ void USBUartTypeCdcAcm::enable_channels() { } // namespace usb_uart } // namespace esphome -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index a5e7905ac5..96c17bd155 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -1,6 +1,6 @@ #pragma once -#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/components/uart/uart_component.h" @@ -173,4 +173,4 @@ class USBUartTypeCH34X : public USBUartTypeCdcAcm { } // namespace usb_uart } // namespace esphome -#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 381d9061de..fed113afc2 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -12,25 +12,25 @@ static const char *const TAG = "valve"; const float VALVE_OPEN = 1.0f; const float VALVE_CLOSED = 0.0f; -const char *valve_command_to_str(float pos) { +const LogString *valve_command_to_str(float pos) { if (pos == VALVE_OPEN) { - return "OPEN"; + return LOG_STR("OPEN"); } else if (pos == VALVE_CLOSED) { - return "CLOSE"; + return LOG_STR("CLOSE"); } else { - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } -const char *valve_operation_to_str(ValveOperation op) { +const LogString *valve_operation_to_str(ValveOperation op) { switch (op) { case VALVE_OPERATION_IDLE: - return "IDLE"; + return LOG_STR("IDLE"); case VALVE_OPERATION_OPENING: - return "OPENING"; + return LOG_STR("OPENING"); case VALVE_OPERATION_CLOSING: - return "CLOSING"; + return LOG_STR("CLOSING"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } @@ -82,7 +82,7 @@ void ValveCall::perform() { if (traits.get_supports_position()) { ESP_LOGD(TAG, " Position: %.0f%%", *this->position_ * 100.0f); } else { - ESP_LOGD(TAG, " Command: %s", valve_command_to_str(*this->position_)); + ESP_LOGD(TAG, " Command: %s", LOG_STR_ARG(valve_command_to_str(*this->position_))); } } if (this->toggle_.has_value()) { @@ -146,7 +146,7 @@ void Valve::publish_state(bool save) { ESP_LOGD(TAG, " State: UNKNOWN"); } } - ESP_LOGD(TAG, " Current Operation: %s", valve_operation_to_str(this->current_operation)); + ESP_LOGD(TAG, " Current Operation: %s", LOG_STR_ARG(valve_operation_to_str(this->current_operation))); this->state_callback_.call(); #if defined(USE_VALVE) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index ab7ff5abe1..2b3419b67a 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include "esphome/core/preferences.h" #include "valve_traits.h" @@ -81,7 +82,7 @@ enum ValveOperation : uint8_t { VALVE_OPERATION_CLOSING, }; -const char *valve_operation_to_str(ValveOperation op); +const LogString *valve_operation_to_str(ValveOperation op); /** Base class for all valve devices. * @@ -143,7 +144,7 @@ class Valve : public EntityBase, public EntityBase_DeviceClass { optional restore_state_(); - CallbackManager state_callback_{}; + LazyCallbackManager state_callback_{}; ESPPreferenceObject rtc_; }; diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp index e474dcfe17..b9496a08de 100644 --- a/esphome/components/vbus/vbus.cpp +++ b/esphome/components/vbus/vbus.cpp @@ -8,6 +8,9 @@ namespace vbus { static const char *const TAG = "vbus"; +// Maximum bytes to log in verbose hex output (16 frames * 4 bytes = 64 bytes typical) +static constexpr size_t VBUS_MAX_LOG_BYTES = 64; + void VBus::dump_config() { ESP_LOGCONFIG(TAG, "VBus:"); check_uart_settings(9600); @@ -101,8 +104,11 @@ void VBus::loop() { this->buffer_.push_back(this->fbytes_[i]); if (++this->cframe_ < this->frames_) continue; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char hex_buf[format_hex_size(VBUS_MAX_LOG_BYTES)]; +#endif ESP_LOGV(TAG, "P2 C%04x %04x->%04x: %s", this->command_, this->source_, this->dest_, - format_hex(this->buffer_).c_str()); + format_hex_to(hex_buf, this->buffer_.data(), this->buffer_.size())); for (auto &listener : this->listeners_) listener->on_message(this->command_, this->source_, this->dest_, this->buffer_); this->state_ = 0; diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 65dbfd27cf..584b8abfb2 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -1,8 +1,9 @@ #include "version_text_sensor.h" -#include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/log.h" #include "esphome/core/version.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" namespace esphome { namespace version { @@ -10,11 +11,26 @@ namespace version { static const char *const TAG = "version.text_sensor"; void VersionTextSensor::setup() { - if (this->hide_timestamp_) { - this->publish_state(ESPHOME_VERSION); - } else { - this->publish_state(str_sprintf(ESPHOME_VERSION " %s", App.get_compilation_time().c_str())); + static const char PREFIX[] PROGMEM = ESPHOME_VERSION " (config hash 0x"; + static const char BUILT_STR[] PROGMEM = ", built "; + // Buffer size: PREFIX + 8 hex chars + BUILT_STR + BUILD_TIME_STR_SIZE + ")" + null + constexpr size_t buf_size = sizeof(PREFIX) + 8 + sizeof(BUILT_STR) + esphome::Application::BUILD_TIME_STR_SIZE + 2; + char version_str[buf_size]; + + ESPHOME_strncpy_P(version_str, PREFIX, sizeof(version_str)); + + size_t len = strlen(version_str); + snprintf(version_str + len, sizeof(version_str) - len, "%08" PRIx32, App.get_config_hash()); + + if (!this->hide_timestamp_) { + size_t len = strlen(version_str); + ESPHOME_strncat_P(version_str, BUILT_STR, sizeof(version_str) - len - 1); + ESPHOME_strncat_P(version_str, ESPHOME_BUILD_TIME_STR, sizeof(version_str) - strlen(version_str) - 1); } + + strncat(version_str, ")", sizeof(version_str) - strlen(version_str) - 1); + version_str[sizeof(version_str) - 1] = '\0'; + this->publish_state(version_str); } float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; } void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index d2548a5bbd..e833657fc4 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -27,8 +27,10 @@ void VL53L0XSensor::dump_config() { if (this->enable_pin_ != nullptr) { LOG_PIN(" Enable Pin: ", this->enable_pin_); } - ESP_LOGCONFIG(TAG, " Timeout: %u%s", this->timeout_us_, this->timeout_us_ > 0 ? "us" : " (no timeout)"); - ESP_LOGCONFIG(TAG, " Timing Budget %uus ", this->measurement_timing_budget_us_); + ESP_LOGCONFIG(TAG, + " Timeout: %u%s\n" + " Timing Budget %uus ", + this->timeout_us_, this->timeout_us_ > 0 ? "us" : " (no timeout)", this->measurement_timing_budget_us_); } void VL53L0XSensor::setup() { diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 59c7ec8383..d28c786dd8 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_ON_CLIENT_DISCONNECTED, CONF_ON_ERROR, CONF_ON_IDLE, + CONF_ON_START, CONF_SPEAKER, ) @@ -24,7 +25,6 @@ CONF_ON_INTENT_END = "on_intent_end" CONF_ON_INTENT_PROGRESS = "on_intent_progress" CONF_ON_INTENT_START = "on_intent_start" CONF_ON_LISTENING = "on_listening" -CONF_ON_START = "on_start" CONF_ON_STT_END = "on_stt_end" CONF_ON_STT_VAD_END = "on_stt_vad_end" CONF_ON_STT_VAD_START = "on_stt_vad_start" diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index fd35dc7d09..e2516d5fb8 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -3,6 +3,7 @@ #ifdef USE_VOICE_ASSISTANT +#include "esphome/components/socket/socket.h" #include "esphome/core/log.h" #include @@ -206,7 +207,7 @@ void VoiceAssistant::loop() { case State::START_MICROPHONE: { ESP_LOGD(TAG, "Starting Microphone"); if (!this->allocate_buffers_()) { - this->status_set_error("Failed to allocate buffers"); + this->status_set_error(LOG_STR("Failed to allocate buffers")); return; } if (this->status_has_error()) { @@ -238,10 +239,10 @@ void VoiceAssistant::loop() { api::VoiceAssistantRequest msg; msg.start = true; - msg.set_conversation_id(StringRef(this->conversation_id_)); + msg.conversation_id = StringRef(this->conversation_id_); msg.flags = flags; msg.audio_settings = audio_settings; - msg.set_wake_word_phrase(StringRef(this->wake_word_)); + msg.wake_word_phrase = StringRef(this->wake_word_); // Reset media player state tracking #ifdef USE_MEDIA_PLAYER @@ -272,7 +273,8 @@ void VoiceAssistant::loop() { size_t read_bytes = this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0); if (this->audio_mode_ == AUDIO_MODE_API) { api::VoiceAssistantAudio msg; - msg.set_data(this->send_buffer_, read_bytes); + msg.data = this->send_buffer_; + msg.data_len = read_bytes; this->api_client_->send_message(msg, api::VoiceAssistantAudio::MESSAGE_TYPE); } else { if (!this->udp_socket_running_) { @@ -428,10 +430,12 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr } if (this->api_client_ != nullptr) { - ESP_LOGE(TAG, "Multiple API Clients attempting to connect to Voice Assistant"); - ESP_LOGE(TAG, "Current client: %s (%s)", this->api_client_->get_name().c_str(), - this->api_client_->get_peername().c_str()); - ESP_LOGE(TAG, "New client: %s (%s)", client->get_name().c_str(), client->get_peername().c_str()); + ESP_LOGE(TAG, + "Multiple API Clients attempting to connect to Voice Assistant\n" + "Current client: %s (%s)\n" + "New client: %s (%s)", + this->api_client_->get_name(), this->api_client_->get_peername(), client->get_name(), + client->get_peername()); return; } @@ -627,9 +631,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGD(TAG, "Assist Pipeline running"); #ifdef USE_MEDIA_PLAYER this->started_streaming_tts_ = false; - for (auto arg : msg.data) { + for (const auto &arg : msg.data) { if (arg.name == "url") { - this->tts_response_url_ = std::move(arg.value); + this->tts_response_url_ = arg.value; } } #endif @@ -648,9 +652,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { break; case api::enums::VOICE_ASSISTANT_STT_END: { std::string text; - for (auto arg : msg.data) { + for (const auto &arg : msg.data) { if (arg.name == "text") { - text = std::move(arg.value); + text = arg.value; } } if (text.empty()) { @@ -693,9 +697,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { break; } case api::enums::VOICE_ASSISTANT_INTENT_END: { - for (auto arg : msg.data) { + for (const auto &arg : msg.data) { if (arg.name == "conversation_id") { - this->conversation_id_ = std::move(arg.value); + this->conversation_id_ = arg.value; } else if (arg.name == "continue_conversation") { this->continue_conversation_ = (arg.value == "1"); } @@ -705,9 +709,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } case api::enums::VOICE_ASSISTANT_TTS_START: { std::string text; - for (auto arg : msg.data) { + for (const auto &arg : msg.data) { if (arg.name == "text") { - text = std::move(arg.value); + text = arg.value; } } if (text.empty()) { @@ -731,9 +735,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } case api::enums::VOICE_ASSISTANT_TTS_END: { std::string url; - for (auto arg : msg.data) { + for (const auto &arg : msg.data) { if (arg.name == "url") { - url = std::move(arg.value); + url = arg.value; } } if (url.empty()) { @@ -778,11 +782,11 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { case api::enums::VOICE_ASSISTANT_ERROR: { std::string code = ""; std::string message = ""; - for (auto arg : msg.data) { + for (const auto &arg : msg.data) { if (arg.name == "code") { - code = std::move(arg.value); + code = arg.value; } else if (arg.name == "message") { - message = std::move(arg.value); + message = arg.value; } } if (code == "wake-word-timeout" || code == "wake_word_detection_aborted" || code == "no_wake_word") { @@ -841,12 +845,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) { #ifdef USE_SPEAKER // We should never get to this function if there is no speaker anyway if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { - if (this->speaker_buffer_index_ + msg.data.length() < SPEAKER_BUFFER_SIZE) { - memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data.data(), msg.data.length()); - this->speaker_buffer_index_ += msg.data.length(); - this->speaker_buffer_size_ += msg.data.length(); - this->speaker_bytes_received_ += msg.data.length(); - ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data.length()); + if (this->speaker_buffer_index_ + msg.data_len < SPEAKER_BUFFER_SIZE) { + memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data, msg.data_len); + this->speaker_buffer_index_ += msg.data_len; + this->speaker_buffer_size_ += msg.data_len; + this->speaker_bytes_received_ += msg.data_len; + ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data_len); } else { ESP_LOGE(TAG, "Cannot receive audio, buffer is full"); } @@ -863,9 +867,12 @@ void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse .is_active = msg.is_active, }; this->timers_[timer.id] = timer; - ESP_LOGD(TAG, "Timer Event"); - ESP_LOGD(TAG, " Type: %" PRId32, msg.event_type); - ESP_LOGD(TAG, " %s", timer.to_string().c_str()); + char timer_buf[Timer::TO_STR_BUFFER_SIZE]; + ESP_LOGD(TAG, + "Timer Event\n" + " Type: %" PRId32 "\n" + " %s", + msg.event_type, timer.to_str(timer_buf)); switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_TIMER_STARTED: @@ -947,7 +954,7 @@ void VoiceAssistant::on_set_configuration(const std::vector &active } // Enable only active wake words - for (auto ww_id : active_wake_words) { + for (const auto &ww_id : active_wake_words) { for (auto &model : this->micro_wake_word_->get_wake_words()) { if (model->get_id() == ww_id) { model->enable(); diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 8d3d3497ec..b1b3df7bbd 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -23,6 +23,7 @@ #endif #include "esphome/components/socket/socket.h" +#include #include #include @@ -71,10 +72,18 @@ struct Timer { uint32_t seconds_left; bool is_active; + /// Buffer size for to_str() - sufficient for typical timer names + static constexpr size_t TO_STR_BUFFER_SIZE = 128; + /// Format to buffer, returns pointer to buffer (may truncate long names) + const char *to_str(std::span buffer) const { + snprintf(buffer.data(), buffer.size(), + "Timer(id=%s, name=%s, total_seconds=%" PRIu32 ", seconds_left=%" PRIu32 ", is_active=%s)", + this->id.c_str(), this->name.c_str(), this->total_seconds, this->seconds_left, YESNO(this->is_active)); + return buffer.data(); + } std::string to_string() const { - return str_sprintf("Timer(id=%s, name=%s, total_seconds=%" PRIu32 ", seconds_left=%" PRIu32 ", is_active=%s)", - this->id.c_str(), this->name.c_str(), this->total_seconds, this->seconds_left, - YESNO(this->is_active)); + char buffer[TO_STR_BUFFER_SIZE]; + return this->to_str(buffer); } }; diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index adf5a080e5..8c5bdac54b 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -67,8 +67,8 @@ void WakeOnLanButton::setup() { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); if (this->broadcast_socket_ == nullptr) { + this->status_set_error(LOG_STR("Could not create socket")); this->mark_failed(); - this->status_set_error("Could not create socket"); return; } int enable = 1; diff --git a/esphome/components/water_heater/__init__.py b/esphome/components/water_heater/__init__.py new file mode 100644 index 0000000000..5420e7c435 --- /dev/null +++ b/esphome/components/water_heater/__init__.py @@ -0,0 +1,111 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_MAX_TEMPERATURE, + CONF_MIN_TEMPERATURE, + CONF_VISUAL, +) +from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity +from esphome.cpp_generator import MockObjClass +from esphome.types import ConfigType + +CODEOWNERS = ["@dhoeben"] + +IS_PLATFORM_COMPONENT = True + +water_heater_ns = cg.esphome_ns.namespace("water_heater") +WaterHeater = water_heater_ns.class_("WaterHeater", cg.EntityBase, cg.Component) +WaterHeaterCall = water_heater_ns.class_("WaterHeaterCall") +WaterHeaterTraits = water_heater_ns.class_("WaterHeaterTraits") + +CONF_TARGET_TEMPERATURE_STEP = "target_temperature_step" + +WaterHeaterMode = water_heater_ns.enum("WaterHeaterMode") +WATER_HEATER_MODES = { + "OFF": WaterHeaterMode.WATER_HEATER_MODE_OFF, + "ECO": WaterHeaterMode.WATER_HEATER_MODE_ECO, + "ELECTRIC": WaterHeaterMode.WATER_HEATER_MODE_ELECTRIC, + "PERFORMANCE": WaterHeaterMode.WATER_HEATER_MODE_PERFORMANCE, + "HIGH_DEMAND": WaterHeaterMode.WATER_HEATER_MODE_HIGH_DEMAND, + "HEAT_PUMP": WaterHeaterMode.WATER_HEATER_MODE_HEAT_PUMP, + "GAS": WaterHeaterMode.WATER_HEATER_MODE_GAS, +} +validate_water_heater_mode = cv.enum(WATER_HEATER_MODES, upper=True) + +_WATER_HEATER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( + { + cv.Optional(CONF_VISUAL, default={}): cv.Schema( + { + cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, + cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, + cv.Optional(CONF_TARGET_TEMPERATURE_STEP): cv.float_, + } + ), + } +).extend(cv.COMPONENT_SCHEMA) + +_WATER_HEATER_SCHEMA.add_extra(entity_duplicate_validator("water_heater")) + + +def water_heater_schema( + class_: MockObjClass, + *, + icon: str = cv.UNDEFINED, + entity_category: str = cv.UNDEFINED, +) -> cv.Schema: + schema = {cv.GenerateID(): cv.declare_id(class_)} + + for key, default, validator in [ + (CONF_ICON, icon, cv.icon), + (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), + ]: + if default is not cv.UNDEFINED: + schema[cv.Optional(key, default=default)] = validator + + return _WATER_HEATER_SCHEMA.extend(schema) + + +async def setup_water_heater_core_(var: cg.Pvariable, config: ConfigType) -> None: + """Set up the core water heater properties in C++.""" + await setup_entity(var, config, "water_heater") + + visual = config[CONF_VISUAL] + if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None: + cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES") + cg.add(var.set_visual_min_temperature_override(min_temp)) + if (max_temp := visual.get(CONF_MAX_TEMPERATURE)) is not None: + cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES") + cg.add(var.set_visual_max_temperature_override(max_temp)) + if (temp_step := visual.get(CONF_TARGET_TEMPERATURE_STEP)) is not None: + cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES") + cg.add(var.set_visual_target_temperature_step_override(temp_step)) + + +async def register_water_heater(var: cg.Pvariable, config: ConfigType) -> cg.Pvariable: + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + + cg.add_define("USE_WATER_HEATER") + + await cg.register_component(var, config) + + cg.add(cg.App.register_water_heater(var)) + + CORE.register_platform_component("water_heater", var) + await setup_water_heater_core_(var, config) + return var + + +async def new_water_heater(config: ConfigType, *args) -> cg.Pvariable: + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_water_heater(var, config) + return var + + +@coroutine_with_priority(CoroPriority.CORE) +async def to_code(config: ConfigType) -> None: + cg.add_global(water_heater_ns.using) diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp new file mode 100644 index 0000000000..d092203d06 --- /dev/null +++ b/esphome/components/water_heater/water_heater.cpp @@ -0,0 +1,283 @@ +#include "water_heater.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/controller_registry.h" + +#include + +namespace esphome::water_heater { + +static const char *const TAG = "water_heater"; + +void log_water_heater(const char *tag, const char *prefix, const char *type, WaterHeater *obj) { + if (obj != nullptr) { + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + } +} + +WaterHeaterCall::WaterHeaterCall(WaterHeater *parent) : parent_(parent) {} + +WaterHeaterCall &WaterHeaterCall::set_mode(WaterHeaterMode mode) { + this->mode_ = mode; + return *this; +} + +WaterHeaterCall &WaterHeaterCall::set_mode(const std::string &mode) { + if (str_equals_case_insensitive(mode, "OFF")) { + this->set_mode(WATER_HEATER_MODE_OFF); + } else if (str_equals_case_insensitive(mode, "ECO")) { + this->set_mode(WATER_HEATER_MODE_ECO); + } else if (str_equals_case_insensitive(mode, "ELECTRIC")) { + this->set_mode(WATER_HEATER_MODE_ELECTRIC); + } else if (str_equals_case_insensitive(mode, "PERFORMANCE")) { + this->set_mode(WATER_HEATER_MODE_PERFORMANCE); + } else if (str_equals_case_insensitive(mode, "HIGH_DEMAND")) { + this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND); + } else if (str_equals_case_insensitive(mode, "HEAT_PUMP")) { + this->set_mode(WATER_HEATER_MODE_HEAT_PUMP); + } else if (str_equals_case_insensitive(mode, "GAS")) { + this->set_mode(WATER_HEATER_MODE_GAS); + } else { + ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); + } + return *this; +} + +WaterHeaterCall &WaterHeaterCall::set_target_temperature(float temperature) { + this->target_temperature_ = temperature; + return *this; +} + +WaterHeaterCall &WaterHeaterCall::set_target_temperature_low(float temperature) { + this->target_temperature_low_ = temperature; + return *this; +} + +WaterHeaterCall &WaterHeaterCall::set_target_temperature_high(float temperature) { + this->target_temperature_high_ = temperature; + return *this; +} + +WaterHeaterCall &WaterHeaterCall::set_away(bool away) { + if (away) { + this->state_ |= WATER_HEATER_STATE_AWAY; + } else { + this->state_ &= ~WATER_HEATER_STATE_AWAY; + } + return *this; +} + +WaterHeaterCall &WaterHeaterCall::set_on(bool on) { + if (on) { + this->state_ |= WATER_HEATER_STATE_ON; + } else { + this->state_ &= ~WATER_HEATER_STATE_ON; + } + return *this; +} + +void WaterHeaterCall::perform() { + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + this->validate_(); + if (this->mode_.has_value()) { + ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(water_heater_mode_to_string(*this->mode_))); + } + if (!std::isnan(this->target_temperature_)) { + ESP_LOGD(TAG, " Target Temperature: %.2f", this->target_temperature_); + } + if (!std::isnan(this->target_temperature_low_)) { + ESP_LOGD(TAG, " Target Temperature Low: %.2f", this->target_temperature_low_); + } + if (!std::isnan(this->target_temperature_high_)) { + ESP_LOGD(TAG, " Target Temperature High: %.2f", this->target_temperature_high_); + } + if (this->state_ & WATER_HEATER_STATE_AWAY) { + ESP_LOGD(TAG, " Away: YES"); + } + if (this->state_ & WATER_HEATER_STATE_ON) { + ESP_LOGD(TAG, " On: YES"); + } + this->parent_->control(*this); +} + +void WaterHeaterCall::validate_() { + auto traits = this->parent_->get_traits(); + if (this->mode_.has_value()) { + if (!traits.supports_mode(*this->mode_)) { + ESP_LOGW(TAG, "'%s' - Mode %d not supported", this->parent_->get_name().c_str(), *this->mode_); + this->mode_.reset(); + } + } + if (!std::isnan(this->target_temperature_)) { + if (traits.get_supports_two_point_target_temperature()) { + ESP_LOGW(TAG, "'%s' - Cannot set target temperature for device with two-point target temperature", + this->parent_->get_name().c_str()); + this->target_temperature_ = NAN; + } else if (this->target_temperature_ < traits.get_min_temperature() || + this->target_temperature_ > traits.get_max_temperature()) { + ESP_LOGW(TAG, "'%s' - Target temperature %.1f is out of range [%.1f - %.1f]", this->parent_->get_name().c_str(), + this->target_temperature_, traits.get_min_temperature(), traits.get_max_temperature()); + this->target_temperature_ = + std::max(traits.get_min_temperature(), std::min(this->target_temperature_, traits.get_max_temperature())); + } + } + if (!std::isnan(this->target_temperature_low_) || !std::isnan(this->target_temperature_high_)) { + if (!traits.get_supports_two_point_target_temperature()) { + ESP_LOGW(TAG, "'%s' - Cannot set low/high target temperature", this->parent_->get_name().c_str()); + this->target_temperature_low_ = NAN; + this->target_temperature_high_ = NAN; + } + } + if (!std::isnan(this->target_temperature_low_) && !std::isnan(this->target_temperature_high_)) { + if (this->target_temperature_low_ > this->target_temperature_high_) { + ESP_LOGW(TAG, "'%s' - Target temperature low %.2f must be less than high %.2f", this->parent_->get_name().c_str(), + this->target_temperature_low_, this->target_temperature_high_); + this->target_temperature_low_ = NAN; + this->target_temperature_high_ = NAN; + } + } + if ((this->state_ & WATER_HEATER_STATE_AWAY) && !traits.get_supports_away_mode()) { + ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str()); + this->state_ &= ~WATER_HEATER_STATE_AWAY; + } + // If ON/OFF not supported, device is always on - clear the flag silently + if (!traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) { + this->state_ &= ~WATER_HEATER_STATE_ON; + } +} + +void WaterHeater::setup() { + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); +} + +void WaterHeater::publish_state() { + auto traits = this->get_traits(); + ESP_LOGD(TAG, + "'%s' - Sending state:\n" + " Mode: %s", + this->name_.c_str(), LOG_STR_ARG(water_heater_mode_to_string(this->mode_))); + if (!std::isnan(this->current_temperature_)) { + ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature_); + } + if (traits.get_supports_two_point_target_temperature()) { + ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low_, + this->target_temperature_high_); + } else if (!std::isnan(this->target_temperature_)) { + ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature_); + } + if (this->state_ & WATER_HEATER_STATE_AWAY) { + ESP_LOGD(TAG, " Away: YES"); + } + if (traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) { + ESP_LOGD(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); + } + +#if defined(USE_WATER_HEATER) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_water_heater_update(this); +#endif + + SavedWaterHeaterState saved{}; + saved.mode = this->mode_; + if (traits.get_supports_two_point_target_temperature()) { + saved.target_temperature_low = this->target_temperature_low_; + saved.target_temperature_high = this->target_temperature_high_; + } else { + saved.target_temperature = this->target_temperature_; + } + saved.state = this->state_; + this->pref_.save(&saved); +} + +optional WaterHeater::restore_state() { + SavedWaterHeaterState recovered{}; + if (!this->pref_.load(&recovered)) + return {}; + + auto traits = this->get_traits(); + auto call = this->make_call(); + call.set_mode(recovered.mode); + if (traits.get_supports_two_point_target_temperature()) { + call.set_target_temperature_low(recovered.target_temperature_low); + call.set_target_temperature_high(recovered.target_temperature_high); + } else { + call.set_target_temperature(recovered.target_temperature); + } + call.set_away((recovered.state & WATER_HEATER_STATE_AWAY) != 0); + call.set_on((recovered.state & WATER_HEATER_STATE_ON) != 0); + return call; +} + +WaterHeaterTraits WaterHeater::get_traits() { + auto traits = this->traits(); +#ifdef USE_WATER_HEATER_VISUAL_OVERRIDES + if (!std::isnan(this->visual_min_temperature_override_)) { + traits.set_min_temperature(this->visual_min_temperature_override_); + } + if (!std::isnan(this->visual_max_temperature_override_)) { + traits.set_max_temperature(this->visual_max_temperature_override_); + } + if (!std::isnan(this->visual_target_temperature_step_override_)) { + traits.set_target_temperature_step(this->visual_target_temperature_step_override_); + } +#endif + return traits; +} + +#ifdef USE_WATER_HEATER_VISUAL_OVERRIDES +void WaterHeater::set_visual_min_temperature_override(float min_temperature_override) { + this->visual_min_temperature_override_ = min_temperature_override; +} +void WaterHeater::set_visual_max_temperature_override(float max_temperature_override) { + this->visual_max_temperature_override_ = max_temperature_override; +} +void WaterHeater::set_visual_target_temperature_step_override(float visual_target_temperature_step_override) { + this->visual_target_temperature_step_override_ = visual_target_temperature_step_override; +} +#endif + +const LogString *water_heater_mode_to_string(WaterHeaterMode mode) { + switch (mode) { + case WATER_HEATER_MODE_OFF: + return LOG_STR("OFF"); + case WATER_HEATER_MODE_ECO: + return LOG_STR("ECO"); + case WATER_HEATER_MODE_ELECTRIC: + return LOG_STR("ELECTRIC"); + case WATER_HEATER_MODE_PERFORMANCE: + return LOG_STR("PERFORMANCE"); + case WATER_HEATER_MODE_HIGH_DEMAND: + return LOG_STR("HIGH_DEMAND"); + case WATER_HEATER_MODE_HEAT_PUMP: + return LOG_STR("HEAT_PUMP"); + case WATER_HEATER_MODE_GAS: + return LOG_STR("GAS"); + default: + return LOG_STR("UNKNOWN"); + } +} + +void WaterHeater::dump_traits_(const char *tag) { + auto traits = this->get_traits(); + ESP_LOGCONFIG(tag, + " Min Temperature: %.1f°C\n" + " Max Temperature: %.1f°C\n" + " Temperature Step: %.1f", + traits.get_min_temperature(), traits.get_max_temperature(), traits.get_target_temperature_step()); + if (traits.get_supports_two_point_target_temperature()) { + ESP_LOGCONFIG(tag, " Supports Two-Point Target Temperature: YES"); + } + if (traits.get_supports_away_mode()) { + ESP_LOGCONFIG(tag, " Supports Away Mode: YES"); + } + if (traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) { + ESP_LOGCONFIG(tag, " Supports On/Off: YES"); + } + if (!traits.get_supported_modes().empty()) { + ESP_LOGCONFIG(tag, " Supported Modes:"); + for (WaterHeaterMode m : traits.get_supported_modes()) { + ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(water_heater_mode_to_string(m))); + } + } +} + +} // namespace esphome::water_heater diff --git a/esphome/components/water_heater/water_heater.h b/esphome/components/water_heater/water_heater.h new file mode 100644 index 0000000000..e223dd59b2 --- /dev/null +++ b/esphome/components/water_heater/water_heater.h @@ -0,0 +1,259 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/finite_set_mask.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/preferences.h" + +namespace esphome::water_heater { + +class WaterHeater; +struct WaterHeaterCallInternal; + +void log_water_heater(const char *tag, const char *prefix, const char *type, WaterHeater *obj); +#define LOG_WATER_HEATER(prefix, type, obj) log_water_heater(TAG, prefix, LOG_STR_LITERAL(type), obj) + +enum WaterHeaterMode : uint32_t { + WATER_HEATER_MODE_OFF = 0, + WATER_HEATER_MODE_ECO = 1, + WATER_HEATER_MODE_ELECTRIC = 2, + WATER_HEATER_MODE_PERFORMANCE = 3, + WATER_HEATER_MODE_HIGH_DEMAND = 4, + WATER_HEATER_MODE_HEAT_PUMP = 5, + WATER_HEATER_MODE_GAS = 6, +}; + +// Type alias for water heater mode bitmask +// Replaces std::set to eliminate red-black tree overhead +using WaterHeaterModeMask = + FiniteSetMask>; + +/// Feature flags for water heater capabilities (matches Home Assistant WaterHeaterEntityFeature) +enum WaterHeaterFeature : uint32_t { + /// The water heater supports reporting the current temperature. + WATER_HEATER_SUPPORTS_CURRENT_TEMPERATURE = 1 << 0, + /// The water heater supports a target temperature. + WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE = 1 << 1, + /// The water heater supports operation mode selection. + WATER_HEATER_SUPPORTS_OPERATION_MODE = 1 << 2, + /// The water heater supports an away/vacation mode. + WATER_HEATER_SUPPORTS_AWAY_MODE = 1 << 3, + /// The water heater can be turned on/off. + WATER_HEATER_SUPPORTS_ON_OFF = 1 << 4, + /// The water heater supports two-point target temperature (low/high range). + WATER_HEATER_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE = 1 << 5, +}; + +/// State flags for water heater current state (bitmask) +enum WaterHeaterStateFlag : uint32_t { + /// Away/vacation mode is currently active + WATER_HEATER_STATE_AWAY = 1 << 0, + /// Water heater is on (not in standby) + WATER_HEATER_STATE_ON = 1 << 1, +}; + +struct SavedWaterHeaterState { + WaterHeaterMode mode; + union { + float target_temperature; + struct { + float target_temperature_low; + float target_temperature_high; + }; + } __attribute__((packed)); + uint32_t state; +} __attribute__((packed)); + +class WaterHeaterCall { + friend struct WaterHeaterCallInternal; + + public: + WaterHeaterCall() : parent_(nullptr) {} + + WaterHeaterCall(WaterHeater *parent); + + WaterHeaterCall &set_mode(WaterHeaterMode mode); + WaterHeaterCall &set_mode(const std::string &mode); + WaterHeaterCall &set_target_temperature(float temperature); + WaterHeaterCall &set_target_temperature_low(float temperature); + WaterHeaterCall &set_target_temperature_high(float temperature); + WaterHeaterCall &set_away(bool away); + WaterHeaterCall &set_on(bool on); + + void perform(); + + const optional &get_mode() const { return this->mode_; } + float get_target_temperature() const { return this->target_temperature_; } + float get_target_temperature_low() const { return this->target_temperature_low_; } + float get_target_temperature_high() const { return this->target_temperature_high_; } + /// Get state flags value + uint32_t get_state() const { return this->state_; } + + protected: + void validate_(); + WaterHeater *parent_; + optional mode_; + float target_temperature_{NAN}; + float target_temperature_low_{NAN}; + float target_temperature_high_{NAN}; + uint32_t state_{0}; +}; + +struct WaterHeaterCallInternal : public WaterHeaterCall { + WaterHeaterCallInternal(WaterHeater *parent) : WaterHeaterCall(parent) {} + + WaterHeaterCallInternal &set_from_restore(const WaterHeaterCall &restore) { + this->mode_ = restore.mode_; + this->target_temperature_ = restore.target_temperature_; + this->target_temperature_low_ = restore.target_temperature_low_; + this->target_temperature_high_ = restore.target_temperature_high_; + this->state_ = restore.state_; + return *this; + } +}; + +class WaterHeaterTraits { + public: + /// Get/set feature flags (see WaterHeaterFeature enum) + void add_feature_flags(uint32_t flags) { this->feature_flags_ |= flags; } + void clear_feature_flags(uint32_t flags) { this->feature_flags_ &= ~flags; } + bool has_feature_flags(uint32_t flags) const { return (this->feature_flags_ & flags) == flags; } + uint32_t get_feature_flags() const { return this->feature_flags_; } + + bool get_supports_current_temperature() const { + return this->has_feature_flags(WATER_HEATER_SUPPORTS_CURRENT_TEMPERATURE); + } + void set_supports_current_temperature(bool supports) { + if (supports) { + this->add_feature_flags(WATER_HEATER_SUPPORTS_CURRENT_TEMPERATURE); + } else { + this->clear_feature_flags(WATER_HEATER_SUPPORTS_CURRENT_TEMPERATURE); + } + } + + bool get_supports_away_mode() const { return this->has_feature_flags(WATER_HEATER_SUPPORTS_AWAY_MODE); } + void set_supports_away_mode(bool supports) { + if (supports) { + this->add_feature_flags(WATER_HEATER_SUPPORTS_AWAY_MODE); + } else { + this->clear_feature_flags(WATER_HEATER_SUPPORTS_AWAY_MODE); + } + } + + bool get_supports_two_point_target_temperature() const { + return this->has_feature_flags(WATER_HEATER_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE); + } + void set_supports_two_point_target_temperature(bool supports) { + if (supports) { + this->add_feature_flags(WATER_HEATER_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE); + } else { + this->clear_feature_flags(WATER_HEATER_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE); + } + } + + void set_min_temperature(float min_temperature) { this->min_temperature_ = min_temperature; } + float get_min_temperature() const { return this->min_temperature_; } + + void set_max_temperature(float max_temperature) { this->max_temperature_ = max_temperature; } + float get_max_temperature() const { return this->max_temperature_; } + + void set_target_temperature_step(float target_temperature_step) { + this->target_temperature_step_ = target_temperature_step; + } + float get_target_temperature_step() const { return this->target_temperature_step_; } + + void set_supported_modes(WaterHeaterModeMask modes) { this->supported_modes_ = modes; } + const WaterHeaterModeMask &get_supported_modes() const { return this->supported_modes_; } + bool supports_mode(WaterHeaterMode mode) const { return this->supported_modes_.count(mode); } + + protected: + // Ordered to minimize padding: 4-byte members first + uint32_t feature_flags_{0}; + float min_temperature_{0.0f}; + float max_temperature_{0.0f}; + float target_temperature_step_{0.0f}; + WaterHeaterModeMask supported_modes_; +}; + +class WaterHeater : public EntityBase, public Component { + public: + WaterHeaterMode get_mode() const { return this->mode_; } + float get_current_temperature() const { return this->current_temperature_; } + float get_target_temperature() const { return this->target_temperature_; } + float get_target_temperature_low() const { return this->target_temperature_low_; } + float get_target_temperature_high() const { return this->target_temperature_high_; } + /// Get the current state flags bitmask + uint32_t get_state() const { return this->state_; } + /// Check if away mode is currently active + bool is_away() const { return (this->state_ & WATER_HEATER_STATE_AWAY) != 0; } + /// Check if the water heater is on + bool is_on() const { return (this->state_ & WATER_HEATER_STATE_ON) != 0; } + + void set_current_temperature(float current_temperature) { this->current_temperature_ = current_temperature; } + + virtual void publish_state(); + virtual WaterHeaterTraits get_traits(); + virtual WaterHeaterCallInternal make_call() = 0; + +#ifdef USE_WATER_HEATER_VISUAL_OVERRIDES + void set_visual_min_temperature_override(float min_temperature_override); + void set_visual_max_temperature_override(float max_temperature_override); + void set_visual_target_temperature_step_override(float visual_target_temperature_step_override); +#endif + virtual void control(const WaterHeaterCall &call) = 0; + + void setup() override; + + optional restore_state(); + + protected: + virtual WaterHeaterTraits traits() = 0; + + /// Log the traits of this water heater for dump_config(). + void dump_traits_(const char *tag); + + /// Set the mode of the water heater. Should only be called from control(). + void set_mode_(WaterHeaterMode mode) { this->mode_ = mode; } + /// Set the target temperature of the water heater. Should only be called from control(). + void set_target_temperature_(float target_temperature) { this->target_temperature_ = target_temperature; } + /// Set the low target temperature (for two-point control). Should only be called from control(). + void set_target_temperature_low_(float target_temperature_low) { + this->target_temperature_low_ = target_temperature_low; + } + /// Set the high target temperature (for two-point control). Should only be called from control(). + void set_target_temperature_high_(float target_temperature_high) { + this->target_temperature_high_ = target_temperature_high; + } + /// Set the state flags. Should only be called from control(). + void set_state_(uint32_t state) { this->state_ = state; } + /// Set or clear a state flag. Should only be called from control(). + void set_state_flag_(uint32_t flag, bool value) { + if (value) { + this->state_ |= flag; + } else { + this->state_ &= ~flag; + } + } + + WaterHeaterMode mode_{WATER_HEATER_MODE_OFF}; + float current_temperature_{NAN}; + float target_temperature_{NAN}; + float target_temperature_low_{NAN}; + float target_temperature_high_{NAN}; + uint32_t state_{0}; // Bitmask of WaterHeaterStateFlag + +#ifdef USE_WATER_HEATER_VISUAL_OVERRIDES + float visual_min_temperature_override_{NAN}; + float visual_max_temperature_override_{NAN}; + float visual_target_temperature_step_override_{NAN}; +#endif + + ESPPreferenceObject pref_; +}; + +/// Convert the given WaterHeaterMode to a human-readable string for logging. +const LogString *water_heater_mode_to_string(WaterHeaterMode mode); + +} // namespace esphome::water_heater diff --git a/esphome/components/waveshare_epaper/waveshare_213v3.cpp b/esphome/components/waveshare_epaper/waveshare_213v3.cpp index 068cb91d31..b55f3c8d26 100644 --- a/esphome/components/waveshare_epaper/waveshare_213v3.cpp +++ b/esphome/components/waveshare_epaper/waveshare_213v3.cpp @@ -177,10 +177,10 @@ uint32_t WaveshareEPaper2P13InV3::idle_timeout_() { return 5000; } void WaveshareEPaper2P13InV3::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this) ESP_LOGCONFIG(TAG, " Model: 2.13inV3"); - LOG_PIN(" CS Pin: ", this->cs_) - LOG_PIN(" Reset Pin: ", this->reset_pin_) - LOG_PIN(" DC Pin: ", this->dc_pin_) - LOG_PIN(" Busy Pin: ", this->busy_pin_) + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 3510d157d6..4db9438206 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -172,6 +172,12 @@ void WaveshareEPaperBase::update() { this->display(); } void WaveshareEPaper::fill(Color color) { + // If clipping is active, fall back to base implementation + if (this->get_clipping().is_set()) { + Display::fill(color); + return; + } + // flip logic const uint8_t fill = color.is_on() ? 0x00 : 0xFF; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) @@ -234,6 +240,12 @@ uint8_t WaveshareEPaper7C::color_to_hex(Color color) { return hex_code; } void WaveshareEPaper7C::fill(Color color) { + // If clipping is active, use base class (3-bit packing is complex for partial fills) + if (this->get_clipping().is_set()) { + display::Display::fill(color); + return; + } + uint8_t pixel_color; if (color.is_on()) { pixel_color = this->color_to_hex(color); @@ -1813,8 +1825,10 @@ void WaveshareEPaper2P9InV2R2::write_lut_(const uint8_t *lut, const uint8_t size void WaveshareEPaper2P9InV2R2::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); - ESP_LOGCONFIG(TAG, " Model: 2.9inV2R2"); - ESP_LOGCONFIG(TAG, " Full Update Every: %" PRIu32, this->full_update_every_); + ESP_LOGCONFIG(TAG, + " Model: 2.9inV2R2\n" + " Full Update Every: %" PRIu32, + this->full_update_every_); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); @@ -2516,8 +2530,10 @@ int GDEY042T81::get_height_internal() { return 300; } uint32_t GDEY042T81::idle_timeout_() { return 5000; } void GDEY042T81::dump_config() { LOG_DISPLAY("", "GoodDisplay E-Paper", this); - ESP_LOGCONFIG(TAG, " Model: 4.2in B/W GDEY042T81"); - ESP_LOGCONFIG(TAG, " Full Update Every: %" PRIu32, this->full_update_every_); + ESP_LOGCONFIG(TAG, + " Model: 4.2in B/W GDEY042T81\n" + " Full Update Every: %" PRIu32, + this->full_update_every_); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); @@ -3147,8 +3163,10 @@ int GDEY0583T81::get_height_internal() { return 480; } uint32_t GDEY0583T81::idle_timeout_() { return 5000; } void GDEY0583T81::dump_config() { LOG_DISPLAY("", "GoodDisplay E-Paper", this); - ESP_LOGCONFIG(TAG, " Model: 5.83in B/W GDEY0583T81"); - ESP_LOGCONFIG(TAG, " Full Update Every: %" PRIu32, this->full_update_every_); + ESP_LOGCONFIG(TAG, + " Model: 5.83in B/W GDEY0583T81\n" + " Full Update Every: %" PRIu32, + this->full_update_every_); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); @@ -4328,8 +4346,10 @@ int WaveshareEPaper7P5InV2P::get_height_internal() { return 480; } uint32_t WaveshareEPaper7P5InV2P::idle_timeout_() { return 10000; } void WaveshareEPaper7P5InV2P::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); - ESP_LOGCONFIG(TAG, " Model: 7.50inv2p"); - ESP_LOGCONFIG(TAG, " Full Update Every: %" PRIu32, this->full_update_every_); + ESP_LOGCONFIG(TAG, + " Model: 7.50inv2p\n" + " Full Update Every: %" PRIu32, + this->full_update_every_); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 6b27545549..55beed812f 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -6,8 +6,7 @@ #include "web_server.h" -namespace esphome { -namespace web_server { +namespace esphome::web_server { #ifdef USE_ESP32 ListEntitiesIterator::ListEntitiesIterator(const WebServer *ws, AsyncEventSource *es) : web_server_(ws), events_(es) {} @@ -135,6 +134,13 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont } #endif +#ifdef USE_WATER_HEATER +bool ListEntitiesIterator::on_water_heater(water_heater::WaterHeater *obj) { + // Water heater web_server support not yet implemented - this stub acknowledges the entity + return true; +} +#endif + #ifdef USE_EVENT bool ListEntitiesIterator::on_event(event::Event *obj) { // Null event type, since we are just iterating over entities @@ -150,6 +156,5 @@ bool ListEntitiesIterator::on_update(update::UpdateEntity *obj) { } #endif -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 43e1cc2544..56fd91a8c6 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -4,13 +4,13 @@ #ifdef USE_WEBSERVER #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" -namespace esphome { +namespace esphome::web_server_idf { #ifdef USE_ESP32 -namespace web_server_idf { class AsyncEventSource; -} #endif -namespace web_server { +} // namespace esphome::web_server_idf + +namespace esphome::web_server { #if !defined(USE_ESP32) && defined(USE_ARDUINO) class DeferredUpdateEventSource; @@ -79,6 +79,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_ALARM_CONTROL_PANEL bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *obj) override; #endif +#ifdef USE_WATER_HEATER + bool on_water_heater(water_heater::WaterHeater *obj) override; +#endif #ifdef USE_EVENT bool on_event(event::Event *obj) override; #endif @@ -96,6 +99,5 @@ class ListEntitiesIterator : public ComponentIterator { #endif }; -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 7929f3647f..3793f01eb5 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -10,9 +10,7 @@ #endif #ifdef USE_ARDUINO -#ifdef USE_ESP8266 -#include -#elif defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_LIBRETINY) #include #endif #endif // USE_ARDUINO @@ -23,8 +21,7 @@ using PlatformString = std::string; using PlatformString = String; #endif -namespace esphome { -namespace web_server { +namespace esphome::web_server { static const char *const TAG = "web_server.ota"; @@ -84,9 +81,9 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) { } else { ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_); } -#ifdef USE_OTA_STATE_CALLBACK - // Report progress - use call_deferred since we're in web server task - this->parent_->state_callback_.call_deferred(ota::OTA_IN_PROGRESS, percentage, 0); +#ifdef USE_OTA_STATE_LISTENER + // Report progress - use notify_state_deferred_ since we're in web server task + this->parent_->notify_state_deferred_(ota::OTA_IN_PROGRESS, percentage, 0); #endif this->last_ota_progress_ = now; } @@ -114,17 +111,14 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf // Initialize OTA on first call this->ota_init_(filename.c_str()); -#ifdef USE_OTA_STATE_CALLBACK - // Notify OTA started - use call_deferred since we're in web server task - this->parent_->state_callback_.call_deferred(ota::OTA_STARTED, 0.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + // Notify OTA started - use notify_state_deferred_ since we're in web server task + this->parent_->notify_state_deferred_(ota::OTA_STARTED, 0.0f, 0); #endif // Platform-specific pre-initialization #ifdef USE_ARDUINO -#ifdef USE_ESP8266 - Update.runAsync(true); -#endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) +#if defined(USE_LIBRETINY) if (Update.isRunning()) { Update.abort(); } @@ -134,9 +128,9 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf this->ota_backend_ = ota::make_ota_backend(); if (!this->ota_backend_) { ESP_LOGE(TAG, "Failed to create OTA backend"); -#ifdef USE_OTA_STATE_CALLBACK - this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, - static_cast(ota::OTA_RESPONSE_ERROR_UNKNOWN)); +#ifdef USE_OTA_STATE_LISTENER + this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, + static_cast(ota::OTA_RESPONSE_ERROR_UNKNOWN)); #endif return; } @@ -148,8 +142,8 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf if (error_code != ota::OTA_RESPONSE_OK) { ESP_LOGE(TAG, "OTA begin failed: %d", error_code); this->ota_backend_.reset(); -#ifdef USE_OTA_STATE_CALLBACK - this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#ifdef USE_OTA_STATE_LISTENER + this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif return; } @@ -166,8 +160,8 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf ESP_LOGE(TAG, "OTA write failed: %d", error_code); this->ota_backend_->abort(); this->ota_backend_.reset(); -#ifdef USE_OTA_STATE_CALLBACK - this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#ifdef USE_OTA_STATE_LISTENER + this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif return; } @@ -186,15 +180,15 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf error_code = this->ota_backend_->end(); if (error_code == ota::OTA_RESPONSE_OK) { this->ota_success_ = true; -#ifdef USE_OTA_STATE_CALLBACK - // Report completion before reboot - use call_deferred since we're in web server task - this->parent_->state_callback_.call_deferred(ota::OTA_COMPLETED, 100.0f, 0); +#ifdef USE_OTA_STATE_LISTENER + // Report completion before reboot - use notify_state_deferred_ since we're in web server task + this->parent_->notify_state_deferred_(ota::OTA_COMPLETED, 100.0f, 0); #endif this->schedule_ota_reboot_(); } else { ESP_LOGE(TAG, "OTA end failed: %d", error_code); -#ifdef USE_OTA_STATE_CALLBACK - this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast(error_code)); +#ifdef USE_OTA_STATE_LISTENER + this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast(error_code)); #endif } this->ota_backend_.reset(); @@ -232,15 +226,10 @@ void WebServerOTAComponent::setup() { // AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed base->add_handler(new OTARequestHandler(this)); // NOLINT -#ifdef USE_OTA_STATE_CALLBACK - // Register with global OTA callback system - ota::register_ota_platform(this); -#endif } void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); } -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif // USE_WEBSERVER_OTA diff --git a/esphome/components/web_server/ota/ota_web_server.h b/esphome/components/web_server/ota/ota_web_server.h index a7170c0e34..53ff99899c 100644 --- a/esphome/components/web_server/ota/ota_web_server.h +++ b/esphome/components/web_server/ota/ota_web_server.h @@ -7,8 +7,7 @@ #include "esphome/components/web_server_base/web_server_base.h" #include "esphome/core/component.h" -namespace esphome { -namespace web_server { +namespace esphome::web_server { class WebServerOTAComponent : public ota::OTAComponent { public: @@ -20,7 +19,6 @@ class WebServerOTAComponent : public ota::OTAComponent { friend class OTARequestHandler; }; -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif // USE_WEBSERVER_OTA diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index e675d81552..4f2ea8a6ab 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -11,638 +11,644 @@ namespace web_server { const uint8_t INDEX_GZ[] PROGMEM = { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcd, 0x7d, 0xdb, 0x72, 0xdb, 0xc6, 0xb6, 0xe0, 0xf3, - 0xe4, 0x2b, 0x20, 0x44, 0x5b, 0x41, 0x6f, 0x36, 0x21, 0x92, 0x92, 0x6c, 0x19, 0x54, 0x93, 0x5b, 0x96, 0x9d, 0xed, - 0x64, 0xfb, 0x16, 0xcb, 0x4e, 0x76, 0xc2, 0x68, 0x4b, 0x10, 0xd1, 0x24, 0x3a, 0x06, 0xd1, 0x0c, 0xd0, 0xa4, 0xa4, - 0x90, 0x38, 0x35, 0x1f, 0x30, 0x55, 0x53, 0x35, 0x4f, 0xf3, 0x32, 0x35, 0xe7, 0x61, 0x3e, 0x62, 0x9e, 0xcf, 0xa7, - 0x9c, 0x1f, 0x98, 0xf9, 0x84, 0xa9, 0xd5, 0x17, 0xa0, 0xc1, 0x8b, 0xac, 0x5c, 0xce, 0x39, 0x53, 0x2e, 0xdb, 0x44, - 0xa3, 0x2f, 0xab, 0x57, 0xaf, 0x5e, 0xf7, 0x6e, 0x9c, 0xec, 0x44, 0x7c, 0x28, 0xee, 0xa6, 0xd4, 0x89, 0xc5, 0x24, - 0xe9, 0x9d, 0xe8, 0x7f, 0x69, 0x18, 0xf5, 0x4e, 0x12, 0x96, 0x7e, 0x74, 0x32, 0x9a, 0x10, 0x36, 0xe4, 0xa9, 0x13, - 0x67, 0x74, 0x44, 0xa2, 0x50, 0x84, 0x01, 0x9b, 0x84, 0x63, 0xea, 0xec, 0xf7, 0x4e, 0x26, 0x54, 0x84, 0xce, 0x30, - 0x0e, 0xb3, 0x9c, 0x0a, 0xf2, 0xe1, 0xfd, 0x97, 0xcd, 0xe3, 0xde, 0x49, 0x3e, 0xcc, 0xd8, 0x54, 0x38, 0xd0, 0x25, - 0x99, 0xf0, 0x68, 0x96, 0xd0, 0xde, 0xfe, 0xfe, 0xcd, 0xcd, 0x8d, 0xff, 0x53, 0xfe, 0xd9, 0x90, 0xa7, 0xb9, 0x70, - 0x5e, 0x92, 0x1b, 0x96, 0x46, 0xfc, 0x06, 0x33, 0x41, 0x5e, 0xfa, 0xe7, 0x71, 0x18, 0xf1, 0x9b, 0x77, 0x9c, 0x8b, - 0xbd, 0x3d, 0x4f, 0x3d, 0xde, 0x9d, 0x9d, 0x9f, 0x13, 0x42, 0xe6, 0x9c, 0x45, 0x4e, 0x6b, 0xb9, 0xac, 0x0a, 0xfd, - 0x34, 0x14, 0x6c, 0x4e, 0x55, 0x13, 0xb4, 0xb7, 0xe7, 0x86, 0x11, 0x9f, 0x0a, 0x1a, 0x9d, 0x8b, 0xbb, 0x84, 0x9e, - 0xc7, 0x94, 0x8a, 0xdc, 0x65, 0xa9, 0xf3, 0x8c, 0x0f, 0x67, 0x13, 0x9a, 0x0a, 0x7f, 0x9a, 0x71, 0xc1, 0x01, 0x92, - 0xbd, 0x3d, 0x37, 0xa3, 0xd3, 0x24, 0x1c, 0x52, 0x78, 0x7f, 0x76, 0x7e, 0x5e, 0xb5, 0xa8, 0x2a, 0xe1, 0x5c, 0x90, - 0xf3, 0xbb, 0xc9, 0x35, 0x4f, 0x3c, 0x84, 0x43, 0x41, 0x52, 0x7a, 0xe3, 0x7c, 0x47, 0xc3, 0x8f, 0xaf, 0xc2, 0x69, - 0x77, 0x98, 0x84, 0x79, 0xee, 0xdc, 0x88, 0x85, 0x9c, 0x42, 0x36, 0x1b, 0x0a, 0x9e, 0x79, 0x02, 0x53, 0xcc, 0xd0, - 0x82, 0x8d, 0x3c, 0x11, 0xb3, 0xdc, 0xbf, 0xdc, 0x1d, 0xe6, 0xf9, 0x3b, 0x9a, 0xcf, 0x12, 0xb1, 0x4b, 0x76, 0x5a, - 0x98, 0xed, 0x10, 0x92, 0x0b, 0x24, 0xe2, 0x8c, 0xdf, 0x38, 0xcf, 0xb3, 0x8c, 0x67, 0x9e, 0x7b, 0x76, 0x7e, 0xae, - 0x6a, 0x38, 0x2c, 0x77, 0x52, 0x2e, 0x9c, 0xb2, 0xbf, 0xf0, 0x3a, 0xa1, 0xbe, 0xf3, 0x21, 0xa7, 0xce, 0xd5, 0x2c, - 0xcd, 0xc3, 0x11, 0x3d, 0x3b, 0x3f, 0xbf, 0x72, 0x78, 0xe6, 0x5c, 0x0d, 0xf3, 0xfc, 0xca, 0x61, 0x69, 0x2e, 0x68, - 0x18, 0xf9, 0x2e, 0xea, 0xca, 0xc1, 0x86, 0x79, 0xfe, 0x9e, 0xde, 0x0a, 0x22, 0xb0, 0x7c, 0x14, 0x84, 0x16, 0x63, - 0x2a, 0x9c, 0xbc, 0x9c, 0x97, 0x87, 0x16, 0x09, 0x15, 0x8e, 0x20, 0xf2, 0x3d, 0xef, 0x2a, 0xdc, 0x53, 0xf5, 0x28, - 0xba, 0x6c, 0xe4, 0x31, 0xb1, 0xb7, 0x27, 0x4a, 0x3c, 0x23, 0x35, 0x35, 0x87, 0x11, 0xba, 0x63, 0xca, 0xf6, 0xf6, - 0xa8, 0x9f, 0xd0, 0x74, 0x2c, 0x62, 0x42, 0x48, 0xbb, 0xcb, 0xf6, 0xf6, 0x3c, 0x41, 0x42, 0xe1, 0x8f, 0xa9, 0xf0, - 0x28, 0x42, 0xb8, 0x6a, 0xbd, 0xb7, 0xe7, 0x29, 0x24, 0x70, 0xa2, 0x10, 0x57, 0xc3, 0x31, 0xf2, 0x35, 0xf6, 0xcf, - 0xef, 0xd2, 0xa1, 0x67, 0xc3, 0x8f, 0x30, 0xdb, 0xdb, 0x0b, 0x85, 0x9f, 0x43, 0x8f, 0x58, 0x20, 0x54, 0x64, 0x54, - 0xcc, 0xb2, 0xd4, 0x11, 0x85, 0xe0, 0xe7, 0x22, 0x63, 0xe9, 0xd8, 0x43, 0x0b, 0x53, 0x66, 0x35, 0x2c, 0x0a, 0x05, - 0xee, 0x07, 0x41, 0x52, 0xd2, 0x83, 0x11, 0x6f, 0x84, 0x07, 0xab, 0xc8, 0x47, 0x4e, 0x4a, 0x88, 0x9b, 0xcb, 0xb6, - 0x6e, 0x3f, 0x0d, 0xd2, 0x86, 0xeb, 0x62, 0x05, 0x25, 0xce, 0x05, 0xc2, 0x6f, 0x88, 0x97, 0x62, 0xdf, 0xf7, 0x05, - 0x22, 0xbd, 0x85, 0xc1, 0x4a, 0x6a, 0xcd, 0xb3, 0x9f, 0x0e, 0x5a, 0x17, 0x81, 0xf0, 0x33, 0x1a, 0xcd, 0x86, 0xd4, - 0xf3, 0x18, 0xce, 0x71, 0x86, 0x48, 0x8f, 0x35, 0x3c, 0x4e, 0x7a, 0xb0, 0xdc, 0xbc, 0xbe, 0xd6, 0x84, 0xec, 0xb4, - 0x90, 0x86, 0x91, 0x1b, 0x00, 0x01, 0xc3, 0x1a, 0x1e, 0x4e, 0x88, 0x9b, 0xce, 0x26, 0xd7, 0x34, 0x73, 0xcb, 0x6a, - 0xdd, 0x1a, 0x59, 0xcc, 0x72, 0xea, 0x0c, 0xf3, 0xdc, 0x19, 0xcd, 0xd2, 0xa1, 0x60, 0x3c, 0x75, 0xdc, 0x06, 0x6f, - 0xb8, 0x8a, 0x1c, 0x4a, 0x6a, 0x70, 0x51, 0x81, 0xbc, 0x1c, 0x35, 0xd2, 0x41, 0xd6, 0x68, 0x5f, 0x60, 0x80, 0x12, - 0x75, 0x75, 0x7f, 0x1a, 0x01, 0x14, 0xa7, 0x30, 0xc7, 0x02, 0xbf, 0x17, 0x30, 0x4b, 0x39, 0x45, 0x26, 0xfa, 0xa9, - 0xbf, 0xbe, 0x51, 0x88, 0xf0, 0x27, 0xe1, 0xd4, 0xa3, 0xa4, 0x47, 0x25, 0x71, 0x85, 0xe9, 0x10, 0x60, 0xad, 0xad, - 0x5b, 0x9f, 0x06, 0xd4, 0xaf, 0x48, 0x0a, 0x05, 0xc2, 0x1f, 0xf1, 0xec, 0x79, 0x38, 0x8c, 0xa1, 0x5d, 0x49, 0x30, - 0x91, 0xd9, 0x6f, 0xc3, 0x8c, 0x86, 0x82, 0x3e, 0x4f, 0x28, 0x3c, 0x79, 0xae, 0x6c, 0xe9, 0x22, 0x9c, 0x93, 0x97, - 0x7e, 0xc2, 0xc4, 0x6b, 0x9e, 0x0e, 0x69, 0x37, 0xb7, 0xa8, 0x8b, 0xc1, 0xba, 0x9f, 0x0a, 0x91, 0xb1, 0xeb, 0x99, - 0xa0, 0x9e, 0x9b, 0x42, 0x0d, 0x17, 0xe7, 0x08, 0x33, 0x5f, 0xd0, 0x5b, 0x71, 0xc6, 0x53, 0x41, 0x53, 0x41, 0xa8, - 0x41, 0x2a, 0x4e, 0xfd, 0x70, 0x3a, 0xa5, 0x69, 0x74, 0x16, 0xb3, 0x24, 0xf2, 0x18, 0x2a, 0x50, 0x81, 0x63, 0x41, - 0x60, 0x8e, 0xa4, 0x97, 0x06, 0xf0, 0xcf, 0xf6, 0xd9, 0x78, 0x82, 0xf4, 0xe4, 0xa6, 0xa0, 0xc4, 0x75, 0xbb, 0x23, - 0x9e, 0x79, 0x7a, 0x06, 0x0e, 0x1f, 0x39, 0x02, 0xc6, 0x78, 0x37, 0x4b, 0x68, 0x8e, 0x68, 0x83, 0xb0, 0x72, 0x19, - 0x35, 0x82, 0x3f, 0x00, 0xc5, 0x17, 0xc8, 0x4b, 0x51, 0x90, 0x76, 0xe7, 0x61, 0xe6, 0x7c, 0xa7, 0x77, 0xd4, 0x33, - 0xc3, 0xcd, 0x86, 0x82, 0x3c, 0xf3, 0x45, 0x36, 0xcb, 0x05, 0x8d, 0xde, 0xdf, 0x4d, 0x69, 0x8e, 0x5f, 0x0b, 0x32, - 0x14, 0xfd, 0xa1, 0xf0, 0xe9, 0x64, 0x2a, 0xee, 0xce, 0x25, 0x63, 0x0c, 0x5c, 0x17, 0x47, 0x50, 0x33, 0xa3, 0xe1, - 0x10, 0x98, 0x99, 0xc6, 0xd6, 0x5b, 0x9e, 0xdc, 0x8d, 0x58, 0x92, 0x9c, 0xcf, 0xa6, 0x53, 0x9e, 0x09, 0xfc, 0x77, - 0xb2, 0x10, 0xbc, 0x42, 0x0d, 0xac, 0xe5, 0x22, 0xbf, 0x61, 0x62, 0x18, 0x7b, 0x02, 0x2d, 0x86, 0x61, 0x4e, 0x9d, - 0xa7, 0x9c, 0x27, 0x34, 0x84, 0x49, 0xa7, 0xfd, 0xd7, 0x22, 0x48, 0x67, 0x49, 0xd2, 0xbd, 0xce, 0x68, 0xf8, 0xb1, - 0x2b, 0x5f, 0xbf, 0xb9, 0xfe, 0x89, 0x0e, 0x45, 0x20, 0x7f, 0x9f, 0x66, 0x59, 0x78, 0x07, 0x15, 0x09, 0x81, 0x6a, - 0xfd, 0x34, 0xf8, 0xfa, 0xfc, 0xcd, 0x6b, 0x5f, 0x6d, 0x12, 0x36, 0xba, 0xf3, 0xd2, 0x72, 0xe3, 0xa5, 0x05, 0x1e, - 0x65, 0x7c, 0xb2, 0x32, 0xb4, 0xc2, 0x5a, 0xda, 0xdd, 0x02, 0x02, 0x25, 0xe9, 0x8e, 0xea, 0xda, 0x86, 0xe0, 0xb5, - 0xa4, 0x79, 0x78, 0x49, 0xcc, 0xb8, 0xb3, 0x24, 0x09, 0x54, 0xb1, 0x97, 0xa2, 0xfb, 0xa1, 0x15, 0xd9, 0xdd, 0x82, - 0x12, 0x09, 0xe7, 0x14, 0x24, 0x0c, 0xc0, 0x38, 0x0c, 0xc5, 0x30, 0x5e, 0x50, 0xd9, 0x59, 0x61, 0x20, 0xa6, 0x45, - 0x81, 0x6f, 0x4b, 0x7a, 0x17, 0x00, 0x88, 0x64, 0x54, 0x44, 0x2c, 0x97, 0x30, 0x61, 0x84, 0x7f, 0x20, 0x8b, 0xd0, - 0xcc, 0x27, 0xd8, 0x69, 0x61, 0xd8, 0x97, 0x81, 0xe2, 0x2e, 0x78, 0xc8, 0xd3, 0x39, 0xcd, 0x04, 0xcd, 0x82, 0xbf, - 0xe3, 0x8c, 0x8e, 0x12, 0x80, 0x62, 0xa7, 0x8d, 0xe3, 0x30, 0x3f, 0x8b, 0xc3, 0x74, 0x4c, 0xa3, 0xe0, 0x56, 0x14, - 0x58, 0x08, 0xe2, 0x8e, 0x58, 0x1a, 0x26, 0xec, 0x17, 0x1a, 0xb9, 0x5a, 0x1c, 0x3c, 0x77, 0xe8, 0xad, 0xa0, 0x69, - 0x94, 0x3b, 0x2f, 0xde, 0xbf, 0x7a, 0xa9, 0x17, 0xb2, 0x26, 0x21, 0xd0, 0x22, 0x9f, 0x4d, 0x69, 0xe6, 0x21, 0xac, - 0x25, 0xc4, 0x73, 0x26, 0xb9, 0xe3, 0xab, 0x70, 0xaa, 0x4a, 0x58, 0xfe, 0x61, 0x1a, 0x85, 0x82, 0xbe, 0xa5, 0x69, - 0xc4, 0xd2, 0x31, 0xd9, 0x69, 0xab, 0xf2, 0x38, 0xd4, 0x2f, 0xa2, 0xb2, 0xe8, 0x72, 0xf7, 0x79, 0x22, 0x27, 0x5e, - 0x3e, 0xce, 0x3c, 0x54, 0xe4, 0x22, 0x14, 0x6c, 0xe8, 0x84, 0x51, 0xf4, 0x55, 0xca, 0x04, 0x93, 0x00, 0x66, 0xb0, - 0x3e, 0x40, 0xa3, 0x54, 0xc9, 0x0a, 0x03, 0xb8, 0x87, 0xb0, 0xe7, 0x69, 0x09, 0x10, 0x23, 0xbd, 0x60, 0x7b, 0x7b, - 0x15, 0xbf, 0xef, 0xd3, 0x40, 0xbd, 0x24, 0x83, 0x0b, 0xe4, 0x4f, 0x67, 0x39, 0xac, 0xb4, 0x19, 0x02, 0xc4, 0x0b, - 0xbf, 0xce, 0x69, 0x36, 0xa7, 0x51, 0x49, 0x1d, 0xb9, 0x87, 0x16, 0x2b, 0x63, 0xe8, 0x7d, 0x21, 0xc8, 0xe0, 0xa2, - 0x6b, 0x33, 0x6e, 0xaa, 0x09, 0x3d, 0xe3, 0x53, 0x9a, 0x09, 0x46, 0xf3, 0x92, 0x97, 0x78, 0x20, 0x46, 0x4b, 0x7e, - 0x92, 0x13, 0x33, 0xbf, 0xa9, 0xc7, 0x30, 0x45, 0x35, 0x8e, 0x61, 0x24, 0xed, 0xf3, 0xb9, 0x14, 0x19, 0x39, 0x66, - 0x08, 0x0b, 0x05, 0x69, 0x8e, 0x50, 0x81, 0xb0, 0x30, 0xe0, 0x2a, 0x5e, 0xa4, 0x47, 0xbb, 0x03, 0x59, 0x4d, 0x7e, - 0x90, 0xb2, 0x1a, 0x38, 0x5a, 0x28, 0xe8, 0xde, 0x9e, 0x47, 0xfd, 0x92, 0x2a, 0xc8, 0x4e, 0x5b, 0xaf, 0x91, 0x85, - 0xac, 0x2d, 0x60, 0xc3, 0xc0, 0x02, 0x53, 0x84, 0x77, 0xa8, 0x9f, 0xf2, 0xd3, 0xe1, 0x90, 0xe6, 0x39, 0xcf, 0xf6, - 0xf6, 0x76, 0x64, 0xfd, 0x52, 0x9d, 0x80, 0x35, 0x7c, 0x73, 0x93, 0x56, 0x10, 0xa0, 0x4a, 0xc4, 0x6a, 0xc1, 0x20, - 0x40, 0x50, 0x49, 0x8d, 0xc3, 0xed, 0x1b, 0xcd, 0x23, 0x70, 0x2f, 0x2f, 0xdd, 0x86, 0xc0, 0x1a, 0x0d, 0x63, 0x6a, - 0x86, 0xbe, 0x7b, 0x46, 0x95, 0x6e, 0x25, 0x35, 0x8f, 0x35, 0xcc, 0xa8, 0x0d, 0xe4, 0x47, 0x74, 0xc4, 0x52, 0x6b, - 0xda, 0x35, 0x90, 0xb0, 0xc0, 0x39, 0x2a, 0xac, 0x05, 0xdd, 0xd8, 0xb5, 0x54, 0x6a, 0xd4, 0xca, 0x2d, 0xc6, 0x52, - 0x91, 0xb0, 0x96, 0x71, 0x40, 0x2f, 0x0a, 0x2c, 0x51, 0x6f, 0x66, 0x93, 0x49, 0x40, 0x07, 0xe2, 0xa2, 0xab, 0xdf, - 0x93, 0x5c, 0x61, 0x2e, 0xa3, 0x3f, 0xcf, 0x68, 0x2e, 0x14, 0x1d, 0x7b, 0x02, 0x67, 0x98, 0xa1, 0x02, 0xf6, 0xdb, - 0x88, 0x8d, 0x67, 0x19, 0xe8, 0x3b, 0xb0, 0x17, 0x69, 0x3a, 0x9b, 0x50, 0xf3, 0xb4, 0x09, 0xb6, 0x37, 0x53, 0x90, - 0x88, 0x39, 0xd0, 0xf4, 0xfd, 0xe4, 0x04, 0xb0, 0x0a, 0xb4, 0x5c, 0xfe, 0x60, 0x3a, 0xa9, 0x96, 0xb2, 0xd4, 0xd1, - 0x56, 0xd7, 0x44, 0x20, 0x2d, 0x91, 0x77, 0xda, 0x0a, 0x7c, 0x21, 0x2e, 0xc8, 0x4e, 0xab, 0xa4, 0x61, 0x8d, 0x55, - 0x05, 0x8e, 0x42, 0xe2, 0x1b, 0xd5, 0x15, 0x92, 0x02, 0xbe, 0x46, 0x2e, 0x7e, 0xbc, 0x46, 0xa9, 0x31, 0x19, 0x80, - 0xaa, 0xe1, 0xc7, 0x17, 0xdb, 0xc8, 0xc9, 0xf0, 0x03, 0x4f, 0xac, 0xbf, 0xab, 0xd8, 0xc6, 0xbc, 0xce, 0x36, 0x56, - 0xa6, 0xe1, 0x4e, 0xcb, 0x26, 0x6e, 0x49, 0x65, 0x7a, 0xa3, 0x57, 0xaf, 0x30, 0x93, 0xc0, 0x54, 0x53, 0xb2, 0xba, - 0x78, 0x1d, 0x4e, 0x68, 0xee, 0x51, 0x84, 0xb7, 0x55, 0x50, 0xe4, 0x09, 0x55, 0x2e, 0x2c, 0xc9, 0x99, 0x83, 0xe4, - 0x64, 0x48, 0x29, 0x66, 0xf5, 0x0d, 0x97, 0x63, 0x3a, 0xc8, 0x2f, 0x2a, 0x7d, 0xce, 0x9a, 0xbc, 0x14, 0xc9, 0x9a, - 0xbe, 0x0d, 0xfe, 0x54, 0x99, 0x42, 0x9a, 0xd4, 0x1b, 0x72, 0x84, 0x77, 0x5a, 0xab, 0x2b, 0x69, 0x6a, 0x55, 0x73, - 0x1c, 0x5c, 0xc0, 0x3a, 0x48, 0x89, 0xe1, 0xb3, 0x5c, 0xfe, 0x5f, 0xdb, 0x69, 0x80, 0xb6, 0x73, 0x20, 0x0c, 0x7f, - 0x94, 0x84, 0xc2, 0x6b, 0xef, 0xb7, 0x40, 0x19, 0x9d, 0x53, 0x10, 0x28, 0x08, 0xad, 0x4f, 0x85, 0xfa, 0xb3, 0x34, - 0x8f, 0xd9, 0x48, 0x78, 0xb1, 0x90, 0x2c, 0x85, 0x26, 0x39, 0x75, 0x44, 0x4d, 0x25, 0x96, 0xec, 0x26, 0x06, 0x62, - 0x2b, 0xf5, 0x2f, 0x6a, 0x20, 0x95, 0x6c, 0x0b, 0xb8, 0x43, 0xa5, 0x4e, 0x57, 0x5c, 0xc6, 0xd4, 0x66, 0xa0, 0x32, - 0xb6, 0xfb, 0xaa, 0xc7, 0x40, 0x33, 0x03, 0x66, 0x69, 0xad, 0x2c, 0xb0, 0x39, 0x84, 0x2e, 0x14, 0xbe, 0xe0, 0x2f, - 0xf9, 0x0d, 0xcd, 0xce, 0x42, 0x00, 0x3e, 0x50, 0xcd, 0x0b, 0x25, 0x08, 0x24, 0xbf, 0x17, 0x5d, 0x43, 0x2f, 0x97, - 0x72, 0xe2, 0x6f, 0x33, 0x3e, 0x61, 0x39, 0x05, 0x65, 0x4d, 0xe1, 0x3f, 0x85, 0x7d, 0x26, 0x37, 0x24, 0x08, 0x1b, - 0x5a, 0xd2, 0xd7, 0xe9, 0xcb, 0x3a, 0x7d, 0x5d, 0xee, 0x3e, 0x1f, 0x1b, 0x06, 0x58, 0xdf, 0xc6, 0x08, 0x7b, 0xda, - 0xa4, 0xb0, 0xe4, 0x9c, 0x1f, 0x23, 0x2d, 0xe1, 0x97, 0x4b, 0x61, 0x59, 0x6e, 0x35, 0x75, 0x91, 0xaa, 0x6d, 0x83, - 0x8a, 0x30, 0x8a, 0x40, 0xb1, 0xcb, 0x78, 0x92, 0x58, 0xa2, 0x0a, 0xb3, 0x6e, 0x29, 0x9c, 0x2e, 0x77, 0x9f, 0x9f, - 0xdf, 0x27, 0x9f, 0xe0, 0xbd, 0x2d, 0xa2, 0x0c, 0xa0, 0x69, 0x44, 0x33, 0xb0, 0x24, 0xad, 0xd5, 0xd2, 0x52, 0xf6, - 0x8c, 0xa7, 0x29, 0x1d, 0x0a, 0x1a, 0x81, 0xa1, 0xc2, 0x88, 0xf0, 0x63, 0x9e, 0x8b, 0xb2, 0xb0, 0x82, 0x9e, 0x59, - 0xd0, 0x33, 0x7f, 0x18, 0x26, 0x89, 0xa7, 0x8c, 0x92, 0x09, 0x9f, 0xd3, 0x0d, 0x50, 0x77, 0x6b, 0x20, 0x97, 0xdd, - 0x50, 0xab, 0x1b, 0xea, 0xe7, 0xd3, 0x84, 0x0d, 0x69, 0x29, 0xba, 0xce, 0x7d, 0x96, 0x46, 0xf4, 0x16, 0xf8, 0x08, - 0xea, 0xf5, 0x7a, 0x2d, 0xdc, 0x46, 0x85, 0x42, 0xf8, 0x62, 0x0d, 0xb1, 0xf7, 0x08, 0x4d, 0x20, 0x32, 0xd2, 0x5b, - 0x6c, 0xe2, 0x07, 0x14, 0x59, 0x92, 0x92, 0x19, 0xe3, 0x4a, 0x71, 0x67, 0x84, 0x23, 0x9a, 0x50, 0x41, 0x0d, 0x37, - 0x07, 0x15, 0x5a, 0x6d, 0xdd, 0x77, 0x25, 0xfe, 0x4a, 0x72, 0x32, 0xbb, 0xcc, 0xac, 0x79, 0x5e, 0x1a, 0xeb, 0xd5, - 0xf2, 0x54, 0xd8, 0xee, 0x0b, 0xb5, 0x3c, 0xa1, 0x10, 0xe1, 0x30, 0x56, 0x56, 0xba, 0xb7, 0x36, 0xa5, 0xaa, 0x0f, - 0xcd, 0xd9, 0xcb, 0x4d, 0xf4, 0xde, 0x80, 0xb9, 0x09, 0x05, 0xe7, 0x9a, 0x29, 0x50, 0x30, 0xfc, 0xd4, 0xb2, 0x9d, - 0x85, 0x49, 0x72, 0x1d, 0x0e, 0x3f, 0xd6, 0xa9, 0xbf, 0x22, 0x03, 0xb2, 0xca, 0x8d, 0xad, 0x57, 0x16, 0xcb, 0xb2, - 0xe7, 0x6d, 0xb8, 0x74, 0x6d, 0xa3, 0x78, 0x3b, 0xad, 0x8a, 0xec, 0xeb, 0x0b, 0xbd, 0x95, 0xda, 0x25, 0x44, 0x4c, - 0xcf, 0xcc, 0x03, 0x2e, 0xf0, 0x49, 0x8a, 0x33, 0xfc, 0x40, 0xd3, 0x1d, 0x98, 0x1b, 0xc5, 0x0a, 0x20, 0x02, 0x2d, - 0x8a, 0x88, 0xe5, 0xdb, 0x31, 0xf0, 0x87, 0x40, 0xf9, 0xcc, 0x1a, 0xe1, 0xa1, 0x80, 0x96, 0x3c, 0x4e, 0x6b, 0xcd, - 0x25, 0x64, 0x5a, 0x9f, 0x30, 0x8c, 0xe6, 0x6f, 0xa0, 0xbb, 0x48, 0x7a, 0x7f, 0xa3, 0x5e, 0x81, 0x56, 0x06, 0x50, - 0xe4, 0x5d, 0x5b, 0x9d, 0xa8, 0x51, 0x80, 0xe6, 0xa9, 0x4c, 0x8a, 0xdc, 0xac, 0x66, 0x3f, 0x6a, 0x8d, 0x5d, 0x99, - 0xe0, 0x9a, 0xe5, 0x72, 0xe2, 0x79, 0x5e, 0x0e, 0x26, 0x9c, 0x51, 0xed, 0xab, 0x49, 0xe4, 0x6b, 0x93, 0xc8, 0x7d, - 0xcb, 0xce, 0x42, 0x15, 0x2d, 0x5b, 0xcd, 0x83, 0xbf, 0x23, 0xbb, 0x12, 0xa8, 0xab, 0x3e, 0xf0, 0x67, 0x54, 0xb2, - 0xdb, 0x84, 0x08, 0xcc, 0xb5, 0x8d, 0xa3, 0x29, 0x0d, 0x18, 0x46, 0xd5, 0x24, 0x43, 0x6a, 0x6b, 0xd4, 0xec, 0xdd, - 0x0c, 0x73, 0xb4, 0xa2, 0xdb, 0x17, 0x85, 0xc6, 0x11, 0x45, 0x7a, 0x6d, 0x6a, 0x4a, 0xb1, 0x85, 0x15, 0x9c, 0x11, - 0xad, 0x08, 0x2b, 0xbd, 0x67, 0x15, 0x37, 0x65, 0xbf, 0x3b, 0x84, 0x64, 0x15, 0x6a, 0x6a, 0x1a, 0xa5, 0x51, 0xad, - 0x32, 0x84, 0x63, 0xa3, 0x93, 0xf2, 0x6a, 0xde, 0x84, 0xb8, 0xc6, 0x21, 0xe1, 0xf6, 0x17, 0x35, 0xab, 0x30, 0xb0, - 0xaa, 0x15, 0x01, 0xb0, 0x54, 0xbe, 0x09, 0xdd, 0x9b, 0x68, 0xa6, 0xd6, 0x8f, 0x85, 0x70, 0x6e, 0x23, 0xdc, 0xc2, - 0x6c, 0xa6, 0x38, 0x57, 0x76, 0x41, 0xe2, 0x7a, 0x5b, 0x8f, 0x62, 0xae, 0xd6, 0x61, 0x0d, 0x89, 0xab, 0xaa, 0xa7, - 0x24, 0x41, 0xb0, 0x61, 0x73, 0x50, 0xee, 0x6c, 0xf9, 0xe0, 0x01, 0xec, 0x6c, 0xb9, 0x5c, 0x23, 0xba, 0x8d, 0x1a, - 0x28, 0xf2, 0x2b, 0xbb, 0x70, 0xb9, 0xbc, 0x15, 0xc8, 0xd3, 0xba, 0x2f, 0xa6, 0xa8, 0x6f, 0x38, 0xee, 0xe9, 0x4b, - 0xa8, 0x25, 0x55, 0xd1, 0xaa, 0xa4, 0x34, 0x1a, 0xea, 0x34, 0x5b, 0x5f, 0x27, 0x61, 0xb1, 0xed, 0xb3, 0x35, 0xee, - 0x25, 0x0b, 0xb5, 0x98, 0xae, 0xa6, 0x7c, 0xa6, 0xbb, 0x66, 0x08, 0xa1, 0x20, 0x97, 0x76, 0xcc, 0xce, 0x26, 0xd3, - 0x72, 0x6f, 0x2f, 0xb7, 0x3a, 0xba, 0x2c, 0xd9, 0xc4, 0x4f, 0x1e, 0x88, 0xe4, 0xfc, 0x2e, 0x95, 0xba, 0xcb, 0x4f, - 0x46, 0x08, 0xad, 0x19, 0xa6, 0xad, 0x2e, 0x18, 0xe4, 0xe1, 0x4d, 0xc8, 0x84, 0x53, 0xf6, 0xa2, 0x0c, 0x72, 0x8f, - 0xa2, 0x85, 0x56, 0x35, 0xfc, 0x8c, 0x82, 0xf2, 0x08, 0x3c, 0xc1, 0xa8, 0xd0, 0x8a, 0xee, 0x87, 0x31, 0x05, 0x5f, - 0xb0, 0xd1, 0x22, 0x4a, 0xcb, 0x70, 0x47, 0x4b, 0x11, 0xdd, 0xf1, 0x66, 0xd8, 0x8b, 0xd5, 0xe6, 0x35, 0x4b, 0x60, - 0x4a, 0xb3, 0x11, 0xcf, 0x26, 0xe6, 0x5d, 0xb1, 0xf2, 0xac, 0x39, 0x23, 0x1b, 0x79, 0x1b, 0xfb, 0xd6, 0xfa, 0x7f, - 0x77, 0xc5, 0xec, 0xae, 0x0c, 0xf6, 0x9a, 0x28, 0x2d, 0xa5, 0xaf, 0x72, 0x09, 0x1a, 0xca, 0xcc, 0x6d, 0x03, 0x5f, - 0xfb, 0x53, 0xbb, 0xca, 0x67, 0xb2, 0xd3, 0xee, 0x96, 0x56, 0x9f, 0xa1, 0x86, 0xae, 0xf2, 0x6d, 0x68, 0x91, 0xca, - 0x67, 0x49, 0xa4, 0x81, 0x65, 0x08, 0x53, 0x4d, 0x47, 0x37, 0x2c, 0x49, 0xaa, 0xd2, 0x5f, 0xc3, 0xd7, 0x73, 0xcd, - 0xd7, 0x33, 0xc3, 0xd7, 0x81, 0x53, 0x00, 0x5f, 0x57, 0xdd, 0x55, 0xcd, 0xb3, 0xb5, 0xdd, 0x99, 0x29, 0x8e, 0x9e, - 0x4b, 0x4b, 0x1a, 0xc6, 0x9b, 0x19, 0x08, 0x50, 0xa9, 0x79, 0x7d, 0xf4, 0xb4, 0x1f, 0x06, 0x4c, 0x40, 0xe5, 0xc5, - 0xa4, 0xb6, 0x93, 0xe2, 0xa3, 0x87, 0x70, 0x5e, 0xd0, 0x92, 0xb2, 0x4f, 0x9f, 0x83, 0x9f, 0xce, 0x9a, 0x0e, 0x08, - 0x31, 0x59, 0xfc, 0xab, 0x94, 0x28, 0x33, 0x3b, 0xa6, 0x67, 0x97, 0x9b, 0xd9, 0x01, 0xa7, 0xaf, 0x66, 0x17, 0xdd, - 0xcf, 0xeb, 0xe5, 0xf4, 0x58, 0x39, 0xbd, 0x6a, 0xbd, 0x97, 0x4b, 0x6f, 0xa5, 0x04, 0x5c, 0xf8, 0xda, 0x44, 0xc9, - 0xca, 0xde, 0x81, 0x07, 0xd8, 0x98, 0x81, 0x82, 0x42, 0x4d, 0xba, 0x14, 0x71, 0x2f, 0x3f, 0xe5, 0xe2, 0x91, 0x9e, - 0x7a, 0xd5, 0xfe, 0x8c, 0x4f, 0xa6, 0xa0, 0x8d, 0xad, 0x90, 0xf4, 0x98, 0xea, 0x01, 0xab, 0xf7, 0xc5, 0x86, 0xb2, - 0x5a, 0x1b, 0xb9, 0x1f, 0x6b, 0xd4, 0x54, 0x5a, 0xcc, 0x3b, 0xad, 0x62, 0x56, 0x16, 0x95, 0x8c, 0x63, 0x93, 0x5b, - 0xe5, 0x6c, 0xd5, 0x29, 0x63, 0x5e, 0xbc, 0xf1, 0x98, 0xe2, 0xc3, 0x0c, 0x78, 0x9d, 0xc5, 0x7e, 0x0c, 0xb9, 0xdb, - 0xeb, 0x5f, 0x54, 0xc8, 0x59, 0x14, 0x2b, 0xe8, 0x5b, 0x14, 0xc5, 0x73, 0x6d, 0x65, 0xe3, 0xe7, 0xdb, 0xcd, 0xe1, - 0xea, 0x9d, 0xb6, 0x16, 0x07, 0x17, 0xf8, 0xf9, 0xba, 0xee, 0x48, 0x16, 0x13, 0x1e, 0xd1, 0xc0, 0xe5, 0x53, 0x9a, - 0xba, 0x05, 0x78, 0x56, 0xf5, 0xe2, 0x47, 0xc2, 0x5b, 0xbc, 0xab, 0xbb, 0x58, 0x83, 0xe7, 0x05, 0x38, 0xc0, 0xbe, - 0x5b, 0x77, 0xbe, 0x7e, 0x4b, 0xb3, 0x5c, 0x6a, 0xa2, 0xa5, 0x52, 0xfb, 0x5d, 0x25, 0x97, 0xbe, 0x0b, 0xb6, 0xd6, - 0xaf, 0x6c, 0x10, 0xb7, 0xed, 0x3f, 0xf2, 0x0f, 0x5c, 0x24, 0x5d, 0xc3, 0x5f, 0xeb, 0x1d, 0xff, 0x93, 0x71, 0x0d, - 0x9f, 0x93, 0x9f, 0xea, 0x9e, 0xe1, 0x99, 0x20, 0xe7, 0xfd, 0x73, 0x63, 0x32, 0xf3, 0x84, 0x0d, 0xef, 0x3c, 0x37, - 0x61, 0xa2, 0x09, 0xe1, 0x37, 0x17, 0x2f, 0xd4, 0x0b, 0xf0, 0x2a, 0x4a, 0x97, 0x76, 0x61, 0x8c, 0x3d, 0x4c, 0x05, - 0x71, 0x77, 0x13, 0x26, 0x76, 0x5d, 0x3c, 0x21, 0x57, 0xf0, 0x63, 0x77, 0xe1, 0xbd, 0x0a, 0x45, 0xec, 0x67, 0x61, - 0x1a, 0xf1, 0x89, 0x87, 0x1a, 0xae, 0x8b, 0xfc, 0x5c, 0x1a, 0x1c, 0x4f, 0x50, 0xb1, 0x7b, 0x85, 0x4f, 0x05, 0x71, - 0xfb, 0x6e, 0x63, 0x82, 0xdf, 0x09, 0x72, 0x75, 0xb2, 0xbb, 0x38, 0x15, 0x45, 0xef, 0x0a, 0xdf, 0x96, 0x5e, 0x7b, - 0xfc, 0x81, 0x78, 0x88, 0xf4, 0x6e, 0x35, 0x34, 0x67, 0x7c, 0xa2, 0xbc, 0xf7, 0x2e, 0xc2, 0xef, 0x65, 0x6c, 0xa5, - 0x62, 0x37, 0x3a, 0xbc, 0xb2, 0x43, 0x5c, 0x2e, 0x7d, 0x04, 0xee, 0xde, 0x9e, 0x55, 0x56, 0xea, 0x0a, 0xf8, 0xb9, - 0x20, 0x35, 0x8b, 0x1c, 0xbf, 0x90, 0x51, 0x9a, 0xe7, 0xc2, 0x4b, 0x91, 0xe9, 0xc6, 0x33, 0xbe, 0x68, 0xbd, 0x37, - 0xd3, 0x81, 0x72, 0x31, 0xf8, 0x4c, 0xd0, 0x2c, 0x14, 0x3c, 0xbb, 0x40, 0xb6, 0xfe, 0x81, 0xff, 0x46, 0xae, 0x06, - 0xce, 0x7f, 0xfa, 0xec, 0xc7, 0xd1, 0x8f, 0xd9, 0xc5, 0x15, 0x7e, 0x4b, 0xf6, 0x4f, 0xbc, 0x7e, 0xe0, 0xed, 0x34, - 0x9b, 0xcb, 0x1f, 0xf7, 0x07, 0xff, 0x08, 0x9b, 0xbf, 0x9c, 0x36, 0x7f, 0xb8, 0x40, 0x4b, 0xef, 0xc7, 0xfd, 0xfe, - 0x40, 0x3f, 0x0d, 0xfe, 0xd1, 0xfb, 0x31, 0xbf, 0xf8, 0xb3, 0x2a, 0xdc, 0x45, 0x68, 0x7f, 0x8c, 0xa7, 0x82, 0xec, - 0x37, 0x9b, 0xbd, 0xfd, 0x31, 0x1e, 0x0b, 0xb2, 0x0f, 0xff, 0x5f, 0x93, 0x77, 0x74, 0xfc, 0xfc, 0x76, 0xea, 0x5d, - 0xf5, 0x96, 0xbb, 0x8b, 0xbf, 0x15, 0xd0, 0xeb, 0xe0, 0x1f, 0x3f, 0xfe, 0x98, 0xbb, 0x5f, 0xf4, 0xc8, 0xfe, 0x45, - 0x03, 0x79, 0x50, 0xfa, 0x67, 0x22, 0xff, 0xf5, 0xfa, 0xc1, 0xe0, 0x1f, 0x1a, 0x0a, 0xf7, 0x8b, 0x1f, 0xaf, 0x4e, - 0x7a, 0xe4, 0x62, 0xe9, 0xb9, 0xcb, 0x2f, 0xd0, 0x12, 0xa1, 0xe5, 0x2e, 0xba, 0xc2, 0xee, 0xd8, 0x45, 0x78, 0x2e, - 0xc8, 0xfe, 0x17, 0xfb, 0x63, 0x3c, 0x12, 0x64, 0xdf, 0xdd, 0x1f, 0xe3, 0x73, 0x41, 0xf6, 0xff, 0xe1, 0xf5, 0x03, - 0xe5, 0x64, 0x5b, 0x4a, 0xff, 0xc6, 0x12, 0x02, 0x1c, 0x61, 0x46, 0xc3, 0xa5, 0x60, 0x22, 0xa1, 0x68, 0x77, 0x9f, - 0xe1, 0x33, 0x89, 0x26, 0x4f, 0x80, 0x17, 0x06, 0x8c, 0x3b, 0x6f, 0x71, 0x09, 0x8b, 0x0d, 0x34, 0xb3, 0x1b, 0x40, - 0x64, 0x07, 0x1c, 0x01, 0x79, 0x20, 0xf0, 0x3c, 0x4c, 0x66, 0x34, 0x0f, 0x68, 0x81, 0xf0, 0x90, 0x9c, 0x09, 0xaf, - 0x8d, 0xf0, 0x53, 0x01, 0x3f, 0x3a, 0x08, 0x9f, 0xe9, 0x20, 0x26, 0xec, 0x64, 0x45, 0x54, 0x29, 0x57, 0x2a, 0x8b, - 0x8b, 0xf0, 0x74, 0xc3, 0x4b, 0x11, 0x83, 0x7b, 0x01, 0xe1, 0xdd, 0x5a, 0xc8, 0x13, 0xdf, 0x10, 0x43, 0x12, 0xef, - 0x33, 0x4a, 0xbf, 0x0b, 0x93, 0x8f, 0x34, 0xf3, 0x6e, 0x71, 0xbb, 0xf3, 0x04, 0x4b, 0x2f, 0xf4, 0x4e, 0x1b, 0x75, - 0xcb, 0x78, 0xd5, 0x47, 0xa1, 0xe2, 0x04, 0x20, 0x65, 0xeb, 0xce, 0x18, 0x58, 0xf1, 0x9d, 0x74, 0xcd, 0x63, 0x95, - 0x85, 0x37, 0x2e, 0xaa, 0xc7, 0x46, 0x59, 0x3a, 0x0f, 0x13, 0x16, 0x39, 0x82, 0x4e, 0xa6, 0x49, 0x28, 0xa8, 0xa3, - 0xe7, 0xeb, 0x84, 0xd0, 0x91, 0x5b, 0xea, 0x0c, 0x33, 0xcb, 0xe2, 0x9c, 0x99, 0xa0, 0x13, 0xec, 0x15, 0x0f, 0x22, - 0x54, 0x5a, 0xef, 0x78, 0x55, 0x05, 0xc0, 0x56, 0x63, 0x7c, 0xcd, 0x36, 0x78, 0xc2, 0x2e, 0xa4, 0x7c, 0xce, 0x71, - 0x46, 0x40, 0x8a, 0x76, 0xfa, 0xee, 0x49, 0x3e, 0x1f, 0xf7, 0x5c, 0x88, 0xcf, 0x70, 0xf2, 0x56, 0x3a, 0x86, 0xa0, - 0x42, 0x4c, 0x5a, 0xdd, 0xf8, 0x84, 0x76, 0xe3, 0x46, 0xc3, 0x28, 0xd1, 0x09, 0x49, 0x07, 0xb1, 0x6a, 0x1e, 0xe2, - 0x08, 0xcf, 0x48, 0xb3, 0x8d, 0xc7, 0xa4, 0x25, 0x9b, 0x74, 0xc7, 0x27, 0x89, 0x1e, 0x66, 0x6f, 0xcf, 0xe3, 0x7e, - 0x12, 0xe6, 0xe2, 0x2b, 0xb0, 0xf6, 0xc9, 0x18, 0x47, 0x84, 0xfb, 0xf4, 0x96, 0x0e, 0xbd, 0x04, 0xe1, 0x48, 0x73, - 0x1a, 0xd4, 0x45, 0x63, 0x62, 0x55, 0x03, 0x2b, 0x82, 0xbc, 0xed, 0x47, 0x83, 0xf6, 0x05, 0x21, 0xc4, 0xdd, 0x69, - 0x36, 0xdd, 0x3e, 0x27, 0x53, 0x11, 0x40, 0x89, 0xa5, 0x2b, 0x93, 0x31, 0x14, 0x75, 0xac, 0x22, 0xef, 0x5c, 0xf8, - 0x82, 0xe6, 0xc2, 0x83, 0x62, 0xb0, 0xff, 0x73, 0x43, 0xd8, 0xee, 0xc9, 0xbe, 0xdb, 0x80, 0x52, 0x49, 0x9c, 0x08, - 0x73, 0x72, 0x8d, 0x82, 0x68, 0x70, 0x70, 0x61, 0x0b, 0x00, 0x59, 0x08, 0x83, 0x5f, 0xf7, 0xa3, 0x41, 0x4b, 0x0e, - 0xde, 0x73, 0xfb, 0x1e, 0x27, 0xb9, 0xd2, 0xd0, 0xfa, 0x79, 0xf0, 0x56, 0x4e, 0x15, 0x05, 0x1a, 0x38, 0xb3, 0x02, - 0xa4, 0xd9, 0x09, 0xbc, 0x99, 0x3d, 0x89, 0x26, 0x0c, 0xa6, 0xb1, 0x80, 0x43, 0x02, 0xf5, 0x31, 0x27, 0x30, 0x62, - 0xd5, 0xec, 0x3a, 0xd0, 0xcf, 0x5f, 0xb8, 0x5f, 0xf4, 0x47, 0x22, 0x98, 0x0b, 0x35, 0xfc, 0x48, 0x2c, 0x97, 0xf0, - 0xff, 0x5c, 0xf4, 0x39, 0xb9, 0x96, 0x45, 0x53, 0x5d, 0x34, 0x86, 0xa2, 0xb7, 0x01, 0x80, 0x8a, 0xf3, 0x52, 0xcb, - 0x52, 0x6b, 0x32, 0x27, 0x12, 0xf6, 0xbd, 0xbd, 0x74, 0x10, 0x37, 0xda, 0x17, 0xe0, 0xe2, 0xcf, 0x44, 0xfe, 0x1d, - 0x13, 0xb1, 0xe7, 0xee, 0xf7, 0x5c, 0xd4, 0x77, 0x1d, 0x58, 0xda, 0x6e, 0xd6, 0x20, 0x0a, 0xc3, 0x49, 0xe3, 0x9d, - 0x08, 0x66, 0x3d, 0xd2, 0xea, 0x7b, 0x4c, 0xb1, 0xf0, 0x10, 0xe1, 0x44, 0x33, 0xce, 0x16, 0x9e, 0xa1, 0x06, 0x15, - 0x0d, 0xf3, 0x3c, 0x43, 0x8d, 0x49, 0x63, 0x8e, 0x82, 0xa4, 0x31, 0x69, 0x78, 0x33, 0x42, 0x48, 0xb3, 0x53, 0x36, - 0x33, 0xe2, 0x2f, 0x46, 0xc1, 0xdc, 0x78, 0x3b, 0x07, 0x72, 0x3b, 0x64, 0x0d, 0x2f, 0x1d, 0xd0, 0x8b, 0xe5, 0xd2, - 0x3d, 0xe9, 0xf7, 0x5c, 0xd4, 0xf0, 0x0c, 0xa1, 0xed, 0x1b, 0x4a, 0x43, 0x08, 0xb3, 0x8b, 0x42, 0x47, 0x93, 0x5e, - 0xd7, 0x22, 0x47, 0x8b, 0x6a, 0xb3, 0x5b, 0x3c, 0x80, 0x16, 0xa5, 0x21, 0xa3, 0x14, 0xd6, 0x29, 0x4c, 0xd3, 0x10, - 0x73, 0x46, 0x5a, 0x98, 0x13, 0xe3, 0xbc, 0x8e, 0x89, 0xa8, 0x08, 0x3e, 0x21, 0x55, 0x75, 0x3c, 0x08, 0x71, 0x74, - 0x41, 0x5e, 0x29, 0x83, 0xa4, 0x6b, 0x5c, 0xe3, 0x34, 0x21, 0xaf, 0x57, 0x22, 0xb8, 0x21, 0x84, 0x57, 0x6e, 0xfc, - 0xe1, 0x2c, 0xcb, 0x68, 0x2a, 0x5e, 0xf3, 0x48, 0xeb, 0x69, 0x34, 0x01, 0x53, 0x09, 0x42, 0xb3, 0x18, 0x94, 0xb4, - 0x8e, 0xd9, 0x19, 0xb3, 0xb5, 0xd7, 0x63, 0x32, 0x53, 0xfa, 0x93, 0x0c, 0xd8, 0x76, 0xc7, 0xda, 0x30, 0xf6, 0x10, - 0x9e, 0xe9, 0x48, 0xae, 0xe7, 0xfb, 0xfe, 0xd8, 0x1f, 0xc2, 0x6b, 0x18, 0x20, 0x47, 0x85, 0xdc, 0x47, 0x5e, 0x4e, - 0x6e, 0xfc, 0x94, 0xde, 0xca, 0x51, 0x3d, 0x54, 0x49, 0x66, 0xb3, 0xbd, 0x4e, 0xe2, 0xae, 0x64, 0x37, 0xb9, 0x9f, - 0xf2, 0x88, 0x02, 0x7a, 0x20, 0x76, 0xaf, 0x8b, 0xe2, 0x30, 0xb7, 0x43, 0x54, 0x15, 0x7c, 0x03, 0xdb, 0x7b, 0x3d, - 0x06, 0x97, 0xaf, 0x54, 0xb6, 0xca, 0xca, 0xca, 0x0f, 0x8e, 0x10, 0x1b, 0x79, 0x63, 0x1f, 0x42, 0x7b, 0x92, 0x84, - 0x28, 0xd8, 0x72, 0x63, 0x9b, 0xa8, 0x26, 0x65, 0x9f, 0x73, 0x12, 0x0d, 0x78, 0xa3, 0x21, 0xdd, 0xd0, 0x33, 0x45, - 0x12, 0x63, 0x84, 0xe7, 0xe5, 0xde, 0x32, 0xf5, 0xbe, 0x24, 0xf5, 0x91, 0xbc, 0x79, 0xdd, 0x9d, 0xdb, 0x80, 0x34, - 0x09, 0xf0, 0x14, 0x0a, 0x6f, 0x82, 0xf0, 0x29, 0xd9, 0xf7, 0x06, 0x7e, 0xff, 0x2f, 0x17, 0xa8, 0xef, 0xf9, 0x7f, - 0x46, 0xfb, 0x8a, 0x71, 0xcc, 0x51, 0x37, 0x51, 0x43, 0x2c, 0x64, 0x08, 0xb3, 0x8d, 0xa5, 0x27, 0x31, 0xc8, 0x70, - 0x1a, 0x4e, 0x68, 0x70, 0x0a, 0x7b, 0xdc, 0xd0, 0xcd, 0x97, 0x18, 0xe8, 0x28, 0x38, 0xd5, 0x9c, 0xc4, 0x77, 0xfb, - 0xcf, 0x44, 0xf9, 0xd4, 0x77, 0xfb, 0x5f, 0x55, 0x4f, 0x7f, 0x71, 0xfb, 0x3f, 0x8b, 0xe0, 0x97, 0x42, 0x3b, 0xbb, - 0x6b, 0x43, 0x3c, 0x32, 0x43, 0x14, 0x6a, 0x61, 0x2c, 0xcc, 0xcd, 0xd0, 0xba, 0x9f, 0x63, 0x8c, 0x0a, 0x36, 0x2a, - 0x59, 0x51, 0xee, 0x8b, 0x70, 0x0c, 0x28, 0xb5, 0x56, 0x20, 0xb7, 0x23, 0xfb, 0xd5, 0x84, 0x81, 0x50, 0x0c, 0xb5, - 0x02, 0x2a, 0xc7, 0xbd, 0x16, 0x5a, 0xd4, 0xea, 0x4a, 0x8d, 0xa9, 0x1e, 0x49, 0x2f, 0xb9, 0xf4, 0x9c, 0xb4, 0xba, - 0xf3, 0x93, 0x71, 0x77, 0xde, 0x68, 0xa0, 0xdc, 0x10, 0xd6, 0x6c, 0x30, 0xbf, 0xc0, 0x1f, 0xc0, 0xa7, 0x67, 0x53, - 0x12, 0xae, 0x4d, 0xaf, 0xa3, 0xa7, 0xd7, 0x68, 0x64, 0x05, 0xea, 0x5a, 0x4d, 0xc7, 0xaa, 0x69, 0x51, 0x28, 0x9c, - 0xac, 0x12, 0xda, 0x31, 0x92, 0x25, 0x90, 0x0e, 0x45, 0x08, 0x39, 0x15, 0x68, 0x63, 0xaf, 0xd0, 0x27, 0x34, 0x97, - 0x3b, 0x16, 0x98, 0xa7, 0x92, 0x11, 0x1e, 0x60, 0x01, 0x9a, 0x96, 0x8e, 0xe0, 0x09, 0x9e, 0x35, 0xda, 0x92, 0xc8, - 0x9b, 0xed, 0x6e, 0xbd, 0xaf, 0xc7, 0x55, 0x5f, 0x78, 0xd6, 0x20, 0x93, 0x12, 0x4b, 0x45, 0xd6, 0x68, 0x14, 0xf5, - 0x68, 0xa7, 0xd9, 0xb7, 0xb5, 0xf8, 0xc3, 0xed, 0x6a, 0x5a, 0x86, 0x91, 0xaf, 0x95, 0x44, 0x65, 0x3e, 0x4b, 0x53, - 0x9a, 0x81, 0x0c, 0x25, 0x02, 0xb3, 0xa2, 0xa8, 0xe4, 0x3a, 0x08, 0x51, 0x4c, 0x49, 0x0a, 0x7c, 0x47, 0x9a, 0x5d, - 0x38, 0xc3, 0x1c, 0xc7, 0x92, 0x6b, 0x10, 0x42, 0xce, 0x4c, 0x42, 0x8b, 0x90, 0x1c, 0x28, 0x21, 0xcc, 0x92, 0x48, - 0x39, 0xa1, 0xfe, 0xe5, 0xee, 0x19, 0xbf, 0xd7, 0x24, 0x1b, 0xb0, 0x8b, 0x40, 0x56, 0x4b, 0x34, 0xdf, 0x0a, 0xc9, - 0x7b, 0x4f, 0xa0, 0x32, 0x38, 0xe2, 0x4b, 0xf6, 0xf7, 0x8c, 0x65, 0x54, 0x6a, 0xe0, 0xbb, 0xc6, 0xec, 0x4b, 0xea, - 0xea, 0x63, 0x62, 0x3b, 0x6f, 0x00, 0x91, 0x21, 0xf8, 0x76, 0x32, 0xb2, 0x56, 0xed, 0x72, 0xf7, 0xf4, 0xcd, 0x26, - 0x13, 0x78, 0xb9, 0xd4, 0xc6, 0xaf, 0xd4, 0x6c, 0x70, 0x58, 0x41, 0x9a, 0xe8, 0x1f, 0x81, 0x97, 0x48, 0x05, 0x29, - 0xf4, 0x52, 0xa0, 0xa2, 0xcb, 0xdd, 0xd3, 0xf7, 0x5e, 0x2a, 0x5d, 0x4b, 0x08, 0xdb, 0xd3, 0xf6, 0x38, 0xf1, 0x62, - 0x42, 0x91, 0x9a, 0x7b, 0xc9, 0xb8, 0xb8, 0x25, 0xbe, 0x83, 0x58, 0xbe, 0x04, 0xfb, 0x61, 0xc0, 0x2e, 0x48, 0xa2, - 0x31, 0x40, 0x12, 0x84, 0x93, 0x9a, 0x59, 0x46, 0x60, 0x01, 0xe4, 0x58, 0xe7, 0xb0, 0x12, 0xbe, 0x52, 0xfc, 0x10, - 0x4e, 0xe4, 0xa8, 0xa2, 0x50, 0xa2, 0xe3, 0xe5, 0x5a, 0x5e, 0x5a, 0x65, 0x8d, 0x7e, 0x0b, 0x96, 0x93, 0x79, 0x78, - 0xad, 0xbb, 0x2e, 0x0b, 0x9e, 0x99, 0x04, 0xb2, 0xcb, 0xdd, 0xd3, 0x57, 0x3a, 0x87, 0x6c, 0x1a, 0x1a, 0x6e, 0xbf, - 0x66, 0x61, 0x9e, 0xbe, 0xf2, 0xab, 0xb7, 0xb2, 0xf2, 0xe5, 0xee, 0xe9, 0x87, 0x4d, 0xd5, 0xa0, 0xbc, 0x98, 0x55, - 0x26, 0xbe, 0x84, 0x6f, 0x41, 0x93, 0x60, 0xa1, 0x45, 0x43, 0xc0, 0x0a, 0x2c, 0xc5, 0x51, 0x90, 0x17, 0xa5, 0x67, - 0xe4, 0x19, 0xce, 0x88, 0x8c, 0x02, 0xd5, 0x57, 0x4d, 0x2b, 0x79, 0x8c, 0xa7, 0xe7, 0x43, 0x3e, 0xa5, 0x5b, 0x42, - 0x43, 0xb7, 0xc8, 0x67, 0x13, 0x48, 0x9e, 0x91, 0xa0, 0x33, 0xbc, 0xd3, 0x42, 0xdd, 0xba, 0xf0, 0xca, 0x24, 0x91, - 0xf2, 0x9a, 0x64, 0xc1, 0x31, 0x69, 0xe1, 0x84, 0xb4, 0x70, 0x48, 0xf2, 0x41, 0x4b, 0x89, 0x87, 0x6e, 0x58, 0xf6, - 0xab, 0x84, 0x0c, 0xe4, 0x85, 0xe9, 0xdd, 0xaa, 0xc4, 0x6f, 0xd4, 0x0d, 0xa5, 0xeb, 0x51, 0x4a, 0xf4, 0x48, 0x92, - 0xc5, 0x0b, 0x8f, 0x63, 0x2e, 0x3b, 0x3e, 0x67, 0xd7, 0x09, 0xa4, 0x96, 0xc0, 0xac, 0xb0, 0x40, 0x41, 0x59, 0xb5, - 0xad, 0xab, 0x86, 0xbe, 0x5c, 0x27, 0x8e, 0x43, 0x1f, 0x18, 0x37, 0x0e, 0x75, 0x26, 0x4e, 0xbe, 0xde, 0xe4, 0xd1, - 0xde, 0x9e, 0xa7, 0x1a, 0xfd, 0x22, 0x3c, 0x6e, 0xde, 0x57, 0x81, 0xbb, 0x6f, 0x15, 0xaf, 0x88, 0x90, 0x84, 0xbf, - 0xd1, 0x48, 0x2e, 0x0a, 0x88, 0x42, 0x7b, 0x61, 0x1d, 0x83, 0x06, 0x78, 0xa9, 0xe9, 0xd5, 0xa7, 0xdf, 0x68, 0x94, - 0x41, 0xda, 0x3a, 0xb6, 0x6e, 0x71, 0x56, 0xcc, 0xbd, 0x32, 0xf9, 0xa7, 0xb5, 0x96, 0x31, 0x65, 0x40, 0x40, 0xcc, - 0xa6, 0x59, 0x66, 0x26, 0x63, 0x6d, 0x09, 0x06, 0xf5, 0xbe, 0xd2, 0x69, 0x0b, 0x58, 0xe6, 0x57, 0xe9, 0x4a, 0x86, - 0x9d, 0x75, 0x50, 0x60, 0x2a, 0x41, 0x50, 0x0a, 0x2a, 0x35, 0x0a, 0x4d, 0xde, 0x2f, 0xd6, 0xb3, 0x2e, 0x71, 0x8e, - 0xb4, 0x8f, 0x4b, 0x42, 0x21, 0x91, 0xd5, 0x29, 0x91, 0xf2, 0x82, 0x4c, 0xb7, 0x93, 0xfc, 0xa9, 0x45, 0xf2, 0x4f, - 0x09, 0xb5, 0xc8, 0x5f, 0x79, 0x38, 0x7c, 0xae, 0x5d, 0x0b, 0xb9, 0x79, 0x75, 0x36, 0x25, 0xe0, 0x43, 0xab, 0x63, - 0xb4, 0x16, 0x55, 0xdc, 0xc2, 0x50, 0xec, 0x1d, 0x22, 0xbd, 0x90, 0xd8, 0x84, 0x80, 0xbd, 0x2a, 0xa6, 0x06, 0x43, - 0x6f, 0x72, 0xe9, 0xd9, 0x1c, 0xf0, 0xf4, 0xc3, 0xfd, 0xe1, 0xd0, 0xb3, 0xe9, 0xfa, 0xce, 0xb5, 0xb2, 0x3f, 0x61, - 0xd6, 0xd6, 0xc6, 0xad, 0xe7, 0x82, 0xc2, 0xf8, 0x65, 0x18, 0xbb, 0xce, 0x7c, 0x56, 0x36, 0xa1, 0x91, 0x7f, 0x00, - 0x6d, 0xbb, 0x2d, 0x6b, 0x50, 0xab, 0x5b, 0xe0, 0x47, 0x2a, 0x07, 0x35, 0xcc, 0xb6, 0xb0, 0x8f, 0x53, 0x59, 0x81, - 0xa6, 0xd1, 0xe6, 0xd7, 0x4f, 0x0b, 0x4d, 0x26, 0x0a, 0x34, 0xb4, 0x00, 0xfe, 0xa7, 0x48, 0x1e, 0xe8, 0x46, 0xca, - 0x05, 0x40, 0xd0, 0x54, 0xe2, 0xa9, 0x42, 0x98, 0xeb, 0x56, 0xce, 0xf7, 0x17, 0x3b, 0x84, 0x4c, 0x2b, 0xe7, 0xe3, - 0xbb, 0x2a, 0xf7, 0x0a, 0xc8, 0x02, 0x05, 0x60, 0x3c, 0x96, 0x05, 0x2a, 0x7a, 0x79, 0x66, 0xaa, 0x4b, 0x03, 0xd2, - 0xaf, 0xf4, 0x6d, 0x2b, 0xb2, 0x29, 0xbd, 0x72, 0xea, 0xbd, 0x41, 0xc3, 0xca, 0xdb, 0x5d, 0x78, 0xfb, 0x42, 0x48, - 0x18, 0xe1, 0xf9, 0xbd, 0xac, 0x6d, 0xfa, 0x2d, 0x3e, 0xae, 0x26, 0xb0, 0xac, 0x2c, 0x8a, 0xcf, 0xd2, 0x9c, 0x66, - 0xe2, 0x29, 0x1d, 0xf1, 0x0c, 0x42, 0x16, 0x25, 0x4e, 0x50, 0xb1, 0x6b, 0xb9, 0xed, 0xe4, 0xfc, 0xac, 0x38, 0xc1, - 0xca, 0x04, 0xe5, 0xaf, 0x8f, 0x32, 0x66, 0x7d, 0xb9, 0xda, 0x6a, 0xba, 0xb7, 0xf7, 0xbe, 0x42, 0x93, 0x86, 0x52, - 0x42, 0x61, 0x31, 0x2d, 0xa5, 0xd2, 0xe8, 0x40, 0xee, 0xae, 0x57, 0xba, 0x00, 0x0c, 0xc3, 0xb0, 0x79, 0xcf, 0x0b, - 0x22, 0x8a, 0xf1, 0x2a, 0x8b, 0xd7, 0xae, 0x09, 0x66, 0x9b, 0x2d, 0xc0, 0xe1, 0xc1, 0xd0, 0x56, 0xbe, 0xa2, 0xbc, - 0x4a, 0x87, 0x2d, 0x61, 0x38, 0x03, 0x64, 0x79, 0xd2, 0x08, 0xb1, 0x28, 0x70, 0xa3, 0x51, 0xf2, 0x11, 0xf4, 0xca, - 0x18, 0xe7, 0x7e, 0x0c, 0x09, 0xb0, 0xb5, 0x2d, 0x8b, 0x10, 0x56, 0x79, 0x39, 0x56, 0x26, 0xc1, 0xe9, 0x8b, 0x4d, - 0x1e, 0x65, 0x43, 0xd4, 0x54, 0x4a, 0x1d, 0xa8, 0x91, 0xa1, 0xb2, 0x81, 0x3f, 0xf7, 0x98, 0x56, 0xdc, 0x4c, 0xd8, - 0x0c, 0x18, 0xf0, 0x4b, 0xe1, 0xa9, 0x58, 0x14, 0xc8, 0x0c, 0xee, 0xcf, 0xbc, 0xda, 0xd0, 0x5d, 0x2e, 0x9b, 0x61, - 0x8d, 0xb8, 0xd8, 0x46, 0x13, 0x97, 0x61, 0xbd, 0xb3, 0x8a, 0x97, 0xee, 0xaa, 0x1c, 0x6a, 0x61, 0xb8, 0x60, 0x95, - 0x47, 0x62, 0x4d, 0x7f, 0x57, 0xa5, 0x45, 0x97, 0x95, 0x40, 0x0d, 0xa3, 0x37, 0xce, 0x6b, 0xb9, 0x06, 0xb4, 0x00, - 0xfa, 0x5a, 0x3c, 0x17, 0xd6, 0x8a, 0x1a, 0x1f, 0xb6, 0x1c, 0xd3, 0x92, 0xfa, 0xef, 0x20, 0xd3, 0x65, 0x75, 0xcf, - 0xbf, 0x90, 0xb2, 0x90, 0xe1, 0xbc, 0xc6, 0xd8, 0x33, 0xc9, 0xd8, 0x11, 0xe8, 0x69, 0x26, 0xf5, 0xbb, 0xaf, 0x13, - 0x5e, 0x98, 0x96, 0x72, 0x9a, 0xc4, 0x3e, 0x94, 0xc1, 0x72, 0xeb, 0xf7, 0xca, 0x6a, 0x04, 0x8c, 0x40, 0x12, 0x10, - 0xd6, 0x9c, 0x3d, 0x43, 0x38, 0x6f, 0x34, 0xba, 0xf9, 0x09, 0xad, 0x5c, 0x24, 0x15, 0x8c, 0x0c, 0xe2, 0xb9, 0x40, - 0xf0, 0x35, 0x19, 0x0a, 0x11, 0x7f, 0x93, 0x9b, 0x9d, 0x83, 0xab, 0xfd, 0xf4, 0x9d, 0x67, 0x73, 0x35, 0xbb, 0x6e, - 0x19, 0x33, 0x85, 0xf9, 0x78, 0x55, 0xbc, 0xe5, 0xed, 0xfd, 0xf9, 0x1d, 0x00, 0xf7, 0x4e, 0x1b, 0x43, 0x2e, 0x1a, - 0xea, 0x0a, 0xc5, 0x12, 0xca, 0xdd, 0xd7, 0x45, 0x55, 0x5a, 0xa2, 0x3d, 0x58, 0x57, 0x54, 0xa6, 0xac, 0x20, 0x79, - 0x51, 0xe4, 0xb4, 0x8a, 0xee, 0xaf, 0xe4, 0x5f, 0x4a, 0xe1, 0xb2, 0xee, 0x6c, 0x3f, 0x9b, 0x12, 0x81, 0x2d, 0x42, - 0x7d, 0xbb, 0x2d, 0xf4, 0x51, 0x81, 0x09, 0xfb, 0x5a, 0x0b, 0xc5, 0x5f, 0x36, 0x09, 0x45, 0x9c, 0xe9, 0x2d, 0x2f, - 0x05, 0x62, 0xfb, 0x01, 0x02, 0x51, 0x3b, 0xd9, 0x8d, 0x4c, 0x04, 0x75, 0xa4, 0x26, 0x13, 0xeb, 0x4b, 0x4a, 0x32, - 0xcc, 0xf4, 0x6a, 0xf4, 0x3a, 0xcb, 0x25, 0x1b, 0xb4, 0xc0, 0x89, 0xe4, 0xba, 0xf0, 0xb3, 0xad, 0x7e, 0x5a, 0x9c, - 0x58, 0x39, 0x81, 0x3d, 0x56, 0x9a, 0x2c, 0xc8, 0x87, 0x14, 0x67, 0x4f, 0xe6, 0x64, 0x49, 0x9a, 0xd6, 0x14, 0xa4, - 0x09, 0x9c, 0xb0, 0x32, 0xca, 0x04, 0x10, 0x4b, 0x59, 0xa1, 0x0d, 0x48, 0x6f, 0x63, 0xf2, 0x9f, 0x31, 0x2f, 0x3f, - 0xad, 0x89, 0xd6, 0xe4, 0x8a, 0x52, 0x1f, 0x6a, 0xe9, 0x06, 0x1a, 0x02, 0xad, 0x1f, 0xee, 0x48, 0x13, 0xb4, 0x12, - 0xe5, 0xc8, 0x96, 0x43, 0xb8, 0x05, 0x2e, 0xb4, 0x9d, 0xf7, 0x2a, 0xc0, 0xbb, 0x41, 0x9a, 0x60, 0x6e, 0xd1, 0xf5, - 0x0b, 0x22, 0x6a, 0xac, 0x24, 0x26, 0xda, 0x52, 0xc2, 0xa1, 0x24, 0x53, 0x41, 0xb2, 0x41, 0xeb, 0x02, 0x14, 0xd0, - 0x6e, 0x72, 0x92, 0x55, 0x26, 0x70, 0xd2, 0x68, 0xa0, 0xd0, 0x8c, 0x1a, 0x0f, 0x58, 0x23, 0xb9, 0xc0, 0x14, 0x27, - 0xca, 0x30, 0x39, 0xdb, 0xdb, 0xf3, 0xc2, 0x6a, 0xdc, 0x41, 0x72, 0x81, 0x30, 0x5f, 0x2e, 0x3d, 0x09, 0x56, 0x88, - 0x96, 0xcb, 0xd0, 0x06, 0x4b, 0xbe, 0x86, 0x66, 0xd3, 0xbe, 0x20, 0x53, 0x29, 0x00, 0xa7, 0x00, 0x61, 0x83, 0x78, - 0xa1, 0x76, 0xee, 0x85, 0xe0, 0x8c, 0x6a, 0x64, 0x83, 0xa4, 0xd1, 0xbe, 0xb0, 0x18, 0xd7, 0x20, 0xb9, 0x20, 0x61, - 0xc1, 0xf7, 0xf6, 0x76, 0x72, 0x2d, 0x22, 0x7f, 0x02, 0x51, 0xf6, 0x93, 0x94, 0x2c, 0xaa, 0x43, 0x7b, 0x35, 0x56, - 0x9d, 0x01, 0x25, 0x45, 0xe9, 0x65, 0x35, 0xf5, 0x6a, 0x49, 0x10, 0x65, 0x25, 0xac, 0x63, 0xc1, 0x7d, 0xb0, 0xec, - 0x4b, 0x32, 0x7f, 0x26, 0xca, 0x24, 0xeb, 0x5f, 0x36, 0xa6, 0x56, 0xfb, 0xbe, 0x1f, 0x66, 0x63, 0x19, 0xc9, 0x30, - 0x51, 0x58, 0x49, 0xfc, 0x07, 0x1a, 0x4c, 0x6b, 0xe0, 0x41, 0x39, 0xd6, 0x05, 0x51, 0xe0, 0x1b, 0xd5, 0xc6, 0x9c, - 0x26, 0xf9, 0x69, 0xa3, 0x97, 0x41, 0x41, 0xf2, 0xd5, 0x6f, 0x85, 0xe4, 0x50, 0x43, 0xa2, 0xc8, 0x63, 0x05, 0x67, - 0x5b, 0x70, 0xf1, 0x93, 0x58, 0xc1, 0xd9, 0x76, 0xdc, 0x1a, 0x4c, 0xfd, 0xbc, 0x0d, 0x3e, 0x8b, 0x37, 0x28, 0x40, - 0xab, 0x02, 0x0b, 0xca, 0xa3, 0x55, 0xdd, 0x4b, 0xb1, 0x52, 0x10, 0xa6, 0x82, 0x78, 0xac, 0xbe, 0x01, 0x2a, 0x6d, - 0xd4, 0x32, 0x7c, 0x59, 0x30, 0x45, 0x96, 0x4b, 0xa0, 0x9e, 0xb9, 0x02, 0xe4, 0xa4, 0x7d, 0xed, 0xd3, 0xbd, 0x3d, - 0xb0, 0x0d, 0x40, 0x89, 0xf3, 0x87, 0xe1, 0x54, 0xcc, 0x32, 0x50, 0xa5, 0x72, 0xf3, 0x1b, 0x8a, 0xe1, 0x1c, 0x88, - 0x2c, 0x83, 0x1f, 0x50, 0x30, 0x0d, 0xf3, 0x9c, 0xcd, 0x55, 0x99, 0xfe, 0x8d, 0x39, 0x31, 0xa4, 0x9c, 0x2b, 0x9d, - 0x30, 0x43, 0xdd, 0x4c, 0xd3, 0x69, 0x1d, 0x6d, 0xcf, 0xe7, 0x34, 0x15, 0x2f, 0x59, 0x2e, 0x68, 0x0a, 0xd3, 0xaf, - 0x28, 0x0e, 0x66, 0x94, 0x23, 0xd8, 0xb0, 0xb5, 0x56, 0x61, 0x14, 0xdd, 0xdb, 0x44, 0xd4, 0x75, 0xa0, 0x38, 0x4c, - 0xa3, 0x44, 0x0d, 0x62, 0xa7, 0x33, 0x9a, 0x14, 0xce, 0xb2, 0xa6, 0x9d, 0x4e, 0x53, 0x29, 0x1b, 0x92, 0xbb, 0x7b, - 0x8c, 0x18, 0x49, 0x60, 0xa4, 0xe7, 0xbd, 0x5a, 0x0b, 0x04, 0xbc, 0xb7, 0x2c, 0x82, 0x3d, 0x13, 0x2c, 0x2c, 0x8e, - 0xea, 0xd7, 0xe1, 0x2c, 0x05, 0xc9, 0xc6, 0x43, 0x6d, 0x9b, 0x84, 0x83, 0xa4, 0x93, 0x47, 0xdb, 0x2d, 0xab, 0x57, - 0x46, 0x72, 0x18, 0x69, 0xc1, 0x1e, 0xca, 0x98, 0xd1, 0xc2, 0x90, 0x17, 0x32, 0x5b, 0xf1, 0x52, 0x90, 0x9f, 0xe0, - 0xd4, 0xd0, 0x0b, 0x31, 0x49, 0x56, 0x0e, 0xc7, 0x74, 0x2f, 0x4b, 0xed, 0xff, 0x52, 0x78, 0xaf, 0xf1, 0x0b, 0x08, - 0xeb, 0x7e, 0x5d, 0x55, 0x5f, 0x0f, 0xe7, 0x7e, 0x5d, 0x21, 0xe8, 0xeb, 0x60, 0xad, 0x9e, 0x15, 0xc6, 0xed, 0xf8, - 0xc7, 0x7e, 0xcb, 0x35, 0xda, 0xd2, 0xb7, 0x2a, 0x88, 0xa4, 0x12, 0x2d, 0xe5, 0x7e, 0xc0, 0x55, 0x9a, 0x1a, 0xa4, - 0xcb, 0xd5, 0x2d, 0x24, 0xaa, 0x13, 0x0c, 0x95, 0x0e, 0xbf, 0x6d, 0x79, 0xb4, 0x8c, 0xc9, 0x94, 0x9d, 0xf1, 0x36, - 0xcc, 0xc4, 0x2e, 0xec, 0x32, 0xbe, 0x76, 0x12, 0x2f, 0x26, 0xe0, 0x41, 0x7b, 0xd8, 0x10, 0x96, 0xb1, 0x9d, 0xab, - 0x93, 0x40, 0x76, 0xff, 0x84, 0x1b, 0xdd, 0xad, 0x6e, 0x65, 0x7c, 0x00, 0xfb, 0x1f, 0xe1, 0xd8, 0x1c, 0x8f, 0xa3, - 0x9a, 0x03, 0xd3, 0x60, 0x51, 0x94, 0x4e, 0x01, 0xae, 0x94, 0xb7, 0x14, 0x61, 0x5e, 0xc8, 0xf0, 0xf6, 0x37, 0xf8, - 0x7b, 0xcd, 0x12, 0x47, 0x25, 0xc7, 0x79, 0xfe, 0x50, 0x8e, 0xa8, 0xc0, 0x2f, 0xa3, 0xf7, 0x40, 0xc7, 0x92, 0x42, - 0x0b, 0x43, 0x45, 0xcf, 0xb8, 0x9e, 0xc8, 0xd6, 0xac, 0x54, 0x4c, 0xcb, 0x8c, 0x1a, 0x39, 0xcc, 0x86, 0x34, 0x4e, - 0x63, 0x65, 0x8b, 0x72, 0x57, 0xd5, 0xc6, 0x45, 0x5b, 0xb0, 0x58, 0x05, 0x16, 0x97, 0x4b, 0xaf, 0x8e, 0x6a, 0xc2, - 0xac, 0x38, 0x06, 0xc2, 0xcc, 0x4a, 0xa8, 0xa8, 0x69, 0xd6, 0xaa, 0x8d, 0x87, 0x56, 0xf3, 0x89, 0x8c, 0x6e, 0x5e, - 0x83, 0xc3, 0x76, 0x21, 0xa8, 0xe6, 0xb6, 0x4f, 0x01, 0xab, 0xd9, 0x95, 0x03, 0x59, 0x18, 0xfa, 0xb6, 0xcc, 0x94, - 0xad, 0x52, 0x5a, 0x37, 0xe0, 0x17, 0xdd, 0x93, 0x2b, 0xab, 0x51, 0xb7, 0xfe, 0xde, 0xca, 0x35, 0x7a, 0xc6, 0xb7, - 0xe5, 0x1a, 0xd5, 0xb4, 0xdd, 0x9d, 0x16, 0xba, 0x3f, 0x2b, 0x55, 0x8d, 0xb5, 0xb9, 0xca, 0x6f, 0x18, 0xae, 0x0d, - 0xb4, 0xa9, 0xd0, 0x6c, 0xb8, 0xca, 0x59, 0x51, 0x8c, 0xca, 0xb3, 0x04, 0x32, 0x75, 0x67, 0xa4, 0xe8, 0x5f, 0x5b, - 0x8d, 0xf2, 0x40, 0xae, 0xf7, 0x0d, 0x19, 0x27, 0xfc, 0x3a, 0x4c, 0xde, 0xc3, 0x78, 0xd5, 0xcb, 0x17, 0x77, 0x51, - 0x16, 0x0a, 0xaa, 0xb9, 0x4b, 0x05, 0xc3, 0x37, 0x16, 0x0c, 0xdf, 0x28, 0x3e, 0x5d, 0xb5, 0xc7, 0x8b, 0x97, 0x65, - 0x07, 0xc1, 0xa8, 0x30, 0x2c, 0x63, 0x22, 0x36, 0x8f, 0xb1, 0xca, 0xc2, 0x26, 0x25, 0x0b, 0x9b, 0x08, 0x6f, 0xb5, - 0x2b, 0xcf, 0xfb, 0x7e, 0x73, 0x2f, 0xeb, 0x9c, 0xed, 0xfb, 0x6a, 0xe3, 0x7f, 0x1f, 0xdc, 0xdb, 0xc6, 0xe2, 0x72, - 0x07, 0xfe, 0x81, 0x4c, 0x56, 0x51, 0x20, 0x3f, 0x85, 0xa4, 0x03, 0x41, 0x7a, 0xd6, 0x91, 0x83, 0x4a, 0x4e, 0x99, - 0x3c, 0x20, 0x6f, 0x38, 0xcb, 0x05, 0x9f, 0xe8, 0x3e, 0x73, 0x7d, 0xce, 0x48, 0xbe, 0x04, 0x57, 0xb4, 0x8c, 0xb5, - 0x07, 0xf5, 0x93, 0x5c, 0x8b, 0x8f, 0x2c, 0x8d, 0x82, 0x1c, 0x6b, 0x29, 0x92, 0x07, 0x59, 0x41, 0x4c, 0xae, 0xf1, - 0xfa, 0x3b, 0x3c, 0x62, 0x29, 0xcb, 0x63, 0x9a, 0x79, 0x1c, 0x2d, 0xb6, 0x0d, 0xc6, 0x21, 0x20, 0xa3, 0x06, 0xc3, - 0x5f, 0x56, 0x47, 0xfe, 0x7c, 0xe8, 0x0d, 0xfc, 0x40, 0x13, 0x2a, 0x62, 0x1e, 0x41, 0x5a, 0x8a, 0x1f, 0x95, 0x47, - 0x9a, 0xf6, 0xf6, 0x76, 0x3c, 0x57, 0xba, 0x25, 0xe0, 0xf0, 0xb7, 0xfd, 0x06, 0xf5, 0x17, 0x70, 0x3a, 0xa7, 0x1a, - 0x9a, 0xa2, 0x05, 0x5d, 0x3d, 0xc8, 0x22, 0xfc, 0x8f, 0xf4, 0x0e, 0xa7, 0xa8, 0x28, 0x02, 0x05, 0xb5, 0x3b, 0x62, - 0x34, 0x89, 0x5c, 0xfc, 0x91, 0xde, 0x05, 0xe5, 0x79, 0x71, 0x79, 0xbc, 0x59, 0x2e, 0xa0, 0xcb, 0x6f, 0x52, 0x17, - 0x57, 0x83, 0x04, 0x8b, 0x02, 0xf3, 0x8c, 0x8d, 0x81, 0x38, 0xff, 0x46, 0xef, 0x02, 0xd5, 0x1f, 0xb3, 0x4e, 0xeb, - 0xa1, 0x85, 0x41, 0xbd, 0x6f, 0x15, 0xdb, 0xcb, 0xa0, 0x0d, 0x8a, 0x81, 0x6c, 0x7b, 0x41, 0x6a, 0xf5, 0x2a, 0xf3, - 0x10, 0xa1, 0xe2, 0xa1, 0x53, 0xc1, 0xdf, 0xd9, 0xa2, 0x4d, 0xd4, 0x32, 0x5f, 0x57, 0x1a, 0x51, 0x68, 0x50, 0x65, - 0x7a, 0x5c, 0x7a, 0xa9, 0xd9, 0x75, 0xfa, 0x08, 0x82, 0xe5, 0x08, 0xfb, 0x4e, 0xe8, 0x4e, 0x83, 0x2f, 0x55, 0x42, - 0x48, 0x15, 0x49, 0x7a, 0x55, 0xb5, 0x73, 0x2e, 0x3d, 0xc0, 0x3b, 0x24, 0xb4, 0x84, 0xf2, 0x40, 0x66, 0x61, 0xb2, - 0x45, 0x7f, 0x10, 0xc4, 0x5b, 0x98, 0x29, 0x04, 0xa9, 0x8d, 0x45, 0x51, 0x00, 0x15, 0x6a, 0xfa, 0x52, 0x09, 0x80, - 0x70, 0x86, 0x7d, 0x4d, 0x6a, 0x66, 0x52, 0x6a, 0xfa, 0x16, 0xc6, 0xb7, 0x48, 0x49, 0x2a, 0x91, 0x21, 0x95, 0x48, - 0x29, 0xf4, 0xf4, 0xe2, 0x6a, 0x12, 0xb2, 0x17, 0xb4, 0x3c, 0x3f, 0xa7, 0xd6, 0x3c, 0xab, 0x81, 0xe5, 0xc9, 0x7e, - 0x50, 0x11, 0xc0, 0x94, 0xa8, 0xaa, 0x50, 0x94, 0xc7, 0xb2, 0x4d, 0x7a, 0xab, 0xc7, 0x7d, 0x33, 0x2d, 0x62, 0x50, - 0xe2, 0xc5, 0x68, 0x91, 0x7a, 0x31, 0xce, 0x20, 0x1d, 0x91, 0x17, 0x25, 0xfc, 0xd4, 0x5e, 0x8d, 0x5a, 0xb2, 0xf2, - 0xe6, 0x33, 0x7e, 0xa0, 0xcc, 0x0b, 0x48, 0xd1, 0xc4, 0xa9, 0xe1, 0x29, 0xa9, 0x27, 0x0f, 0xdb, 0x59, 0xcb, 0xf6, - 0xb5, 0x4e, 0xd0, 0xd1, 0x80, 0xfd, 0x20, 0xbc, 0x85, 0x35, 0x0b, 0xfb, 0x34, 0xb7, 0x3e, 0xf3, 0xa7, 0x83, 0x7d, - 0x55, 0x0e, 0xa9, 0x97, 0x93, 0x15, 0x89, 0x73, 0x7f, 0xaa, 0xe5, 0xcf, 0x33, 0x9a, 0xdd, 0x9d, 0x53, 0x48, 0x75, - 0xe6, 0x70, 0xda, 0xb7, 0x5a, 0x86, 0x2a, 0x4d, 0xbd, 0x9f, 0x49, 0x65, 0xa5, 0xa8, 0x9f, 0x02, 0x5c, 0x3d, 0x23, - 0x58, 0xc8, 0x68, 0xa3, 0xe5, 0x88, 0x51, 0xbb, 0x85, 0x6e, 0x3d, 0x3d, 0x49, 0xbb, 0x0c, 0xfc, 0x6b, 0x15, 0xa6, - 0x75, 0xb0, 0x00, 0x73, 0xfb, 0x44, 0xea, 0x20, 0xbf, 0x58, 0xf5, 0xca, 0x40, 0x11, 0x84, 0xef, 0xb2, 0xed, 0x53, - 0xdd, 0x94, 0x34, 0xbb, 0x7d, 0xaa, 0xb5, 0xa0, 0x9f, 0x4c, 0xf8, 0xc1, 0x7a, 0x9c, 0xf2, 0xf8, 0x32, 0x2b, 0x0a, - 0x54, 0x00, 0x78, 0x7f, 0xed, 0x7a, 0xde, 0x5f, 0x75, 0xca, 0xa0, 0x0f, 0xb1, 0xd8, 0xf3, 0x84, 0x1b, 0x26, 0x5e, - 0x8d, 0xff, 0xd7, 0xb5, 0xf1, 0xff, 0x6a, 0x9d, 0x39, 0x05, 0xd3, 0x68, 0x9c, 0xd2, 0xc8, 0xb0, 0x4e, 0xa4, 0x08, - 0x50, 0xea, 0x6d, 0xa9, 0x20, 0x6f, 0xae, 0x02, 0xd0, 0xb8, 0x16, 0x23, 0x9e, 0x8a, 0xe6, 0x28, 0x9c, 0xb0, 0xe4, - 0x2e, 0x98, 0xb1, 0xe6, 0x84, 0xa7, 0x3c, 0x9f, 0x86, 0x43, 0x8a, 0xf3, 0xbb, 0x5c, 0xd0, 0x49, 0x73, 0xc6, 0xf0, - 0x0b, 0x9a, 0xcc, 0xa9, 0x60, 0xc3, 0x10, 0xbb, 0xa7, 0x19, 0x0b, 0x13, 0xe7, 0x75, 0x98, 0x65, 0xfc, 0xc6, 0xc5, - 0xef, 0xf8, 0x35, 0x17, 0x1c, 0xbf, 0xb9, 0xbd, 0x1b, 0xd3, 0x14, 0x7f, 0xb8, 0x9e, 0xa5, 0x62, 0x86, 0xf3, 0x30, - 0xcd, 0x9b, 0x39, 0xcd, 0xd8, 0xa8, 0x3b, 0xe4, 0x09, 0xcf, 0x9a, 0x90, 0xb1, 0x3d, 0xa1, 0x41, 0xc2, 0xc6, 0xb1, - 0x70, 0xa2, 0x30, 0xfb, 0xd8, 0x6d, 0x36, 0xa7, 0x19, 0x9b, 0x84, 0xd9, 0x5d, 0x53, 0xd6, 0x08, 0x3e, 0x6f, 0x1d, - 0x84, 0x4f, 0x46, 0x87, 0x5d, 0x91, 0x85, 0x69, 0xce, 0x60, 0x99, 0x82, 0x30, 0x49, 0x9c, 0x83, 0xa3, 0xd6, 0x24, - 0xdf, 0x51, 0x81, 0xbc, 0x30, 0x15, 0xc5, 0x15, 0x7e, 0x03, 0x70, 0xfb, 0xd7, 0x22, 0xc5, 0xd7, 0x33, 0x21, 0x78, - 0xba, 0x18, 0xce, 0xb2, 0x9c, 0x67, 0xc1, 0x94, 0xb3, 0x54, 0xd0, 0xac, 0x7b, 0xcd, 0xb3, 0x88, 0x66, 0xcd, 0x2c, - 0x8c, 0xd8, 0x2c, 0x0f, 0x0e, 0xa7, 0xb7, 0x5d, 0xd0, 0x2c, 0xc6, 0x19, 0x9f, 0xa5, 0x91, 0x1e, 0x8b, 0xa5, 0x31, - 0xcd, 0x98, 0xb0, 0x5f, 0xc8, 0x4b, 0x4c, 0x82, 0x84, 0xa5, 0x34, 0xcc, 0x9a, 0x63, 0x68, 0x0c, 0x66, 0x51, 0x2b, - 0xa2, 0x63, 0x9c, 0x8d, 0xaf, 0x43, 0xaf, 0xdd, 0x79, 0x8c, 0xcd, 0x5f, 0xff, 0x08, 0x39, 0xad, 0xcd, 0xc5, 0xed, - 0x56, 0xeb, 0x4f, 0xa8, 0xbb, 0x32, 0x8a, 0x04, 0x28, 0x68, 0x4f, 0x6f, 0x9d, 0x9c, 0x43, 0x46, 0xdb, 0xa6, 0x96, - 0xdd, 0x69, 0x18, 0x41, 0x3e, 0x70, 0xd0, 0x99, 0xde, 0x16, 0x30, 0xbb, 0x40, 0xa5, 0x98, 0xea, 0x49, 0xea, 0xa7, - 0xc5, 0x6f, 0x85, 0xf8, 0x78, 0x33, 0xc4, 0x1d, 0x03, 0x71, 0x85, 0xf5, 0x66, 0x34, 0xcb, 0x64, 0x6c, 0x35, 0x68, - 0xe7, 0x0a, 0x90, 0x98, 0xcf, 0x69, 0x66, 0xe0, 0x90, 0x0f, 0xbf, 0x19, 0x8c, 0xce, 0x66, 0x30, 0x8e, 0x3f, 0x05, - 0x46, 0x96, 0x46, 0x8b, 0xfa, 0xba, 0xb6, 0x33, 0x3a, 0xe9, 0xc6, 0x14, 0xe8, 0x29, 0xe8, 0xc0, 0xef, 0x1b, 0x16, - 0x89, 0x58, 0xfd, 0x94, 0xe4, 0x7c, 0xa3, 0xde, 0x1d, 0xb5, 0x5a, 0xea, 0x39, 0x67, 0xbf, 0xd0, 0xa0, 0xed, 0x43, - 0x85, 0xe2, 0x0a, 0xff, 0xad, 0x3c, 0xcb, 0x5b, 0xe7, 0x9e, 0xf8, 0x1b, 0xfb, 0x90, 0xaf, 0x95, 0xa2, 0x58, 0x1d, - 0x89, 0xc6, 0x99, 0x91, 0x95, 0x4a, 0xf8, 0x80, 0xdb, 0x4e, 0x72, 0x47, 0xc2, 0x7a, 0xe5, 0x21, 0x4e, 0xd6, 0xff, - 0x46, 0xe5, 0x5d, 0x04, 0x10, 0xe9, 0xb0, 0x52, 0x0d, 0x79, 0x37, 0xeb, 0x91, 0x56, 0x37, 0x6b, 0x36, 0x91, 0xc7, - 0x49, 0x3a, 0xc8, 0x74, 0x72, 0x9e, 0xc7, 0xfa, 0x5c, 0x1a, 0xdb, 0x39, 0x0a, 0x38, 0x9c, 0x34, 0x5d, 0x2e, 0xab, - 0x30, 0x00, 0x93, 0xa7, 0x35, 0xfe, 0x26, 0x74, 0x05, 0x9c, 0x5b, 0x9c, 0x9c, 0x9b, 0xab, 0x5d, 0x52, 0xc3, 0x2b, - 0x12, 0x3e, 0x94, 0x98, 0xf3, 0xa7, 0xa1, 0x88, 0xc1, 0x4b, 0x51, 0x8a, 0x9f, 0x2a, 0x85, 0xc9, 0xdd, 0x77, 0x51, - 0x3f, 0x2d, 0xf3, 0xdb, 0x20, 0x8f, 0x2f, 0x2d, 0xa0, 0x97, 0xef, 0x05, 0x81, 0x1e, 0xf1, 0x57, 0x44, 0xd9, 0x74, - 0xc6, 0xa2, 0x1b, 0x3d, 0xd4, 0xa2, 0xa3, 0xa9, 0x60, 0x32, 0x73, 0xdb, 0x44, 0x1c, 0xe2, 0x30, 0xbf, 0x1c, 0xaa, - 0xa3, 0x92, 0x79, 0x75, 0x30, 0x20, 0x94, 0xd0, 0x2b, 0x23, 0x8d, 0x66, 0xd2, 0x1e, 0xfd, 0xab, 0xd8, 0x6a, 0x9f, - 0xa4, 0xf7, 0xd9, 0x27, 0xe5, 0xc4, 0x73, 0x3e, 0xcb, 0x86, 0x10, 0x8e, 0xd4, 0x52, 0x6f, 0xdd, 0x71, 0xe3, 0x4a, - 0x15, 0xc3, 0xc5, 0xc2, 0xca, 0x03, 0x15, 0x98, 0xd9, 0xd7, 0x4a, 0x50, 0x19, 0xf2, 0x52, 0xc7, 0x35, 0xb4, 0x88, - 0x33, 0x53, 0x02, 0x99, 0x1d, 0xc9, 0x94, 0x46, 0x2f, 0x23, 0xbd, 0xcc, 0x9f, 0xa5, 0xec, 0xe7, 0x19, 0xbd, 0x64, - 0xa0, 0x6b, 0x32, 0x9f, 0x45, 0x32, 0xd6, 0x04, 0xb2, 0xaf, 0xd9, 0x86, 0xe0, 0x05, 0x8b, 0xd4, 0xc2, 0x64, 0xf2, - 0xa5, 0xce, 0x6d, 0x72, 0x9b, 0x2e, 0xf8, 0x8b, 0x41, 0x3b, 0x60, 0x38, 0xe2, 0x93, 0x90, 0xa5, 0x81, 0x74, 0xf9, - 0x96, 0x9d, 0x05, 0x50, 0x1b, 0xb3, 0x28, 0xc8, 0xf4, 0xf2, 0xb4, 0x91, 0xff, 0x13, 0x67, 0xa9, 0x6c, 0x5a, 0x74, - 0xb9, 0x44, 0xa8, 0x42, 0x1f, 0x31, 0x08, 0x3e, 0x55, 0x72, 0x8d, 0x23, 0x6c, 0xbf, 0x2e, 0x4f, 0x9d, 0xd7, 0x56, - 0xa0, 0xb5, 0xb2, 0x50, 0xca, 0x08, 0xe0, 0xab, 0xa5, 0x39, 0xcf, 0x84, 0xe7, 0xc5, 0x38, 0x41, 0xa4, 0x17, 0x4b, - 0x67, 0xd7, 0x49, 0x22, 0xff, 0xeb, 0x37, 0xdb, 0x41, 0xbb, 0x34, 0xdf, 0x6b, 0x87, 0x81, 0x55, 0x72, 0x94, 0x3e, - 0x50, 0x2a, 0xa7, 0x51, 0xfe, 0x56, 0x53, 0xad, 0x9e, 0xcb, 0xe9, 0x62, 0xbd, 0xdd, 0x94, 0xa8, 0xf2, 0x6a, 0x40, - 0xc8, 0x60, 0xd1, 0x96, 0xa1, 0x50, 0x51, 0xcd, 0xbb, 0x54, 0x25, 0xaf, 0x94, 0x88, 0xbe, 0xdc, 0x5d, 0xa4, 0x7a, - 0xc4, 0xe2, 0x8a, 0x19, 0x27, 0x53, 0x9d, 0xe4, 0x0a, 0x8d, 0x11, 0x4b, 0x0f, 0xdd, 0x54, 0x4d, 0xc1, 0x72, 0x47, - 0xd2, 0x8d, 0x74, 0xeb, 0xab, 0x47, 0xaa, 0x14, 0x84, 0xcd, 0x55, 0x64, 0xaa, 0xde, 0x26, 0xc0, 0xc0, 0x6c, 0xcd, - 0x85, 0x99, 0x02, 0x68, 0x63, 0x23, 0x0a, 0xe7, 0x68, 0xae, 0x76, 0x17, 0xdf, 0x8b, 0x62, 0xdf, 0xaa, 0x2a, 0x7f, - 0xb3, 0x08, 0xfe, 0x07, 0x09, 0xb8, 0x50, 0x4a, 0x69, 0xe0, 0xbe, 0x7d, 0x73, 0xfe, 0xde, 0xc5, 0x70, 0x3b, 0x17, - 0xcd, 0xf2, 0x60, 0xe1, 0xea, 0xd4, 0xb8, 0x26, 0x84, 0x59, 0xdd, 0xc0, 0x0d, 0xa7, 0x70, 0xd2, 0x58, 0xf2, 0x82, - 0xfd, 0xdb, 0xe6, 0xcd, 0xcd, 0x4d, 0x13, 0x0e, 0x42, 0x35, 0x67, 0x59, 0x42, 0xd3, 0x21, 0x8f, 0x68, 0xe4, 0x16, - 0x05, 0xf2, 0x45, 0x4c, 0xd3, 0xf2, 0xfe, 0x1e, 0x9e, 0x50, 0x3f, 0xe1, 0x63, 0x75, 0x88, 0x73, 0xd5, 0xaa, 0x1e, - 0x5e, 0x9d, 0xc8, 0x7b, 0xa9, 0x7a, 0x27, 0x42, 0xdd, 0x08, 0x26, 0x32, 0xf8, 0xd9, 0x83, 0x98, 0xcb, 0xc9, 0xbe, - 0x88, 0xe5, 0xc3, 0x39, 0xec, 0x30, 0xf9, 0xb4, 0xbb, 0x58, 0xa3, 0xbe, 0x3e, 0x74, 0x11, 0xf7, 0xd4, 0x9c, 0x73, - 0x59, 0xeb, 0x2a, 0x18, 0x5e, 0x5d, 0x15, 0x27, 0xfb, 0xd0, 0xd7, 0xbe, 0xe9, 0xf7, 0x9a, 0x47, 0x77, 0xa6, 0x7d, - 0x49, 0x91, 0x70, 0x3f, 0x51, 0x4a, 0x7a, 0xd0, 0x05, 0x8c, 0x1b, 0xf5, 0x00, 0x2b, 0x40, 0x91, 0xd0, 0x3a, 0x2a, - 0x4b, 0xe4, 0x16, 0x57, 0x45, 0xdb, 0x20, 0x50, 0x15, 0xab, 0x8d, 0xa2, 0xdc, 0xaf, 0x15, 0x41, 0x18, 0x90, 0x22, - 0x1b, 0xba, 0x2b, 0x04, 0xff, 0x4b, 0xc8, 0x4e, 0xf6, 0x15, 0x1e, 0xae, 0xec, 0xcb, 0x50, 0xd4, 0x35, 0x05, 0x25, - 0xb6, 0x06, 0xa9, 0xc0, 0x6f, 0x04, 0x7e, 0x73, 0x25, 0xab, 0x1a, 0xe9, 0x05, 0x6a, 0x15, 0x48, 0xf9, 0x96, 0x51, - 0x53, 0x86, 0x3c, 0x49, 0xc2, 0x69, 0x4e, 0x03, 0xf3, 0x43, 0x0b, 0x32, 0x90, 0x87, 0xeb, 0x9a, 0x83, 0xce, 0xc7, - 0x39, 0x03, 0xfd, 0x62, 0x5d, 0xad, 0x99, 0x87, 0x99, 0xd7, 0x6c, 0x0e, 0x9b, 0xd7, 0x63, 0x54, 0x88, 0x78, 0x61, - 0x8b, 0xc1, 0x47, 0xad, 0x56, 0x17, 0x92, 0x27, 0x9b, 0x61, 0xc2, 0xc6, 0x69, 0x90, 0xd0, 0x91, 0x28, 0x04, 0x9c, - 0x6a, 0x5b, 0x18, 0xbd, 0xc3, 0xef, 0x1c, 0x65, 0x74, 0xe2, 0xf8, 0xf0, 0xef, 0xfd, 0x03, 0x17, 0x22, 0x0a, 0x52, - 0x11, 0x37, 0x65, 0x92, 0x2e, 0x1c, 0x31, 0x10, 0x71, 0xed, 0x79, 0x61, 0x0d, 0x34, 0xa4, 0xa0, 0x93, 0x15, 0x22, - 0x73, 0x44, 0x8c, 0x45, 0x66, 0xd7, 0x4b, 0xd1, 0x62, 0x6d, 0x06, 0xeb, 0xaa, 0xc1, 0x01, 0x2a, 0x72, 0xa9, 0x49, - 0xaf, 0x57, 0x36, 0xfa, 0x55, 0xfd, 0x69, 0x0d, 0x7d, 0x96, 0x26, 0x58, 0x28, 0x4f, 0xf4, 0x42, 0xb5, 0x78, 0x08, - 0x32, 0x6b, 0x3a, 0x2a, 0xb6, 0x5b, 0xa0, 0x82, 0xa5, 0xd3, 0x99, 0x18, 0x48, 0x2f, 0x78, 0x06, 0xe7, 0x29, 0x2e, - 0xb0, 0x55, 0x02, 0x38, 0xb8, 0x58, 0x28, 0x60, 0x86, 0x61, 0x32, 0xf4, 0x00, 0x22, 0xa7, 0xe9, 0x1c, 0x67, 0x74, - 0x82, 0xba, 0x13, 0x96, 0x36, 0xd5, 0xbb, 0x23, 0x4b, 0x8f, 0xf1, 0x1f, 0xc3, 0x53, 0xe1, 0xcb, 0xde, 0xb0, 0x4c, - 0x76, 0xdd, 0x80, 0xcb, 0xab, 0x8b, 0xa2, 0xe8, 0x66, 0xc2, 0x1b, 0xbc, 0xf2, 0xd0, 0x05, 0xfe, 0xca, 0xba, 0xce, - 0xc5, 0x35, 0x5b, 0xc5, 0xc5, 0x1d, 0xb4, 0xa5, 0x8a, 0xbd, 0x17, 0x64, 0xb5, 0xaf, 0x08, 0x54, 0x7c, 0xea, 0xb9, - 0x34, 0x9f, 0x36, 0x15, 0xb3, 0x6b, 0x4a, 0x92, 0x75, 0xa1, 0x29, 0xd2, 0xae, 0xdd, 0xbf, 0x8a, 0x85, 0xe4, 0x63, - 0xfa, 0x4c, 0x87, 0xf2, 0x3e, 0x5c, 0x94, 0x67, 0x80, 0xf4, 0xb3, 0x7d, 0xea, 0x07, 0xd5, 0xf8, 0xc9, 0xd5, 0x69, - 0x9d, 0x29, 0x02, 0x23, 0x2b, 0xef, 0xbc, 0x0b, 0x93, 0x04, 0x06, 0xbc, 0x32, 0xfa, 0x8e, 0x7d, 0x49, 0xc8, 0x40, - 0x5c, 0x78, 0xa8, 0xd0, 0xfb, 0xf4, 0xa9, 0xd4, 0x41, 0xad, 0x8b, 0xf6, 0x76, 0x84, 0x89, 0x2e, 0x29, 0x71, 0xcd, - 0x20, 0x3e, 0x5e, 0xcb, 0xa3, 0xee, 0x56, 0xbc, 0x4b, 0x69, 0xb0, 0x8e, 0x9c, 0x10, 0x71, 0xb3, 0x34, 0x72, 0x9d, - 0xbf, 0x0c, 0x13, 0x36, 0xfc, 0x48, 0xdc, 0xdd, 0x85, 0x87, 0xd6, 0x8f, 0x49, 0x4a, 0xae, 0x60, 0x38, 0x3c, 0xaa, - 0x7b, 0xde, 0x33, 0xdf, 0x62, 0xde, 0xea, 0x1e, 0x1d, 0xb7, 0xb7, 0xbb, 0x00, 0xc6, 0xa3, 0xc6, 0xe9, 0x5d, 0x15, - 0x97, 0xd5, 0xf5, 0x58, 0x15, 0x14, 0x80, 0x66, 0x55, 0xee, 0x48, 0xa2, 0x22, 0xee, 0x27, 0x29, 0xcd, 0x75, 0x14, - 0x53, 0x03, 0x38, 0x85, 0xe6, 0x6f, 0xae, 0xf3, 0x97, 0xb2, 0x8c, 0x96, 0x2e, 0x10, 0x99, 0xc3, 0x41, 0x5c, 0x18, - 0x0b, 0xec, 0x5e, 0x3f, 0xa2, 0x22, 0x64, 0x89, 0x6a, 0xd2, 0x35, 0x16, 0xfb, 0xca, 0x8c, 0x96, 0xcb, 0xbc, 0x3e, - 0x17, 0x56, 0xc7, 0xa0, 0x9c, 0xd9, 0xc9, 0x7e, 0x05, 0xb7, 0x9c, 0x99, 0xdc, 0x93, 0x76, 0x2c, 0xb1, 0x9a, 0xa1, - 0x7a, 0xe7, 0xfc, 0x65, 0x28, 0x4f, 0x19, 0x01, 0x80, 0x5c, 0x03, 0x08, 0x51, 0x6e, 0x75, 0x8a, 0xc6, 0x4b, 0x08, - 0xf7, 0x45, 0x98, 0x8d, 0xa9, 0x58, 0x41, 0x6c, 0xa2, 0x92, 0x5a, 0xbb, 0x26, 0xa2, 0xbd, 0x06, 0x6d, 0x58, 0x87, - 0xf6, 0x0a, 0x90, 0xde, 0xdf, 0x5d, 0xb0, 0x82, 0xec, 0x2e, 0x94, 0x5c, 0xfb, 0xf0, 0xee, 0x2b, 0x38, 0x14, 0xc9, - 0x53, 0xb0, 0x44, 0x62, 0x04, 0x92, 0x56, 0x2e, 0x8e, 0x12, 0x21, 0x5c, 0x8a, 0x10, 0xc5, 0x09, 0x1c, 0x39, 0x96, - 0x04, 0xb1, 0x70, 0x9d, 0xbe, 0x82, 0x9c, 0x46, 0x0a, 0x66, 0x92, 0xc9, 0x56, 0xbc, 0x38, 0xd9, 0x57, 0xb5, 0x95, - 0x08, 0x50, 0x95, 0x00, 0x09, 0x72, 0x9f, 0x56, 0x38, 0x80, 0x44, 0x68, 0x1b, 0x0f, 0x11, 0x9b, 0x97, 0xc4, 0x26, - 0xcf, 0x5b, 0xf5, 0x4e, 0x92, 0xf0, 0x9a, 0x26, 0xbd, 0xdd, 0x45, 0xb6, 0x5c, 0xb6, 0x8a, 0x93, 0x7d, 0xf5, 0xe8, - 0x9c, 0x48, 0xbe, 0xa1, 0xee, 0xc8, 0x94, 0x4b, 0x0c, 0x87, 0x18, 0x21, 0x3d, 0xd4, 0xe4, 0x45, 0x05, 0xba, 0x83, - 0xc2, 0x75, 0x64, 0x46, 0x86, 0xac, 0x54, 0x6a, 0x50, 0x85, 0xeb, 0xb0, 0x68, 0xbd, 0x2c, 0x17, 0x74, 0x0a, 0xa5, - 0xf1, 0x72, 0xd9, 0x2e, 0x5c, 0x67, 0xc2, 0x52, 0x78, 0xca, 0x96, 0x4b, 0x79, 0x3e, 0x70, 0xc2, 0x52, 0xaf, 0x05, - 0x64, 0xeb, 0x3a, 0x93, 0xf0, 0x56, 0x4e, 0xd8, 0xbc, 0x09, 0x6f, 0xbd, 0xb6, 0x7e, 0xe5, 0x97, 0xf8, 0xc9, 0x81, - 0xe2, 0xaa, 0x15, 0x4d, 0xf4, 0x8a, 0x46, 0x78, 0xa6, 0x4e, 0x3e, 0x11, 0x2f, 0x22, 0xc9, 0xe6, 0x15, 0x8d, 0xcc, - 0x8a, 0xce, 0xb6, 0xac, 0xe8, 0xec, 0x9e, 0x15, 0x0d, 0xf5, 0xea, 0x39, 0x25, 0xee, 0xf8, 0x72, 0xd9, 0x6e, 0x55, - 0xd8, 0x3b, 0xd9, 0x8f, 0xd8, 0x1c, 0x56, 0x03, 0xf4, 0x42, 0xc1, 0x26, 0x74, 0x33, 0x51, 0xd6, 0x51, 0x4c, 0x7f, - 0x15, 0x26, 0x2b, 0x2c, 0x64, 0x75, 0x2c, 0xd8, 0x74, 0x5d, 0x06, 0xe9, 0xfe, 0x48, 0xca, 0x66, 0x80, 0x87, 0x1c, - 0xf0, 0x10, 0x9b, 0x3b, 0x33, 0x3d, 0xf7, 0xbd, 0x8b, 0x5d, 0xc7, 0x35, 0x64, 0x7d, 0x55, 0x5c, 0x82, 0x8c, 0x90, - 0xf3, 0x7b, 0x10, 0x2d, 0x42, 0x6d, 0xb7, 0xb7, 0x9d, 0xe6, 0x20, 0x9e, 0x7e, 0xc3, 0xb3, 0xc8, 0x0d, 0x54, 0xd5, - 0x5f, 0x85, 0xaa, 0x09, 0x4b, 0x75, 0x76, 0xd6, 0x56, 0x5a, 0xab, 0xde, 0xdb, 0x14, 0xd7, 0x39, 0x3a, 0x52, 0x35, - 0xa6, 0xa1, 0x10, 0x34, 0x4b, 0x35, 0xe5, 0xba, 0xee, 0xff, 0x17, 0x54, 0xb8, 0x81, 0xaf, 0x84, 0x66, 0x01, 0x0c, - 0x01, 0x6a, 0x0d, 0x5f, 0xf3, 0x7c, 0x25, 0x9e, 0x76, 0x2a, 0x0d, 0xf6, 0x0e, 0xd9, 0x56, 0x86, 0x2a, 0x02, 0xa3, - 0x67, 0x36, 0xa1, 0xd1, 0xa5, 0x64, 0xd0, 0xfd, 0xe1, 0x95, 0x56, 0x58, 0x57, 0xc4, 0x5d, 0xd5, 0x00, 0xbb, 0x3f, - 0xce, 0x3a, 0x8f, 0x0f, 0xcf, 0x5c, 0xac, 0x78, 0x3c, 0x1f, 0x8d, 0x5c, 0x54, 0x38, 0x0f, 0x6b, 0xd6, 0x3e, 0xfc, - 0x71, 0xf6, 0xe5, 0xf3, 0xd6, 0x97, 0x65, 0xe3, 0x14, 0x88, 0x48, 0x27, 0x04, 0x18, 0x51, 0x65, 0xc1, 0x6b, 0x66, - 0x34, 0x0a, 0xd3, 0xed, 0xd3, 0x19, 0xd8, 0xd3, 0xc9, 0xa7, 0x94, 0x46, 0x40, 0x9c, 0x78, 0xad, 0xf4, 0x32, 0xa1, - 0x73, 0x6a, 0xee, 0x2a, 0xdc, 0x30, 0xd8, 0x86, 0x16, 0x43, 0x3e, 0x4b, 0x85, 0xce, 0x8c, 0xd0, 0xac, 0xd6, 0x9a, - 0xd2, 0x95, 0x9c, 0x83, 0x6d, 0x23, 0xdc, 0x29, 0x39, 0x57, 0x97, 0x5e, 0xc5, 0x15, 0x76, 0x2d, 0x00, 0xb6, 0x42, - 0xd6, 0xdf, 0x52, 0x1e, 0xb4, 0x70, 0x6b, 0x1b, 0x6c, 0xb8, 0x8d, 0x02, 0xd7, 0xbd, 0x30, 0x78, 0x92, 0xce, 0xcd, - 0xda, 0x05, 0x13, 0x5b, 0xf1, 0xf5, 0x49, 0x0c, 0x5c, 0x67, 0xd0, 0x59, 0x4a, 0xf3, 0x7c, 0x2b, 0x02, 0xca, 0x45, - 0xc4, 0x6e, 0x55, 0xdb, 0xdd, 0xd2, 0x0b, 0x6e, 0x61, 0xd8, 0x61, 0x12, 0xe0, 0x32, 0xc4, 0xaa, 0x6b, 0xd1, 0xd1, - 0x88, 0x0e, 0x4b, 0xdf, 0x30, 0x04, 0xcb, 0x46, 0x2c, 0x11, 0x10, 0x33, 0x92, 0xc1, 0x1c, 0xf7, 0x35, 0x4f, 0xa9, - 0x8b, 0x4c, 0xfa, 0xa7, 0x86, 0x5f, 0xcb, 0xff, 0xcd, 0xf0, 0xa8, 0x1e, 0xeb, 0xb0, 0xe8, 0x51, 0x96, 0x4b, 0xe3, - 0x17, 0xaa, 0x95, 0xd7, 0x11, 0xc9, 0xa5, 0xe3, 0x67, 0xdb, 0x06, 0x7a, 0xd8, 0x36, 0x59, 0xb4, 0xbf, 0x3c, 0x6a, - 0xb7, 0x0a, 0x17, 0xbb, 0xd0, 0xdd, 0x43, 0x77, 0x89, 0x6c, 0x75, 0x00, 0xad, 0x66, 0xe9, 0xaf, 0x69, 0xd7, 0x69, - 0x3f, 0x69, 0xbb, 0x58, 0xdd, 0x3b, 0x80, 0x8a, 0x92, 0x19, 0x0c, 0xc1, 0x5b, 0xfa, 0xbb, 0xa7, 0x52, 0xef, 0xfc, - 0x61, 0xf0, 0x3c, 0x6a, 0xb7, 0x5c, 0xec, 0xe6, 0x82, 0x4f, 0x7f, 0xc5, 0x14, 0x0e, 0x5c, 0xec, 0x0e, 0x13, 0x9e, - 0x53, 0x7b, 0x0e, 0x4a, 0x9d, 0xfd, 0xfd, 0x93, 0x50, 0x10, 0x4d, 0x33, 0x9a, 0xe7, 0x8e, 0xdd, 0xbf, 0x26, 0xa5, - 0x4f, 0x30, 0xcc, 0x8d, 0x14, 0x97, 0x53, 0x21, 0xf1, 0xa2, 0xae, 0x04, 0xb0, 0xa9, 0x4a, 0x95, 0xad, 0x11, 0x9b, - 0x14, 0x01, 0x25, 0x63, 0x53, 0xda, 0xd5, 0x27, 0x47, 0xde, 0xb0, 0xf5, 0xd4, 0xc0, 0x2a, 0x88, 0xbc, 0x3e, 0x40, - 0xad, 0x64, 0xc2, 0xd2, 0xcb, 0x0d, 0xa5, 0xe1, 0xed, 0x86, 0x52, 0x50, 0xd9, 0x4a, 0xe8, 0xf4, 0x75, 0x35, 0x9f, - 0xc6, 0x7a, 0xa5, 0xf8, 0xd8, 0x20, 0x46, 0xd2, 0xd1, 0xf9, 0x09, 0x48, 0xad, 0x65, 0x90, 0x3d, 0xfc, 0xf6, 0xe1, - 0xa0, 0xe4, 0xd7, 0x0c, 0x57, 0xf6, 0xf2, 0xfb, 0x66, 0x08, 0xa5, 0x4d, 0x70, 0x78, 0x27, 0xbf, 0x6a, 0xae, 0xf4, - 0xf6, 0xd3, 0x04, 0x67, 0x69, 0x55, 0xbf, 0x63, 0xe9, 0xf5, 0xb1, 0xf7, 0xd5, 0xb5, 0xdf, 0x50, 0xac, 0x15, 0x9f, - 0x72, 0xfd, 0x87, 0x09, 0x9b, 0x54, 0x24, 0xb0, 0x0e, 0xa6, 0xd4, 0x78, 0x20, 0xfb, 0xc9, 0xee, 0x44, 0xa9, 0x3e, - 0x97, 0x70, 0xa6, 0x13, 0xae, 0xcd, 0x98, 0x65, 0xf4, 0x32, 0xe1, 0x37, 0xab, 0xf7, 0x80, 0x6d, 0xaf, 0x1c, 0xb3, - 0x71, 0x6c, 0x1d, 0xd4, 0xa2, 0xa4, 0x5c, 0x84, 0x7b, 0x07, 0x28, 0xfe, 0xe5, 0x9f, 0x7d, 0xff, 0x5f, 0xfe, 0xf9, - 0x93, 0x55, 0xa1, 0xfb, 0xe2, 0x0a, 0x8b, 0xaa, 0xdb, 0xed, 0xbb, 0x6b, 0xf3, 0x48, 0x75, 0x9c, 0x6f, 0xae, 0xb3, - 0xb6, 0x08, 0xf0, 0x7e, 0x6d, 0x09, 0xd6, 0x0a, 0xd5, 0xee, 0x73, 0x7e, 0x0b, 0x60, 0x30, 0xaf, 0x4f, 0x42, 0x06, - 0x95, 0x7e, 0x17, 0x68, 0x57, 0x28, 0x78, 0xd0, 0x8a, 0xfc, 0x76, 0x0c, 0x7f, 0x6a, 0x0e, 0xbf, 0x13, 0x7c, 0xed, - 0x9f, 0x18, 0x5e, 0x5d, 0x95, 0x19, 0x79, 0x76, 0x53, 0x38, 0xef, 0xdf, 0x5f, 0x2b, 0xd1, 0x8a, 0x47, 0xd0, 0x42, - 0x3d, 0x79, 0x9e, 0x90, 0x0c, 0xaf, 0x5e, 0xc1, 0x25, 0x3f, 0x27, 0xd7, 0x99, 0x71, 0xf0, 0xde, 0x23, 0x1c, 0xa0, - 0x8b, 0xfa, 0xac, 0x64, 0xa7, 0x6b, 0x92, 0x01, 0x4a, 0xc1, 0xdc, 0x00, 0x30, 0xf1, 0xf0, 0x4a, 0x5b, 0x9b, 0x67, - 0xca, 0x0d, 0x13, 0xac, 0x92, 0xb6, 0x76, 0xcf, 0xd4, 0x90, 0x8e, 0x9d, 0xf7, 0x12, 0x5f, 0xb2, 0x32, 0xad, 0xac, - 0x7b, 0xe9, 0xea, 0x02, 0x3b, 0xa2, 0x64, 0x3f, 0xf3, 0x30, 0x99, 0x3f, 0x8c, 0xf1, 0x6d, 0x17, 0xa8, 0x4b, 0x67, - 0xf9, 0x6f, 0xad, 0x12, 0x2c, 0x9b, 0xcb, 0x9a, 0x3e, 0x20, 0xb3, 0x12, 0xfe, 0xbe, 0x2d, 0x70, 0x2a, 0xe8, 0x27, - 0x03, 0xa7, 0xc9, 0x83, 0x02, 0xa7, 0xea, 0x86, 0xbe, 0x3f, 0x32, 0x70, 0xfa, 0x77, 0x3b, 0x70, 0x0a, 0x24, 0xf8, - 0xf3, 0x83, 0x82, 0x9b, 0x26, 0xf0, 0xc4, 0x6f, 0x72, 0xd2, 0xd6, 0x46, 0x40, 0xc2, 0xc7, 0x10, 0xd9, 0xfc, 0xb7, - 0x0f, 0x54, 0x26, 0x7c, 0x6c, 0x87, 0x29, 0xe1, 0x8e, 0x5a, 0x88, 0x4b, 0xe2, 0x8c, 0x2c, 0xdc, 0x1f, 0x6f, 0xdb, - 0x4f, 0x07, 0xed, 0xee, 0x41, 0x7b, 0xe2, 0x06, 0x2e, 0x48, 0x5d, 0x59, 0xd0, 0xea, 0x1e, 0x1c, 0x40, 0xc1, 0x8d, - 0x55, 0xd0, 0x81, 0x02, 0x66, 0x15, 0x1c, 0x41, 0xc1, 0xd0, 0x2a, 0x78, 0x04, 0x05, 0x91, 0x55, 0xf0, 0x18, 0x0a, - 0xe6, 0x6e, 0x31, 0x60, 0x65, 0x74, 0xf8, 0x31, 0x92, 0xd7, 0x59, 0xec, 0x64, 0xf5, 0x54, 0xfe, 0x98, 0x98, 0x2a, - 0x8f, 0xcb, 0x63, 0x40, 0xcd, 0x43, 0x73, 0x6b, 0xc5, 0xd5, 0x67, 0x57, 0x08, 0x27, 0x04, 0x4e, 0xe5, 0x61, 0x30, - 0xca, 0x55, 0xcd, 0x03, 0xf3, 0xda, 0x0d, 0xca, 0x7b, 0xa9, 0x5a, 0xb8, 0x63, 0x22, 0x9c, 0x81, 0x8b, 0xf0, 0xac, - 0xac, 0x7c, 0xd4, 0x88, 0x74, 0xb7, 0x70, 0x21, 0x44, 0x75, 0x1b, 0xcb, 0x01, 0xc2, 0xea, 0x02, 0xec, 0x67, 0x52, - 0x3e, 0xfa, 0x82, 0xbf, 0x67, 0x13, 0x6a, 0x3e, 0x0f, 0x62, 0x06, 0x70, 0x5c, 0x04, 0x07, 0xb8, 0xe3, 0xea, 0x0a, - 0xb3, 0x2f, 0xf1, 0x69, 0x75, 0x01, 0xd0, 0x5b, 0x41, 0xd4, 0x8d, 0x0a, 0x19, 0x56, 0x86, 0xde, 0x18, 0x8b, 0x70, - 0x1c, 0x40, 0xc8, 0x12, 0x7c, 0xa6, 0xc1, 0x29, 0x21, 0xa4, 0xd5, 0x9f, 0x05, 0x5f, 0xe2, 0x9b, 0x98, 0xa6, 0xc1, - 0xbc, 0xe8, 0x96, 0x04, 0xa0, 0x22, 0xa6, 0x6f, 0x45, 0x79, 0x6f, 0x9c, 0xa4, 0x8a, 0xea, 0xb5, 0x82, 0xb3, 0x59, - 0x52, 0xcf, 0x96, 0x58, 0x9a, 0xe5, 0x93, 0x19, 0x25, 0xfc, 0xa6, 0x79, 0xeb, 0xf6, 0x36, 0xc7, 0xd7, 0x60, 0x76, - 0x65, 0x7c, 0xed, 0x25, 0x00, 0x5b, 0x3e, 0xbd, 0x0f, 0xc7, 0xe5, 0xef, 0x57, 0x34, 0xcf, 0xc3, 0xb1, 0xae, 0xb9, - 0x3d, 0x9e, 0x26, 0x41, 0xb4, 0x63, 0x69, 0x06, 0x08, 0x88, 0x89, 0x01, 0x46, 0xc0, 0xa7, 0xa1, 0x43, 0x64, 0x30, - 0xf5, 0x7a, 0x74, 0x4d, 0xe2, 0xaa, 0x5e, 0x24, 0xc2, 0x71, 0x55, 0x70, 0x32, 0xcd, 0xa8, 0x2c, 0x55, 0x68, 0x2c, - 0x4e, 0xf6, 0xa1, 0x40, 0xbd, 0xde, 0x12, 0x45, 0x33, 0x0e, 0x94, 0xed, 0xb1, 0x34, 0xc7, 0x44, 0xd1, 0xec, 0x44, - 0xa5, 0x32, 0x4b, 0x69, 0x3d, 0x76, 0xf3, 0x79, 0x7b, 0x08, 0x7f, 0x74, 0x64, 0xe8, 0xf3, 0xd1, 0x68, 0x74, 0x6f, - 0x54, 0xed, 0xf3, 0x68, 0x44, 0x3b, 0xf4, 0xa8, 0x0b, 0x49, 0x2c, 0x4d, 0x1d, 0x8b, 0x69, 0x17, 0x12, 0x77, 0x8b, - 0x87, 0x55, 0x86, 0xb0, 0x8d, 0x88, 0x17, 0x0f, 0x8f, 0xb0, 0x15, 0xd3, 0x8c, 0x2e, 0x26, 0x61, 0x36, 0x66, 0x69, - 0xd0, 0x2a, 0xfc, 0xb9, 0x0e, 0x49, 0x7d, 0x7e, 0x7c, 0x7c, 0x5c, 0xf8, 0x91, 0x79, 0x6a, 0x45, 0x51, 0xe1, 0x0f, - 0x17, 0xe5, 0x34, 0x5a, 0xad, 0xd1, 0xa8, 0xf0, 0x99, 0x29, 0x38, 0xe8, 0x0c, 0xa3, 0x83, 0x4e, 0xe1, 0xdf, 0x58, - 0x35, 0x0a, 0x9f, 0xea, 0xa7, 0x8c, 0x46, 0xb5, 0x4c, 0x98, 0xc7, 0xad, 0x56, 0xe1, 0x2b, 0x42, 0x5b, 0x80, 0x59, - 0xaa, 0x7e, 0x06, 0xe1, 0x4c, 0x70, 0x60, 0xee, 0xdd, 0x44, 0x78, 0x83, 0x4b, 0x7d, 0xcb, 0x88, 0xfa, 0x26, 0x47, - 0x81, 0x2e, 0xf0, 0xcf, 0x76, 0xf0, 0x08, 0x88, 0x59, 0x06, 0x8d, 0x12, 0x13, 0x5b, 0xaa, 0xbd, 0x06, 0xca, 0x92, - 0xaf, 0x7f, 0x26, 0x49, 0x15, 0x53, 0x02, 0x4e, 0x06, 0x35, 0xd5, 0x65, 0x78, 0x94, 0x6e, 0x91, 0x1f, 0xec, 0xd3, - 0xf2, 0xe3, 0xee, 0x21, 0xe2, 0x83, 0xfd, 0xe1, 0xe2, 0x83, 0x52, 0x4b, 0x7c, 0x28, 0xe6, 0x71, 0x27, 0x88, 0x3b, - 0x8c, 0xe9, 0xf0, 0xe3, 0x35, 0xbf, 0x6d, 0xc2, 0x96, 0xc8, 0x5c, 0x29, 0x58, 0x76, 0x7f, 0x6b, 0xd6, 0x8c, 0xe9, - 0xcc, 0xfa, 0xa2, 0x87, 0x54, 0x1f, 0xde, 0xa4, 0xc4, 0x7d, 0x63, 0x6c, 0x5b, 0x55, 0x32, 0x1a, 0x11, 0xf7, 0xcd, - 0x68, 0xe4, 0x9a, 0xb3, 0x92, 0xa1, 0xa0, 0xb2, 0xd6, 0xeb, 0x5a, 0x89, 0xac, 0xf5, 0xe5, 0x97, 0x76, 0x99, 0x5d, - 0xa0, 0x43, 0x4f, 0x76, 0x98, 0x49, 0xbf, 0x89, 0x58, 0x0e, 0x5b, 0x0d, 0x3e, 0x34, 0x52, 0xbf, 0xab, 0x31, 0xad, - 0x5d, 0xab, 0x5d, 0x02, 0xbc, 0xe1, 0x2e, 0xf0, 0xd5, 0x8b, 0x02, 0xc6, 0xd4, 0xe4, 0x2d, 0x3e, 0xbd, 0xfb, 0x2a, - 0xf2, 0xee, 0x04, 0x2a, 0x58, 0xfe, 0x26, 0x5d, 0x39, 0x04, 0xa4, 0x60, 0x24, 0xc4, 0x9e, 0x56, 0x21, 0xf8, 0x78, - 0x9c, 0xc0, 0xb7, 0x5e, 0x16, 0xb5, 0xfb, 0x63, 0x55, 0xf3, 0x7e, 0x6d, 0xbe, 0x81, 0xdd, 0x50, 0xdf, 0xb6, 0x2a, - 0x3f, 0x3d, 0xa5, 0x92, 0xc7, 0xe7, 0xfa, 0x1b, 0x44, 0xd2, 0x2c, 0x5e, 0x68, 0x26, 0xbf, 0x50, 0x29, 0xc7, 0x02, - 0xd2, 0x6d, 0x54, 0xc7, 0x51, 0x51, 0xe8, 0xc3, 0x1a, 0x11, 0xcb, 0xa7, 0x70, 0xaf, 0xa9, 0x6a, 0x49, 0x3f, 0xc5, - 0xc2, 0xf3, 0x1b, 0x2b, 0xbe, 0x53, 0x5b, 0xae, 0xc2, 0x04, 0x78, 0x94, 0xc3, 0xfc, 0x4e, 0x14, 0xae, 0xf6, 0xbb, - 0x1b, 0x24, 0xba, 0x8e, 0xc2, 0xa7, 0x8a, 0x3c, 0x59, 0x33, 0x04, 0xe7, 0x77, 0xb9, 0x20, 0xe6, 0x95, 0x29, 0x28, - 0xec, 0xf8, 0xa5, 0x7c, 0xa3, 0xb0, 0x25, 0xa3, 0x25, 0xf9, 0x34, 0x4c, 0x15, 0x1b, 0x25, 0xae, 0xe2, 0x07, 0xbb, - 0x8b, 0x6a, 0xe5, 0x0b, 0xd7, 0x80, 0xad, 0x88, 0xb7, 0x77, 0xb2, 0x0f, 0x0d, 0x7a, 0x4e, 0x0d, 0xf4, 0x74, 0x2d, - 0xc8, 0xf2, 0x89, 0x74, 0x87, 0x2b, 0x3f, 0xbf, 0xc1, 0x7e, 0x7e, 0xe3, 0xfc, 0x79, 0xd1, 0xbc, 0xa1, 0xd7, 0x1f, - 0x99, 0x68, 0x8a, 0x70, 0xda, 0x04, 0xc3, 0x47, 0x3a, 0x47, 0x35, 0x7b, 0x96, 0x59, 0x7e, 0xea, 0xaa, 0x83, 0xee, - 0x2c, 0x87, 0xac, 0x08, 0xa9, 0xbe, 0x07, 0x29, 0x4f, 0x69, 0xb7, 0x9e, 0xcd, 0x69, 0x07, 0xd9, 0x0d, 0xb6, 0x2e, - 0x16, 0x1c, 0xb2, 0x28, 0xc4, 0x5d, 0xd0, 0xd2, 0x6c, 0xbd, 0x65, 0x22, 0xe8, 0xad, 0x8d, 0xf5, 0x03, 0x8d, 0xdc, - 0x86, 0x94, 0x5e, 0xd9, 0x7a, 0x26, 0xc1, 0xb6, 0x4c, 0x80, 0x4f, 0xe5, 0x36, 0x82, 0x4b, 0xd5, 0xfc, 0xb5, 0x92, - 0x42, 0x57, 0x8b, 0x65, 0x6e, 0xe3, 0x43, 0x20, 0x0b, 0xc2, 0x91, 0xa0, 0x19, 0x7e, 0x48, 0xcd, 0x6b, 0x79, 0x0c, - 0x69, 0x01, 0x62, 0x26, 0x68, 0x1f, 0x4f, 0x6f, 0x1f, 0xde, 0xfd, 0xfd, 0xd3, 0x2f, 0x34, 0x8e, 0xcc, 0xb5, 0x3c, - 0xae, 0xdb, 0x85, 0x8d, 0x90, 0x84, 0x77, 0x01, 0x4b, 0xa5, 0xcc, 0xbb, 0x06, 0xbf, 0x68, 0x77, 0xca, 0x75, 0x92, - 0x6e, 0x46, 0x13, 0xf9, 0x15, 0x3e, 0xbd, 0x14, 0x07, 0x8f, 0xa6, 0xb7, 0x66, 0x35, 0xda, 0x2b, 0xc9, 0xb7, 0x7f, - 0x68, 0x8e, 0xed, 0xf6, 0xa4, 0xde, 0x7a, 0x9e, 0xe8, 0xd1, 0xf4, 0xb6, 0xab, 0x04, 0x6d, 0x33, 0x53, 0x50, 0xb5, - 0xa6, 0xb7, 0x76, 0x96, 0x71, 0xd5, 0x91, 0xe3, 0x1f, 0xe4, 0x0e, 0x0d, 0x73, 0xda, 0x85, 0x7b, 0xc7, 0xd9, 0x30, - 0x4c, 0xb4, 0x30, 0x9f, 0xb0, 0x28, 0x4a, 0x68, 0xd7, 0xc8, 0x6b, 0xa7, 0xfd, 0x08, 0x92, 0x74, 0xed, 0x25, 0xab, - 0xaf, 0x8a, 0x85, 0xbc, 0x12, 0x4f, 0xe1, 0x75, 0xce, 0x13, 0xf8, 0xe8, 0xc7, 0x46, 0x74, 0xea, 0xec, 0xd5, 0x56, - 0x85, 0x3c, 0xf9, 0xbb, 0x3e, 0x97, 0xa3, 0xd6, 0x9f, 0xba, 0x72, 0xc1, 0x5b, 0x5d, 0xc1, 0xa7, 0x41, 0xf3, 0xa0, - 0x3e, 0x11, 0x78, 0x55, 0x4e, 0x01, 0x6f, 0x98, 0x16, 0x06, 0x69, 0xa5, 0xf8, 0xb4, 0xe3, 0xb7, 0x75, 0x99, 0xec, - 0x00, 0xf2, 0xc2, 0xca, 0xa2, 0xa2, 0x3e, 0x99, 0x7f, 0x9b, 0xdd, 0xf2, 0x64, 0xf3, 0x6e, 0x79, 0x62, 0x76, 0xcb, - 0xfd, 0x14, 0xfb, 0xf9, 0xa8, 0x0d, 0x7f, 0xba, 0xd5, 0x84, 0x82, 0x96, 0x73, 0x30, 0xbd, 0x75, 0x40, 0x4f, 0x6b, - 0x76, 0xa6, 0xb7, 0x2a, 0xc7, 0x1a, 0x62, 0x37, 0x2d, 0xc8, 0x3a, 0xc6, 0x2d, 0x07, 0x0a, 0xe1, 0x6f, 0xab, 0xf6, - 0xaa, 0x7d, 0x08, 0xef, 0xa0, 0xd5, 0xd1, 0xfa, 0xbb, 0xce, 0xfd, 0x9b, 0x36, 0x48, 0xb9, 0xf0, 0x02, 0xc3, 0x8d, - 0x91, 0x2f, 0xc2, 0xeb, 0x6b, 0x1a, 0x05, 0x23, 0x3e, 0x9c, 0xe5, 0xff, 0xa4, 0xe1, 0xd7, 0x48, 0xbc, 0x77, 0x4b, - 0xaf, 0xf4, 0x63, 0x9a, 0xaa, 0x8c, 0x6f, 0xd3, 0xc3, 0xa2, 0x5c, 0xa7, 0x20, 0x1f, 0x86, 0x09, 0xf5, 0x3a, 0xfe, - 0xe1, 0x86, 0x4d, 0xf0, 0xef, 0xb2, 0x36, 0x1b, 0x27, 0xf3, 0x7b, 0x91, 0x71, 0x2f, 0x12, 0x7e, 0x15, 0x0e, 0xec, - 0x35, 0x6c, 0x1d, 0x6f, 0x06, 0x77, 0x60, 0x46, 0xba, 0x30, 0x42, 0x41, 0xcb, 0x9d, 0x88, 0x8e, 0xc2, 0x59, 0x22, - 0xee, 0xef, 0x75, 0x1b, 0x65, 0xac, 0xf5, 0x7a, 0x0f, 0x43, 0xaf, 0xea, 0x3e, 0x90, 0x4b, 0x7f, 0xfe, 0xe4, 0x10, - 0xfe, 0xa8, 0xfc, 0xaf, 0xbb, 0x4a, 0x57, 0x57, 0x76, 0x2f, 0xe8, 0xea, 0xbb, 0x35, 0x65, 0x5c, 0x89, 0x70, 0xa9, - 0x8f, 0x3f, 0xb4, 0x36, 0x68, 0x95, 0x0f, 0xaa, 0xae, 0xb5, 0xac, 0x5f, 0x55, 0xfb, 0xd7, 0x75, 0xfe, 0xc0, 0xba, - 0x43, 0xa5, 0xb9, 0xd6, 0xeb, 0xea, 0xcf, 0x10, 0xae, 0x55, 0x36, 0x18, 0x97, 0xf5, 0x77, 0xc9, 0x5d, 0x69, 0xa2, - 0xa8, 0x68, 0x2c, 0x58, 0x29, 0xbb, 0xca, 0x4a, 0xc9, 0x29, 0xb9, 0x3a, 0xe9, 0xdf, 0x4e, 0x12, 0x67, 0xae, 0x8e, - 0x4b, 0x12, 0xb7, 0xed, 0xb7, 0x5c, 0x47, 0xe6, 0x01, 0xc0, 0xad, 0xed, 0xae, 0xfc, 0xbc, 0xad, 0xdb, 0x07, 0x4d, - 0x6b, 0x3e, 0x96, 0x9a, 0xdd, 0xcb, 0xf0, 0x8e, 0x66, 0x97, 0x1d, 0xd7, 0x01, 0x3f, 0x4d, 0x53, 0xa5, 0x4c, 0xc8, - 0x32, 0xa7, 0xe3, 0x3a, 0xb7, 0x93, 0x24, 0xcd, 0x89, 0x1b, 0x0b, 0x31, 0x0d, 0xd4, 0xf7, 0x6f, 0x6f, 0x0e, 0x7c, - 0x9e, 0x8d, 0xf7, 0x3b, 0xad, 0x56, 0x0b, 0x2e, 0x80, 0x75, 0x9d, 0x39, 0xa3, 0x37, 0x4f, 0xf9, 0x2d, 0x71, 0x5b, - 0x4e, 0xcb, 0x69, 0x77, 0x8e, 0x9d, 0x76, 0xe7, 0xd0, 0x7f, 0x74, 0xec, 0xf6, 0x3e, 0x73, 0x9c, 0x93, 0x88, 0x8e, - 0x72, 0xf8, 0xe1, 0x38, 0x27, 0x52, 0xf1, 0x52, 0xbf, 0x1d, 0xc7, 0x1f, 0x26, 0x79, 0xb3, 0xed, 0x2c, 0xf4, 0xa3, - 0xe3, 0xc0, 0xa1, 0xd2, 0xc0, 0xf9, 0x7c, 0xd4, 0x19, 0x1d, 0x8e, 0x9e, 0x74, 0x75, 0x71, 0xf1, 0x59, 0xad, 0x3a, - 0x56, 0xff, 0x77, 0xac, 0x66, 0xb9, 0xc8, 0xf8, 0x47, 0xaa, 0x73, 0x12, 0x1d, 0x10, 0x3d, 0x1b, 0x9b, 0x76, 0xd6, - 0x47, 0x6a, 0x1f, 0x5f, 0x0f, 0x47, 0x9d, 0xaa, 0xba, 0x84, 0x71, 0xbf, 0x04, 0xf2, 0x64, 0xdf, 0x80, 0x7e, 0x62, - 0xa3, 0xa9, 0xdd, 0xdc, 0x84, 0xa8, 0xb6, 0xab, 0xe7, 0x38, 0x36, 0xf3, 0x3b, 0x81, 0x33, 0x0c, 0x46, 0x57, 0x95, - 0x10, 0xb8, 0x4e, 0x44, 0xdc, 0x57, 0xed, 0xce, 0x31, 0x6e, 0xb7, 0x1f, 0xf9, 0x8f, 0x8e, 0x87, 0x2d, 0x7c, 0xe8, - 0x1f, 0x36, 0x0f, 0xfc, 0x47, 0xf8, 0xb8, 0x79, 0x8c, 0x8f, 0x5f, 0x1c, 0x0f, 0x9b, 0x87, 0xfe, 0x21, 0x6e, 0x35, - 0x8f, 0xa1, 0xb0, 0x79, 0xdc, 0x3c, 0x9e, 0x37, 0x0f, 0x8f, 0x87, 0x2d, 0x59, 0xda, 0xf1, 0x8f, 0x8e, 0x9a, 0xed, - 0x96, 0x7f, 0x74, 0x84, 0x8f, 0xfc, 0x47, 0x8f, 0x9a, 0xed, 0x03, 0xff, 0xd1, 0xa3, 0x97, 0x47, 0xc7, 0xfe, 0x01, - 0xbc, 0x3b, 0x38, 0x18, 0x1e, 0xf8, 0xed, 0x76, 0x13, 0xfe, 0xc1, 0xc7, 0x7e, 0x47, 0xfd, 0x68, 0xb7, 0xfd, 0x83, - 0x36, 0x6e, 0x25, 0x47, 0x1d, 0xff, 0xd1, 0x13, 0x2c, 0xff, 0x95, 0xd5, 0xb0, 0xfc, 0x07, 0xba, 0xc1, 0x4f, 0xfc, - 0xce, 0x23, 0xf5, 0x4b, 0x76, 0x38, 0x3f, 0x3c, 0xfe, 0xc1, 0xdd, 0xdf, 0x3a, 0x87, 0xb6, 0x9a, 0xc3, 0xf1, 0x91, - 0x7f, 0x70, 0x80, 0x0f, 0xdb, 0xfe, 0xf1, 0x41, 0xdc, 0x3c, 0xec, 0xf8, 0x8f, 0x1e, 0x0f, 0x9b, 0x6d, 0xff, 0xf1, - 0x63, 0xdc, 0x6a, 0x1e, 0xf8, 0x1d, 0xdc, 0xf6, 0x0f, 0x0f, 0xe4, 0x8f, 0x03, 0xbf, 0x33, 0x7f, 0xfc, 0xc4, 0x7f, - 0x74, 0x14, 0x3f, 0xf2, 0x0f, 0xbf, 0x3d, 0x3c, 0xf6, 0x3b, 0x07, 0xf1, 0xc1, 0x23, 0xbf, 0xf3, 0x78, 0xfe, 0xc8, - 0x3f, 0x8c, 0x9b, 0x9d, 0x47, 0xf7, 0xb6, 0x6c, 0x77, 0x7c, 0xc0, 0x91, 0x7c, 0x0d, 0x2f, 0xb0, 0x7e, 0x01, 0x7f, - 0x63, 0xd9, 0xf6, 0xdf, 0xb1, 0x9b, 0x7c, 0xbd, 0xe9, 0x13, 0xff, 0xf8, 0xf1, 0x50, 0x55, 0x87, 0x82, 0xa6, 0xa9, - 0x01, 0x4d, 0xe6, 0x4d, 0x35, 0xac, 0xec, 0xae, 0x69, 0x3a, 0x32, 0x7f, 0xf5, 0x60, 0xf3, 0x26, 0x0c, 0xac, 0xc6, - 0xfd, 0x0f, 0xed, 0xa7, 0x5c, 0xf2, 0x93, 0xfd, 0xb1, 0x22, 0xfd, 0x71, 0xef, 0x33, 0x75, 0xbb, 0xf3, 0x67, 0x57, - 0x38, 0xdd, 0xe6, 0xf8, 0xc8, 0x3e, 0xed, 0xf8, 0xe0, 0xf4, 0x21, 0x9e, 0x8f, 0xec, 0x0f, 0xf7, 0x7c, 0xa4, 0x74, - 0xc5, 0x71, 0x7e, 0x2d, 0xd6, 0x1c, 0x1c, 0xab, 0x56, 0xf1, 0x53, 0xe1, 0x0d, 0x72, 0xf8, 0x8e, 0x58, 0xd1, 0xbd, - 0x16, 0x84, 0x53, 0xdb, 0x0f, 0xc4, 0x81, 0xc5, 0x5e, 0x0b, 0xc5, 0x63, 0x93, 0x6d, 0x08, 0x09, 0x3f, 0x8d, 0x90, - 0x6f, 0x1f, 0x82, 0x8f, 0xf0, 0x0f, 0xc7, 0x47, 0x62, 0xe3, 0xa3, 0xe6, 0xcb, 0x97, 0x9e, 0x06, 0xe9, 0x29, 0x38, - 0x97, 0xcf, 0x1e, 0x1c, 0xa2, 0x6a, 0xb8, 0xfb, 0x14, 0x8a, 0x72, 0x57, 0x45, 0xbe, 0xde, 0xfd, 0x9a, 0xb0, 0x83, - 0x3a, 0x31, 0x49, 0x5c, 0xed, 0x96, 0x99, 0x4a, 0xa9, 0xa3, 0x1f, 0x4a, 0xa1, 0xd4, 0xf1, 0x5b, 0x7e, 0xab, 0x74, - 0xe9, 0xc0, 0x29, 0x59, 0xb2, 0xe0, 0x22, 0x84, 0x2f, 0xd6, 0x26, 0x7c, 0x2c, 0xbf, 0x6d, 0x0b, 0x5f, 0x13, 0x80, - 0xa4, 0x9f, 0xa1, 0xfa, 0x90, 0x43, 0xe0, 0xba, 0xfa, 0x6e, 0x0d, 0x38, 0x85, 0xf9, 0x0d, 0x9c, 0x54, 0x35, 0x51, - 0x89, 0x09, 0x78, 0x3b, 0x5e, 0xd1, 0x88, 0x85, 0x9e, 0xeb, 0x4d, 0x33, 0x3a, 0xa2, 0x59, 0xde, 0xac, 0x1d, 0xdf, - 0x94, 0x27, 0x37, 0x91, 0x6b, 0x3e, 0x8d, 0x9a, 0xc1, 0xed, 0xd8, 0x64, 0xa0, 0xfd, 0x8d, 0xae, 0x36, 0xc0, 0xdc, - 0x02, 0x9b, 0x92, 0x0c, 0x64, 0x6d, 0xa5, 0xb4, 0xb9, 0x4a, 0x6b, 0x6b, 0xfb, 0x9d, 0x23, 0xe4, 0xc8, 0x62, 0xb8, - 0x77, 0xf8, 0x7b, 0xaf, 0x79, 0xd0, 0xfa, 0x13, 0xb2, 0x9a, 0x95, 0x1d, 0x5d, 0x68, 0x77, 0x5b, 0x5a, 0x7d, 0x53, - 0xba, 0x7e, 0xb6, 0xd6, 0x55, 0x14, 0xf1, 0xb9, 0x9a, 0xbb, 0x8b, 0xba, 0xa9, 0x8e, 0x70, 0xab, 0x1b, 0x22, 0x46, - 0x6c, 0xec, 0xd9, 0x5f, 0x0c, 0x56, 0xf7, 0x1a, 0xcb, 0x0f, 0x8d, 0xa3, 0xa2, 0xaa, 0x92, 0xa2, 0x85, 0x8c, 0xb7, - 0xb0, 0xd4, 0x49, 0x97, 0x4b, 0x2f, 0x05, 0x17, 0x39, 0xb1, 0x70, 0x0a, 0xcf, 0xa8, 0x86, 0xe4, 0x14, 0x97, 0x00, - 0x49, 0x04, 0x93, 0x54, 0xfd, 0x5f, 0x15, 0x9b, 0x1f, 0xda, 0xf1, 0xe5, 0x27, 0x61, 0x3a, 0x06, 0x2a, 0x0c, 0xd3, - 0xf1, 0x9a, 0x5b, 0x4d, 0x85, 0x8c, 0x56, 0x4a, 0xab, 0xae, 0x2a, 0xf7, 0x59, 0xfe, 0xf4, 0xee, 0xbd, 0xbe, 0x00, - 0xcd, 0x05, 0xef, 0xb4, 0x8c, 0x70, 0x54, 0x97, 0x35, 0x37, 0xc8, 0x17, 0x27, 0x13, 0x2a, 0x42, 0x95, 0xaf, 0x09, - 0xfa, 0x04, 0x9c, 0x9a, 0x75, 0xb4, 0x35, 0x4a, 0x5c, 0x29, 0xdd, 0x49, 0x44, 0xe7, 0x6c, 0xa8, 0x45, 0x3d, 0x76, - 0xf4, 0xcd, 0x01, 0x4d, 0xb9, 0x34, 0xa4, 0x8d, 0x95, 0x3f, 0x66, 0x18, 0xca, 0x8c, 0x7c, 0x92, 0x72, 0xb7, 0xf7, - 0x45, 0xf9, 0xf5, 0xd3, 0x6d, 0x8b, 0x90, 0xb0, 0xf4, 0xe3, 0x20, 0xa3, 0xc9, 0x3f, 0x91, 0x2f, 0xd8, 0x90, 0xa7, - 0x5f, 0x5c, 0xc0, 0x57, 0xe9, 0xfd, 0x38, 0xa3, 0x23, 0xf2, 0x05, 0xc8, 0xf8, 0x40, 0x5a, 0x1f, 0xc0, 0x08, 0x1b, - 0xb7, 0x93, 0x04, 0x4b, 0x8d, 0xe9, 0x01, 0x0a, 0x91, 0x02, 0xd7, 0xed, 0x1c, 0xb9, 0x8e, 0xb2, 0x89, 0xe5, 0xef, - 0x9e, 0x12, 0xa7, 0x52, 0x09, 0x70, 0xda, 0x1d, 0xff, 0x28, 0xee, 0xf8, 0x4f, 0xe6, 0x8f, 0xfd, 0xe3, 0xb8, 0xfd, - 0x78, 0xde, 0x84, 0xff, 0x3b, 0xfe, 0x93, 0xa4, 0xd9, 0xf1, 0x9f, 0xc0, 0xdf, 0x6f, 0x0f, 0xfd, 0xa3, 0xb8, 0xd9, - 0xf6, 0x8f, 0xe7, 0x07, 0xfe, 0xc1, 0xcb, 0x76, 0xc7, 0x3f, 0x70, 0xda, 0x8e, 0x6a, 0x07, 0xec, 0x5a, 0x71, 0xe7, - 0x2f, 0x56, 0x36, 0xc4, 0x86, 0x70, 0x9c, 0xca, 0x39, 0x75, 0xb1, 0x57, 0x7e, 0x63, 0x51, 0xef, 0x4f, 0xed, 0xac, - 0x7b, 0x16, 0x66, 0xf0, 0xa1, 0x9b, 0xfa, 0xde, 0xad, 0xbd, 0xc3, 0x35, 0x7e, 0xb1, 0x61, 0x08, 0xd8, 0xe1, 0x2e, - 0xb6, 0x8f, 0xde, 0xc3, 0xb9, 0x75, 0x79, 0x2f, 0xb8, 0xb9, 0x1e, 0x71, 0x3b, 0x69, 0xab, 0x8a, 0xe6, 0x0a, 0x46, - 0xc9, 0x2c, 0x98, 0xfc, 0x02, 0x83, 0x1c, 0xe4, 0xab, 0xa8, 0x58, 0x1d, 0x1f, 0x52, 0x5f, 0x33, 0x6e, 0xdd, 0x3e, - 0x40, 0xab, 0x03, 0x1b, 0x11, 0x83, 0xfb, 0x22, 0x8a, 0xc2, 0x80, 0x5e, 0x73, 0xd3, 0x56, 0x58, 0x92, 0xfc, 0x82, - 0xe6, 0x7d, 0x17, 0x8a, 0xdc, 0xc0, 0x95, 0x2e, 0x3e, 0xb7, 0xfc, 0xd8, 0x4f, 0x49, 0xd8, 0x55, 0x01, 0x96, 0x87, - 0xae, 0x60, 0xd7, 0x02, 0x7e, 0x5c, 0xb4, 0xb7, 0xb7, 0x75, 0xbf, 0x48, 0x05, 0x12, 0xe6, 0x5a, 0x7d, 0x23, 0xc4, - 0x66, 0x45, 0xae, 0x8d, 0xe8, 0xb2, 0x5f, 0x89, 0x42, 0xa4, 0xf1, 0x74, 0x4d, 0x43, 0xe1, 0x87, 0xa9, 0x4a, 0xa2, - 0xb1, 0x18, 0x16, 0x6e, 0xd3, 0x03, 0x54, 0x70, 0x11, 0x5a, 0xdf, 0x01, 0xd6, 0xfb, 0x9c, 0x8b, 0xd0, 0x9c, 0xa5, - 0xb5, 0xae, 0x0d, 0x02, 0x47, 0x6f, 0xdc, 0xe9, 0xbd, 0x79, 0x7f, 0xea, 0xa8, 0xed, 0x79, 0xb2, 0x1f, 0x77, 0x7a, - 0x27, 0xd2, 0x67, 0xa2, 0x4e, 0xe2, 0x11, 0x75, 0x12, 0xcf, 0xd1, 0xa7, 0x32, 0x21, 0x92, 0x56, 0xec, 0xab, 0x69, - 0x4b, 0x9b, 0x41, 0x79, 0x7b, 0x27, 0xb3, 0x44, 0x30, 0xb8, 0xe3, 0x7a, 0x5f, 0x1e, 0xc3, 0x83, 0x05, 0x2b, 0xf3, - 0xb0, 0xb5, 0x76, 0x78, 0x2d, 0x52, 0xe3, 0x1b, 0x1e, 0xb1, 0x84, 0x9a, 0xcc, 0x6b, 0xdd, 0x55, 0x79, 0x52, 0x60, - 0xbd, 0x76, 0x3e, 0xbb, 0x9e, 0x30, 0xe1, 0x9a, 0xf3, 0x0c, 0x1f, 0x74, 0x83, 0x13, 0x39, 0x54, 0xef, 0xaa, 0xd0, - 0xce, 0x6b, 0xf3, 0x35, 0x9f, 0xfa, 0x92, 0xea, 0xd9, 0x6b, 0x09, 0x01, 0x27, 0xe4, 0xe2, 0x83, 0x5e, 0xe9, 0x2e, - 0xb6, 0xdf, 0x15, 0x27, 0xfb, 0xf1, 0x41, 0xef, 0x2a, 0x98, 0xea, 0xfe, 0x5e, 0xf2, 0xf1, 0xe6, 0xbe, 0x12, 0x3e, - 0xee, 0xcb, 0xa3, 0x20, 0xea, 0x90, 0xb2, 0x51, 0x7e, 0x79, 0xe2, 0xf6, 0x4e, 0xb4, 0x32, 0xe0, 0xc8, 0xc0, 0xba, - 0x7b, 0xd4, 0x32, 0xa7, 0x4b, 0x12, 0x3e, 0x86, 0x0d, 0xa9, 0x9a, 0x58, 0x83, 0xd4, 0x3c, 0xee, 0x71, 0xbb, 0x77, - 0x12, 0x3a, 0x92, 0xb7, 0x48, 0xe6, 0x91, 0x07, 0xfb, 0xd0, 0x38, 0xe6, 0x13, 0xea, 0x33, 0xbe, 0x7f, 0x43, 0xaf, - 0x9b, 0xe1, 0x94, 0x55, 0xee, 0x6d, 0x50, 0x3a, 0xca, 0x21, 0xb9, 0xf1, 0x88, 0xeb, 0xb3, 0x57, 0x9d, 0xca, 0xdd, - 0x76, 0x08, 0x36, 0x8f, 0x71, 0xcd, 0x49, 0x9f, 0x9c, 0x05, 0x16, 0xef, 0x9d, 0xec, 0x87, 0x2b, 0x18, 0x91, 0xfc, - 0xbe, 0xd0, 0x8e, 0x76, 0x30, 0x6c, 0x80, 0xde, 0x5c, 0x47, 0x89, 0x03, 0xe3, 0x90, 0xd7, 0x82, 0xba, 0x70, 0x7b, - 0xff, 0xfa, 0x3f, 0xfe, 0x97, 0xf6, 0xb1, 0x9f, 0xec, 0xc7, 0x6d, 0xd3, 0xd7, 0xca, 0xaa, 0x14, 0x27, 0x70, 0xdc, - 0xb3, 0x0a, 0x0a, 0xd3, 0xdb, 0xe6, 0x38, 0x63, 0x51, 0x33, 0x0e, 0x93, 0x91, 0xdb, 0xdb, 0x8e, 0x4d, 0xfb, 0xd8, - 0x96, 0x86, 0xba, 0x5e, 0x04, 0xf4, 0xfa, 0x9b, 0x0e, 0x1e, 0x99, 0xf3, 0x2b, 0x72, 0x6b, 0xdb, 0xc7, 0x90, 0xaa, - 0xdd, 0x57, 0x3b, 0x8a, 0x94, 0xea, 0x4f, 0x84, 0x69, 0x0e, 0x98, 0xd6, 0x4e, 0x20, 0x15, 0xae, 0x53, 0x06, 0xb5, - 0xfe, 0xef, 0xff, 0xfc, 0x2f, 0xff, 0xcd, 0x3c, 0x42, 0xac, 0xea, 0x5f, 0xff, 0xfb, 0x7f, 0xfe, 0x3f, 0xff, 0xfb, - 0xbf, 0xc2, 0xa9, 0x15, 0x1d, 0xcf, 0x92, 0x4c, 0xc5, 0xa9, 0x82, 0x59, 0x8a, 0xbb, 0x38, 0x90, 0xd8, 0x39, 0x61, - 0xb9, 0x60, 0xc3, 0xfa, 0x99, 0xa4, 0x73, 0x39, 0xa0, 0xdc, 0x99, 0x1a, 0x3a, 0xb9, 0xc3, 0x8b, 0x8a, 0xa0, 0x6a, - 0x28, 0x97, 0x84, 0x5b, 0x9c, 0xec, 0x03, 0xbe, 0x1f, 0x76, 0x8c, 0xd3, 0x2f, 0x97, 0x63, 0x61, 0xc8, 0x04, 0x4a, - 0x8a, 0xaa, 0xdc, 0x81, 0xd8, 0xca, 0x02, 0x1e, 0x83, 0x8e, 0x55, 0x2c, 0x57, 0xaf, 0xd6, 0xa6, 0xfb, 0xd3, 0x2c, - 0x17, 0x6c, 0x04, 0x28, 0x57, 0x7e, 0x62, 0x19, 0xc6, 0x6e, 0x82, 0xae, 0x98, 0xdc, 0x15, 0xb2, 0x17, 0x45, 0xa0, - 0x87, 0xc7, 0x7f, 0x2a, 0xfe, 0x32, 0x01, 0x8d, 0xcc, 0xf1, 0x26, 0xe1, 0xad, 0x36, 0xcf, 0x1f, 0xb5, 0x5a, 0xd3, - 0x5b, 0xb4, 0xa8, 0x46, 0xc0, 0xdb, 0x06, 0x93, 0x74, 0x6c, 0x77, 0x28, 0xe3, 0xdf, 0xa5, 0x1b, 0xbb, 0xe5, 0x80, - 0x2f, 0xdc, 0x69, 0x15, 0xc5, 0x9f, 0x17, 0xd2, 0x93, 0xca, 0x7e, 0x81, 0x38, 0xb5, 0x76, 0x3a, 0x5f, 0x73, 0x7b, - 0x72, 0x0b, 0xab, 0x55, 0x47, 0xb5, 0x8a, 0xdb, 0xeb, 0xa7, 0x13, 0xed, 0x38, 0xbb, 0x1d, 0x21, 0x3f, 0x84, 0x98, - 0x77, 0xdc, 0xc6, 0x71, 0x67, 0x51, 0x76, 0x2f, 0x04, 0x9f, 0xd8, 0x81, 0x75, 0x1a, 0xd2, 0x21, 0x1d, 0x19, 0x67, - 0xbd, 0x7e, 0xaf, 0x82, 0xe6, 0x45, 0x7c, 0xb0, 0x61, 0x2c, 0x0d, 0x92, 0x0c, 0xa8, 0x3b, 0xad, 0xe2, 0x73, 0xd8, - 0x81, 0x8b, 0x51, 0xc2, 0x43, 0x11, 0x48, 0x82, 0xed, 0xda, 0xe1, 0xf9, 0x10, 0x78, 0x12, 0x5f, 0x58, 0xf0, 0x74, - 0x55, 0x55, 0x70, 0x9b, 0xd7, 0xcf, 0x90, 0x16, 0xbe, 0x6c, 0x6e, 0x77, 0xa5, 0xbc, 0x6e, 0xdf, 0xea, 0xa8, 0xf7, - 0xbb, 0x9a, 0xbb, 0x4a, 0x0b, 0xa4, 0x0e, 0xda, 0xfc, 0x5e, 0xc9, 0x75, 0xf5, 0xf6, 0x6b, 0xe1, 0xb9, 0x12, 0x4c, - 0x77, 0xb5, 0x96, 0x2c, 0x84, 0x5a, 0xef, 0xc8, 0xb7, 0xa5, 0xc9, 0x14, 0x4e, 0xa7, 0xb2, 0x22, 0xea, 0x9e, 0xec, - 0x2b, 0x4d, 0x17, 0xb8, 0x87, 0x4c, 0xe9, 0x50, 0x19, 0x14, 0xba, 0x92, 0xde, 0x0a, 0xea, 0x97, 0xce, 0xad, 0x80, - 0x4f, 0xc7, 0xf5, 0xfe, 0x1f, 0xe7, 0xe0, 0x1c, 0x12, 0xcf, 0x89, 0x00, 0x00}; + 0xe4, 0x2b, 0x20, 0x44, 0x5b, 0x41, 0x6f, 0x36, 0xc1, 0x8b, 0x2e, 0x96, 0x41, 0x36, 0x19, 0x59, 0x76, 0xe2, 0x64, + 0xfb, 0x16, 0xcb, 0x4e, 0x76, 0xc2, 0x70, 0x4b, 0x10, 0xd1, 0x24, 0xda, 0x06, 0x01, 0x06, 0x68, 0x52, 0x52, 0x48, + 0x9c, 0x9a, 0x0f, 0x98, 0xaa, 0xa9, 0x9a, 0xa7, 0x79, 0x99, 0x9a, 0xf3, 0x30, 0x1f, 0x31, 0xcf, 0xe7, 0x53, 0xce, + 0x0f, 0xcc, 0x7c, 0xc2, 0xd4, 0xea, 0x0b, 0xd0, 0xe0, 0x45, 0x56, 0x2e, 0xe7, 0x9c, 0x29, 0x97, 0x6d, 0xa2, 0xd1, + 0x97, 0xd5, 0xab, 0x57, 0xaf, 0x7b, 0x37, 0xba, 0x7b, 0x41, 0x32, 0xe2, 0x77, 0x33, 0x6a, 0x85, 0x7c, 0x1a, 0xf5, + 0xba, 0xea, 0x5f, 0xea, 0x07, 0xbd, 0x6e, 0xc4, 0xe2, 0x8f, 0x56, 0x4a, 0x23, 0xc2, 0x46, 0x49, 0x6c, 0x85, 0x29, + 0x1d, 0x93, 0xc0, 0xe7, 0xbe, 0xc7, 0xa6, 0xfe, 0x84, 0x5a, 0x8d, 0x5e, 0x77, 0x4a, 0xb9, 0x6f, 0x8d, 0x42, 0x3f, + 0xcd, 0x28, 0x27, 0xef, 0xdf, 0x7d, 0x55, 0x3f, 0xed, 0x75, 0xb3, 0x51, 0xca, 0x66, 0xdc, 0x82, 0x2e, 0xc9, 0x34, + 0x09, 0xe6, 0x11, 0xed, 0x35, 0x1a, 0x37, 0x37, 0x37, 0xee, 0x87, 0xec, 0xb3, 0x51, 0x12, 0x67, 0xdc, 0x7a, 0x41, + 0x6e, 0x58, 0x1c, 0x24, 0x37, 0x98, 0x71, 0xf2, 0xc2, 0xbd, 0x08, 0xfd, 0x20, 0xb9, 0x79, 0x9b, 0x24, 0xfc, 0xe0, + 0xc0, 0x91, 0x8f, 0x77, 0xe7, 0x17, 0x17, 0x84, 0x90, 0x45, 0xc2, 0x02, 0xab, 0xb9, 0x5a, 0x95, 0x85, 0x6e, 0xec, + 0x73, 0xb6, 0xa0, 0xb2, 0x09, 0x3a, 0x38, 0xb0, 0xfd, 0x20, 0x99, 0x71, 0x1a, 0x5c, 0xf0, 0xbb, 0x88, 0x5e, 0x84, + 0x94, 0xf2, 0xcc, 0x66, 0xb1, 0xf5, 0x34, 0x19, 0xcd, 0xa7, 0x34, 0xe6, 0xee, 0x2c, 0x4d, 0x78, 0x02, 0x90, 0x1c, + 0x1c, 0xd8, 0x29, 0x9d, 0x45, 0xfe, 0x88, 0xc2, 0xfb, 0xf3, 0x8b, 0x8b, 0xb2, 0x45, 0x59, 0x09, 0x67, 0x9c, 0x5c, + 0xdc, 0x4d, 0xaf, 0x93, 0xc8, 0x41, 0xd8, 0xe7, 0x24, 0xa6, 0x37, 0xd6, 0x0f, 0xd4, 0xff, 0xf8, 0xd2, 0x9f, 0x75, + 0x46, 0x91, 0x9f, 0x65, 0xd6, 0x2d, 0x5f, 0x8a, 0x29, 0xa4, 0xf3, 0x11, 0x4f, 0x52, 0x87, 0x63, 0x8a, 0x19, 0x5a, + 0xb2, 0xb1, 0xc3, 0x43, 0x96, 0xb9, 0x97, 0xfb, 0xa3, 0x2c, 0x7b, 0x4b, 0xb3, 0x79, 0xc4, 0xf7, 0xc9, 0x5e, 0x13, + 0xb3, 0x3d, 0x42, 0x32, 0x8e, 0x78, 0x98, 0x26, 0x37, 0xd6, 0xb3, 0x34, 0x4d, 0x52, 0xc7, 0x3e, 0xbf, 0xb8, 0x90, + 0x35, 0x2c, 0x96, 0x59, 0x71, 0xc2, 0xad, 0xa2, 0x3f, 0xff, 0x3a, 0xa2, 0xae, 0xf5, 0x3e, 0xa3, 0xd6, 0xd5, 0x3c, + 0xce, 0xfc, 0x31, 0x3d, 0xbf, 0xb8, 0xb8, 0xb2, 0x92, 0xd4, 0xba, 0x1a, 0x65, 0xd9, 0x95, 0xc5, 0xe2, 0x8c, 0x53, + 0x3f, 0x70, 0x6d, 0xd4, 0x11, 0x83, 0x8d, 0xb2, 0xec, 0x1d, 0xbd, 0xe5, 0x84, 0x63, 0xf1, 0xc8, 0x09, 0xcd, 0x27, + 0x94, 0x5b, 0x59, 0x31, 0x2f, 0x07, 0x2d, 0x23, 0xca, 0x2d, 0x4e, 0xc4, 0xfb, 0xa4, 0x23, 0x71, 0x4f, 0xe5, 0x23, + 0xef, 0xb0, 0xb1, 0xc3, 0xf8, 0xc1, 0x01, 0x2f, 0xf0, 0x8c, 0xe4, 0xd4, 0x2c, 0x46, 0xe8, 0x9e, 0x2e, 0x3b, 0x38, + 0xa0, 0x6e, 0x44, 0xe3, 0x09, 0x0f, 0x09, 0x21, 0xad, 0x0e, 0x3b, 0x38, 0x70, 0x38, 0xf1, 0xb9, 0x3b, 0xa1, 0xdc, + 0xa1, 0x08, 0xe1, 0xb2, 0xf5, 0xc1, 0x81, 0x23, 0x91, 0x90, 0x10, 0x89, 0xb8, 0x0a, 0x8e, 0x91, 0xab, 0xb0, 0x7f, + 0x71, 0x17, 0x8f, 0x1c, 0x13, 0x7e, 0x84, 0xd9, 0xc1, 0x81, 0xcf, 0xdd, 0x0c, 0x7a, 0xc4, 0x1c, 0xa1, 0x3c, 0xa5, + 0x7c, 0x9e, 0xc6, 0x16, 0xcf, 0x79, 0x72, 0xc1, 0x53, 0x16, 0x4f, 0x1c, 0xb4, 0xd4, 0x65, 0x46, 0xc3, 0x3c, 0x97, + 0xe0, 0xbe, 0xe2, 0x24, 0x26, 0x3d, 0x18, 0xf1, 0x96, 0x3b, 0xb0, 0x8a, 0xc9, 0xd8, 0x8a, 0x09, 0xb1, 0x33, 0xd1, + 0xd6, 0xee, 0xc7, 0x5e, 0x5c, 0xb3, 0x6d, 0x2c, 0xa1, 0xc4, 0x19, 0x47, 0xf8, 0x35, 0x71, 0x62, 0xec, 0xba, 0x2e, + 0x47, 0xa4, 0xb7, 0xd4, 0x58, 0x89, 0x8d, 0x79, 0xf6, 0xe3, 0x41, 0x73, 0xe8, 0x71, 0x37, 0xa5, 0xc1, 0x7c, 0x44, + 0x1d, 0x87, 0xe1, 0x0c, 0xa7, 0x88, 0xf4, 0x58, 0xcd, 0x49, 0x48, 0x0f, 0x96, 0x3b, 0xa9, 0xae, 0x35, 0x21, 0x7b, + 0x4d, 0xa4, 0x60, 0x4c, 0x34, 0x80, 0x80, 0x61, 0x05, 0x4f, 0x42, 0x88, 0x1d, 0xcf, 0xa7, 0xd7, 0x34, 0xb5, 0x8b, + 0x6a, 0x9d, 0x0a, 0x59, 0xcc, 0x33, 0x6a, 0x8d, 0xb2, 0xcc, 0x1a, 0xcf, 0xe3, 0x11, 0x67, 0x49, 0x6c, 0xd9, 0xb5, + 0xa4, 0x66, 0x4b, 0x72, 0x28, 0xa8, 0xc1, 0x46, 0x39, 0x72, 0x32, 0x54, 0x8b, 0x07, 0x69, 0xad, 0x35, 0xc4, 0x00, + 0x25, 0xea, 0xa8, 0xfe, 0x14, 0x02, 0x28, 0x8e, 0x61, 0x8e, 0x39, 0x7e, 0xcb, 0x61, 0x96, 0x62, 0x8a, 0x8c, 0xf7, + 0x63, 0x77, 0x73, 0xa3, 0x10, 0xee, 0x4e, 0xfd, 0x99, 0x43, 0x49, 0x8f, 0x0a, 0xe2, 0xf2, 0xe3, 0x11, 0xc0, 0x5a, + 0x59, 0xb7, 0x3e, 0xf5, 0xa8, 0x5b, 0x92, 0x14, 0xf2, 0xb8, 0x3b, 0x4e, 0xd2, 0x67, 0xfe, 0x28, 0x84, 0x76, 0x05, + 0xc1, 0x04, 0x7a, 0xbf, 0x8d, 0x52, 0xea, 0x73, 0xfa, 0x2c, 0xa2, 0xf0, 0xe4, 0xd8, 0xa2, 0xa5, 0x8d, 0x70, 0x46, + 0x5e, 0xb8, 0x11, 0xe3, 0xaf, 0x92, 0x78, 0x44, 0x3b, 0x99, 0x41, 0x5d, 0x0c, 0xd6, 0xfd, 0x8c, 0xf3, 0x94, 0x5d, + 0xcf, 0x39, 0x75, 0xec, 0x18, 0x6a, 0xd8, 0x38, 0x43, 0x98, 0xb9, 0x9c, 0xde, 0xf2, 0xf3, 0x24, 0xe6, 0x34, 0xe6, + 0x84, 0x6a, 0xa4, 0xe2, 0xd8, 0xf5, 0x67, 0x33, 0x1a, 0x07, 0xe7, 0x21, 0x8b, 0x02, 0x87, 0xa1, 0x1c, 0xe5, 0x38, + 0xe4, 0x04, 0xe6, 0x48, 0x7a, 0xb1, 0x07, 0xff, 0xec, 0x9e, 0x8d, 0xc3, 0x49, 0x4f, 0x6c, 0x0a, 0x4a, 0x6c, 0xbb, + 0x33, 0x4e, 0x52, 0x47, 0xcd, 0xc0, 0x4a, 0xc6, 0x16, 0x87, 0x31, 0xde, 0xce, 0x23, 0x9a, 0x21, 0x5a, 0x23, 0xac, + 0x58, 0x46, 0x85, 0xe0, 0x57, 0x40, 0xf1, 0x39, 0x72, 0x62, 0xe4, 0xc5, 0x9d, 0x85, 0x9f, 0x5a, 0x3f, 0xa8, 0x1d, + 0xf5, 0x54, 0x73, 0xb3, 0x11, 0x27, 0x4f, 0x5d, 0x9e, 0xce, 0x33, 0x4e, 0x83, 0x77, 0x77, 0x33, 0x9a, 0xe1, 0xe7, + 0x9c, 0x8c, 0x78, 0x7f, 0xc4, 0x5d, 0x3a, 0x9d, 0xf1, 0xbb, 0x0b, 0xc1, 0x18, 0x3d, 0xdb, 0xc6, 0x01, 0xd4, 0x4c, + 0xa9, 0x3f, 0x02, 0x66, 0xa6, 0xb0, 0xf5, 0x26, 0x89, 0xee, 0xc6, 0x2c, 0x8a, 0x2e, 0xe6, 0xb3, 0x59, 0x92, 0x72, + 0xfc, 0x77, 0xb2, 0xe4, 0x49, 0x89, 0x1a, 0x58, 0xcb, 0x65, 0x76, 0xc3, 0xf8, 0x28, 0x74, 0x38, 0x5a, 0x8e, 0xfc, + 0x8c, 0x5a, 0x4f, 0x92, 0x24, 0xa2, 0x3e, 0x4c, 0x3a, 0xee, 0x3f, 0xe7, 0x5e, 0x3c, 0x8f, 0xa2, 0xce, 0x75, 0x4a, + 0xfd, 0x8f, 0x1d, 0xf1, 0xfa, 0xf5, 0xf5, 0x07, 0x3a, 0xe2, 0x9e, 0xf8, 0x7d, 0x96, 0xa6, 0xfe, 0x1d, 0x54, 0x24, + 0x04, 0xaa, 0xf5, 0x63, 0xef, 0xdb, 0x8b, 0xd7, 0xaf, 0x5c, 0xb9, 0x49, 0xd8, 0xf8, 0xce, 0x89, 0x8b, 0x8d, 0x17, + 0xe7, 0x78, 0x9c, 0x26, 0xd3, 0xb5, 0xa1, 0x25, 0xd6, 0xe2, 0xce, 0x0e, 0x10, 0x28, 0x89, 0xf7, 0x64, 0xd7, 0x26, + 0x04, 0xaf, 0x04, 0xcd, 0xc3, 0x4b, 0xa2, 0xc7, 0x9d, 0x47, 0x91, 0x27, 0x8b, 0x9d, 0x18, 0xdd, 0x0f, 0x2d, 0x4f, + 0xef, 0x96, 0x94, 0x08, 0x38, 0x67, 0x20, 0x61, 0x00, 0xc6, 0x91, 0xcf, 0x47, 0xe1, 0x92, 0x8a, 0xce, 0x72, 0x0d, + 0x31, 0xcd, 0x73, 0x7c, 0x56, 0xd0, 0x3b, 0x07, 0x40, 0x04, 0xa3, 0x22, 0x7c, 0xb5, 0x82, 0x09, 0x23, 0xfc, 0x13, + 0x59, 0xfa, 0x7a, 0x3e, 0xde, 0x5e, 0x13, 0xc3, 0xbe, 0xf4, 0x24, 0x77, 0xc1, 0xa3, 0x24, 0x5e, 0xd0, 0x94, 0xd3, + 0xd4, 0xfb, 0x3b, 0x4e, 0xe9, 0x38, 0x02, 0x28, 0xf6, 0x5a, 0x38, 0xf4, 0xb3, 0xf3, 0xd0, 0x8f, 0x27, 0x34, 0xf0, + 0xce, 0x78, 0x8e, 0x39, 0x27, 0xf6, 0x98, 0xc5, 0x7e, 0xc4, 0x7e, 0xa5, 0x81, 0xad, 0xc4, 0xc1, 0x33, 0x8b, 0xde, + 0x72, 0x1a, 0x07, 0x99, 0xf5, 0xfc, 0xdd, 0xcb, 0x17, 0x6a, 0x21, 0x2b, 0x12, 0x02, 0x2d, 0xb3, 0xf9, 0x8c, 0xa6, + 0x0e, 0xc2, 0x4a, 0x42, 0x3c, 0x63, 0x82, 0x3b, 0xbe, 0xf4, 0x67, 0xb2, 0x84, 0x65, 0xef, 0x67, 0x81, 0xcf, 0xe9, + 0x1b, 0x1a, 0x07, 0x2c, 0x9e, 0x90, 0xbd, 0x96, 0x2c, 0x0f, 0x7d, 0xf5, 0x22, 0x28, 0x8a, 0x2e, 0xf7, 0x9f, 0x45, + 0x62, 0xe2, 0xc5, 0xe3, 0xdc, 0x41, 0x79, 0xc6, 0x7d, 0xce, 0x46, 0x96, 0x1f, 0x04, 0xdf, 0xc4, 0x8c, 0x33, 0x01, + 0x60, 0x0a, 0xeb, 0x03, 0x34, 0x4a, 0xa5, 0xac, 0xd0, 0x80, 0x3b, 0x08, 0x3b, 0x8e, 0x92, 0x00, 0x21, 0x52, 0x0b, + 0x76, 0x70, 0x50, 0xf2, 0xfb, 0x3e, 0xf5, 0xe4, 0x4b, 0x32, 0x18, 0x22, 0x77, 0x36, 0xcf, 0x60, 0xa5, 0xf5, 0x10, + 0x20, 0x5e, 0x92, 0xeb, 0x8c, 0xa6, 0x0b, 0x1a, 0x14, 0xd4, 0x91, 0x39, 0x68, 0xb9, 0x36, 0x86, 0xda, 0x17, 0x9c, + 0x0c, 0x86, 0x1d, 0x93, 0x71, 0x53, 0x45, 0xe8, 0x69, 0x32, 0xa3, 0x29, 0x67, 0x34, 0x2b, 0x78, 0x89, 0x03, 0x62, + 0xb4, 0xe0, 0x27, 0x19, 0xd1, 0xf3, 0x9b, 0x39, 0x0c, 0x53, 0x54, 0xe1, 0x18, 0x5a, 0xd2, 0x3e, 0x5b, 0x08, 0x91, + 0x91, 0x61, 0x86, 0x30, 0x97, 0x90, 0x66, 0x08, 0xe5, 0x08, 0x73, 0x0d, 0xae, 0xe4, 0x45, 0x6a, 0xb4, 0x3b, 0x90, + 0xd5, 0xe4, 0x27, 0x21, 0xab, 0x81, 0xa3, 0xf9, 0x9c, 0x1e, 0x1c, 0x38, 0xd4, 0x2d, 0xa8, 0x82, 0xec, 0xb5, 0xd4, + 0x1a, 0x19, 0xc8, 0xda, 0x01, 0x36, 0x0c, 0xcc, 0x31, 0x45, 0x78, 0x8f, 0xba, 0x71, 0x72, 0x36, 0x1a, 0xd1, 0x2c, + 0x4b, 0xd2, 0x83, 0x83, 0x3d, 0x51, 0xbf, 0x50, 0x27, 0x60, 0x0d, 0x5f, 0xdf, 0xc4, 0x25, 0x04, 0xa8, 0x14, 0xb1, + 0x4a, 0x30, 0x70, 0x10, 0x54, 0x42, 0xe3, 0xb0, 0xfb, 0x5a, 0xf3, 0xf0, 0xec, 0xcb, 0x4b, 0xbb, 0xc6, 0xb1, 0x42, + 0xc3, 0x84, 0xea, 0xa1, 0xef, 0x9e, 0x52, 0xa9, 0x5b, 0x09, 0xcd, 0x63, 0x03, 0x33, 0x72, 0x03, 0xb9, 0x01, 0x1d, + 0xb3, 0xd8, 0x98, 0x76, 0x05, 0x24, 0xcc, 0x71, 0x86, 0x72, 0x63, 0x41, 0xb7, 0x76, 0x2d, 0x94, 0x1a, 0xb9, 0x72, + 0xcb, 0x89, 0x50, 0x24, 0x8c, 0x65, 0x1c, 0xd0, 0x61, 0x8e, 0x05, 0xea, 0xf5, 0x6c, 0x52, 0x01, 0xe8, 0x80, 0x0f, + 0x3b, 0xea, 0x3d, 0xc9, 0x24, 0xe6, 0x52, 0xfa, 0xcb, 0x9c, 0x66, 0x5c, 0xd2, 0xb1, 0xc3, 0x71, 0x8a, 0x19, 0xca, + 0x61, 0xbf, 0x8d, 0xd9, 0x64, 0x9e, 0x82, 0xbe, 0x03, 0x7b, 0x91, 0xc6, 0xf3, 0x29, 0xd5, 0x4f, 0xdb, 0x60, 0x7b, + 0x3d, 0x03, 0x89, 0x98, 0x01, 0x4d, 0xdf, 0x4f, 0x4e, 0x00, 0x2b, 0x47, 0xab, 0xd5, 0x4f, 0xba, 0x93, 0x72, 0x29, + 0x0b, 0x1d, 0x6d, 0x7d, 0x4d, 0x38, 0x52, 0x12, 0x79, 0xaf, 0x25, 0xc1, 0xe7, 0x7c, 0x48, 0xf6, 0x9a, 0x05, 0x0d, + 0x2b, 0xac, 0x4a, 0x70, 0x24, 0x12, 0x5f, 0xcb, 0xae, 0x90, 0x10, 0xf0, 0x15, 0x72, 0x71, 0xc3, 0x0d, 0x4a, 0x0d, + 0xc9, 0x00, 0x54, 0x0d, 0x37, 0x1c, 0xee, 0x22, 0x27, 0xcd, 0x0f, 0x1c, 0xbe, 0xf9, 0xae, 0x64, 0x1b, 0x8b, 0x2a, + 0xdb, 0x58, 0x9b, 0x86, 0x3d, 0x2b, 0x9a, 0xd8, 0x05, 0x95, 0xa9, 0x8d, 0x5e, 0xbe, 0xc2, 0x4c, 0x00, 0x53, 0x4e, + 0xc9, 0xe8, 0xe2, 0x95, 0x3f, 0xa5, 0x99, 0x43, 0x11, 0xde, 0x55, 0x41, 0x92, 0x27, 0x54, 0x19, 0x1a, 0x92, 0x33, + 0x03, 0xc9, 0xc9, 0x90, 0x54, 0xcc, 0xaa, 0x1b, 0x2e, 0xc3, 0x74, 0x90, 0x0d, 0x4b, 0x7d, 0xce, 0x98, 0xbc, 0x10, + 0xc9, 0x8a, 0xbe, 0x35, 0xfe, 0x64, 0x99, 0x44, 0x9a, 0xd0, 0x1b, 0x32, 0x84, 0xf7, 0x9a, 0xeb, 0x2b, 0xa9, 0x6b, + 0x95, 0x73, 0x1c, 0x0c, 0x61, 0x1d, 0x84, 0xc4, 0x70, 0x59, 0x26, 0xfe, 0xaf, 0xec, 0x34, 0x40, 0xdb, 0x05, 0x10, + 0x86, 0x3b, 0x8e, 0x7c, 0xee, 0xb4, 0x1a, 0x4d, 0x50, 0x46, 0x17, 0x14, 0x04, 0x0a, 0x42, 0x9b, 0x53, 0xa1, 0xee, + 0x3c, 0xce, 0x42, 0x36, 0xe6, 0x4e, 0xc8, 0x05, 0x4b, 0xa1, 0x51, 0x46, 0x2d, 0x5e, 0x51, 0x89, 0x05, 0xbb, 0x09, + 0x81, 0xd8, 0x0a, 0xfd, 0x8b, 0x6a, 0x48, 0x05, 0xdb, 0x02, 0xee, 0x50, 0xaa, 0xd3, 0x25, 0x97, 0xd1, 0xb5, 0x19, + 0xa8, 0x8c, 0xad, 0xbe, 0xec, 0xd1, 0x53, 0xcc, 0x80, 0x19, 0x5a, 0x2b, 0xf3, 0x4c, 0x0e, 0xa1, 0x0a, 0xb9, 0xcb, + 0x93, 0x17, 0xc9, 0x0d, 0x4d, 0xcf, 0x7d, 0x00, 0xde, 0x93, 0xcd, 0x73, 0x29, 0x08, 0x04, 0xbf, 0xe7, 0x1d, 0x4d, + 0x2f, 0x97, 0x62, 0xe2, 0x6f, 0xd2, 0x64, 0xca, 0x32, 0x0a, 0xca, 0x9a, 0xc4, 0x7f, 0x0c, 0xfb, 0x4c, 0x6c, 0x48, + 0x10, 0x36, 0xb4, 0xa0, 0xaf, 0xb3, 0x17, 0x55, 0xfa, 0xba, 0xdc, 0x7f, 0x36, 0xd1, 0x0c, 0xb0, 0xba, 0x8d, 0x11, + 0x76, 0x94, 0x49, 0x61, 0xc8, 0x39, 0x37, 0x44, 0x4a, 0xc2, 0xaf, 0x56, 0xdc, 0xb0, 0xdc, 0x2a, 0xea, 0x22, 0x95, + 0xdb, 0x06, 0xe5, 0x7e, 0x10, 0x80, 0x62, 0x97, 0x26, 0x51, 0x64, 0x88, 0x2a, 0xcc, 0x3a, 0x85, 0x70, 0xba, 0xdc, + 0x7f, 0x76, 0x71, 0x9f, 0x7c, 0x82, 0xf7, 0xa6, 0x88, 0xd2, 0x80, 0xc6, 0x01, 0x4d, 0xc1, 0x92, 0x34, 0x56, 0x4b, + 0x49, 0xd9, 0xf3, 0x24, 0x8e, 0xe9, 0x88, 0xd3, 0x00, 0x0c, 0x15, 0x46, 0xb8, 0x1b, 0x26, 0x19, 0x2f, 0x0a, 0x4b, + 0xe8, 0x99, 0x01, 0x3d, 0x73, 0x47, 0x7e, 0x14, 0x39, 0xd2, 0x28, 0x99, 0x26, 0x0b, 0xba, 0x05, 0xea, 0x4e, 0x05, + 0xe4, 0xa2, 0x1b, 0x6a, 0x74, 0x43, 0xdd, 0x6c, 0x16, 0xb1, 0x11, 0x2d, 0x44, 0xd7, 0x85, 0xcb, 0xe2, 0x80, 0xde, + 0x02, 0x1f, 0x41, 0xbd, 0x5e, 0xaf, 0x89, 0x5b, 0x28, 0x97, 0x08, 0x5f, 0x6e, 0x20, 0xf6, 0x1e, 0xa1, 0x09, 0x44, + 0x46, 0x7a, 0xcb, 0x6d, 0xfc, 0x80, 0x22, 0x43, 0x52, 0x32, 0x6d, 0x5c, 0x49, 0xee, 0x8c, 0x70, 0x40, 0x23, 0xca, + 0xa9, 0xe6, 0xe6, 0xa0, 0x42, 0xcb, 0xad, 0xfb, 0xb6, 0xc0, 0x5f, 0x41, 0x4e, 0x7a, 0x97, 0xe9, 0x35, 0xcf, 0x0a, + 0x63, 0xbd, 0x5c, 0x9e, 0x12, 0xdb, 0x7d, 0x2e, 0x97, 0xc7, 0xe7, 0xdc, 0x1f, 0x85, 0xd2, 0x4a, 0x77, 0x36, 0xa6, + 0x54, 0xf6, 0xa1, 0x38, 0x7b, 0xb1, 0x89, 0xde, 0x6a, 0x30, 0xb7, 0xa1, 0xe0, 0x42, 0x31, 0x05, 0x0a, 0x86, 0x9f, + 0x5c, 0xb6, 0x73, 0x3f, 0x8a, 0xae, 0xfd, 0xd1, 0xc7, 0x2a, 0xf5, 0x97, 0x64, 0x40, 0xd6, 0xb9, 0xb1, 0xf1, 0xca, + 0x60, 0x59, 0xe6, 0xbc, 0x35, 0x97, 0xae, 0x6c, 0x14, 0x67, 0xaf, 0x59, 0x92, 0x7d, 0x75, 0xa1, 0x77, 0x52, 0xbb, + 0x80, 0x88, 0xa9, 0x99, 0x39, 0xc0, 0x05, 0x3e, 0x49, 0x71, 0x9a, 0x1f, 0x28, 0xba, 0x03, 0x73, 0x23, 0x5f, 0x03, + 0x84, 0xa3, 0x65, 0x1e, 0xb0, 0x6c, 0x37, 0x06, 0xfe, 0x14, 0x28, 0x9f, 0x1a, 0x23, 0x3c, 0x14, 0xd0, 0x82, 0xc7, + 0x29, 0xad, 0xb9, 0x80, 0x4c, 0xe9, 0x13, 0x9a, 0xd1, 0xfc, 0x0d, 0x74, 0x17, 0x41, 0xef, 0xaf, 0xe5, 0x2b, 0xd0, + 0xca, 0x00, 0x8a, 0xac, 0x63, 0xaa, 0x13, 0x15, 0x0a, 0x50, 0x3c, 0x95, 0x09, 0x91, 0x9b, 0x56, 0xec, 0x47, 0xa5, + 0xb1, 0x4b, 0x13, 0x5c, 0xb1, 0xdc, 0x84, 0x38, 0x8e, 0x93, 0x81, 0x09, 0xa7, 0x55, 0xfb, 0x72, 0x12, 0xd9, 0xc6, + 0x24, 0x32, 0xd7, 0xb0, 0xb3, 0x50, 0x49, 0xcb, 0x46, 0x73, 0xef, 0xef, 0xc8, 0xac, 0x04, 0xea, 0xaa, 0x0b, 0xfc, + 0x19, 0x15, 0xec, 0x36, 0x22, 0x1c, 0x27, 0xca, 0xc6, 0x51, 0x94, 0x06, 0x0c, 0xa3, 0x6c, 0x92, 0x22, 0xb9, 0x35, + 0x2a, 0xf6, 0x6e, 0x8a, 0x13, 0xb4, 0xa6, 0xdb, 0xe7, 0xb9, 0xc2, 0x11, 0x45, 0x6a, 0x6d, 0x2a, 0x4a, 0xb1, 0x81, + 0x15, 0x9c, 0x12, 0xa5, 0x08, 0x4b, 0xbd, 0x67, 0x1d, 0x37, 0x45, 0xbf, 0x7b, 0x84, 0xa4, 0x25, 0x6a, 0x2a, 0x1a, + 0xa5, 0x56, 0xad, 0x52, 0x84, 0x43, 0xad, 0x93, 0x26, 0xe5, 0xbc, 0x09, 0xb1, 0xb5, 0x43, 0xc2, 0xee, 0x2f, 0x2b, + 0x56, 0xa1, 0x67, 0x54, 0xcb, 0x3d, 0x60, 0xa9, 0xc9, 0x36, 0x74, 0x6f, 0xa3, 0x99, 0x4a, 0x3f, 0x06, 0xc2, 0x13, + 0x13, 0xe1, 0x06, 0x66, 0x53, 0xc9, 0xb9, 0xd2, 0x21, 0x09, 0xab, 0x6d, 0x1d, 0x8a, 0x13, 0xb9, 0x0e, 0x1b, 0x48, + 0x5c, 0x57, 0x3d, 0x05, 0x09, 0x82, 0x0d, 0x9b, 0x81, 0x72, 0x67, 0xca, 0x07, 0x07, 0x60, 0x67, 0xab, 0xd5, 0x06, + 0xd1, 0x6d, 0xd5, 0x40, 0x91, 0x5b, 0xda, 0x85, 0xab, 0xd5, 0x19, 0x47, 0x8e, 0xd2, 0x7d, 0x31, 0x45, 0x7d, 0xcd, + 0x71, 0xcf, 0x5e, 0x40, 0x2d, 0xa1, 0x8a, 0x96, 0x25, 0x85, 0xd1, 0x50, 0xa5, 0xd9, 0xea, 0x3a, 0x71, 0x83, 0x6d, + 0x9f, 0x6f, 0x70, 0x2f, 0x51, 0xa8, 0xc4, 0x74, 0x39, 0xe5, 0x73, 0xd5, 0x35, 0x43, 0x08, 0x79, 0x99, 0xb0, 0x63, + 0xf6, 0xb6, 0x99, 0x96, 0x07, 0x07, 0x99, 0xd1, 0xd1, 0x65, 0xc1, 0x26, 0x3e, 0x38, 0x20, 0x92, 0xb3, 0xbb, 0x58, + 0xe8, 0x2e, 0x1f, 0xb4, 0x10, 0xda, 0x30, 0x4c, 0x9b, 0x1d, 0x30, 0xc8, 0xfd, 0x1b, 0x9f, 0x71, 0xab, 0xe8, 0x45, + 0x1a, 0xe4, 0x0e, 0x45, 0x4b, 0xa5, 0x6a, 0xb8, 0x29, 0x05, 0xe5, 0x11, 0x78, 0x82, 0x56, 0xa1, 0x25, 0xdd, 0x8f, + 0x42, 0x0a, 0xbe, 0x60, 0xad, 0x45, 0x14, 0x96, 0xe1, 0x9e, 0x92, 0x22, 0xaa, 0xe3, 0xed, 0xb0, 0xe7, 0xeb, 0xcd, + 0x2b, 0x96, 0xc0, 0x8c, 0xa6, 0xe3, 0x24, 0x9d, 0xea, 0x77, 0xf9, 0xda, 0xb3, 0xe2, 0x8c, 0x6c, 0xec, 0x6c, 0xed, + 0x5b, 0xe9, 0xff, 0x9d, 0x35, 0xb3, 0xbb, 0x34, 0xd8, 0x2b, 0xa2, 0xb4, 0x90, 0xbe, 0xd2, 0x25, 0xa8, 0x29, 0x33, + 0x33, 0x0d, 0x7c, 0xe5, 0x4f, 0xed, 0x48, 0x9f, 0xc9, 0x5e, 0xab, 0x53, 0x58, 0x7d, 0x9a, 0x1a, 0x3a, 0xd2, 0xb7, + 0xa1, 0x44, 0x6a, 0x32, 0x8f, 0x02, 0x05, 0x2c, 0x43, 0x98, 0x2a, 0x3a, 0xba, 0x61, 0x51, 0x54, 0x96, 0xfe, 0x16, + 0xbe, 0x9e, 0x29, 0xbe, 0x9e, 0x6a, 0xbe, 0x0e, 0x9c, 0x02, 0xf8, 0xba, 0xec, 0xae, 0x6c, 0x9e, 0x6e, 0xec, 0xce, + 0x54, 0x72, 0xf4, 0x4c, 0x58, 0xd2, 0x30, 0xde, 0x5c, 0x43, 0x80, 0x0a, 0xcd, 0xeb, 0xa3, 0xa3, 0xfc, 0x30, 0x60, + 0x02, 0x4a, 0x2f, 0x26, 0x35, 0x9d, 0x14, 0x1f, 0x1d, 0x84, 0xb3, 0x9c, 0x16, 0x94, 0x7d, 0xf6, 0x0c, 0xfc, 0x74, + 0xc6, 0x74, 0x40, 0x88, 0x89, 0xe2, 0xdf, 0xa4, 0x44, 0xe9, 0xd9, 0x31, 0x35, 0xbb, 0x4c, 0xcf, 0x0e, 0x38, 0x7d, + 0x39, 0xbb, 0xe0, 0x7e, 0x5e, 0x2f, 0xa6, 0xc7, 0x8a, 0xe9, 0x95, 0xeb, 0xbd, 0x5a, 0x39, 0x6b, 0x25, 0xe0, 0xc2, + 0x57, 0x26, 0x4a, 0x5a, 0xf4, 0x0e, 0x3c, 0xc0, 0xc4, 0x0c, 0x14, 0xe4, 0x72, 0xd2, 0x85, 0x88, 0x7b, 0xf1, 0x29, + 0x17, 0x8f, 0xf0, 0xd4, 0xcb, 0xf6, 0xe7, 0xc9, 0x74, 0x06, 0xda, 0xd8, 0x1a, 0x49, 0x4f, 0xa8, 0x1a, 0xb0, 0x7c, + 0x9f, 0x6f, 0x29, 0xab, 0xb4, 0x11, 0xfb, 0xb1, 0x42, 0x4d, 0x85, 0xc5, 0xbc, 0xd7, 0xcc, 0xe7, 0x45, 0x51, 0xc1, + 0x38, 0xb6, 0xb9, 0x55, 0xce, 0xd7, 0x9d, 0x32, 0xfa, 0xc5, 0x6b, 0x87, 0x49, 0x3e, 0xcc, 0x80, 0xd7, 0x19, 0xec, + 0x47, 0x93, 0xbb, 0xb9, 0xfe, 0x79, 0x89, 0x9c, 0x65, 0xbe, 0x86, 0xbe, 0x65, 0x9e, 0x3f, 0x53, 0x56, 0x36, 0x7e, + 0xb6, 0xdb, 0x1c, 0x2e, 0xdf, 0x29, 0x6b, 0x71, 0x30, 0xc4, 0xcf, 0x36, 0x75, 0x47, 0xb2, 0x9c, 0x26, 0x01, 0xf5, + 0xec, 0x64, 0x46, 0x63, 0x3b, 0x07, 0xcf, 0xaa, 0x5a, 0xfc, 0x80, 0x3b, 0xcb, 0xb7, 0x55, 0x17, 0xab, 0xf7, 0x2c, + 0x07, 0x07, 0xd8, 0x0f, 0x9b, 0xce, 0xd7, 0xef, 0x69, 0x9a, 0x09, 0x4d, 0xb4, 0x50, 0x6a, 0x7f, 0x28, 0xe5, 0xd2, + 0x0f, 0xde, 0xce, 0xfa, 0xa5, 0x0d, 0x62, 0xb7, 0xdc, 0x13, 0xf7, 0xd0, 0x46, 0xc2, 0x35, 0xfc, 0xad, 0xda, 0xf1, + 0x1f, 0xb4, 0x6b, 0xf8, 0x82, 0x7c, 0xa8, 0x7a, 0x86, 0xe7, 0x9c, 0x5c, 0xf4, 0x2f, 0xb4, 0xc9, 0x9c, 0x44, 0x6c, + 0x74, 0xe7, 0xd8, 0x11, 0xe3, 0x75, 0x08, 0xbf, 0xd9, 0x78, 0x29, 0x5f, 0x80, 0x57, 0x51, 0xb8, 0xb4, 0x73, 0x6d, + 0xec, 0x61, 0xca, 0x89, 0xbd, 0x1f, 0x31, 0xbe, 0x6f, 0xe3, 0x29, 0xb9, 0x82, 0x1f, 0xfb, 0x4b, 0xe7, 0xa5, 0xcf, + 0x43, 0x37, 0xf5, 0xe3, 0x20, 0x99, 0x3a, 0xa8, 0x66, 0xdb, 0xc8, 0xcd, 0x84, 0xc1, 0xf1, 0x18, 0xe5, 0xfb, 0x57, + 0xf8, 0x19, 0x27, 0x76, 0xdf, 0xae, 0x4d, 0xf1, 0x13, 0x4e, 0xae, 0xba, 0xfb, 0xcb, 0x67, 0x3c, 0xef, 0x5d, 0xe1, + 0xdb, 0xc2, 0x6b, 0x8f, 0xdf, 0x13, 0x07, 0x91, 0xde, 0xad, 0x82, 0xe6, 0x3c, 0x99, 0x4a, 0xef, 0xbd, 0x8d, 0xf0, + 0x3b, 0x11, 0x5b, 0x29, 0xd9, 0x8d, 0x0a, 0xaf, 0xec, 0x11, 0x3b, 0x11, 0x3e, 0x02, 0xfb, 0xe0, 0xc0, 0x28, 0x2b, + 0x74, 0x05, 0x7c, 0xc1, 0x49, 0xc5, 0x22, 0xc7, 0x2f, 0x45, 0x94, 0xe6, 0x82, 0x3b, 0x31, 0xd2, 0xdd, 0x38, 0xda, + 0x17, 0xad, 0xf6, 0x66, 0x3c, 0x90, 0x2e, 0x06, 0x97, 0x71, 0x9a, 0xfa, 0x3c, 0x49, 0x87, 0xc8, 0xd4, 0x3f, 0xf0, + 0xdf, 0xc8, 0xd5, 0xc0, 0xfa, 0x4f, 0x9f, 0xfd, 0x3c, 0xfe, 0x39, 0x1d, 0x5e, 0xe1, 0x37, 0xa4, 0xd1, 0x75, 0xfa, + 0x9e, 0xb3, 0x57, 0xaf, 0xaf, 0x7e, 0x6e, 0x0c, 0xfe, 0xe1, 0xd7, 0x7f, 0x3d, 0xab, 0xff, 0x34, 0x44, 0x2b, 0xe7, + 0xe7, 0x46, 0x7f, 0xa0, 0x9e, 0x06, 0xff, 0xe8, 0xfd, 0x9c, 0x0d, 0xff, 0x2a, 0x0b, 0xf7, 0x11, 0x6a, 0x4c, 0xf0, + 0x8c, 0x93, 0x46, 0xbd, 0xde, 0x6b, 0x4c, 0xf0, 0x84, 0x93, 0x06, 0xfc, 0x7f, 0x4d, 0xde, 0xd2, 0xc9, 0xb3, 0xdb, + 0x99, 0x73, 0xd5, 0x5b, 0xed, 0x2f, 0xff, 0x96, 0x43, 0xaf, 0x83, 0x7f, 0xfc, 0xfc, 0x73, 0x66, 0x7f, 0xd1, 0x23, + 0x8d, 0x61, 0x0d, 0x39, 0x50, 0xfa, 0x57, 0x22, 0xfe, 0x75, 0xfa, 0xde, 0xe0, 0x1f, 0x0a, 0x0a, 0xfb, 0x8b, 0x9f, + 0xaf, 0xba, 0x3d, 0x32, 0x5c, 0x39, 0xf6, 0xea, 0x0b, 0xb4, 0x42, 0x68, 0xb5, 0x8f, 0xae, 0xb0, 0x3d, 0xb1, 0x11, + 0x5e, 0x70, 0xd2, 0xf8, 0xa2, 0x31, 0xc1, 0x63, 0x4e, 0x1a, 0x76, 0x63, 0x82, 0xcf, 0x39, 0x69, 0xfc, 0xc3, 0xe9, + 0x7b, 0xd2, 0xc9, 0xb6, 0x12, 0xfe, 0x8d, 0x15, 0x04, 0x38, 0xfc, 0x94, 0xfa, 0x2b, 0xce, 0x78, 0x44, 0xd1, 0x7e, + 0x83, 0xe1, 0x8f, 0x02, 0x4d, 0x0e, 0x07, 0x2f, 0x0c, 0x18, 0x77, 0xce, 0xf2, 0x12, 0x16, 0x1b, 0x68, 0x66, 0xdf, + 0x83, 0xc8, 0x0e, 0x38, 0x02, 0x32, 0x8f, 0xe3, 0x85, 0x1f, 0xcd, 0x69, 0xe6, 0xd1, 0x1c, 0xe1, 0x11, 0xf9, 0xc8, + 0x9d, 0x16, 0xc2, 0x2f, 0x38, 0xfc, 0x68, 0x23, 0x7c, 0xae, 0x82, 0x98, 0xb0, 0x93, 0x25, 0x51, 0xc5, 0x89, 0x54, + 0x59, 0x6c, 0x84, 0x67, 0x5b, 0x5e, 0xf2, 0x10, 0xdc, 0x0b, 0x08, 0xef, 0x57, 0x42, 0x9e, 0xf8, 0x86, 0x68, 0x92, + 0x78, 0x97, 0x52, 0xfa, 0x83, 0x1f, 0x7d, 0xa4, 0xa9, 0x73, 0x8b, 0x5b, 0xed, 0xc7, 0x58, 0x78, 0xa1, 0xf7, 0x5a, + 0xa8, 0x53, 0xc4, 0xab, 0x5e, 0x73, 0x19, 0x27, 0x00, 0x29, 0x5b, 0x75, 0xc6, 0xc0, 0x8a, 0xef, 0xc5, 0x1b, 0x1e, + 0xab, 0xd4, 0xbf, 0xb1, 0x51, 0x35, 0x36, 0xca, 0xe2, 0x85, 0x1f, 0xb1, 0xc0, 0xe2, 0x74, 0x3a, 0x8b, 0x7c, 0x4e, + 0x2d, 0x35, 0x5f, 0xcb, 0x87, 0x8e, 0xec, 0x42, 0x67, 0x98, 0x1b, 0x16, 0xe7, 0x5c, 0x07, 0x9d, 0x60, 0xaf, 0x38, + 0x10, 0xa1, 0x52, 0x7a, 0xc7, 0xd3, 0x32, 0x00, 0xb6, 0x1e, 0xe3, 0xab, 0xb7, 0xc0, 0x13, 0x36, 0x14, 0xf2, 0x39, + 0xc3, 0x29, 0x01, 0x29, 0xda, 0xee, 0xdb, 0xdd, 0x6c, 0x31, 0xe9, 0xd9, 0x10, 0x9f, 0x49, 0xc8, 0x1b, 0xe1, 0x18, + 0x82, 0x0a, 0x21, 0x69, 0x76, 0xc2, 0x2e, 0xed, 0x84, 0xb5, 0x9a, 0x56, 0xa2, 0x23, 0x12, 0x0f, 0x42, 0xd9, 0xdc, + 0xc7, 0x01, 0x9e, 0x93, 0x7a, 0x0b, 0x4f, 0x48, 0x53, 0x34, 0xe9, 0x4c, 0xba, 0x91, 0x1a, 0xe6, 0xe0, 0xc0, 0x49, + 0xdc, 0xc8, 0xcf, 0xf8, 0x37, 0x60, 0xed, 0x93, 0x09, 0x0e, 0x48, 0xe2, 0xd2, 0x5b, 0x3a, 0x72, 0x22, 0x84, 0x03, + 0xc5, 0x69, 0x50, 0x07, 0x4d, 0x88, 0x51, 0x0d, 0xac, 0x08, 0xf2, 0xa6, 0x1f, 0x0c, 0x5a, 0x43, 0x42, 0x88, 0xbd, + 0x57, 0xaf, 0xdb, 0xfd, 0x84, 0xcc, 0xb8, 0x07, 0x25, 0x86, 0xae, 0x4c, 0x26, 0x50, 0xd4, 0x36, 0x8a, 0x9c, 0x73, + 0xee, 0x72, 0x9a, 0x71, 0x07, 0x8a, 0xc1, 0xfe, 0xcf, 0x34, 0x61, 0xdb, 0xdd, 0x86, 0x5d, 0x83, 0x52, 0x41, 0x9c, + 0x08, 0x27, 0xe4, 0x1a, 0x79, 0xc1, 0xe0, 0x70, 0x68, 0x0a, 0x00, 0x51, 0x08, 0x83, 0x5f, 0xf7, 0x83, 0x41, 0x53, + 0x0c, 0xde, 0xb3, 0xfb, 0x4e, 0x42, 0x32, 0xa9, 0xa1, 0xf5, 0x33, 0xef, 0x8d, 0x98, 0x2a, 0xf2, 0x14, 0x70, 0x7a, + 0x05, 0x48, 0xbd, 0xed, 0x39, 0x73, 0x73, 0x12, 0x75, 0x18, 0x4c, 0x61, 0x01, 0xfb, 0x04, 0xea, 0xe3, 0x84, 0xc0, + 0x88, 0x65, 0xb3, 0x6b, 0x4f, 0x3d, 0x7f, 0x61, 0x7f, 0xd1, 0x1f, 0x73, 0x6f, 0xc1, 0xe5, 0xf0, 0x63, 0xbe, 0x5a, + 0xc1, 0xff, 0x0b, 0xde, 0x4f, 0xc8, 0xb5, 0x28, 0x9a, 0xa9, 0xa2, 0x09, 0x14, 0xbd, 0xf1, 0x00, 0x54, 0x9c, 0x15, + 0x5a, 0x96, 0x5c, 0x93, 0x05, 0x11, 0xb0, 0x1f, 0x1c, 0xc4, 0x83, 0xb0, 0xd6, 0x1a, 0x82, 0x8b, 0x3f, 0xe5, 0xd9, + 0x0f, 0x8c, 0x87, 0x8e, 0xdd, 0xe8, 0xd9, 0xa8, 0x6f, 0x5b, 0xb0, 0xb4, 0x9d, 0xb4, 0x46, 0x24, 0x86, 0xa3, 0xda, + 0x13, 0xee, 0xcd, 0x7b, 0xa4, 0xd9, 0x77, 0x98, 0x64, 0xe1, 0x3e, 0xc2, 0x91, 0x62, 0x9c, 0x4d, 0x3c, 0x47, 0x35, + 0xca, 0x6b, 0xfa, 0x79, 0x8e, 0x6a, 0xd3, 0xda, 0x02, 0x79, 0x51, 0x6d, 0x5a, 0x73, 0xe6, 0x84, 0x90, 0x7a, 0xbb, + 0x68, 0xa6, 0xc5, 0x5f, 0x88, 0xbc, 0x85, 0xf6, 0x76, 0x0e, 0xc4, 0x76, 0x48, 0x6b, 0x4e, 0x3c, 0xa0, 0xc3, 0xd5, + 0xca, 0xee, 0xf6, 0x7b, 0x36, 0xaa, 0x39, 0x9a, 0xd0, 0x1a, 0x9a, 0xd2, 0x10, 0xc2, 0x6c, 0x98, 0xab, 0x68, 0xd2, + 0xab, 0x4a, 0xe4, 0x68, 0x59, 0x6e, 0x76, 0x83, 0x07, 0xd0, 0xbc, 0x30, 0x64, 0xa4, 0xc2, 0x3a, 0x83, 0x69, 0x6a, + 0x62, 0x4e, 0x49, 0x13, 0x27, 0x44, 0x3b, 0xaf, 0x43, 0xc2, 0x4b, 0x82, 0x8f, 0x48, 0x59, 0x1d, 0x0f, 0x7c, 0x1c, + 0x0c, 0xc9, 0x53, 0x69, 0x90, 0x74, 0xb4, 0x6b, 0x9c, 0x46, 0xe4, 0xd5, 0x5a, 0x04, 0xd7, 0x87, 0xf0, 0xca, 0x8d, + 0x3b, 0x9a, 0xa7, 0x29, 0x8d, 0xf9, 0xab, 0x24, 0x50, 0x7a, 0x1a, 0x8d, 0xc0, 0x54, 0x82, 0xd0, 0x2c, 0x06, 0x25, + 0xad, 0xad, 0x77, 0xc6, 0x7c, 0xe3, 0xf5, 0x84, 0xcc, 0xa5, 0xfe, 0x24, 0x02, 0xb6, 0x9d, 0x89, 0x32, 0x8c, 0x1d, + 0x84, 0xe7, 0x2a, 0x92, 0xeb, 0xb8, 0xae, 0x3b, 0x71, 0x47, 0xf0, 0x1a, 0x06, 0xc8, 0x50, 0x2e, 0xf6, 0x91, 0x93, + 0x91, 0x1b, 0x37, 0xa6, 0xb7, 0x62, 0x54, 0x07, 0x95, 0x92, 0x59, 0x6f, 0xaf, 0x6e, 0xd8, 0x11, 0xec, 0x26, 0x73, + 0xe3, 0x24, 0xa0, 0x80, 0x1e, 0x88, 0xdd, 0xab, 0xa2, 0xd0, 0xcf, 0xcc, 0x10, 0x55, 0x09, 0xdf, 0xc0, 0xf4, 0x5e, + 0x4f, 0xc0, 0xe5, 0x2b, 0x94, 0xad, 0xa2, 0xb2, 0xf4, 0x83, 0x23, 0xc4, 0xc6, 0xce, 0xc4, 0x85, 0xd0, 0x9e, 0x20, + 0x21, 0x0a, 0xb6, 0xdc, 0xc4, 0x24, 0xaa, 0x69, 0xd1, 0xe7, 0x82, 0x04, 0x83, 0xa4, 0x56, 0x13, 0x6e, 0xe8, 0xb9, + 0x24, 0x89, 0x09, 0xc2, 0x8b, 0x62, 0x6f, 0xe9, 0x7a, 0x5f, 0x91, 0xea, 0x48, 0xce, 0xa2, 0xea, 0xce, 0xad, 0x41, + 0x9a, 0x04, 0x78, 0x0a, 0xb9, 0x33, 0x45, 0xf8, 0x8c, 0x34, 0x9c, 0x81, 0xdb, 0xff, 0x72, 0x88, 0xfa, 0x8e, 0xfb, + 0x57, 0xd4, 0x90, 0x8c, 0x63, 0x81, 0x3a, 0x91, 0x1c, 0x62, 0x29, 0x42, 0x98, 0x2d, 0x2c, 0x3c, 0x89, 0x5e, 0x8a, + 0x63, 0x7f, 0x4a, 0xbd, 0x33, 0xd8, 0xe3, 0x9a, 0x6e, 0xbe, 0xc2, 0x40, 0x47, 0xde, 0x99, 0xe2, 0x24, 0xae, 0xdd, + 0xff, 0x86, 0x17, 0x4f, 0x7d, 0xbb, 0xff, 0x6b, 0xf9, 0xf4, 0xa5, 0xdd, 0xff, 0x9e, 0x7b, 0xbf, 0xe6, 0xca, 0xd9, + 0x5d, 0x19, 0xe2, 0x44, 0x0f, 0x91, 0xcb, 0x85, 0x31, 0x30, 0x37, 0x47, 0x9b, 0x7e, 0x8e, 0x09, 0xca, 0xd9, 0xb8, + 0x60, 0x45, 0x99, 0xcb, 0xfd, 0x09, 0xa0, 0xd4, 0x58, 0x81, 0xcc, 0x8c, 0xec, 0x97, 0x13, 0x06, 0x42, 0xd1, 0xd4, + 0x0a, 0xa8, 0x9c, 0xf4, 0x9a, 0x68, 0x59, 0xa9, 0x2b, 0x34, 0xa6, 0x6a, 0x24, 0xbd, 0xe0, 0xd2, 0x0b, 0xd2, 0xec, + 0x2c, 0xba, 0x93, 0xce, 0xa2, 0x56, 0x43, 0x99, 0x26, 0xac, 0xf9, 0x60, 0x31, 0xc4, 0xef, 0xc1, 0xa7, 0x67, 0x52, + 0x12, 0xae, 0x4c, 0xaf, 0xad, 0xa6, 0x57, 0xab, 0xa5, 0x39, 0xea, 0x18, 0x4d, 0x27, 0xb2, 0x69, 0x9e, 0x4b, 0x9c, + 0xac, 0x13, 0xda, 0x29, 0x12, 0x25, 0x90, 0x0e, 0x45, 0x08, 0x79, 0xc6, 0xd1, 0xd6, 0x5e, 0xa1, 0x4f, 0x68, 0x2e, + 0x76, 0x2c, 0x30, 0x4f, 0x29, 0x23, 0x1c, 0xc0, 0x02, 0x34, 0x2d, 0x1c, 0xc1, 0x53, 0x3c, 0xaf, 0xb5, 0x04, 0x91, + 0xd7, 0x5b, 0x9d, 0x6a, 0x5f, 0x8f, 0xca, 0xbe, 0xf0, 0xbc, 0x46, 0xa6, 0x05, 0x96, 0xf2, 0xb4, 0x56, 0xcb, 0xab, + 0xd1, 0x4e, 0xbd, 0x6f, 0x2b, 0xf1, 0x87, 0xdb, 0xf5, 0xb4, 0x0c, 0x2d, 0x5f, 0x4b, 0x89, 0xca, 0x5c, 0x16, 0xc7, + 0x34, 0x05, 0x19, 0x4a, 0x38, 0x66, 0x79, 0x5e, 0xc8, 0xf5, 0x8f, 0x20, 0x44, 0x31, 0x25, 0x31, 0xf0, 0x1d, 0x61, + 0x76, 0xe1, 0x14, 0x27, 0x38, 0x14, 0x5c, 0x83, 0x10, 0x72, 0xae, 0x13, 0x5a, 0xb8, 0xe0, 0x40, 0x11, 0x61, 0x86, + 0x44, 0xca, 0x08, 0x75, 0x2f, 0xf7, 0xcf, 0x93, 0x7b, 0x4d, 0xb2, 0x01, 0x1b, 0x7a, 0xa2, 0x5a, 0xa4, 0xf8, 0x96, + 0x4f, 0xde, 0x39, 0x1c, 0x15, 0xc1, 0x11, 0x57, 0xb0, 0xbf, 0xa7, 0x2c, 0xa5, 0x42, 0x03, 0xdf, 0xd7, 0x66, 0x5f, + 0x54, 0x55, 0x1f, 0x23, 0xd3, 0x79, 0x03, 0x88, 0xf4, 0xc1, 0xb7, 0x93, 0x92, 0x8d, 0x6a, 0x97, 0xfb, 0x67, 0xaf, + 0xb7, 0x99, 0xc0, 0xab, 0x95, 0x32, 0x7e, 0x85, 0x66, 0x83, 0xfd, 0x12, 0xd2, 0x48, 0xfd, 0xf0, 0x9c, 0x48, 0x28, + 0x48, 0xbe, 0x13, 0x03, 0x15, 0x5d, 0xee, 0x9f, 0xbd, 0x73, 0x62, 0xe1, 0x5a, 0x42, 0xd8, 0x9c, 0xb6, 0x93, 0x10, + 0x27, 0x24, 0x14, 0xc9, 0xb9, 0x17, 0x8c, 0x2b, 0x31, 0xc4, 0xb7, 0x17, 0x8a, 0x97, 0x60, 0x3f, 0x0c, 0xd8, 0x90, + 0x44, 0x0a, 0x03, 0x24, 0x42, 0x38, 0xaa, 0x98, 0x65, 0x04, 0x16, 0x40, 0x8c, 0x75, 0x01, 0x2b, 0xe1, 0x4a, 0xc5, + 0x0f, 0xe1, 0x48, 0x8c, 0xca, 0x73, 0x29, 0x3a, 0x3e, 0x6c, 0xe4, 0xa5, 0x95, 0xd6, 0xe8, 0xf7, 0x60, 0x39, 0xe9, + 0x87, 0x57, 0xaa, 0xeb, 0xa2, 0xe0, 0xa9, 0x4e, 0x20, 0xbb, 0xdc, 0x3f, 0x7b, 0xa9, 0x72, 0xc8, 0x66, 0xbe, 0xe6, + 0xf6, 0x1b, 0x16, 0xe6, 0xd9, 0x4b, 0xb7, 0x7c, 0x2b, 0x2a, 0x5f, 0xee, 0x9f, 0xbd, 0xdf, 0x56, 0x0d, 0xca, 0xf3, + 0x79, 0x69, 0xe2, 0x0b, 0xf8, 0x96, 0x34, 0xf2, 0x96, 0x4a, 0x34, 0x78, 0x2c, 0xc7, 0x42, 0x1c, 0x79, 0x59, 0x5e, + 0x78, 0x46, 0x9e, 0xe2, 0x94, 0x88, 0x28, 0x50, 0x75, 0xd5, 0x94, 0x92, 0xc7, 0x92, 0xf8, 0x62, 0x94, 0xcc, 0xe8, + 0x8e, 0xd0, 0xd0, 0x2d, 0x72, 0xd9, 0x14, 0x92, 0x67, 0x04, 0xe8, 0x0c, 0xef, 0x35, 0x51, 0xa7, 0x2a, 0xbc, 0x52, + 0x41, 0xa4, 0x49, 0x45, 0xb2, 0xe0, 0x90, 0x34, 0x71, 0x44, 0x9a, 0xd8, 0x27, 0xd9, 0xa0, 0x29, 0xc5, 0x43, 0xc7, + 0x2f, 0xfa, 0x95, 0x42, 0x06, 0xf2, 0xc2, 0xd4, 0x6e, 0x95, 0xe2, 0x37, 0xe8, 0xf8, 0xc2, 0xf5, 0x28, 0x24, 0x7a, + 0x20, 0xc8, 0xe2, 0xb9, 0x93, 0xe0, 0x44, 0x74, 0x7c, 0xc1, 0xae, 0x23, 0x48, 0x2d, 0x81, 0x59, 0x61, 0x8e, 0xbc, + 0xa2, 0x6a, 0x4b, 0x55, 0xf5, 0x5d, 0xb1, 0x4e, 0x09, 0xf6, 0x5d, 0x60, 0xdc, 0xd8, 0x57, 0x99, 0x38, 0xd9, 0x66, + 0x93, 0x93, 0x83, 0x03, 0x47, 0x36, 0xfa, 0x8a, 0x3b, 0x89, 0x7e, 0x5f, 0x06, 0xee, 0xbe, 0x97, 0xbc, 0x22, 0x40, + 0x02, 0xfe, 0x5a, 0x2d, 0x1a, 0xe6, 0x10, 0x85, 0x76, 0xfc, 0x2a, 0x06, 0x35, 0xf0, 0x42, 0xd3, 0xab, 0x4e, 0xbf, + 0x56, 0x2b, 0x82, 0xb4, 0x55, 0x6c, 0xdd, 0xe2, 0x34, 0x5f, 0x38, 0x45, 0xf2, 0x4f, 0x73, 0x23, 0x63, 0x4a, 0x83, + 0x80, 0x98, 0x49, 0xb3, 0x4c, 0x4f, 0xc6, 0xd8, 0x12, 0x0c, 0xea, 0x7d, 0xa3, 0xd2, 0x16, 0xb0, 0xc8, 0xaf, 0x52, + 0x95, 0x34, 0x3b, 0x6b, 0x23, 0x4f, 0x57, 0x82, 0xa0, 0x14, 0x54, 0xaa, 0xe5, 0x8a, 0xbc, 0x9f, 0x6f, 0x66, 0x5d, + 0xe2, 0x0c, 0x29, 0x1f, 0x97, 0x80, 0x42, 0x20, 0xab, 0x5d, 0x20, 0xe5, 0x39, 0x99, 0xed, 0x26, 0xf9, 0x33, 0x83, + 0xe4, 0x9f, 0x10, 0x6a, 0x90, 0xbf, 0xf4, 0x70, 0xb8, 0x89, 0x72, 0x2d, 0x64, 0xfa, 0xd5, 0xf9, 0x8c, 0x80, 0x0f, + 0xad, 0x8a, 0xd1, 0x4a, 0x54, 0x71, 0x07, 0x43, 0x31, 0x77, 0x88, 0xf0, 0x42, 0x62, 0x1d, 0x02, 0x76, 0xca, 0x98, + 0x1a, 0x0c, 0xbd, 0xcd, 0xa5, 0x67, 0x72, 0xc0, 0xb3, 0xf7, 0xf7, 0x87, 0x43, 0xcf, 0x67, 0x9b, 0x3b, 0xd7, 0xc8, + 0xfe, 0x84, 0x59, 0x1b, 0x1b, 0xb7, 0x9a, 0x0b, 0x0a, 0xe3, 0x17, 0x61, 0xec, 0x2a, 0xf3, 0x59, 0xdb, 0x84, 0x5a, + 0xfe, 0x01, 0xb4, 0xad, 0x96, 0xa8, 0x41, 0x8d, 0x6e, 0x81, 0x1f, 0xc9, 0x1c, 0x54, 0x3f, 0xdd, 0xc1, 0x3e, 0xce, + 0x44, 0x05, 0x1a, 0x07, 0xdb, 0x5f, 0x3f, 0xc9, 0x15, 0x99, 0x48, 0xd0, 0xd0, 0x12, 0xf8, 0x9f, 0x24, 0x79, 0xa0, + 0x1b, 0x21, 0x17, 0x00, 0x41, 0x33, 0x81, 0xa7, 0x12, 0x61, 0xb6, 0x5d, 0x3a, 0xdf, 0x9f, 0xef, 0x11, 0x32, 0x2b, + 0x9d, 0x8f, 0x6f, 0xcb, 0xdc, 0x2b, 0x20, 0x0b, 0xe4, 0x81, 0xf1, 0x58, 0x14, 0xc8, 0xe8, 0xe5, 0xb9, 0xae, 0x2e, + 0x0c, 0x48, 0xb7, 0xd4, 0xb7, 0x8d, 0xc8, 0xa6, 0xf0, 0xca, 0xc9, 0xf7, 0x1a, 0x0d, 0x6b, 0x6f, 0xf7, 0xe1, 0xed, + 0x4b, 0x2e, 0x60, 0x84, 0xe7, 0x77, 0xa2, 0xb6, 0xee, 0x37, 0xff, 0xb8, 0x9e, 0xc0, 0xb2, 0xb6, 0x28, 0x2e, 0x8b, + 0x33, 0x9a, 0xf2, 0x27, 0x74, 0x9c, 0xa4, 0x10, 0xb2, 0x28, 0x70, 0x82, 0xf2, 0x7d, 0xc3, 0x6d, 0x27, 0xe6, 0x67, + 0xc4, 0x09, 0xd6, 0x26, 0x28, 0x7e, 0x7d, 0x14, 0x31, 0xeb, 0xcb, 0xf5, 0x56, 0xb3, 0x83, 0x83, 0x77, 0x25, 0x9a, + 0x14, 0x94, 0x02, 0x0a, 0x83, 0x69, 0x49, 0x95, 0x46, 0x05, 0x72, 0xf7, 0x9d, 0xc2, 0x05, 0xa0, 0x19, 0x86, 0xc9, + 0x7b, 0x9e, 0x13, 0x9e, 0x4f, 0xd6, 0x59, 0xbc, 0x72, 0x4d, 0x30, 0xd3, 0x6c, 0x01, 0x0e, 0x0f, 0x86, 0xb6, 0xf4, + 0x15, 0x65, 0x65, 0x3a, 0x6c, 0x01, 0xc3, 0x39, 0x20, 0xcb, 0x11, 0x46, 0x88, 0x41, 0x81, 0x5b, 0x8d, 0x92, 0xd7, + 0xa0, 0x57, 0x86, 0x38, 0x73, 0x43, 0x48, 0x80, 0xad, 0x6c, 0x59, 0x84, 0xb0, 0xcc, 0xcb, 0x31, 0x32, 0x09, 0xce, + 0x9e, 0x6f, 0xf3, 0x28, 0x6b, 0xa2, 0xa6, 0x42, 0xea, 0x40, 0x8d, 0x14, 0x15, 0x0d, 0xdc, 0x85, 0xc3, 0x94, 0xe2, + 0xa6, 0xc3, 0x66, 0xc0, 0x80, 0x3f, 0x70, 0x47, 0xc6, 0xa2, 0x40, 0x66, 0x24, 0xee, 0xdc, 0xa9, 0x0c, 0xdd, 0x49, + 0x44, 0x33, 0xac, 0x10, 0x17, 0x9a, 0x68, 0x4a, 0x44, 0x58, 0xef, 0xbc, 0xe4, 0xa5, 0xfb, 0x32, 0x87, 0x9a, 0x6b, + 0x2e, 0x58, 0xe6, 0x91, 0x18, 0xd3, 0xdf, 0x97, 0x69, 0xd1, 0x45, 0x25, 0x50, 0xc3, 0xe8, 0x8d, 0xf5, 0x4a, 0xac, + 0x01, 0xcd, 0x81, 0xbe, 0x96, 0x17, 0xdc, 0x58, 0x51, 0xed, 0xc3, 0x16, 0x63, 0x1a, 0x52, 0xff, 0x2d, 0x64, 0xba, + 0xac, 0xef, 0xf9, 0xe7, 0x42, 0x16, 0x32, 0x9c, 0x55, 0x18, 0x7b, 0x2a, 0x18, 0x3b, 0x02, 0x3d, 0x4d, 0xa7, 0x7e, + 0xf7, 0x55, 0xc2, 0x0b, 0x53, 0x52, 0x4e, 0x91, 0xd8, 0xfb, 0x22, 0x58, 0x6e, 0xfc, 0x5e, 0x5b, 0x0d, 0x8f, 0x11, + 0x48, 0x02, 0xc2, 0x8a, 0xb3, 0xa7, 0x08, 0x67, 0xb5, 0x5a, 0x27, 0xeb, 0xd2, 0xd2, 0x45, 0x52, 0xc2, 0xc8, 0x20, + 0x9e, 0x0b, 0x04, 0x5f, 0x91, 0xa1, 0x10, 0xf1, 0xd7, 0xb9, 0xd9, 0x19, 0xb8, 0xda, 0xcf, 0xde, 0x3a, 0x26, 0x57, + 0x33, 0xeb, 0x16, 0x31, 0x53, 0x98, 0x8f, 0x53, 0xc6, 0x5b, 0xde, 0xdc, 0x9f, 0xdf, 0x01, 0x70, 0xef, 0xb5, 0x30, + 0xe4, 0xa2, 0xa1, 0x0e, 0x97, 0x2c, 0xa1, 0xd8, 0x7d, 0x1d, 0x54, 0xa6, 0x25, 0x9a, 0x83, 0x75, 0x78, 0x69, 0xca, + 0x72, 0x92, 0xe5, 0x79, 0x46, 0xcb, 0xe8, 0xfe, 0x5a, 0xfe, 0xa5, 0x10, 0x2e, 0x9b, 0xce, 0xf6, 0xf3, 0x19, 0xe1, + 0xd8, 0x20, 0xd4, 0x37, 0xbb, 0x42, 0x1f, 0x25, 0x98, 0xb0, 0xaf, 0x95, 0x50, 0xfc, 0x75, 0x9b, 0x50, 0xc4, 0xa9, + 0xda, 0xf2, 0x42, 0x20, 0xb6, 0x1e, 0x20, 0x10, 0x95, 0x93, 0x5d, 0xcb, 0x44, 0x50, 0x47, 0x2a, 0x32, 0xb1, 0xba, + 0xa4, 0x24, 0xc5, 0x4c, 0xad, 0x46, 0xaf, 0xbd, 0x5a, 0xb1, 0x41, 0x13, 0x9c, 0x48, 0xb6, 0x0d, 0x3f, 0x5b, 0xf2, + 0xa7, 0xc1, 0x89, 0xa5, 0x13, 0xd8, 0x61, 0x85, 0xc9, 0x82, 0x5c, 0x48, 0x71, 0x76, 0x44, 0x4e, 0x96, 0xa0, 0x69, + 0x45, 0x41, 0x8a, 0xc0, 0x09, 0x2b, 0xa2, 0x4c, 0x00, 0xb1, 0x90, 0x15, 0xca, 0x80, 0x74, 0xb6, 0x26, 0xff, 0x69, + 0xf3, 0xf2, 0xd3, 0x9a, 0x68, 0x45, 0xae, 0x48, 0xf5, 0xa1, 0x92, 0x6e, 0xa0, 0x20, 0x50, 0xfa, 0xe1, 0x9e, 0x30, + 0x41, 0x4b, 0x51, 0x8e, 0x4c, 0x39, 0x84, 0x9b, 0xe0, 0x42, 0xdb, 0x7b, 0x27, 0x03, 0xbc, 0x5b, 0xa4, 0x09, 0x4e, + 0x0c, 0xba, 0x7e, 0x4e, 0x78, 0x85, 0x95, 0x84, 0x44, 0x59, 0x4a, 0xd8, 0x17, 0x64, 0xca, 0x49, 0x3a, 0x68, 0x0e, + 0x41, 0x01, 0xed, 0x44, 0xdd, 0xb4, 0x34, 0x81, 0xa3, 0x5a, 0x0d, 0xf9, 0x7a, 0xd4, 0x70, 0xc0, 0x6a, 0xd1, 0x10, + 0x53, 0x1c, 0x49, 0xc3, 0xe4, 0xfc, 0xe0, 0xc0, 0xf1, 0xcb, 0x71, 0x07, 0xd1, 0x10, 0xe1, 0x64, 0xb5, 0x72, 0x04, + 0x58, 0x3e, 0x5a, 0xad, 0x7c, 0x13, 0x2c, 0xf1, 0x1a, 0x9a, 0xcd, 0xfa, 0x9c, 0xcc, 0x84, 0x00, 0x9c, 0x01, 0x84, + 0x35, 0xe2, 0xf8, 0xca, 0xb9, 0xe7, 0x83, 0x33, 0xaa, 0x96, 0x0e, 0xa2, 0x5a, 0x6b, 0x68, 0x30, 0xae, 0x41, 0x34, + 0x24, 0x7e, 0x9e, 0x1c, 0x1c, 0xec, 0x65, 0x4a, 0x44, 0x7e, 0x00, 0x51, 0xf6, 0x41, 0x48, 0x16, 0xd9, 0xa1, 0xb9, + 0x1a, 0xeb, 0xce, 0x80, 0x82, 0xa2, 0xd4, 0xb2, 0xea, 0x7a, 0x95, 0x24, 0x88, 0xa2, 0x12, 0x56, 0xb1, 0xe0, 0x3e, + 0x58, 0xf6, 0x05, 0x99, 0x7f, 0xc3, 0x8b, 0x24, 0xeb, 0x5f, 0xb7, 0xa6, 0x56, 0xbb, 0xae, 0xeb, 0xa7, 0x13, 0x11, + 0xc9, 0xd0, 0x51, 0x58, 0x41, 0xfc, 0x87, 0x0a, 0x4c, 0x63, 0xe0, 0x41, 0x31, 0xd6, 0x90, 0x48, 0xf0, 0xb5, 0x6a, + 0xa3, 0x4f, 0x93, 0xfc, 0xb2, 0xd5, 0xcb, 0xa0, 0x36, 0xdc, 0xef, 0x85, 0xe4, 0x48, 0x41, 0x22, 0xc9, 0x63, 0x0d, + 0x67, 0x3b, 0x70, 0xf1, 0x0b, 0x5f, 0xc3, 0xd9, 0x6e, 0xdc, 0x6a, 0x4c, 0x7d, 0xbf, 0x0b, 0x3e, 0x83, 0x37, 0x48, + 0x40, 0xcb, 0x02, 0x03, 0xca, 0xe3, 0x75, 0xdd, 0x4b, 0xb2, 0x52, 0x10, 0xa6, 0x9c, 0x38, 0xac, 0xba, 0x01, 0x4a, + 0x6d, 0xd4, 0x30, 0x7c, 0x99, 0x37, 0x43, 0x86, 0x4b, 0xa0, 0x9a, 0xb9, 0x02, 0xe4, 0xa4, 0x7c, 0xed, 0xb3, 0x83, + 0x03, 0xb0, 0x0d, 0x40, 0x89, 0x73, 0x47, 0xfe, 0x8c, 0xcf, 0x53, 0x50, 0xa5, 0x32, 0xfd, 0x1b, 0x8a, 0xe1, 0x1c, + 0x88, 0x28, 0x83, 0x1f, 0x50, 0x30, 0xf3, 0xb3, 0x8c, 0x2d, 0x64, 0x99, 0xfa, 0x8d, 0x13, 0xa2, 0x49, 0x39, 0x93, + 0x3a, 0x61, 0x8a, 0x3a, 0xa9, 0xa2, 0xd3, 0x2a, 0xda, 0x9e, 0x2d, 0x68, 0xcc, 0x5f, 0xb0, 0x8c, 0xd3, 0x18, 0xa6, + 0x5f, 0x52, 0x1c, 0xcc, 0x28, 0x43, 0xb0, 0x61, 0x2b, 0xad, 0xfc, 0x20, 0xb8, 0xb7, 0x09, 0xaf, 0xea, 0x40, 0xa1, + 0x1f, 0x07, 0x91, 0x1c, 0xc4, 0x4c, 0x67, 0xd4, 0x29, 0x9c, 0x45, 0x4d, 0x33, 0x9d, 0xa6, 0x54, 0x36, 0x04, 0x77, + 0x77, 0x18, 0xd1, 0x92, 0x40, 0x4b, 0xcf, 0x7b, 0xb5, 0x16, 0x08, 0x78, 0xef, 0x58, 0x04, 0x73, 0x26, 0x98, 0x1b, + 0x1c, 0xd5, 0xad, 0xc2, 0xa9, 0xe9, 0xe6, 0xab, 0xad, 0x87, 0xda, 0xb6, 0x09, 0x07, 0x41, 0x27, 0x27, 0xbb, 0x2d, + 0xab, 0x97, 0x5a, 0x72, 0x68, 0x69, 0xc1, 0x1e, 0xca, 0x98, 0xd1, 0x52, 0x93, 0x17, 0xd2, 0x5b, 0xf1, 0x92, 0x93, + 0x0f, 0x70, 0x6a, 0xe8, 0x39, 0x9f, 0x46, 0x6b, 0x87, 0x63, 0x3a, 0x97, 0x85, 0xf6, 0x7f, 0xc9, 0x9d, 0x57, 0xf8, + 0x39, 0x84, 0x75, 0xbf, 0x2d, 0xab, 0x6f, 0x86, 0x73, 0xbf, 0x2d, 0x11, 0xf4, 0xad, 0xb7, 0x51, 0xcf, 0x08, 0xe3, + 0xb6, 0xdd, 0x53, 0xb7, 0x69, 0x6b, 0x6d, 0xe9, 0x07, 0x19, 0x44, 0x92, 0x89, 0x96, 0x62, 0x3f, 0xe0, 0x32, 0x4d, + 0x0d, 0xd2, 0xe5, 0xaa, 0x16, 0x12, 0x55, 0x09, 0x86, 0x52, 0x87, 0xdf, 0xb5, 0x3c, 0x4a, 0xc6, 0xa4, 0xd2, 0xce, + 0x78, 0xe3, 0xa7, 0x7c, 0x1f, 0x76, 0x59, 0xb2, 0x71, 0x12, 0x2f, 0x24, 0xe0, 0x41, 0x7b, 0xd8, 0x10, 0x86, 0xb1, + 0x9d, 0xc9, 0x93, 0x40, 0x66, 0xff, 0x24, 0xd1, 0xba, 0x5b, 0xd5, 0xca, 0x78, 0x0f, 0xf6, 0x3f, 0xc2, 0xa1, 0x3e, + 0x1e, 0x47, 0x15, 0x07, 0xa6, 0xde, 0x32, 0x2f, 0x9c, 0x02, 0x89, 0x54, 0xde, 0x62, 0x84, 0x93, 0x5c, 0x84, 0xb7, + 0xbf, 0xc3, 0x3f, 0x2a, 0x96, 0x38, 0x2e, 0x38, 0xce, 0xb3, 0x87, 0x72, 0x44, 0x09, 0x7e, 0x11, 0xbd, 0x07, 0x3a, + 0x16, 0x14, 0x9a, 0x6b, 0x2a, 0x7a, 0x9a, 0xa8, 0x89, 0xec, 0xcc, 0x4a, 0xc5, 0xb4, 0xc8, 0xa8, 0x11, 0xc3, 0x6c, + 0x49, 0xe3, 0xd4, 0x56, 0x36, 0x2f, 0x76, 0x55, 0x65, 0x5c, 0xb4, 0x03, 0x8b, 0x65, 0x60, 0x71, 0xb5, 0x72, 0xaa, + 0xa8, 0x26, 0xcc, 0x88, 0x63, 0x20, 0xcc, 0x8c, 0x84, 0x8a, 0x8a, 0x66, 0x2d, 0xdb, 0x38, 0x68, 0x3d, 0x9f, 0x48, + 0xeb, 0xe6, 0x15, 0x38, 0x4c, 0x17, 0x82, 0x6c, 0x6e, 0xfa, 0x14, 0xb0, 0x9c, 0x5d, 0x31, 0x90, 0x81, 0xa1, 0x1f, + 0x8a, 0x4c, 0xd9, 0x32, 0xa5, 0x75, 0x0b, 0x7e, 0xd1, 0x3d, 0xb9, 0xb2, 0x0a, 0x75, 0x9b, 0xef, 0x8d, 0x5c, 0xa3, + 0xa7, 0xc9, 0xae, 0x5c, 0xa3, 0x8a, 0xb6, 0xbb, 0xd7, 0x44, 0xf7, 0x67, 0xa5, 0xca, 0xb1, 0xb6, 0x57, 0xf9, 0x1d, + 0xc3, 0xb5, 0x80, 0x36, 0x25, 0x9a, 0x35, 0x57, 0x39, 0xcf, 0xf3, 0x71, 0x71, 0x96, 0x40, 0xa4, 0xee, 0x8c, 0x25, + 0xfd, 0x2b, 0xab, 0x51, 0x1c, 0xc8, 0x75, 0xbe, 0x23, 0x93, 0x28, 0xb9, 0xf6, 0xa3, 0x77, 0x30, 0x5e, 0xf9, 0xf2, + 0xf9, 0x5d, 0x90, 0xfa, 0x9c, 0x2a, 0xee, 0x52, 0xc2, 0xf0, 0x9d, 0x01, 0xc3, 0x77, 0x92, 0x4f, 0x97, 0xed, 0xf1, + 0xf2, 0x45, 0xd1, 0x81, 0x37, 0xce, 0x35, 0xcb, 0x98, 0xf2, 0xed, 0x63, 0xac, 0xb3, 0xb0, 0x69, 0xc1, 0xc2, 0xa6, + 0xdc, 0x59, 0xef, 0xca, 0x71, 0x7e, 0xdc, 0xde, 0xcb, 0x26, 0x67, 0xfb, 0xb1, 0xdc, 0xf8, 0x3f, 0x7a, 0xf7, 0xb6, + 0x31, 0xb8, 0xdc, 0xa1, 0x7b, 0x28, 0x92, 0x55, 0x24, 0xc8, 0x4f, 0x20, 0xe9, 0x80, 0x93, 0x9e, 0x71, 0xe4, 0xa0, + 0x94, 0x53, 0x3a, 0x0f, 0xc8, 0x19, 0xcd, 0x33, 0x9e, 0x4c, 0x55, 0x9f, 0x99, 0x3a, 0x67, 0x24, 0x5e, 0x82, 0x2b, + 0x5a, 0xc4, 0xda, 0xbd, 0xea, 0x49, 0xae, 0xe5, 0x47, 0x16, 0x07, 0x5e, 0x86, 0x95, 0x14, 0xc9, 0xbc, 0x34, 0x27, + 0x3a, 0xd7, 0x78, 0xf3, 0x1d, 0x1e, 0xb3, 0x98, 0x65, 0x21, 0x4d, 0x9d, 0x04, 0x2d, 0x77, 0x0d, 0x96, 0x40, 0x40, + 0x46, 0x0e, 0x86, 0x7f, 0x2a, 0x8f, 0xfc, 0xb9, 0xd0, 0x1b, 0xf8, 0x81, 0xa6, 0x94, 0x87, 0x49, 0x00, 0x69, 0x29, + 0x6e, 0x50, 0x1c, 0x69, 0x3a, 0x38, 0xd8, 0x73, 0x6c, 0xe1, 0x96, 0x80, 0xc3, 0xdf, 0xe6, 0x1b, 0xd4, 0x5f, 0xc2, + 0xe9, 0x9c, 0x72, 0x68, 0x8a, 0x96, 0x74, 0xfd, 0x20, 0x0b, 0x77, 0x3f, 0xd2, 0x3b, 0x1c, 0xa3, 0x3c, 0xf7, 0x24, + 0xd4, 0xf6, 0x98, 0xd1, 0x28, 0xb0, 0xf1, 0x47, 0x7a, 0xe7, 0x15, 0xe7, 0xc5, 0xc5, 0xf1, 0x66, 0xb1, 0x80, 0x76, + 0x72, 0x13, 0xdb, 0xb8, 0x1c, 0xc4, 0x5b, 0xe6, 0x38, 0x49, 0xd9, 0x04, 0x88, 0xf3, 0x6f, 0xf4, 0xce, 0x93, 0xfd, + 0x31, 0xe3, 0xb4, 0x1e, 0x5a, 0x6a, 0xd4, 0xbb, 0x46, 0xb1, 0xb9, 0x0c, 0xca, 0xa0, 0x18, 0x88, 0xb6, 0x43, 0x52, + 0xa9, 0x57, 0x9a, 0x87, 0x08, 0xe5, 0x0f, 0x9d, 0x0a, 0xfe, 0xd6, 0x14, 0x6d, 0xbc, 0x92, 0xf9, 0xba, 0xd6, 0x88, + 0x42, 0x83, 0x32, 0xd3, 0xe3, 0xd2, 0x89, 0xf5, 0xae, 0x53, 0x47, 0x10, 0x0c, 0x47, 0xd8, 0xb7, 0x5c, 0x75, 0xea, + 0xfd, 0x24, 0x13, 0x42, 0xca, 0x48, 0xd2, 0xcb, 0xb2, 0x9d, 0x75, 0xe9, 0x00, 0xde, 0x21, 0xa1, 0xc5, 0x17, 0x07, + 0x32, 0x73, 0x9d, 0x2d, 0xfa, 0x37, 0x4e, 0x9c, 0xa5, 0x9e, 0x82, 0x17, 0x9b, 0x58, 0xe4, 0x39, 0x50, 0xa1, 0xa2, + 0x2f, 0x99, 0x00, 0x08, 0x67, 0xd8, 0x37, 0xa4, 0x66, 0x2a, 0xa4, 0xa6, 0x6b, 0x60, 0x7c, 0x87, 0x94, 0xa4, 0x02, + 0x19, 0x42, 0x89, 0x14, 0x42, 0x4f, 0x2d, 0xae, 0x22, 0x21, 0x73, 0x41, 0x8b, 0xf3, 0x73, 0x72, 0xcd, 0xd3, 0x0a, + 0x58, 0x8e, 0xe8, 0x07, 0xe5, 0x1e, 0x4c, 0x89, 0xca, 0x0a, 0x79, 0x71, 0x2c, 0x5b, 0xa7, 0xb7, 0x3a, 0x89, 0xab, + 0xa7, 0x45, 0x34, 0x4a, 0x9c, 0x10, 0x2d, 0x63, 0x27, 0xc4, 0x29, 0xa4, 0x23, 0x26, 0x79, 0x01, 0x3f, 0x35, 0x57, + 0xa3, 0x92, 0xac, 0xbc, 0xfd, 0x8c, 0x1f, 0x28, 0xf3, 0x1c, 0x52, 0x34, 0x71, 0xac, 0x79, 0x4a, 0xec, 0x88, 0xc3, + 0x76, 0xc6, 0xb2, 0x7d, 0xa7, 0x12, 0x74, 0x14, 0x60, 0x7f, 0xe3, 0xce, 0xd2, 0x98, 0x85, 0x79, 0x9a, 0x5b, 0x9d, + 0xf9, 0x53, 0xc1, 0xbe, 0x32, 0x87, 0xd4, 0xc9, 0xc8, 0x9a, 0xc4, 0xb9, 0x3f, 0xd5, 0xf2, 0x97, 0x39, 0x4d, 0xef, + 0x2e, 0x28, 0xa4, 0x3a, 0x27, 0x70, 0xda, 0xb7, 0x5c, 0x86, 0x32, 0x4d, 0xbd, 0x9f, 0x0a, 0x65, 0x25, 0xaf, 0x9e, + 0x02, 0x5c, 0x3f, 0x23, 0x98, 0x8b, 0x68, 0xa3, 0xe1, 0x88, 0x91, 0xbb, 0x85, 0xee, 0x3c, 0x3d, 0x49, 0x3b, 0x0c, + 0xfc, 0x6b, 0x25, 0xa6, 0x55, 0xb0, 0x00, 0x27, 0xe6, 0x89, 0xd4, 0x41, 0x36, 0x5c, 0xf7, 0xca, 0x40, 0x11, 0x84, + 0xef, 0xd2, 0xdd, 0x53, 0xdd, 0x96, 0x34, 0xbb, 0x7b, 0xaa, 0x95, 0xa0, 0x9f, 0x48, 0xf8, 0xc1, 0x6a, 0x9c, 0xe2, + 0xf8, 0x32, 0xcb, 0x73, 0x94, 0x03, 0x78, 0x5f, 0x77, 0x1c, 0xe7, 0x6b, 0x95, 0x32, 0xe8, 0x42, 0x2c, 0xf6, 0x22, + 0x4a, 0x34, 0x13, 0x2f, 0xc7, 0xff, 0x7a, 0x63, 0xfc, 0xaf, 0x8d, 0x33, 0xa7, 0x60, 0x1a, 0x4d, 0x62, 0x1a, 0x68, + 0xd6, 0x89, 0x24, 0x01, 0x0a, 0xbd, 0x2d, 0xe6, 0xe4, 0xf5, 0x95, 0x07, 0x1a, 0xd7, 0x72, 0x9c, 0xc4, 0xbc, 0x3e, + 0xf6, 0xa7, 0x2c, 0xba, 0xf3, 0xe6, 0xac, 0x3e, 0x4d, 0xe2, 0x24, 0x9b, 0xf9, 0x23, 0x8a, 0xb3, 0xbb, 0x8c, 0xd3, + 0x69, 0x7d, 0xce, 0xf0, 0x73, 0x1a, 0x2d, 0x28, 0x67, 0x23, 0x1f, 0xdb, 0x67, 0x29, 0xf3, 0x23, 0xeb, 0x95, 0x9f, + 0xa6, 0xc9, 0x8d, 0x8d, 0xdf, 0x26, 0xd7, 0x09, 0x4f, 0xf0, 0xeb, 0xdb, 0xbb, 0x09, 0x8d, 0xf1, 0xfb, 0xeb, 0x79, + 0xcc, 0xe7, 0x38, 0xf3, 0xe3, 0xac, 0x9e, 0xd1, 0x94, 0x8d, 0x3b, 0xa3, 0x24, 0x4a, 0xd2, 0x3a, 0x64, 0x6c, 0x4f, + 0xa9, 0x17, 0xb1, 0x49, 0xc8, 0xad, 0xc0, 0x4f, 0x3f, 0x76, 0xea, 0xf5, 0x59, 0xca, 0xa6, 0x7e, 0x7a, 0x57, 0x17, + 0x35, 0xbc, 0xcf, 0x9b, 0x87, 0xfe, 0xe3, 0xf1, 0x51, 0x87, 0xa7, 0x7e, 0x9c, 0x31, 0x58, 0x26, 0xcf, 0x8f, 0x22, + 0xeb, 0xf0, 0xb8, 0x39, 0xcd, 0xf6, 0x64, 0x20, 0xcf, 0x8f, 0x79, 0x7e, 0x85, 0xdf, 0x00, 0xdc, 0xee, 0x35, 0x8f, + 0xf1, 0xf5, 0x9c, 0xf3, 0x24, 0x5e, 0x8e, 0xe6, 0x69, 0x96, 0xa4, 0xde, 0x2c, 0x61, 0x31, 0xa7, 0x69, 0xe7, 0x3a, + 0x49, 0x03, 0x9a, 0xd6, 0x53, 0x3f, 0x60, 0xf3, 0xcc, 0x3b, 0x9a, 0xdd, 0x76, 0x40, 0xb3, 0x98, 0xa4, 0xc9, 0x3c, + 0x0e, 0xd4, 0x58, 0x2c, 0x0e, 0x69, 0xca, 0xb8, 0xf9, 0x42, 0x5c, 0x62, 0xe2, 0x45, 0x2c, 0xa6, 0x7e, 0x5a, 0x9f, + 0x40, 0x63, 0x30, 0x8b, 0x9a, 0x01, 0x9d, 0xe0, 0x74, 0x72, 0xed, 0x3b, 0xad, 0xf6, 0x23, 0xac, 0xff, 0xba, 0xc7, + 0xc8, 0x6a, 0x6e, 0x2f, 0x6e, 0x35, 0x9b, 0x7f, 0x41, 0x9d, 0xb5, 0x51, 0x04, 0x40, 0x5e, 0x6b, 0x76, 0x6b, 0x65, + 0x09, 0x64, 0xb4, 0x6d, 0x6b, 0xd9, 0x99, 0xf9, 0x01, 0xe4, 0x03, 0x7b, 0xed, 0xd9, 0x6d, 0x0e, 0xb3, 0xf3, 0x64, + 0x8a, 0xa9, 0x9a, 0xa4, 0x7a, 0x5a, 0xfe, 0x5e, 0x88, 0x4f, 0xb7, 0x43, 0xdc, 0xd6, 0x10, 0x97, 0x58, 0xaf, 0x07, + 0xf3, 0x54, 0xc4, 0x56, 0xbd, 0x56, 0x26, 0x01, 0x09, 0x93, 0x05, 0x4d, 0x35, 0x1c, 0xe2, 0xe1, 0x77, 0x83, 0xd1, + 0xde, 0x0e, 0xc6, 0xe9, 0xa7, 0xc0, 0x48, 0xe3, 0x60, 0x59, 0x5d, 0xd7, 0x56, 0x4a, 0xa7, 0x9d, 0x90, 0x02, 0x3d, + 0x79, 0x6d, 0xf8, 0x7d, 0xc3, 0x02, 0x1e, 0xca, 0x9f, 0x82, 0x9c, 0x6f, 0xe4, 0xbb, 0xe3, 0x66, 0x53, 0x3e, 0x67, + 0xec, 0x57, 0xea, 0xb5, 0x5c, 0xa8, 0x90, 0x5f, 0xe1, 0x1f, 0x8b, 0xb3, 0xbc, 0x55, 0xee, 0x89, 0xbf, 0x36, 0x0f, + 0xf9, 0x1a, 0x29, 0x8a, 0xe5, 0x91, 0x68, 0x9c, 0x6a, 0x59, 0x29, 0x85, 0x0f, 0xb8, 0xed, 0x04, 0x77, 0x24, 0xac, + 0x57, 0x1c, 0xe2, 0x64, 0xfd, 0xaf, 0x65, 0xde, 0x85, 0x07, 0x91, 0x0e, 0x23, 0xd5, 0x30, 0xe9, 0xa4, 0x3d, 0xd2, + 0xec, 0xa4, 0xf5, 0x3a, 0x72, 0x12, 0x12, 0x0f, 0x52, 0x95, 0x9c, 0xe7, 0xb0, 0x7e, 0x22, 0x8c, 0xed, 0x0c, 0x79, + 0x09, 0x9c, 0x34, 0x5d, 0xad, 0xca, 0x30, 0x00, 0x13, 0xa7, 0x35, 0x7e, 0xe4, 0xaa, 0x02, 0xce, 0x0c, 0x4e, 0x9e, + 0xe8, 0xab, 0x5d, 0x62, 0xcd, 0x2b, 0xa2, 0x64, 0x24, 0x30, 0xe7, 0xce, 0x7c, 0x1e, 0x82, 0x97, 0xa2, 0x10, 0x3f, + 0x65, 0x0a, 0x93, 0xdd, 0xb0, 0x51, 0x3f, 0x2e, 0xf2, 0xdb, 0x20, 0x8f, 0x2f, 0xce, 0xa1, 0x97, 0x3b, 0x4e, 0x84, + 0xc5, 0x54, 0xf4, 0xff, 0x9e, 0x1b, 0x92, 0x3a, 0x76, 0x59, 0x3c, 0x8a, 0xe6, 0x01, 0xcd, 0x44, 0x0f, 0xa5, 0x38, + 0xff, 0xbb, 0x59, 0x4b, 0x34, 0x81, 0xde, 0x45, 0x36, 0x0f, 0x54, 0x84, 0x1b, 0x54, 0x8a, 0xe7, 0xba, 0x78, 0x2e, + 0xdb, 0xea, 0x4b, 0x25, 0xd8, 0xd8, 0x81, 0x96, 0xee, 0x3c, 0x66, 0xbf, 0xcc, 0xe9, 0x25, 0x0b, 0x8c, 0x73, 0xbb, + 0x34, 0x1e, 0x25, 0x01, 0x7d, 0xff, 0xf6, 0x1b, 0xc8, 0x76, 0x4f, 0x62, 0x20, 0xb1, 0x58, 0xfa, 0xbb, 0x70, 0x46, + 0x62, 0x37, 0xa0, 0x0b, 0x36, 0xa2, 0xfd, 0xab, 0xfd, 0xe5, 0xd6, 0x8a, 0xf2, 0x35, 0xca, 0x1b, 0x57, 0x22, 0xe9, + 0x4f, 0x40, 0x79, 0xb5, 0xbf, 0xbc, 0xe3, 0x79, 0x63, 0x7f, 0x19, 0xbb, 0x41, 0x32, 0xf5, 0x59, 0x0c, 0xbf, 0xb3, + 0x7c, 0x7f, 0xc9, 0xe0, 0x07, 0xcf, 0xaf, 0xf2, 0x32, 0x51, 0xb4, 0x80, 0xc8, 0x98, 0x82, 0xc2, 0x5d, 0x0b, 0xb9, + 0x1f, 0x12, 0x16, 0x8b, 0xa2, 0xfb, 0x7a, 0xa6, 0xba, 0x57, 0x40, 0xf2, 0x37, 0x44, 0x1a, 0xcc, 0xda, 0x5c, 0x1e, + 0x3f, 0xd4, 0x5c, 0xa6, 0x31, 0x67, 0x22, 0x2d, 0x5e, 0x87, 0x73, 0x42, 0x3f, 0xbb, 0x1c, 0xc9, 0x73, 0xa8, 0x59, + 0x79, 0xea, 0xc2, 0x17, 0x88, 0x95, 0x16, 0x30, 0x4d, 0x85, 0xb1, 0x4f, 0x77, 0x1f, 0x94, 0x8c, 0xef, 0x33, 0xfe, + 0x0a, 0xaa, 0xca, 0x92, 0x79, 0x3a, 0x82, 0x58, 0xaf, 0x52, 0x29, 0x36, 0xbd, 0x62, 0xb6, 0xd0, 0xdf, 0x6c, 0xcc, + 0x8d, 0x24, 0x5b, 0x8e, 0x99, 0x79, 0x67, 0x07, 0x15, 0xf1, 0x44, 0x79, 0x16, 0x46, 0xe9, 0x0f, 0x7a, 0x4a, 0xa0, + 0x10, 0x05, 0x22, 0x5f, 0xd4, 0x49, 0x49, 0x2f, 0x2d, 0x71, 0x4e, 0x08, 0x61, 0x2e, 0x0b, 0x44, 0x20, 0x0f, 0x14, + 0x8b, 0x7a, 0x0b, 0x22, 0x43, 0x2c, 0x28, 0x35, 0x3c, 0xa6, 0xf0, 0xbc, 0x5a, 0xfd, 0x9d, 0x3b, 0xb2, 0xae, 0x74, + 0xaa, 0x80, 0x0e, 0xc6, 0xb0, 0x7c, 0xe9, 0xa5, 0xb8, 0xe8, 0xd2, 0x83, 0x4a, 0x79, 0x27, 0x11, 0xe8, 0x93, 0xc8, + 0x22, 0x1a, 0x9d, 0x67, 0x52, 0x45, 0x48, 0x10, 0x36, 0x5f, 0x17, 0x07, 0xf8, 0x2b, 0xf8, 0x6e, 0xae, 0x2d, 0x8b, + 0xb4, 0xa7, 0x92, 0xf5, 0xd2, 0x2c, 0x49, 0xb9, 0xe3, 0x84, 0x38, 0x42, 0xa4, 0x17, 0x0a, 0xaa, 0xed, 0x46, 0xe2, + 0xbf, 0x7e, 0xbd, 0xe5, 0xb5, 0x0a, 0x4f, 0x48, 0xe5, 0x5c, 0xb5, 0xcc, 0x33, 0x53, 0x67, 0x73, 0x01, 0x5c, 0x5c, + 0xfc, 0x96, 0xf3, 0x29, 0x9f, 0x8b, 0x69, 0x61, 0xc5, 0xb9, 0xa4, 0xd4, 0x77, 0x2a, 0x40, 0x88, 0xb8, 0xdb, 0x8e, + 0xa1, 0x50, 0x5e, 0xce, 0xbb, 0xd8, 0xc5, 0x57, 0x52, 0xdb, 0xb9, 0x34, 0xc8, 0xf8, 0x8a, 0x69, 0x7f, 0x5d, 0x95, + 0xc0, 0x72, 0x85, 0x11, 0x83, 0x05, 0x6c, 0xab, 0x26, 0x61, 0xb9, 0x23, 0xf1, 0x56, 0x2a, 0x75, 0xe5, 0x23, 0x95, + 0xba, 0xd6, 0xf6, 0x2a, 0x22, 0xeb, 0x71, 0x1b, 0x60, 0xe0, 0x01, 0xc8, 0xb8, 0x9e, 0x02, 0x30, 0x93, 0x31, 0x15, + 0x17, 0xd3, 0x48, 0xd6, 0x82, 0x97, 0x52, 0x8d, 0xf7, 0xec, 0x37, 0xaf, 0x2f, 0xde, 0xd9, 0x18, 0xee, 0x33, 0xa3, + 0x69, 0xe6, 0x2d, 0x6d, 0x95, 0x4c, 0x58, 0x87, 0xc0, 0xb4, 0xed, 0xd9, 0xfe, 0x0c, 0xce, 0x66, 0x0b, 0xee, 0xd9, + 0xb8, 0xad, 0xdf, 0xdc, 0xdc, 0xd4, 0xe1, 0xe8, 0x58, 0x7d, 0x9e, 0x46, 0x92, 0xaf, 0x04, 0x76, 0x9e, 0x23, 0x97, + 0x87, 0x34, 0x2e, 0x6e, 0x3c, 0x4a, 0x22, 0xea, 0x46, 0xc9, 0x44, 0x1e, 0x7b, 0x5d, 0xf7, 0x43, 0x8c, 0xae, 0xba, + 0xe2, 0x26, 0xaf, 0x5e, 0x97, 0xcb, 0x3b, 0xd4, 0x78, 0x0a, 0x3f, 0x7b, 0x10, 0xa5, 0xea, 0x36, 0x78, 0x28, 0x1e, + 0x2e, 0x60, 0xdb, 0x88, 0xa7, 0xfd, 0xe5, 0x06, 0x91, 0xf5, 0xa1, 0x8b, 0xb0, 0x27, 0xa7, 0x96, 0x89, 0x5a, 0x57, + 0xde, 0xe8, 0xea, 0x2a, 0xef, 0x36, 0xa0, 0xaf, 0x86, 0xee, 0xf7, 0x3a, 0x09, 0xee, 0x74, 0xfb, 0x82, 0xf0, 0xe0, + 0x46, 0xa7, 0x98, 0xf4, 0xa0, 0x0b, 0x18, 0x37, 0xe8, 0x09, 0x9c, 0x29, 0x5e, 0x39, 0x28, 0x1f, 0xf2, 0xa1, 0x05, + 0x9c, 0x31, 0x87, 0x12, 0xa0, 0x4b, 0xe8, 0x3c, 0x28, 0x1a, 0x88, 0x6d, 0x2d, 0x8b, 0x76, 0x01, 0x28, 0x2b, 0x96, + 0xdb, 0x45, 0xfa, 0xb3, 0x4b, 0xb2, 0xd0, 0x10, 0x07, 0x26, 0xf0, 0x57, 0x08, 0xfe, 0x17, 0x80, 0x77, 0x1b, 0x12, + 0x4d, 0x57, 0xe6, 0xed, 0x32, 0xf2, 0xde, 0x87, 0x02, 0x99, 0x83, 0x98, 0xe3, 0x37, 0x1c, 0xbf, 0xbe, 0x12, 0x55, + 0xb5, 0x3a, 0x00, 0x7a, 0x2a, 0xa8, 0x4d, 0x4d, 0xad, 0xf7, 0x8d, 0x92, 0x28, 0xf2, 0x67, 0x19, 0xf5, 0xf4, 0x0f, + 0xa5, 0x19, 0x80, 0x82, 0xb1, 0xa9, 0x8a, 0xa9, 0x04, 0xa7, 0x73, 0x50, 0xd8, 0x36, 0xf5, 0xc4, 0x85, 0x9f, 0x3a, + 0xf5, 0xfa, 0xa8, 0x7e, 0x3d, 0x41, 0x39, 0x0f, 0x97, 0xa6, 0x5e, 0x71, 0xd2, 0x6c, 0x76, 0x20, 0x1b, 0xb5, 0xee, + 0x47, 0x6c, 0x12, 0x7b, 0x11, 0x1d, 0xf3, 0x9c, 0xc3, 0x31, 0xc1, 0xa5, 0x56, 0xe4, 0xdc, 0xf6, 0x71, 0x4a, 0xa7, + 0x96, 0x0b, 0xff, 0xde, 0x3f, 0x70, 0xce, 0x03, 0x2f, 0xe6, 0x61, 0x5d, 0x64, 0x3d, 0xc3, 0x99, 0x0d, 0x1e, 0x56, + 0x9e, 0x97, 0xc6, 0x40, 0x23, 0x0a, 0x4a, 0x6e, 0xce, 0x53, 0x8b, 0x87, 0x98, 0xa7, 0x66, 0xbd, 0x18, 0x2d, 0x37, + 0x66, 0xb0, 0xa9, 0x6b, 0x1d, 0xa2, 0x3c, 0x13, 0xa6, 0xc9, 0x66, 0x65, 0xad, 0xb0, 0x56, 0x9f, 0x36, 0xd0, 0x67, + 0xa8, 0xd6, 0xb9, 0x74, 0xed, 0x2f, 0x65, 0x8b, 0x87, 0x20, 0xb3, 0xa2, 0xf4, 0x63, 0xb3, 0x05, 0xca, 0x59, 0x3c, + 0x9b, 0xf3, 0x81, 0x08, 0x2b, 0xa4, 0x70, 0x40, 0x65, 0x88, 0x8d, 0x12, 0xc0, 0xc1, 0x70, 0x29, 0x81, 0x19, 0xf9, + 0xd1, 0xc8, 0x01, 0x88, 0xac, 0xba, 0x75, 0x9a, 0xd2, 0x29, 0xea, 0x4c, 0x59, 0x5c, 0x97, 0xef, 0x8e, 0x0d, 0xc5, + 0xd0, 0x7d, 0x04, 0x4f, 0xb9, 0x2b, 0x7a, 0xc3, 0x22, 0x7b, 0x78, 0x0b, 0x2e, 0xaf, 0x86, 0x79, 0xde, 0x49, 0xb9, + 0x33, 0x78, 0xe9, 0xa0, 0x21, 0xfe, 0xc6, 0xb8, 0x1f, 0xc7, 0xd6, 0x3b, 0xc9, 0xc6, 0x6d, 0xb4, 0xa3, 0x8a, 0xb9, + 0x17, 0x44, 0xb5, 0x6f, 0x08, 0x54, 0x7c, 0xe2, 0xd8, 0x34, 0x9b, 0xd5, 0x25, 0xcb, 0xab, 0x0b, 0x92, 0xb5, 0xa1, + 0x29, 0x52, 0xbe, 0x72, 0x4a, 0x97, 0x82, 0x9b, 0xa9, 0x43, 0x32, 0xd2, 0x9d, 0x33, 0x2c, 0x0e, 0x55, 0xa9, 0x67, + 0xf3, 0x18, 0x15, 0xaa, 0xb0, 0x9b, 0xab, 0xb3, 0x2a, 0x6b, 0x04, 0xe5, 0xa2, 0xb8, 0x44, 0xd0, 0x8f, 0x22, 0x18, + 0xf0, 0x4a, 0x6b, 0x24, 0xe6, 0xad, 0x2b, 0x03, 0x3e, 0x74, 0x50, 0xae, 0xf6, 0xe9, 0x13, 0xa1, 0xd4, 0x1b, 0x37, + 0x17, 0xee, 0x71, 0x1d, 0xae, 0x93, 0x22, 0x9a, 0x41, 0xc2, 0x41, 0x25, 0x31, 0xbd, 0x53, 0xb2, 0x36, 0x69, 0x12, + 0x58, 0x62, 0x42, 0xc4, 0x4e, 0xe3, 0xc0, 0xb6, 0xbe, 0x1c, 0x45, 0x6c, 0xf4, 0x91, 0xd8, 0xfb, 0x4b, 0x07, 0x6d, + 0x9e, 0x3b, 0x15, 0x5c, 0x41, 0xf3, 0x79, 0x54, 0x0d, 0x65, 0xa4, 0xae, 0xc1, 0xc2, 0xe5, 0xc5, 0x44, 0x76, 0x0f, + 0xf4, 0xa6, 0x6e, 0x43, 0x8e, 0xd3, 0xbb, 0xca, 0x2f, 0xcb, 0xfb, 0xc6, 0x4a, 0x28, 0x00, 0xcd, 0xb2, 0xdc, 0x12, + 0x44, 0x45, 0xec, 0x4f, 0x52, 0x9a, 0x6d, 0x49, 0xa6, 0x06, 0x70, 0x72, 0xc5, 0xdf, 0x6c, 0xeb, 0xcb, 0xa2, 0x8c, + 0x16, 0x3e, 0x25, 0x91, 0x14, 0x43, 0x6c, 0x18, 0x0b, 0x1c, 0x09, 0x6e, 0x40, 0xb9, 0xcf, 0x22, 0xd9, 0xa4, 0xa3, + 0x5d, 0x20, 0x6b, 0x33, 0x5a, 0xad, 0xb2, 0xea, 0x5c, 0x58, 0x15, 0x83, 0x62, 0x66, 0xdd, 0x46, 0x09, 0xb7, 0x98, + 0x99, 0xd8, 0x93, 0x66, 0x70, 0xb6, 0x9c, 0xa1, 0x7c, 0x67, 0x7d, 0x39, 0x12, 0xc7, 0xb6, 0x00, 0xc0, 0x44, 0x01, + 0x08, 0x69, 0x03, 0xf2, 0x58, 0x92, 0x13, 0x91, 0xc4, 0xe5, 0x7e, 0x3a, 0xa1, 0x7c, 0x0d, 0xb1, 0x91, 0xcc, 0x12, + 0xee, 0xe8, 0x14, 0x81, 0x0d, 0x68, 0xfd, 0x2a, 0xb4, 0xa0, 0x44, 0xe7, 0x7d, 0xd0, 0x83, 0xc9, 0x56, 0x75, 0x3a, + 0x44, 0x20, 0x6f, 0xc5, 0xe2, 0x48, 0x09, 0x93, 0x08, 0x09, 0x23, 0x39, 0x81, 0x25, 0xc6, 0x12, 0x20, 0xe6, 0xb6, + 0xd5, 0x97, 0x90, 0xd3, 0x40, 0xc2, 0x4c, 0x52, 0xd1, 0x2a, 0xc9, 0xbb, 0x0d, 0x59, 0x5b, 0x8a, 0x00, 0x59, 0x09, + 0x90, 0x20, 0xf6, 0x69, 0x89, 0x03, 0xc8, 0x2c, 0x37, 0xf1, 0x10, 0xb0, 0x45, 0x41, 0x6c, 0xe2, 0x00, 0x5b, 0xaf, + 0x1b, 0xf9, 0xd7, 0x34, 0xea, 0xed, 0x2f, 0xd3, 0xd5, 0xaa, 0x99, 0x77, 0x1b, 0xf2, 0xd1, 0xea, 0x0a, 0xbe, 0x21, + 0x2f, 0x1d, 0x15, 0x4b, 0x0c, 0xa7, 0x42, 0x21, 0xdf, 0x56, 0x27, 0x9a, 0x79, 0xaa, 0x83, 0xdc, 0xb6, 0x44, 0x8a, + 0x8b, 0xa8, 0x54, 0xe8, 0x51, 0xb9, 0x6d, 0xb1, 0x60, 0xb3, 0x2c, 0xe3, 0x74, 0x06, 0xa5, 0xe1, 0x6a, 0xd5, 0xca, + 0x6d, 0x6b, 0xca, 0x62, 0x78, 0x4a, 0x57, 0x2b, 0x71, 0xe0, 0x72, 0xca, 0x62, 0xa7, 0x09, 0x64, 0x6b, 0x5b, 0x53, + 0xff, 0x56, 0x4c, 0x58, 0xbf, 0xf1, 0x6f, 0x9d, 0x96, 0x7a, 0xe5, 0x16, 0xf8, 0xc9, 0x80, 0xe2, 0xca, 0x15, 0x8d, + 0xd4, 0x8a, 0x06, 0x78, 0x2e, 0x8f, 0x92, 0x11, 0x27, 0x20, 0xd1, 0xf6, 0x15, 0x0d, 0xf4, 0x8a, 0xce, 0x77, 0xac, + 0xe8, 0xfc, 0x9e, 0x15, 0xf5, 0xd5, 0xea, 0x59, 0x05, 0xee, 0x92, 0xd5, 0xaa, 0xd5, 0x2c, 0xb1, 0xd7, 0x6d, 0x04, + 0x6c, 0x01, 0xab, 0x01, 0xda, 0x21, 0x67, 0x53, 0xba, 0x9d, 0x28, 0xab, 0x28, 0xa6, 0xbf, 0x09, 0x93, 0x25, 0x16, + 0xd2, 0x2a, 0x16, 0x4c, 0xba, 0x2e, 0xa2, 0x9e, 0x7f, 0x26, 0x65, 0x33, 0xc0, 0x43, 0x06, 0x78, 0x08, 0xf5, 0x25, + 0xa4, 0x8e, 0xfd, 0xce, 0xc6, 0xb6, 0x65, 0x6b, 0xb2, 0xbe, 0xca, 0x2f, 0x41, 0x46, 0x88, 0xf9, 0x3d, 0x88, 0x16, + 0xa1, 0xb6, 0xdd, 0xdb, 0x4d, 0x73, 0x90, 0xa0, 0x70, 0x93, 0xa4, 0x81, 0xed, 0xc9, 0xaa, 0xbf, 0x09, 0x55, 0x53, + 0x16, 0xab, 0x74, 0xb7, 0x9d, 0xb4, 0x56, 0xbe, 0x37, 0x29, 0xae, 0x7d, 0x7c, 0x2c, 0x6b, 0xcc, 0x7c, 0xce, 0x69, + 0x1a, 0x2b, 0xca, 0xb5, 0xed, 0xff, 0x2f, 0xa8, 0x70, 0x0b, 0x5f, 0xf1, 0xf5, 0x02, 0x68, 0x02, 0x54, 0x7a, 0xbe, + 0xe2, 0xf9, 0x52, 0x3c, 0xed, 0x95, 0x0a, 0xee, 0x1d, 0x32, 0x6d, 0x0d, 0x59, 0x04, 0xa6, 0xcf, 0x7c, 0x4a, 0x83, + 0x4b, 0xc1, 0xa0, 0xfb, 0xa3, 0x2b, 0xa5, 0xb0, 0xae, 0x89, 0xbb, 0xb2, 0x01, 0xb6, 0x7f, 0x9e, 0xb7, 0x1f, 0x1d, + 0x9d, 0xdb, 0x58, 0xf2, 0xf8, 0x64, 0x3c, 0xb6, 0x51, 0x6e, 0x3d, 0xac, 0x59, 0xeb, 0xe8, 0xe7, 0xf9, 0x57, 0xcf, + 0x9a, 0x5f, 0x15, 0x8d, 0x63, 0x20, 0x22, 0x95, 0x61, 0xa1, 0x45, 0x95, 0x01, 0xaf, 0x9e, 0xd1, 0xd8, 0x8f, 0x77, + 0x4f, 0x67, 0x60, 0x4e, 0x27, 0x9b, 0x51, 0x1a, 0x00, 0x71, 0xe2, 0x8d, 0xd2, 0xcb, 0x88, 0x2e, 0xa8, 0xbe, 0xfc, + 0x71, 0xcb, 0x60, 0x5b, 0x5a, 0x8c, 0x92, 0x79, 0xcc, 0x55, 0xaa, 0x89, 0x62, 0xb5, 0xc6, 0x94, 0xae, 0xc4, 0x1c, + 0x4c, 0x13, 0xe2, 0x4e, 0xca, 0xb9, 0xaa, 0xf4, 0xca, 0xaf, 0xb0, 0x6d, 0x00, 0xb0, 0x13, 0xb2, 0xfe, 0x8e, 0x72, + 0xaf, 0x89, 0x9b, 0xbb, 0x60, 0xc3, 0x2d, 0xe4, 0xd9, 0xf6, 0x50, 0xe3, 0x49, 0x78, 0x8b, 0x2b, 0x37, 0x76, 0xec, + 0xc4, 0xd7, 0x27, 0x31, 0x70, 0x9d, 0x42, 0x67, 0x31, 0xcd, 0xb2, 0x9d, 0x08, 0x28, 0x16, 0x11, 0xdb, 0x65, 0x6d, + 0x7b, 0x47, 0x2f, 0xb8, 0x89, 0x61, 0x87, 0x09, 0x80, 0x8b, 0x98, 0xb5, 0xaa, 0x45, 0xc7, 0x63, 0x3a, 0x2a, 0x9c, + 0xed, 0x10, 0x7d, 0x1c, 0xb3, 0x88, 0x43, 0x10, 0x4e, 0x44, 0xc7, 0xec, 0x57, 0x49, 0x4c, 0x6d, 0xa4, 0xf3, 0x69, + 0x15, 0xfc, 0x4a, 0xfe, 0x6f, 0x87, 0x47, 0xf6, 0x58, 0x85, 0x45, 0x8d, 0xb2, 0x5a, 0x69, 0x5f, 0x50, 0xa5, 0xbc, + 0x8a, 0xc8, 0x44, 0x38, 0x7b, 0x76, 0x6d, 0xa0, 0x87, 0x6d, 0x93, 0x65, 0xeb, 0xab, 0xe3, 0x56, 0x33, 0xb7, 0xb1, + 0x0d, 0xdd, 0x3d, 0x74, 0x97, 0x88, 0x56, 0x87, 0xd0, 0x6a, 0x1e, 0xff, 0x96, 0x76, 0xed, 0xd6, 0xe3, 0x96, 0x8d, + 0xe5, 0x45, 0x0e, 0x28, 0x2f, 0x98, 0xc1, 0x08, 0xdc, 0xcf, 0x7f, 0x78, 0x2a, 0xd5, 0xce, 0x1f, 0x06, 0xcf, 0x49, + 0xab, 0x69, 0x63, 0x3b, 0xe3, 0xc9, 0xec, 0x37, 0x4c, 0xe1, 0xd0, 0xc6, 0xf6, 0x28, 0x4a, 0x32, 0x6a, 0xce, 0x41, + 0xaa, 0xb3, 0x7f, 0x7c, 0x12, 0x12, 0xa2, 0x59, 0x4a, 0xb3, 0xcc, 0x32, 0xfb, 0x57, 0xa4, 0xf4, 0x09, 0x86, 0xb9, + 0x95, 0xe2, 0x32, 0xca, 0x05, 0x5e, 0xe4, 0x1d, 0x0b, 0x26, 0x55, 0xc9, 0xb2, 0x0d, 0x62, 0x13, 0x22, 0xa0, 0x60, + 0x6c, 0x52, 0xbb, 0xfa, 0xe4, 0xc8, 0x5b, 0xb6, 0x9e, 0x1c, 0x58, 0x46, 0xe5, 0x37, 0x07, 0xa8, 0x94, 0x4c, 0x59, + 0x7c, 0xb9, 0xa5, 0xd4, 0xbf, 0xdd, 0x52, 0x0a, 0x2a, 0x5b, 0x01, 0x9d, 0xba, 0xff, 0xe7, 0xd3, 0x58, 0x2f, 0x15, + 0x1f, 0x13, 0xc4, 0x40, 0x38, 0x37, 0x3f, 0x01, 0xa9, 0xb1, 0x0c, 0xa2, 0x87, 0xdf, 0x3f, 0x1c, 0x94, 0xfc, 0x96, + 0xe1, 0x8a, 0x5e, 0xfe, 0xd8, 0x0c, 0xa1, 0xb4, 0x0e, 0x11, 0x84, 0xe8, 0x37, 0xcd, 0x95, 0xde, 0x7e, 0x9a, 0xe0, + 0x0c, 0xad, 0xea, 0x0f, 0x2c, 0xbd, 0xba, 0x47, 0x60, 0x7d, 0xed, 0xb7, 0x14, 0x2b, 0xc5, 0xa7, 0x58, 0xff, 0x51, + 0xc4, 0xa6, 0x25, 0x09, 0x6c, 0x82, 0x29, 0x34, 0x1e, 0x48, 0x27, 0x33, 0x3b, 0x91, 0xaa, 0xcf, 0x25, 0x1c, 0x92, + 0x85, 0x7b, 0x48, 0xe6, 0x29, 0xbd, 0x8c, 0x92, 0x9b, 0xf5, 0x8b, 0xd5, 0x76, 0x57, 0x0e, 0xd9, 0x24, 0x34, 0x4e, + 0xbe, 0x51, 0x52, 0x2c, 0xc2, 0xbd, 0x03, 0xe4, 0xff, 0xf2, 0xcf, 0xae, 0xfb, 0x2f, 0xff, 0xfc, 0xc9, 0xaa, 0xd0, + 0x7d, 0x7e, 0x85, 0x79, 0xd9, 0xed, 0xee, 0xdd, 0xb5, 0x7d, 0xa4, 0x2a, 0xce, 0xb7, 0xd7, 0xd9, 0x58, 0x04, 0x78, + 0xbf, 0xb1, 0x04, 0x1b, 0x85, 0x72, 0xf7, 0x59, 0xbf, 0x07, 0x30, 0x98, 0xd7, 0x27, 0x21, 0x83, 0x4a, 0x7f, 0x08, + 0xb4, 0x2b, 0xe4, 0x3d, 0x68, 0x45, 0x7e, 0x3f, 0x86, 0x3f, 0x35, 0x87, 0x3f, 0x08, 0xbe, 0xf2, 0x4f, 0x8c, 0xae, + 0xae, 0x8a, 0x14, 0x47, 0xb3, 0x29, 0x5c, 0xa0, 0xd0, 0xdf, 0x28, 0x51, 0x8a, 0x87, 0xd7, 0x44, 0x3d, 0x71, 0x40, + 0x93, 0x8c, 0xae, 0x5e, 0xc2, 0xad, 0x49, 0xdd, 0xeb, 0x54, 0x3b, 0x78, 0xef, 0x11, 0x0e, 0xd0, 0x45, 0x75, 0x56, + 0xa2, 0xd3, 0x0d, 0xc9, 0x00, 0xa5, 0x60, 0x6e, 0x00, 0x98, 0x78, 0x74, 0xa5, 0xac, 0xcd, 0x73, 0xe9, 0x86, 0xf1, + 0xd6, 0x49, 0x5b, 0xb9, 0x67, 0x2a, 0x48, 0xc7, 0xd6, 0x3b, 0x81, 0x2f, 0x51, 0x99, 0x96, 0xd6, 0xbd, 0x70, 0x75, + 0x81, 0x1d, 0x51, 0xb0, 0x9f, 0x85, 0x1f, 0x2d, 0x1e, 0xc6, 0xf8, 0x76, 0x0b, 0xd4, 0x95, 0xb5, 0xfa, 0xb7, 0x56, + 0x09, 0x56, 0xf5, 0x55, 0x45, 0x1f, 0x10, 0x69, 0x1e, 0x8c, 0xee, 0x88, 0x44, 0x67, 0xf4, 0x93, 0x91, 0xe8, 0xe8, + 0x41, 0x91, 0xe8, 0x8c, 0xfe, 0xd9, 0x91, 0x68, 0x46, 0x8d, 0x48, 0x34, 0x90, 0xe0, 0x2f, 0x0f, 0x0a, 0x68, 0xea, + 0xf0, 0x53, 0x72, 0x93, 0x91, 0x96, 0x32, 0x02, 0xa2, 0x64, 0x02, 0xd1, 0xcc, 0x7f, 0xfb, 0xe0, 0x64, 0x94, 0x4c, + 0xcc, 0xd0, 0x24, 0x5c, 0xfa, 0x0b, 0xb1, 0x48, 0x9c, 0x92, 0xa5, 0xfd, 0xf3, 0x6d, 0xeb, 0xc9, 0xa0, 0xd5, 0x39, + 0x6c, 0x4d, 0x6d, 0xcf, 0x06, 0xa9, 0x2b, 0x0a, 0x9a, 0x9d, 0xc3, 0x43, 0x28, 0xb8, 0x31, 0x0a, 0xda, 0x50, 0xc0, + 0x8c, 0x82, 0x63, 0x28, 0x18, 0x19, 0x05, 0x27, 0x50, 0x10, 0x18, 0x05, 0x8f, 0xa0, 0x60, 0x61, 0xe7, 0x03, 0x56, + 0x84, 0xdb, 0x1f, 0x21, 0x71, 0x3f, 0xc8, 0x5e, 0x5a, 0x3d, 0x1b, 0x11, 0x12, 0x5d, 0xe5, 0x51, 0x71, 0xae, 0xaa, + 0x7e, 0xa4, 0xaf, 0x01, 0xb9, 0xfa, 0xec, 0x0a, 0xe1, 0x88, 0xc0, 0x31, 0x47, 0x0c, 0x46, 0xb9, 0xac, 0x79, 0xa8, + 0x5f, 0xdb, 0x5e, 0x11, 0x93, 0x6e, 0xe2, 0xb6, 0x8e, 0x4a, 0x7b, 0x36, 0xc2, 0xf3, 0xa2, 0xf2, 0x71, 0x2d, 0x50, + 0xdd, 0xc2, 0x0d, 0x1b, 0xe5, 0xf5, 0x36, 0x87, 0x08, 0xcb, 0x1b, 0xc5, 0x9f, 0x0a, 0xf9, 0xe8, 0xf2, 0xe4, 0x1d, + 0x9b, 0x52, 0xfd, 0xbd, 0x15, 0x3d, 0x80, 0x25, 0xe2, 0xf6, 0x9d, 0xb0, 0xbc, 0x13, 0xee, 0x2b, 0x7c, 0x56, 0xde, + 0xa8, 0xf4, 0x8e, 0x13, 0x79, 0x45, 0x45, 0x8a, 0xa5, 0xa1, 0x37, 0xc1, 0xdc, 0x9f, 0x78, 0x10, 0xb8, 0x04, 0x9f, + 0xa9, 0x77, 0x46, 0x08, 0x69, 0xf6, 0xe7, 0xde, 0x57, 0xf8, 0x26, 0xa4, 0xb1, 0xb7, 0xc8, 0x3b, 0x05, 0x01, 0xc8, + 0xb8, 0xe9, 0x3b, 0x5e, 0x5c, 0xc4, 0x27, 0xa8, 0xa2, 0x7c, 0x2d, 0xe1, 0xac, 0x17, 0xd4, 0xb3, 0x23, 0xd4, 0x66, + 0xf8, 0x64, 0xc6, 0x51, 0x72, 0x53, 0xbf, 0xb5, 0x7b, 0xdb, 0xc3, 0x6f, 0x30, 0xbb, 0x22, 0xfc, 0xf6, 0x02, 0x80, + 0x2d, 0x9e, 0xde, 0xf9, 0x93, 0xe2, 0xf7, 0x4b, 0x9a, 0x65, 0xfe, 0x44, 0xd5, 0xdc, 0x1d, 0x6e, 0x13, 0x20, 0x9a, + 0xa1, 0x36, 0x0d, 0x04, 0xc4, 0xc4, 0x00, 0x23, 0xe0, 0xd3, 0x50, 0x21, 0x32, 0x98, 0x7a, 0x35, 0xba, 0x26, 0x70, + 0x55, 0x2d, 0xe2, 0xfe, 0xa4, 0x2c, 0xe8, 0xce, 0x52, 0xaa, 0xe2, 0x76, 0x80, 0xc6, 0xbc, 0xdb, 0x80, 0x02, 0xf9, + 0x7a, 0x47, 0x14, 0x4d, 0x3b, 0x50, 0x76, 0xc7, 0xd2, 0x2c, 0x1d, 0x45, 0x33, 0x33, 0xbf, 0x8a, 0xb4, 0xaf, 0xcd, + 0xd8, 0xcd, 0xe7, 0xad, 0x11, 0xfc, 0x51, 0x91, 0xa1, 0xcf, 0xc7, 0xe3, 0xf1, 0xbd, 0x51, 0xb5, 0xcf, 0x83, 0x31, + 0x6d, 0xd3, 0xe3, 0x0e, 0x64, 0x05, 0xd5, 0x55, 0x2c, 0xa6, 0x95, 0x0b, 0xdc, 0x2d, 0x1f, 0x56, 0x19, 0xc2, 0x36, + 0x3c, 0x5c, 0x3e, 0x3c, 0xc2, 0x96, 0xcf, 0x52, 0xba, 0x9c, 0xfa, 0xe9, 0x84, 0xc5, 0x5e, 0x33, 0x77, 0x17, 0x2a, + 0x24, 0xf5, 0xf9, 0xe9, 0xe9, 0x69, 0xee, 0x06, 0xfa, 0xa9, 0x19, 0x04, 0xb9, 0x3b, 0x5a, 0x16, 0xd3, 0x68, 0x36, + 0xc7, 0xe3, 0xdc, 0x65, 0xba, 0xe0, 0xb0, 0x3d, 0x0a, 0x0e, 0xdb, 0xb9, 0x7b, 0x63, 0xd4, 0xc8, 0x5d, 0xaa, 0x9e, + 0x52, 0x1a, 0x54, 0x52, 0x8b, 0x1e, 0x35, 0x9b, 0xb9, 0x2b, 0x09, 0x6d, 0x09, 0x66, 0xa9, 0xfc, 0xe9, 0xf9, 0x73, + 0x9e, 0x00, 0x73, 0xef, 0x44, 0xdc, 0x19, 0x5c, 0xaa, 0x6b, 0x5b, 0xe4, 0x47, 0x4e, 0x72, 0x34, 0xc4, 0xbf, 0x98, + 0xc1, 0x23, 0x20, 0x66, 0x11, 0x34, 0x8a, 0x74, 0x6c, 0xa9, 0xf2, 0x1a, 0x28, 0x4b, 0xbc, 0xfe, 0x85, 0x44, 0x65, + 0x4c, 0x09, 0x38, 0x19, 0xd4, 0x94, 0xb7, 0x0b, 0xc6, 0xbb, 0xe4, 0x47, 0xfa, 0x69, 0xf9, 0x71, 0xf7, 0x10, 0xf1, + 0x91, 0xfe, 0xe9, 0xe2, 0x23, 0x36, 0xc5, 0x87, 0x64, 0x1e, 0xd7, 0x9c, 0xd8, 0xa3, 0x90, 0x8e, 0x3e, 0x5e, 0x27, + 0xb7, 0x75, 0xd8, 0x12, 0xa9, 0x2d, 0x04, 0xcb, 0xfe, 0xef, 0xcd, 0x94, 0xd1, 0x9d, 0x19, 0x9f, 0x48, 0x11, 0xea, + 0xc3, 0xeb, 0x98, 0xd8, 0xaf, 0xb5, 0x6d, 0x2b, 0x4b, 0xc6, 0x63, 0x62, 0xbf, 0x1e, 0x8f, 0x6d, 0x7d, 0xf8, 0xd4, + 0xe7, 0x54, 0xd4, 0x7a, 0x55, 0x29, 0x11, 0xb5, 0xbe, 0xfa, 0xca, 0x2c, 0x33, 0x0b, 0x54, 0xe8, 0xc9, 0x0c, 0x33, + 0xa9, 0x37, 0x01, 0xcb, 0x60, 0xab, 0xc1, 0x97, 0x5b, 0xaa, 0x97, 0x5f, 0xc6, 0x95, 0x7b, 0xca, 0x0b, 0x80, 0xb7, + 0x5c, 0xae, 0xbe, 0x7e, 0xf3, 0xc2, 0x84, 0xea, 0x44, 0xd0, 0x27, 0x77, 0xdf, 0x04, 0xce, 0x35, 0x47, 0x39, 0xcb, + 0x5e, 0xc7, 0x6b, 0xa7, 0xaa, 0x24, 0x8c, 0x84, 0x98, 0xd3, 0xca, 0x79, 0x32, 0x99, 0x44, 0xf0, 0xf1, 0x9c, 0x65, + 0xe5, 0x42, 0x5e, 0xd9, 0xbc, 0x5f, 0x99, 0xaf, 0x67, 0x36, 0x54, 0xd7, 0xd7, 0x8a, 0x6f, 0x79, 0xc9, 0x6c, 0xfc, + 0x85, 0xfa, 0xa8, 0x93, 0x30, 0x8b, 0x97, 0x8a, 0xc9, 0x2f, 0x65, 0x0e, 0x37, 0xc7, 0x2c, 0x90, 0xcd, 0x59, 0x90, + 0xe7, 0xea, 0xf4, 0x4b, 0xc0, 0xb2, 0x19, 0x5c, 0x14, 0x2b, 0x5b, 0xd2, 0x4f, 0xb1, 0xf0, 0xec, 0xc6, 0x88, 0xef, + 0x54, 0x96, 0x2b, 0xd7, 0x01, 0x1e, 0xe9, 0x30, 0xbf, 0xe6, 0xb9, 0xad, 0xfc, 0xee, 0x1a, 0x89, 0xb6, 0x25, 0xf1, + 0x29, 0x23, 0x4f, 0xc6, 0x0c, 0xc1, 0xf9, 0x5d, 0x2c, 0x88, 0x7e, 0xa5, 0x0b, 0x72, 0x33, 0x7e, 0x29, 0xde, 0x48, + 0x6c, 0x89, 0x68, 0x49, 0x36, 0xf3, 0x63, 0xc9, 0x46, 0x89, 0x2d, 0xf9, 0xc1, 0xfe, 0xb2, 0x5c, 0xf9, 0xdc, 0xd6, + 0x60, 0x4b, 0xe2, 0xed, 0x75, 0x1b, 0xd0, 0xa0, 0x67, 0x55, 0x40, 0x8f, 0x37, 0x82, 0x2c, 0xf7, 0xa7, 0x3b, 0xbc, + 0xbe, 0x72, 0xb3, 0x1b, 0xec, 0x66, 0x37, 0xd6, 0x5f, 0x97, 0xf5, 0x1b, 0x7a, 0xfd, 0x91, 0xf1, 0x3a, 0xf7, 0x67, + 0x75, 0x30, 0x7c, 0x84, 0x73, 0x54, 0xb1, 0x67, 0x91, 0x36, 0x29, 0xef, 0x8e, 0xe8, 0xcc, 0x33, 0xc8, 0x8a, 0x10, + 0xea, 0xbb, 0x17, 0x27, 0x31, 0xed, 0x54, 0xd3, 0x63, 0xcd, 0x20, 0xbb, 0xc6, 0xd6, 0x70, 0x99, 0x40, 0x16, 0x05, + 0xbf, 0xf3, 0x9a, 0x8a, 0xad, 0x37, 0x75, 0x04, 0xbd, 0xb9, 0xb5, 0xbe, 0xa7, 0x90, 0x5b, 0x13, 0xd2, 0x2b, 0xdd, + 0xcc, 0x24, 0xd8, 0x95, 0x09, 0xf0, 0xa9, 0x64, 0x51, 0x70, 0xa9, 0xea, 0xbf, 0x46, 0x96, 0xed, 0x7a, 0xb1, 0x48, + 0x16, 0x7d, 0x08, 0x64, 0x9e, 0x3f, 0xe6, 0x34, 0xc5, 0x0f, 0xa9, 0x79, 0x2d, 0xce, 0x75, 0x2d, 0x41, 0xcc, 0x78, + 0xad, 0xd3, 0xd9, 0xed, 0xc3, 0xbb, 0xbf, 0x7f, 0xfa, 0xb9, 0xc2, 0x91, 0xbe, 0xe7, 0xc8, 0xb6, 0x3b, 0xb0, 0x11, + 0x22, 0xff, 0xce, 0x63, 0xb1, 0x90, 0x79, 0xd7, 0xe0, 0x17, 0xed, 0xcc, 0x12, 0x95, 0xf5, 0x9c, 0xd2, 0x48, 0x7c, + 0xd6, 0x50, 0x2d, 0xc5, 0xe1, 0xc9, 0xec, 0x56, 0xaf, 0x46, 0x6b, 0x2d, 0x9b, 0xf9, 0x4f, 0x4d, 0x5a, 0xde, 0x9d, + 0x25, 0x5d, 0x4d, 0xbc, 0x3d, 0x9e, 0xdd, 0x76, 0xa4, 0xa0, 0xad, 0xa7, 0x12, 0xaa, 0xe6, 0xec, 0xd6, 0x4c, 0xdb, + 0x2e, 0x3b, 0xb2, 0xdc, 0xc3, 0xcc, 0xa2, 0x7e, 0x46, 0x3b, 0x70, 0x91, 0x3b, 0x1b, 0xf9, 0x91, 0x12, 0xe6, 0x53, + 0x16, 0x04, 0x11, 0xed, 0x68, 0x79, 0x6d, 0xb5, 0x4e, 0x20, 0xeb, 0xd9, 0x5c, 0xb2, 0xea, 0xaa, 0x18, 0xc8, 0x2b, + 0xf0, 0xe4, 0x5f, 0x67, 0x49, 0x04, 0x5f, 0x51, 0xd9, 0x8a, 0x4e, 0x95, 0x0e, 0xdc, 0x2c, 0x91, 0x27, 0x7e, 0x57, + 0xe7, 0x72, 0xdc, 0xfc, 0x4b, 0x47, 0x2c, 0x78, 0xb3, 0xc3, 0x93, 0x99, 0x57, 0x3f, 0xac, 0x4e, 0x04, 0x5e, 0x15, + 0x53, 0xc0, 0x5b, 0xa6, 0x85, 0x41, 0x5a, 0x49, 0x3e, 0x6d, 0xb9, 0x2d, 0x55, 0x26, 0x3a, 0x80, 0xb4, 0xb1, 0xa2, + 0x28, 0xaf, 0x4e, 0xe6, 0xdf, 0x66, 0xb7, 0x3c, 0xde, 0xbe, 0x5b, 0x1e, 0xeb, 0xdd, 0x72, 0x3f, 0xc5, 0x7e, 0x3e, + 0x6e, 0xc1, 0x9f, 0x4e, 0x39, 0x21, 0xaf, 0x69, 0x1d, 0xce, 0x6e, 0x2d, 0xd0, 0xd3, 0xea, 0xed, 0xd9, 0xad, 0x4c, + 0x5a, 0x87, 0xd8, 0x4d, 0x13, 0xd2, 0xb8, 0x71, 0xd3, 0x82, 0x42, 0xf8, 0xdb, 0xac, 0xbc, 0x6a, 0x1d, 0xc1, 0x3b, + 0x68, 0x75, 0xbc, 0xf9, 0xae, 0x7d, 0xff, 0xa6, 0xf5, 0xe2, 0x84, 0x3b, 0x9e, 0xe6, 0xc6, 0xc8, 0xe5, 0xfe, 0xf5, + 0x35, 0x0d, 0xbc, 0x71, 0x32, 0x9a, 0x67, 0xff, 0xa4, 0xe0, 0x57, 0x48, 0xbc, 0x77, 0x4b, 0xaf, 0xf5, 0xa3, 0x9b, + 0xca, 0x14, 0x7a, 0xdd, 0xc3, 0xb2, 0x58, 0x27, 0x2f, 0x1b, 0xf9, 0x11, 0x75, 0xda, 0xee, 0xd1, 0x96, 0x4d, 0xf0, + 0xef, 0xb2, 0x36, 0x5b, 0x27, 0xf3, 0x47, 0x91, 0x71, 0x2f, 0x12, 0x7e, 0x13, 0x0e, 0xcc, 0x35, 0x6c, 0x9e, 0x6e, + 0x07, 0x77, 0xa0, 0x47, 0x1a, 0x6a, 0xa1, 0xa0, 0xe4, 0x4e, 0x40, 0xc7, 0xfe, 0x3c, 0xe2, 0xf7, 0xf7, 0xba, 0x8b, + 0x32, 0x36, 0x7a, 0xbd, 0x87, 0xa1, 0x97, 0x75, 0x1f, 0xc8, 0xa5, 0x3f, 0x7f, 0x7c, 0x04, 0x7f, 0x64, 0xfe, 0xd7, + 0x5d, 0xa9, 0xab, 0x4b, 0xbb, 0x17, 0x74, 0xf5, 0xfd, 0x8a, 0x32, 0x2e, 0x45, 0xb8, 0xd0, 0xc7, 0x1f, 0x5a, 0x1b, + 0xb4, 0xca, 0x07, 0x55, 0x57, 0x5a, 0xd6, 0x6f, 0xaa, 0xfd, 0xdb, 0x3a, 0x7f, 0x60, 0xdd, 0x91, 0xd4, 0x5c, 0xab, + 0x75, 0xd5, 0x77, 0x1d, 0x37, 0x2a, 0x6b, 0x8c, 0x8b, 0xfa, 0xfb, 0xe4, 0xae, 0x30, 0x51, 0x64, 0x34, 0x16, 0xac, + 0x94, 0x7d, 0x69, 0xa5, 0x24, 0x94, 0x5c, 0x75, 0xfb, 0xb7, 0xd3, 0xc8, 0x5a, 0xc8, 0xf3, 0xa7, 0xc4, 0x6e, 0xb9, + 0x4d, 0xdb, 0x12, 0x79, 0x00, 0x70, 0x0d, 0xbe, 0x2d, 0xbe, 0x17, 0x6c, 0xf7, 0x41, 0xd3, 0x5a, 0x4c, 0x84, 0x66, + 0xf7, 0xc2, 0xbf, 0xa3, 0xe9, 0x65, 0xdb, 0xb6, 0xc0, 0x4f, 0x53, 0x97, 0x29, 0x13, 0xa2, 0xcc, 0x6a, 0xdb, 0xd6, + 0xed, 0x34, 0x8a, 0x33, 0x62, 0x87, 0x9c, 0xcf, 0x3c, 0xf9, 0x41, 0xe1, 0x9b, 0x43, 0x37, 0x49, 0x27, 0x8d, 0x76, + 0xb3, 0xd9, 0x84, 0x1b, 0x75, 0x6d, 0x6b, 0xc1, 0xe8, 0xcd, 0x93, 0xe4, 0x96, 0xd8, 0x4d, 0xab, 0x69, 0xb5, 0xda, + 0xa7, 0x56, 0xab, 0x7d, 0xe4, 0x9e, 0x9c, 0xda, 0xbd, 0xcf, 0x2c, 0xab, 0x1b, 0xd0, 0x71, 0x06, 0x3f, 0x2c, 0xab, + 0x2b, 0x14, 0x2f, 0xf9, 0xdb, 0xb2, 0xdc, 0x51, 0x94, 0xd5, 0x5b, 0xd6, 0x52, 0x3d, 0x5a, 0x16, 0x9c, 0xd2, 0xf5, + 0xac, 0xcf, 0xc7, 0xed, 0xf1, 0xd1, 0xf8, 0x71, 0x47, 0x15, 0xe7, 0x9f, 0x55, 0xaa, 0x63, 0xf9, 0x7f, 0xdb, 0x68, + 0x96, 0xf1, 0x34, 0xf9, 0x48, 0x55, 0x4e, 0xa2, 0x05, 0xa2, 0x67, 0x6b, 0xd3, 0xf6, 0xe6, 0x48, 0xad, 0xd3, 0xeb, + 0xd1, 0xb8, 0x5d, 0x56, 0x17, 0x30, 0x36, 0x0a, 0x20, 0xbb, 0x0d, 0x0d, 0x7a, 0xd7, 0x44, 0x53, 0xab, 0xbe, 0x0d, + 0x51, 0x2d, 0x5b, 0xcd, 0x71, 0xa2, 0xe7, 0xd7, 0x85, 0x43, 0x21, 0x5a, 0x57, 0x15, 0x10, 0xd8, 0x56, 0x40, 0xec, + 0x97, 0xad, 0xf6, 0x29, 0x6e, 0xb5, 0x4e, 0xdc, 0x93, 0xd3, 0x51, 0x13, 0x1f, 0xb9, 0x47, 0xf5, 0x43, 0xf7, 0x04, + 0x9f, 0xd6, 0x4f, 0xf1, 0xe9, 0xf3, 0xd3, 0x51, 0xfd, 0xc8, 0x3d, 0xc2, 0xcd, 0xfa, 0x29, 0x14, 0xd6, 0x4f, 0xeb, + 0xa7, 0x8b, 0xfa, 0xd1, 0xe9, 0xa8, 0x29, 0x4a, 0xdb, 0xee, 0xf1, 0x71, 0xbd, 0xd5, 0x74, 0x8f, 0x8f, 0xf1, 0xb1, + 0x7b, 0x72, 0x52, 0x6f, 0x1d, 0xba, 0x27, 0x27, 0x2f, 0x8e, 0x4f, 0xdd, 0x43, 0x78, 0x77, 0x78, 0x38, 0x3a, 0x74, + 0x5b, 0xad, 0x3a, 0xfc, 0x83, 0x4f, 0xdd, 0xb6, 0xfc, 0xd1, 0x6a, 0xb9, 0x87, 0x2d, 0xdc, 0x8c, 0x8e, 0xdb, 0xee, + 0xc9, 0x63, 0x2c, 0xfe, 0x15, 0xd5, 0xb0, 0xf8, 0x07, 0xba, 0xc1, 0x8f, 0xdd, 0xf6, 0x89, 0xfc, 0x25, 0x3a, 0x5c, + 0x1c, 0x9d, 0xfe, 0x64, 0x37, 0x76, 0xce, 0xa1, 0x25, 0xe7, 0x70, 0x7a, 0xec, 0x1e, 0x1e, 0xe2, 0xa3, 0x96, 0x7b, + 0x7a, 0x18, 0xd6, 0x8f, 0xda, 0xee, 0xc9, 0xa3, 0x51, 0xbd, 0xe5, 0x3e, 0x7a, 0x84, 0x9b, 0xf5, 0x43, 0xb7, 0x8d, + 0x5b, 0xee, 0xd1, 0xa1, 0xf8, 0x71, 0xe8, 0xb6, 0x17, 0x8f, 0x1e, 0xbb, 0x27, 0xc7, 0xe1, 0x89, 0x7b, 0xf4, 0xfd, + 0xd1, 0xa9, 0xdb, 0x3e, 0x0c, 0x0f, 0x4f, 0xdc, 0xf6, 0xa3, 0xc5, 0x89, 0x7b, 0x14, 0xd6, 0xdb, 0x27, 0xf7, 0xb6, + 0x6c, 0xb5, 0x5d, 0xc0, 0x91, 0x78, 0x0d, 0x2f, 0xb0, 0x7a, 0x01, 0x7f, 0x43, 0xd1, 0xf6, 0xdf, 0xb1, 0x9b, 0x6c, + 0xb3, 0xe9, 0x63, 0xf7, 0xf4, 0xd1, 0x48, 0x56, 0x87, 0x82, 0xba, 0xae, 0x01, 0x4d, 0x16, 0x75, 0x39, 0xac, 0xe8, + 0xae, 0xae, 0x3b, 0xd2, 0x7f, 0xd5, 0x60, 0x8b, 0x3a, 0x0c, 0x2c, 0xc7, 0xfd, 0x0f, 0xed, 0xa7, 0x58, 0xf2, 0x6e, + 0x63, 0x22, 0x49, 0x7f, 0xd2, 0xfb, 0x4c, 0x5e, 0x97, 0xfd, 0xd9, 0x15, 0x8e, 0x76, 0x39, 0x3e, 0xfc, 0x4f, 0x3b, + 0x3e, 0x42, 0xfa, 0x10, 0xcf, 0x87, 0xff, 0xa7, 0x7b, 0x3e, 0xa2, 0x75, 0xc7, 0xf9, 0x0d, 0xdf, 0x70, 0x70, 0xac, + 0x5b, 0xc5, 0x2f, 0xb8, 0x33, 0x48, 0xe0, 0xc3, 0x6c, 0x79, 0xe7, 0x86, 0x93, 0x90, 0x9a, 0x7e, 0xa0, 0x04, 0x58, + 0xec, 0x0d, 0x97, 0x3c, 0x76, 0xb4, 0x0b, 0x21, 0xc1, 0xa7, 0x11, 0xf2, 0xfd, 0x43, 0xf0, 0x11, 0xfc, 0xe9, 0xf8, + 0x18, 0x99, 0xf8, 0xa8, 0xf8, 0xf2, 0x85, 0xa7, 0x41, 0x78, 0x0a, 0x2e, 0xc4, 0xb3, 0x03, 0xa7, 0xd2, 0x6a, 0x76, + 0x83, 0x42, 0x51, 0x66, 0xcb, 0xc8, 0xd7, 0xdb, 0xdf, 0x12, 0x76, 0x90, 0x47, 0x50, 0x89, 0xad, 0xdc, 0x32, 0x33, + 0x21, 0x75, 0xd4, 0x43, 0x21, 0x94, 0xda, 0x6e, 0xd3, 0x6d, 0x16, 0x2e, 0x1d, 0x38, 0x76, 0x4c, 0x96, 0x09, 0xf7, + 0xe1, 0x13, 0xc0, 0x51, 0x32, 0x11, 0x1f, 0x0b, 0x86, 0xcf, 0x33, 0x40, 0xd2, 0xcf, 0x48, 0x7e, 0x19, 0x03, 0xce, + 0x4d, 0x28, 0x47, 0x8f, 0x9f, 0x7e, 0xfc, 0x0e, 0x8e, 0xfe, 0xea, 0xa8, 0xc4, 0x14, 0xbc, 0x1d, 0x2f, 0x69, 0xc0, + 0x7c, 0xc7, 0x76, 0x66, 0x29, 0x1d, 0xd3, 0x34, 0xab, 0x57, 0xce, 0xc3, 0x8a, 0xa3, 0xb0, 0xc8, 0xd6, 0xdf, 0x9a, + 0x4d, 0xe1, 0xba, 0x71, 0x32, 0x50, 0xfe, 0x46, 0x5b, 0x19, 0x60, 0x76, 0x8e, 0x75, 0x49, 0x0a, 0xb2, 0xb6, 0x54, + 0xda, 0x6c, 0xa9, 0xb5, 0xb5, 0xdc, 0xf6, 0x31, 0xb2, 0x44, 0x31, 0x5c, 0xe4, 0xfc, 0xa3, 0x53, 0x3f, 0x6c, 0xfe, + 0x05, 0x19, 0xcd, 0x8a, 0x8e, 0x86, 0xca, 0xdd, 0x16, 0x97, 0x1f, 0xe9, 0xae, 0x1e, 0x56, 0xb6, 0x25, 0x45, 0x7c, + 0x2e, 0xe7, 0x6e, 0xa3, 0x4e, 0xac, 0x22, 0xdc, 0xf2, 0xca, 0x8d, 0x31, 0x9b, 0x38, 0xe6, 0x27, 0x98, 0xe5, 0x45, + 0xd1, 0xe2, 0xcb, 0xed, 0x28, 0x2f, 0xab, 0xc4, 0x68, 0x29, 0xe2, 0x2d, 0x2c, 0xb6, 0xe2, 0xd5, 0xca, 0x89, 0xc1, + 0x45, 0x4e, 0x0c, 0x9c, 0xc2, 0x33, 0xaa, 0x20, 0x39, 0xc6, 0x05, 0x40, 0x02, 0xc1, 0x24, 0x96, 0xff, 0x97, 0xc5, + 0xfa, 0x87, 0x72, 0x7c, 0xb9, 0x91, 0x1f, 0x4f, 0x80, 0x0a, 0xfd, 0x78, 0xb2, 0xe1, 0x56, 0x93, 0x21, 0xa3, 0xb5, + 0xd2, 0xb2, 0xab, 0xd2, 0x7d, 0x96, 0x3d, 0xb9, 0x7b, 0xa7, 0x6e, 0x94, 0xb3, 0xc1, 0x3b, 0x2d, 0x22, 0x1c, 0xe5, + 0xed, 0xd7, 0x35, 0xf2, 0x45, 0x77, 0x4a, 0xb9, 0x2f, 0xf3, 0x35, 0x41, 0x9f, 0x80, 0x63, 0xc8, 0x96, 0xb2, 0x46, + 0x89, 0x2d, 0xa4, 0x3b, 0x91, 0x67, 0x68, 0xa4, 0xa8, 0xc7, 0x96, 0xba, 0x8a, 0xa1, 0x2e, 0x96, 0x86, 0xb4, 0xb0, + 0xf4, 0xc7, 0x8c, 0x7c, 0x91, 0x91, 0x4f, 0xe2, 0xc4, 0xee, 0x7d, 0x51, 0x7c, 0x4e, 0x76, 0xd7, 0x22, 0x44, 0x2c, + 0xfe, 0x38, 0x48, 0x69, 0xf4, 0x4f, 0xe4, 0x0b, 0x36, 0x4a, 0xe2, 0x2f, 0x86, 0x36, 0xea, 0x70, 0x37, 0x4c, 0xe9, + 0x98, 0x7c, 0x01, 0x32, 0xde, 0x13, 0xd6, 0x07, 0x30, 0xc2, 0xda, 0xed, 0x34, 0xc2, 0x42, 0x63, 0x7a, 0x80, 0x42, + 0x24, 0xc1, 0xb5, 0xdb, 0xc7, 0xb6, 0x25, 0x6d, 0x62, 0xf1, 0xbb, 0x27, 0xc5, 0xa9, 0x50, 0x02, 0xac, 0x56, 0xdb, + 0x3d, 0x0e, 0xdb, 0xee, 0xe3, 0xc5, 0x23, 0xf7, 0x34, 0x6c, 0x3d, 0x5a, 0xd4, 0xe1, 0xff, 0xb6, 0xfb, 0x38, 0xaa, + 0xb7, 0xdd, 0xc7, 0xf0, 0xf7, 0xfb, 0x23, 0xf7, 0x38, 0xac, 0xb7, 0xdc, 0xd3, 0xc5, 0xa1, 0x7b, 0xf8, 0xa2, 0xd5, + 0x76, 0x0f, 0xad, 0x96, 0x25, 0xdb, 0x01, 0xbb, 0x96, 0xdc, 0xf9, 0x8b, 0xb5, 0x0d, 0xb1, 0x25, 0x1c, 0x27, 0x73, + 0x4e, 0x6d, 0xec, 0x14, 0x1f, 0xad, 0x54, 0xfb, 0x53, 0x39, 0xeb, 0x9e, 0xfa, 0x29, 0x7c, 0x39, 0xa8, 0xba, 0x77, + 0x2b, 0xef, 0x70, 0x85, 0x5f, 0x6c, 0x19, 0x02, 0x76, 0xb8, 0x8d, 0xcd, 0xbb, 0x0c, 0xe0, 0x22, 0x00, 0x71, 0xd1, + 0xba, 0xbe, 0x6f, 0x72, 0x37, 0x69, 0xcb, 0x8a, 0xfa, 0x4e, 0x4b, 0xc1, 0x2c, 0x98, 0xf8, 0xa4, 0x85, 0x18, 0xe4, + 0x9b, 0x20, 0x5f, 0x1f, 0x1f, 0x52, 0x5f, 0xd3, 0xc4, 0xb8, 0xce, 0x81, 0x96, 0x07, 0x36, 0x02, 0x06, 0x17, 0x70, + 0xe4, 0xb9, 0x06, 0xbd, 0xe2, 0xa6, 0x2d, 0xb1, 0x24, 0xf8, 0x05, 0xcd, 0xfa, 0x36, 0x14, 0xd9, 0x9e, 0x2d, 0x5c, + 0x7c, 0x76, 0xf1, 0xf5, 0xa4, 0x82, 0xb0, 0xcb, 0x02, 0x2c, 0x0e, 0x5d, 0xc1, 0xae, 0x05, 0xfc, 0xd8, 0xe8, 0xe0, + 0x60, 0xe7, 0x7e, 0x11, 0x0a, 0x24, 0xcc, 0xb5, 0xfc, 0xe8, 0x8a, 0xc9, 0x8a, 0x6c, 0x13, 0xd1, 0x45, 0xbf, 0x02, + 0x85, 0x48, 0xe1, 0xe9, 0x9a, 0xfa, 0xdc, 0xf5, 0x63, 0x99, 0x44, 0x63, 0x30, 0x2c, 0xdc, 0xa2, 0x87, 0x28, 0x4f, + 0xb8, 0x6f, 0x7c, 0x58, 0x59, 0xed, 0xf3, 0x84, 0xfb, 0xfa, 0x70, 0xb2, 0x71, 0x0f, 0x13, 0x38, 0x7a, 0xc3, 0x76, + 0xef, 0xf5, 0xbb, 0x33, 0x4b, 0x6e, 0xcf, 0x6e, 0x23, 0x6c, 0xf7, 0xba, 0xc2, 0x67, 0x22, 0x0f, 0xea, 0x11, 0x79, + 0x50, 0xcf, 0x52, 0x67, 0x33, 0x21, 0x92, 0x96, 0x37, 0xe4, 0xb4, 0x85, 0xcd, 0x20, 0xbd, 0xbd, 0xd3, 0x79, 0xc4, + 0x19, 0x5c, 0x1a, 0xde, 0x10, 0xa7, 0xf4, 0x60, 0xc1, 0x8a, 0x3c, 0x6c, 0xa5, 0x1d, 0x5e, 0xf3, 0x58, 0xfb, 0x86, + 0xc7, 0x2c, 0xa2, 0x3a, 0xf3, 0x5a, 0x75, 0x55, 0x9c, 0x14, 0xd8, 0xac, 0x9d, 0xcd, 0xaf, 0xa7, 0x8c, 0xdb, 0xfa, + 0x3c, 0xc3, 0x7b, 0xd5, 0xa0, 0x2b, 0x86, 0xea, 0x5d, 0xe5, 0xca, 0x79, 0xad, 0x3f, 0x8f, 0x54, 0x5d, 0x52, 0x35, + 0x7b, 0x25, 0x21, 0xe0, 0x84, 0x5c, 0x78, 0xd8, 0x2b, 0xdc, 0xc5, 0xe6, 0xbb, 0xbc, 0xdb, 0x08, 0x0f, 0x7b, 0x57, + 0xde, 0x4c, 0xf5, 0xf7, 0x22, 0x99, 0x6c, 0xef, 0x2b, 0x4a, 0x26, 0x7d, 0x71, 0x14, 0x44, 0x9e, 0x99, 0xd6, 0xca, + 0x6f, 0x12, 0xd9, 0xbd, 0xae, 0x52, 0x06, 0x2c, 0x11, 0x58, 0xb7, 0x8f, 0x9b, 0xfa, 0x74, 0x49, 0x94, 0x4c, 0x60, + 0x43, 0xca, 0x26, 0xc6, 0x20, 0x15, 0x8f, 0x7b, 0xd8, 0xea, 0x75, 0x7d, 0x4b, 0xf0, 0x16, 0xc1, 0x3c, 0x32, 0xaf, + 0x01, 0x8d, 0xc3, 0x64, 0x4a, 0x5d, 0x96, 0x34, 0x6e, 0xe8, 0x75, 0xdd, 0x9f, 0xb1, 0xd2, 0xbd, 0x0d, 0x4a, 0x47, + 0x31, 0x64, 0xa2, 0x3d, 0xe2, 0xea, 0xec, 0x55, 0xbb, 0x74, 0xb7, 0x1d, 0x81, 0xcd, 0xa3, 0x5d, 0x73, 0xc2, 0x27, + 0x67, 0x80, 0x95, 0xf4, 0xba, 0x0d, 0x7f, 0x0d, 0x23, 0x82, 0xdf, 0xe7, 0xca, 0xd1, 0x0e, 0x86, 0x0d, 0xd0, 0x9b, + 0x6d, 0x49, 0x71, 0xa0, 0x1d, 0xf2, 0x4a, 0x50, 0xe7, 0x76, 0xef, 0x5f, 0xff, 0xc7, 0xff, 0x52, 0x3e, 0xf6, 0x6e, + 0x23, 0x6c, 0xe9, 0xbe, 0xd6, 0x56, 0x25, 0xef, 0xc2, 0xf9, 0xd0, 0x32, 0x28, 0x4c, 0x6f, 0xeb, 0x93, 0x94, 0x05, + 0xf5, 0xd0, 0x8f, 0xc6, 0x76, 0x6f, 0x37, 0x36, 0xcd, 0x63, 0x5b, 0x0a, 0xea, 0x6a, 0x11, 0xd0, 0xeb, 0xef, 0x3a, + 0x78, 0xa4, 0xcf, 0xaf, 0x88, 0xad, 0x6d, 0x1e, 0x43, 0x2a, 0x77, 0x5f, 0xe5, 0x28, 0x52, 0xac, 0xbe, 0xb9, 0xa6, + 0x38, 0x60, 0x5c, 0x39, 0x81, 0x94, 0xdb, 0x56, 0x11, 0xd4, 0xfa, 0xbf, 0xff, 0xf3, 0xbf, 0xfc, 0x37, 0xfd, 0x08, + 0xb1, 0xaa, 0x7f, 0xfd, 0xef, 0xff, 0xf9, 0xff, 0xfc, 0xef, 0xff, 0x0a, 0xa7, 0x56, 0x54, 0x3c, 0x4b, 0x30, 0x15, + 0xab, 0x0c, 0x66, 0x49, 0xee, 0x62, 0x41, 0x62, 0xe7, 0x94, 0x65, 0x9c, 0x8d, 0xaa, 0x67, 0x92, 0x2e, 0xc4, 0x80, + 0x62, 0x67, 0x2a, 0xe8, 0xc4, 0x0e, 0xcf, 0x4b, 0x82, 0xaa, 0xa0, 0x5c, 0x10, 0x6e, 0xde, 0x6d, 0x00, 0xbe, 0x1f, + 0x76, 0x8c, 0xd3, 0x2d, 0x96, 0x63, 0xa9, 0xc9, 0x04, 0x4a, 0xf2, 0xb2, 0xdc, 0x82, 0xd8, 0xca, 0x12, 0x1e, 0xbd, + 0xb6, 0x51, 0x2c, 0x56, 0xaf, 0xd2, 0xa6, 0xf3, 0x61, 0x9e, 0x71, 0x36, 0x06, 0x94, 0x4b, 0x3f, 0xb1, 0x08, 0x63, + 0xd7, 0x41, 0x57, 0x8c, 0xee, 0x72, 0xd1, 0x8b, 0x24, 0xd0, 0xa3, 0xd3, 0xbf, 0xe4, 0x5f, 0x4e, 0x41, 0x23, 0xb3, + 0x9c, 0xa9, 0x7f, 0xab, 0xcc, 0xf3, 0x93, 0x66, 0x73, 0x76, 0x8b, 0x96, 0xe5, 0x08, 0x78, 0xd7, 0x60, 0x82, 0x8e, + 0xcd, 0x0e, 0x45, 0xfc, 0xbb, 0x70, 0x63, 0x37, 0x2d, 0xf0, 0x85, 0x5b, 0xcd, 0x3c, 0xff, 0xeb, 0x52, 0x78, 0x52, + 0xd9, 0xaf, 0x10, 0xa7, 0x56, 0x4e, 0xe7, 0xeb, 0xc4, 0x9c, 0xdc, 0xd2, 0x68, 0xd5, 0x96, 0xad, 0xc2, 0xd6, 0xe6, + 0xe9, 0x44, 0x33, 0xce, 0x6e, 0x46, 0xc8, 0x8f, 0x20, 0xe6, 0x1d, 0xb6, 0x70, 0xd8, 0x5e, 0x16, 0xdd, 0x73, 0x9e, + 0x4c, 0xcd, 0xc0, 0x3a, 0xf5, 0xe9, 0x88, 0x8e, 0xb5, 0xb3, 0x5e, 0xbd, 0x97, 0x41, 0xf3, 0x3c, 0x3c, 0xdc, 0x32, + 0x96, 0x02, 0x49, 0x04, 0xd4, 0xad, 0x66, 0xfe, 0x39, 0xec, 0xc0, 0xe5, 0x38, 0x4a, 0x7c, 0xee, 0x09, 0x82, 0xed, + 0x98, 0xe1, 0x79, 0x1f, 0x78, 0x52, 0xb2, 0x34, 0xe0, 0xe9, 0xc8, 0xaa, 0xe0, 0x36, 0xaf, 0x9e, 0x21, 0xcd, 0x5d, + 0xd1, 0xdc, 0xec, 0x4a, 0x7a, 0xdd, 0xbe, 0x57, 0x51, 0xef, 0xb7, 0x15, 0x77, 0x95, 0x12, 0x48, 0x6d, 0xb4, 0xfd, + 0xbd, 0x94, 0xeb, 0xf2, 0xed, 0x77, 0xdc, 0xb1, 0x05, 0x98, 0xf6, 0x7a, 0x2d, 0x51, 0x08, 0xb5, 0xde, 0x92, 0xef, + 0x0b, 0x93, 0xc9, 0x9f, 0xcd, 0x44, 0x45, 0xd4, 0xe9, 0x36, 0xa4, 0xa6, 0x0b, 0xdc, 0x43, 0xa4, 0x74, 0xc8, 0x0c, + 0x0a, 0x55, 0x49, 0x6d, 0x05, 0xf9, 0x4b, 0xe5, 0x56, 0xc0, 0xb7, 0xf8, 0x7a, 0xff, 0x0f, 0x85, 0xa3, 0x0b, 0x12, + 0x20, 0x8b, 0x00, 0x00}; } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server/server_index_v3.h b/esphome/components/web_server/server_index_v3.h index 39518197a3..725bdc34e3 100644 --- a/esphome/components/web_server/server_index_v3.h +++ b/esphome/components/web_server/server_index_v3.h @@ -10,4043 +10,4051 @@ namespace esphome { namespace web_server { const uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcc, 0xbd, 0xeb, 0x7a, 0x1b, 0xb7, 0xb2, 0x20, 0xfa, - 0xfb, 0xcc, 0x53, 0x48, 0xbd, 0x1d, 0xa5, 0x21, 0x82, 0x2d, 0x92, 0xba, 0x58, 0x6e, 0x0a, 0xe2, 0xf8, 0x1a, 0x3b, - 0x71, 0x6c, 0xc7, 0x72, 0xec, 0x38, 0x0c, 0xb7, 0x0c, 0x36, 0x41, 0x12, 0x76, 0x13, 0x60, 0x1a, 0xa0, 0x25, 0x85, - 0xe4, 0xbb, 0x9f, 0xaf, 0x70, 0xe9, 0x46, 0x93, 0xb4, 0xd7, 0x5a, 0x73, 0x66, 0xce, 0x37, 0x3b, 0x7b, 0x59, 0x6c, - 0xdc, 0x51, 0x28, 0x14, 0xaa, 0x0a, 0x55, 0x85, 0x8b, 0xfd, 0x91, 0xcc, 0xf4, 0xdd, 0x9c, 0xed, 0x4d, 0xf5, 0x2c, - 0xbf, 0xbc, 0x70, 0xff, 0x32, 0x3a, 0xba, 0xbc, 0xc8, 0xb9, 0xf8, 0xb2, 0x57, 0xb0, 0x9c, 0xf0, 0x4c, 0x8a, 0xbd, - 0x69, 0xc1, 0xc6, 0x64, 0x44, 0x35, 0x4d, 0xf9, 0x8c, 0x4e, 0xd8, 0xde, 0xd1, 0xe5, 0xc5, 0x8c, 0x69, 0xba, 0x97, - 0x4d, 0x69, 0xa1, 0x98, 0x26, 0xbf, 0xbf, 0x7b, 0xd6, 0x3c, 0xbf, 0xbc, 0x50, 0x59, 0xc1, 0xe7, 0x7a, 0x0f, 0x9a, - 0x24, 0x33, 0x39, 0x5a, 0xe4, 0xec, 0xf2, 0xe8, 0xe8, 0xe6, 0xe6, 0x26, 0xf9, 0xac, 0xfe, 0xc7, 0x57, 0x5a, 0xec, - 0xfd, 0x52, 0x90, 0xd7, 0xc3, 0xcf, 0x2c, 0xd3, 0xc9, 0x88, 0x8d, 0xb9, 0x60, 0x6f, 0x0a, 0x39, 0x67, 0x85, 0xbe, - 0xeb, 0x42, 0xe6, 0x4f, 0x05, 0x89, 0x39, 0xd6, 0x98, 0x21, 0x72, 0xa9, 0xf7, 0xb8, 0xd8, 0xe3, 0xbd, 0x5f, 0x0a, - 0x93, 0xb2, 0x64, 0x62, 0x31, 0x63, 0x05, 0x1d, 0xe6, 0x2c, 0xdd, 0x6f, 0xe1, 0x4c, 0x8a, 0x31, 0x9f, 0x2c, 0xca, - 0xef, 0x9b, 0x82, 0x6b, 0xff, 0xfb, 0x2b, 0xcd, 0x17, 0x2c, 0x65, 0x6b, 0x94, 0xf2, 0xbe, 0x1e, 0x10, 0x66, 0x5a, - 0xfe, 0x52, 0x35, 0x1c, 0xff, 0x64, 0x9a, 0xbc, 0x9b, 0x33, 0x39, 0xde, 0xd3, 0xfb, 0x24, 0x52, 0x77, 0xb3, 0xa1, - 0xcc, 0xa3, 0x9e, 0x6e, 0x44, 0x51, 0x0a, 0x65, 0x30, 0x43, 0xdd, 0x4c, 0x0a, 0xa5, 0xf7, 0x04, 0x27, 0x37, 0x5c, - 0x8c, 0xe4, 0x0d, 0xbe, 0x11, 0x44, 0xf0, 0xe4, 0x6a, 0x4a, 0x47, 0xf2, 0xe6, 0xad, 0x94, 0xfa, 0xe0, 0x20, 0x76, - 0xdf, 0x77, 0x8f, 0xaf, 0xae, 0x08, 0x21, 0x5f, 0x25, 0x1f, 0xed, 0xb5, 0x56, 0xab, 0x20, 0x35, 0x11, 0x54, 0xf3, - 0xaf, 0xcc, 0x56, 0x42, 0x07, 0x07, 0x11, 0x1d, 0xc9, 0xb9, 0x66, 0xa3, 0x2b, 0x7d, 0x97, 0xb3, 0xab, 0x29, 0x63, - 0x5a, 0x45, 0x5c, 0xec, 0x3d, 0x91, 0xd9, 0x62, 0xc6, 0x84, 0x4e, 0xe6, 0x85, 0xd4, 0x12, 0x06, 0x76, 0x70, 0x10, - 0x15, 0x6c, 0x9e, 0xd3, 0x8c, 0x41, 0xfe, 0xe3, 0xab, 0xab, 0xaa, 0x46, 0x55, 0x08, 0x7f, 0x11, 0xe4, 0xca, 0x0c, - 0x3d, 0x46, 0xf8, 0x83, 0x20, 0x82, 0xdd, 0xec, 0x7d, 0x60, 0xf4, 0xcb, 0xaf, 0x74, 0xde, 0xcd, 0x72, 0xaa, 0xd4, - 0xde, 0x33, 0xb9, 0x34, 0xd3, 0x28, 0x16, 0x99, 0x96, 0x45, 0xac, 0x31, 0xc3, 0x02, 0x2d, 0xf9, 0x38, 0xd6, 0x53, - 0xae, 0x92, 0xeb, 0x7b, 0x99, 0x52, 0x6f, 0x99, 0x5a, 0xe4, 0xfa, 0x1e, 0xd9, 0x6f, 0x61, 0xb1, 0x4f, 0xc8, 0x17, - 0x81, 0xf4, 0xb4, 0x90, 0x37, 0x7b, 0x4f, 0x8b, 0x42, 0x16, 0x71, 0xf4, 0xf8, 0xea, 0xca, 0x96, 0xd8, 0xe3, 0x6a, - 0x4f, 0x48, 0xbd, 0x57, 0xb6, 0x07, 0xd0, 0x4e, 0xf6, 0x7e, 0x57, 0x6c, 0xef, 0xd3, 0x42, 0x28, 0x3a, 0x66, 0x8f, - 0xaf, 0xae, 0x3e, 0xed, 0xc9, 0x62, 0xef, 0x53, 0xa6, 0xd4, 0xa7, 0x3d, 0x2e, 0x94, 0x66, 0x74, 0x94, 0x44, 0xa8, - 0x6b, 0x3a, 0xcb, 0x94, 0x7a, 0xc7, 0x6e, 0x35, 0xd1, 0xd8, 0x7c, 0x6a, 0xc2, 0xd6, 0x13, 0xa6, 0xf7, 0x54, 0x39, - 0xaf, 0x18, 0x2d, 0x73, 0xa6, 0xf7, 0x34, 0x31, 0xf9, 0xd2, 0xc1, 0x9f, 0xd9, 0x4f, 0xdd, 0xe5, 0xe3, 0xf8, 0x46, - 0x1c, 0x1c, 0xe8, 0x12, 0xd0, 0x68, 0xe9, 0x56, 0x88, 0xb0, 0x7d, 0x9f, 0x76, 0x70, 0xc0, 0x92, 0x9c, 0x89, 0x89, - 0x9e, 0x12, 0x42, 0xda, 0x5d, 0x71, 0x70, 0x10, 0x6b, 0xf2, 0x41, 0x24, 0x13, 0xa6, 0x63, 0x86, 0x10, 0xae, 0x6a, - 0x1f, 0x1c, 0xc4, 0x16, 0x08, 0x92, 0x68, 0x03, 0xb8, 0x1a, 0x8c, 0x51, 0xe2, 0xa0, 0x7f, 0x75, 0x27, 0xb2, 0x38, - 0x1c, 0x3f, 0xc2, 0xe2, 0xe0, 0xe0, 0x83, 0x48, 0x14, 0xb4, 0x88, 0x35, 0x42, 0xeb, 0x82, 0xe9, 0x45, 0x21, 0xf6, - 0xf4, 0x5a, 0xcb, 0x2b, 0x5d, 0x70, 0x31, 0x89, 0xd1, 0xd2, 0xa7, 0x05, 0x15, 0xd7, 0x6b, 0x3b, 0xdc, 0xdf, 0x0a, - 0xc2, 0xc9, 0x25, 0xf4, 0xf8, 0x4c, 0xc6, 0x0e, 0x07, 0x39, 0x21, 0x91, 0x32, 0x75, 0xa3, 0x1e, 0x4f, 0x79, 0x23, - 0x8a, 0xb0, 0x1d, 0x25, 0xfe, 0x22, 0x10, 0x16, 0x1a, 0x50, 0x37, 0x49, 0x12, 0x8d, 0xc8, 0xe5, 0xd2, 0x83, 0x85, - 0x07, 0x13, 0xed, 0xf1, 0x7e, 0x6b, 0x90, 0xea, 0xa4, 0x60, 0xa3, 0x45, 0xc6, 0xe2, 0x58, 0x60, 0x85, 0x25, 0x22, - 0x97, 0xa2, 0x11, 0x17, 0xe4, 0x12, 0xd6, 0xbb, 0xa8, 0x2f, 0x36, 0x21, 0xfb, 0x2d, 0xe4, 0x06, 0x59, 0xf8, 0x11, - 0x02, 0x88, 0xdd, 0x80, 0x0a, 0x42, 0x22, 0xb1, 0x98, 0x0d, 0x59, 0x11, 0x95, 0xc5, 0xba, 0x35, 0xbc, 0x58, 0x28, - 0xb6, 0x97, 0x29, 0xb5, 0x37, 0x5e, 0x88, 0x4c, 0x73, 0x29, 0xf6, 0xa2, 0x46, 0xd1, 0x88, 0x2c, 0x3e, 0x94, 0xe8, - 0x10, 0xa1, 0x35, 0x8a, 0x15, 0x6a, 0xf0, 0xbe, 0x6c, 0xb4, 0x07, 0x18, 0x46, 0x89, 0xba, 0xae, 0x3d, 0x07, 0x01, - 0x86, 0x39, 0x4c, 0x72, 0x8d, 0xff, 0xb4, 0x3b, 0x1f, 0xa6, 0x78, 0x23, 0x7a, 0x3c, 0xd9, 0xde, 0x29, 0x44, 0x27, - 0x33, 0x3a, 0x8f, 0x19, 0xb9, 0x64, 0x06, 0xbb, 0xa8, 0xc8, 0x60, 0xac, 0xb5, 0x85, 0xeb, 0xb1, 0x94, 0x25, 0x15, - 0x4e, 0xa1, 0x54, 0x27, 0x63, 0x59, 0x3c, 0xa5, 0xd9, 0x14, 0xea, 0x95, 0x18, 0x33, 0xf2, 0x1b, 0x2e, 0x2b, 0x18, - 0xd5, 0xec, 0x69, 0xce, 0xe0, 0x2b, 0x8e, 0x4c, 0xcd, 0x08, 0x61, 0x05, 0x5b, 0x3d, 0xe7, 0xfa, 0x95, 0x14, 0x19, - 0xeb, 0xaa, 0x00, 0xbf, 0xcc, 0xca, 0x3f, 0xd4, 0xba, 0xe0, 0xc3, 0x85, 0x66, 0x71, 0x24, 0xa0, 0x44, 0x84, 0x15, - 0xc2, 0x22, 0xd1, 0xec, 0x56, 0x3f, 0x96, 0x42, 0x33, 0xa1, 0x09, 0xf3, 0x50, 0xc5, 0x3c, 0xa1, 0xf3, 0x39, 0x13, - 0xa3, 0xc7, 0x53, 0x9e, 0x8f, 0x62, 0x81, 0xd6, 0x68, 0x8d, 0x7f, 0x17, 0x04, 0x26, 0x49, 0x2e, 0x79, 0x0a, 0xff, - 0x7c, 0x7b, 0x3a, 0xb1, 0x26, 0x97, 0x66, 0x5b, 0x30, 0x12, 0x45, 0xdd, 0xb1, 0x2c, 0x62, 0x37, 0x85, 0x3d, 0x20, - 0x5d, 0xd0, 0xc7, 0xdb, 0x45, 0xce, 0x14, 0x62, 0x0d, 0x22, 0xca, 0x75, 0x74, 0x10, 0xfe, 0xad, 0x88, 0x19, 0x2c, - 0x00, 0x47, 0x29, 0x37, 0x24, 0xf0, 0x25, 0x77, 0x9b, 0x6a, 0x54, 0x12, 0xb5, 0x8f, 0x82, 0x8c, 0x78, 0xa2, 0x8b, - 0x85, 0xd2, 0x6c, 0xf4, 0xee, 0x6e, 0xce, 0x14, 0xfe, 0xb9, 0x20, 0x1f, 0x45, 0xef, 0xa3, 0x48, 0xd8, 0x6c, 0xae, - 0xef, 0xae, 0x0c, 0x35, 0x4f, 0xa3, 0x08, 0xff, 0x6d, 0x8a, 0x16, 0x8c, 0x66, 0x40, 0xd2, 0x1c, 0xc8, 0xde, 0xc8, - 0xfc, 0x6e, 0xcc, 0xf3, 0xfc, 0x6a, 0x31, 0x9f, 0xcb, 0x42, 0x63, 0x2d, 0xc8, 0x52, 0xcb, 0x0a, 0x3e, 0xb0, 0xa2, - 0x4b, 0x75, 0xc3, 0x75, 0x36, 0x8d, 0x35, 0x5a, 0x66, 0x54, 0xb1, 0xbd, 0x47, 0x52, 0xe6, 0x8c, 0x8a, 0x94, 0x13, - 0xde, 0xfb, 0xb9, 0x48, 0xc5, 0x22, 0xcf, 0xbb, 0xc3, 0x82, 0xd1, 0x2f, 0x5d, 0x93, 0x6d, 0x0f, 0x87, 0xd4, 0xfc, - 0x7e, 0x58, 0x14, 0xf4, 0x0e, 0x0a, 0x12, 0x02, 0xc5, 0x7a, 0x3c, 0xfd, 0xf9, 0xea, 0xf5, 0xab, 0xc4, 0xee, 0x15, - 0x3e, 0xbe, 0x8b, 0x79, 0xb9, 0xff, 0xf8, 0x1a, 0x8f, 0x0b, 0x39, 0xdb, 0xe8, 0xda, 0x82, 0x8e, 0x77, 0xbf, 0x31, - 0x04, 0x46, 0xf8, 0xbe, 0x6d, 0x3a, 0x1c, 0xc1, 0x2b, 0x83, 0xf9, 0x90, 0x49, 0x5c, 0xbf, 0xf0, 0x4f, 0x6a, 0x93, - 0x63, 0x8e, 0xbe, 0x3f, 0x5a, 0x5d, 0xdc, 0x2d, 0x19, 0x31, 0xe3, 0x9c, 0xc3, 0xc1, 0x08, 0x63, 0xcc, 0xa8, 0xce, - 0xa6, 0x4b, 0x66, 0x1a, 0x5b, 0xfb, 0x11, 0xb3, 0xf5, 0x1a, 0xbf, 0x92, 0x1e, 0xeb, 0xf5, 0x3e, 0x21, 0xdc, 0xd0, - 0x2b, 0xa2, 0x57, 0x2b, 0x4e, 0x08, 0x47, 0xf8, 0x2d, 0x27, 0x4b, 0xea, 0x27, 0x04, 0x27, 0x1b, 0x6c, 0xcf, 0xd4, - 0x52, 0x19, 0x38, 0x01, 0xbf, 0xb2, 0x42, 0xb3, 0x22, 0xd5, 0x02, 0x17, 0x6c, 0x9c, 0xc3, 0x38, 0xf6, 0xdb, 0x78, - 0x4a, 0xd5, 0xe3, 0x29, 0x15, 0x13, 0x36, 0x4a, 0x5f, 0xc9, 0x35, 0x66, 0x82, 0x44, 0x63, 0x2e, 0x68, 0xce, 0xff, - 0x61, 0xa3, 0xc8, 0x9d, 0x0b, 0xef, 0xf5, 0x1e, 0xbb, 0xd5, 0x4c, 0x8c, 0xd4, 0xde, 0xf3, 0x77, 0xbf, 0xbe, 0x74, - 0x8b, 0x59, 0x3b, 0x2b, 0xd0, 0x52, 0x2d, 0xe6, 0xac, 0x88, 0x11, 0x76, 0x67, 0xc5, 0x53, 0x6e, 0xe8, 0xe4, 0xaf, - 0x74, 0x6e, 0x53, 0xb8, 0xfa, 0x7d, 0x3e, 0xa2, 0x9a, 0xbd, 0x61, 0x62, 0xc4, 0xc5, 0x84, 0xec, 0xb7, 0x6d, 0xfa, - 0x94, 0xba, 0x8c, 0x51, 0x99, 0x74, 0x7d, 0xef, 0x69, 0x6e, 0xe6, 0x5e, 0x7e, 0x2e, 0x62, 0xb4, 0x56, 0x9a, 0x6a, - 0x9e, 0xed, 0xd1, 0xd1, 0xe8, 0x85, 0xe0, 0x9a, 0x9b, 0x11, 0x16, 0xb0, 0x44, 0x80, 0xab, 0xcc, 0x9e, 0x1a, 0x7e, - 0xe4, 0x31, 0xc2, 0x71, 0xec, 0xce, 0x82, 0x29, 0x72, 0x6b, 0x76, 0x70, 0x50, 0x51, 0xfe, 0x1e, 0x4b, 0x6d, 0x26, - 0xe9, 0x0f, 0x50, 0x32, 0x5f, 0x28, 0x58, 0x6c, 0xdf, 0x05, 0x1c, 0x34, 0x72, 0xa8, 0x58, 0xf1, 0x95, 0x8d, 0x4a, - 0x04, 0x51, 0x31, 0x5a, 0x6e, 0xf4, 0xe1, 0xb6, 0x87, 0x26, 0xfd, 0x41, 0x37, 0x24, 0xe1, 0xcc, 0x21, 0xbb, 0xe5, - 0x54, 0x38, 0x53, 0x25, 0x51, 0x89, 0xe1, 0x40, 0x2d, 0x09, 0x8b, 0x22, 0x7e, 0x7e, 0xf3, 0x58, 0x00, 0x0f, 0x11, - 0x52, 0x0e, 0x7f, 0xe6, 0x3e, 0xfd, 0x6a, 0x0e, 0x0f, 0x85, 0x05, 0xc2, 0xda, 0x8e, 0x54, 0x21, 0xb4, 0x46, 0x58, - 0xfb, 0xe1, 0x5a, 0xa2, 0xe4, 0xf9, 0x22, 0x38, 0xb5, 0xc9, 0x5b, 0x6e, 0x8e, 0x6d, 0xa0, 0x6d, 0x54, 0xb3, 0x83, - 0x83, 0x98, 0x25, 0x25, 0x62, 0x90, 0xfd, 0xb6, 0x5b, 0xa4, 0x00, 0x5a, 0xdf, 0x18, 0x37, 0xf4, 0x6c, 0x18, 0x9c, - 0x7d, 0x96, 0x08, 0xf9, 0x30, 0xcb, 0x98, 0x52, 0xb2, 0x38, 0x38, 0xd8, 0x37, 0xe5, 0x4b, 0xce, 0x02, 0x16, 0xf1, - 0xf5, 0x8d, 0xa8, 0x86, 0x80, 0xaa, 0xd3, 0xd6, 0xf3, 0x4d, 0xa4, 0xe2, 0x9b, 0x3c, 0x13, 0x92, 0x46, 0xd7, 0xd7, - 0x51, 0x43, 0x63, 0x07, 0x87, 0x09, 0xf3, 0x5d, 0xdf, 0x3d, 0x61, 0x96, 0x2d, 0x34, 0x4c, 0xc8, 0x16, 0x68, 0x76, - 0xf2, 0x83, 0x71, 0x7d, 0x48, 0x58, 0x63, 0x85, 0xd6, 0xc1, 0x8a, 0xee, 0x6c, 0xda, 0xf0, 0x37, 0x76, 0xe9, 0x96, - 0x13, 0xc3, 0x53, 0x04, 0xeb, 0xd8, 0x67, 0x83, 0x35, 0x36, 0xb0, 0xf7, 0xb3, 0x91, 0x66, 0xa0, 0x7d, 0x3d, 0xe8, - 0xba, 0x7c, 0xa2, 0x2c, 0xe4, 0x0a, 0xf6, 0xf7, 0x82, 0x29, 0x6d, 0x11, 0x39, 0xd6, 0x58, 0x62, 0x38, 0xa3, 0x36, - 0x99, 0xce, 0x1a, 0x4b, 0xba, 0x6b, 0x6c, 0xaf, 0xe7, 0x70, 0x36, 0x2a, 0x40, 0xea, 0xef, 0xe3, 0x13, 0x8c, 0x55, - 0xa3, 0xd5, 0xea, 0x2d, 0xf7, 0xad, 0x54, 0x6b, 0x59, 0xf2, 0x6b, 0x1b, 0x8b, 0xc2, 0x04, 0x72, 0x87, 0xf3, 0x7e, - 0xdb, 0x8d, 0x5f, 0x0c, 0xc8, 0x7e, 0xab, 0xc4, 0x62, 0x07, 0x56, 0x3b, 0x1e, 0x0b, 0xc5, 0xd7, 0xb6, 0x29, 0x64, - 0xce, 0xfa, 0x1a, 0xbe, 0x24, 0xd3, 0x2d, 0x5c, 0x9d, 0x92, 0x3e, 0x70, 0x1d, 0xc9, 0x74, 0xf0, 0x2d, 0x7c, 0xf2, - 0x14, 0x21, 0xd6, 0xdb, 0x79, 0x15, 0xe1, 0xf8, 0x5a, 0x27, 0x1c, 0x1b, 0xd3, 0x88, 0xe6, 0x65, 0x95, 0xa8, 0x44, - 0x33, 0xb7, 0xd5, 0xab, 0x2c, 0x2c, 0xcc, 0x60, 0xaa, 0x29, 0x05, 0x4d, 0xbc, 0xa2, 0x33, 0xa6, 0x62, 0x86, 0xf0, - 0xb7, 0x0a, 0x58, 0xfc, 0x84, 0x22, 0x83, 0xe0, 0x0c, 0x55, 0x70, 0x86, 0x02, 0xbb, 0x0b, 0x4c, 0x5a, 0x7d, 0xcb, - 0x29, 0xcc, 0xfa, 0x6a, 0x50, 0xf1, 0x76, 0xc1, 0xe4, 0xcd, 0xe1, 0xec, 0x10, 0xdc, 0xc3, 0xcf, 0xa6, 0x59, 0xa0, - 0x19, 0x16, 0x42, 0x21, 0xbc, 0xdf, 0xda, 0x5c, 0x49, 0x5f, 0xaa, 0x9a, 0x63, 0x7f, 0x00, 0xeb, 0x60, 0x8e, 0x8d, - 0x84, 0x2b, 0xf3, 0xb7, 0xb6, 0xd5, 0x00, 0x6c, 0x57, 0x80, 0x19, 0xc9, 0x38, 0xa7, 0x3a, 0x6e, 0x1f, 0xb5, 0x80, - 0x31, 0xfd, 0xca, 0xe0, 0x54, 0x41, 0x68, 0x7b, 0x2a, 0x2c, 0x59, 0x08, 0x35, 0xe5, 0x63, 0x1d, 0xff, 0x2e, 0x0c, - 0x51, 0x61, 0xb9, 0x62, 0x20, 0xe1, 0x04, 0xec, 0xb1, 0x21, 0x38, 0xbf, 0x0b, 0xe8, 0xa7, 0x5b, 0x1e, 0x44, 0x6e, - 0xa4, 0x86, 0x70, 0x01, 0x79, 0xa8, 0x58, 0xeb, 0x8a, 0xcc, 0x94, 0x8c, 0x1b, 0x70, 0x8f, 0xed, 0x9e, 0x6d, 0x31, - 0x75, 0xd4, 0x40, 0x04, 0x1c, 0xac, 0x48, 0x43, 0x12, 0xe1, 0x12, 0x75, 0xa2, 0xe5, 0x4b, 0x79, 0xc3, 0x8a, 0xc7, - 0x14, 0x06, 0x9f, 0xda, 0xea, 0x6b, 0x7b, 0x14, 0x18, 0x8a, 0xaf, 0xbb, 0x1e, 0x5f, 0xae, 0xcd, 0xc4, 0xdf, 0x14, - 0x72, 0xc6, 0x15, 0x03, 0xbe, 0xcd, 0xc2, 0x5f, 0xc0, 0x46, 0x33, 0x3b, 0x12, 0x8e, 0x1b, 0x56, 0xe2, 0xd7, 0xc3, - 0x97, 0x75, 0xfc, 0xba, 0xbe, 0xf7, 0x74, 0xe2, 0x29, 0x60, 0x7d, 0x1f, 0x23, 0x1c, 0x3b, 0xf1, 0x22, 0x38, 0xe9, - 0x92, 0x29, 0x72, 0xc7, 0xfc, 0x6a, 0xa5, 0x03, 0x31, 0xae, 0xc6, 0x39, 0x32, 0xbb, 0x6d, 0xd0, 0x9a, 0x8e, 0x46, - 0xc0, 0xe2, 0x15, 0x32, 0xcf, 0x83, 0xc3, 0x0a, 0x8b, 0x6e, 0x79, 0x3c, 0x5d, 0xdf, 0x7b, 0x7a, 0xf5, 0xbd, 0x13, - 0x0a, 0xf2, 0xc3, 0x43, 0xca, 0x0f, 0x54, 0x8c, 0x58, 0x01, 0x72, 0x65, 0xb0, 0x5a, 0xee, 0x9c, 0x7d, 0x2c, 0x85, - 0x60, 0x99, 0x66, 0x23, 0x10, 0x5a, 0x04, 0xd1, 0xc9, 0x54, 0x2a, 0x5d, 0x26, 0x56, 0xa3, 0x17, 0xa1, 0x10, 0x9a, - 0x64, 0x34, 0xcf, 0x63, 0x2b, 0xa0, 0xcc, 0xe4, 0x57, 0xb6, 0x63, 0xd4, 0xdd, 0xda, 0x90, 0xcb, 0x66, 0x58, 0xd0, - 0x0c, 0x4b, 0xd4, 0x3c, 0xe7, 0x19, 0x2b, 0x0f, 0xaf, 0xab, 0x84, 0x8b, 0x11, 0xbb, 0x05, 0x3a, 0x82, 0x2e, 0x2f, - 0x2f, 0x5b, 0xb8, 0x8d, 0xd6, 0x16, 0xe0, 0xcb, 0x2d, 0xc0, 0x7e, 0xe7, 0xd8, 0xb4, 0x82, 0xf8, 0x72, 0x27, 0x59, - 0x43, 0xc1, 0x59, 0xc9, 0xbd, 0xa0, 0x65, 0xc9, 0x33, 0xc2, 0x23, 0x96, 0x33, 0xcd, 0x3c, 0x39, 0x07, 0x66, 0xda, - 0x6e, 0xdd, 0xb7, 0x25, 0xfc, 0x4a, 0x74, 0xf2, 0xbb, 0xcc, 0xaf, 0xb9, 0x2a, 0x45, 0xf7, 0x6a, 0x79, 0x2a, 0x68, - 0xf7, 0xb4, 0x5d, 0x1e, 0xaa, 0x35, 0xcd, 0xa6, 0x56, 0x62, 0x8f, 0xb7, 0xa6, 0x54, 0xb5, 0xe1, 0x48, 0x7b, 0xb9, - 0x89, 0xfe, 0x2c, 0xdc, 0x30, 0x77, 0x81, 0xe0, 0xca, 0x11, 0x05, 0x06, 0x42, 0xa0, 0x5d, 0xb6, 0xc7, 0x34, 0xcf, - 0x87, 0x34, 0xfb, 0x52, 0xc7, 0xfe, 0x0a, 0x0d, 0xc8, 0x26, 0x35, 0x0e, 0xb2, 0x02, 0x92, 0x15, 0xce, 0xdb, 0x53, - 0xe9, 0xda, 0x46, 0x89, 0xf7, 0x5b, 0x15, 0xda, 0xd7, 0x17, 0xfa, 0x9b, 0xd8, 0x6e, 0x46, 0x24, 0xdc, 0xcc, 0x62, - 0xa0, 0x02, 0xff, 0x12, 0xe3, 0x3c, 0x3d, 0x70, 0x78, 0x07, 0x82, 0xc7, 0x7a, 0x63, 0x20, 0x1a, 0x2d, 0xd7, 0x23, - 0xae, 0xbe, 0x0d, 0x81, 0xff, 0x2d, 0xa3, 0x7c, 0x12, 0xf4, 0xf0, 0xef, 0x0e, 0xb4, 0xa4, 0x71, 0x8e, 0x71, 0x2e, - 0x47, 0xe6, 0x18, 0x0a, 0x4f, 0x68, 0x7e, 0x01, 0xe6, 0xc5, 0xe0, 0xfb, 0x6b, 0x9b, 0x65, 0xf8, 0x32, 0x18, 0x86, - 0xea, 0x86, 0x0c, 0x45, 0x0d, 0x05, 0x1c, 0x51, 0x15, 0xe6, 0xcc, 0x95, 0x35, 0x51, 0xd2, 0x71, 0xed, 0x56, 0x1c, - 0x77, 0x34, 0xb7, 0x20, 0x71, 0x1c, 0x2b, 0x90, 0xe6, 0x3c, 0x7f, 0x5f, 0xcd, 0x42, 0x6d, 0xcd, 0x42, 0x25, 0x81, - 0xb4, 0x85, 0x2a, 0x64, 0x0e, 0xaa, 0xa7, 0x5a, 0xa0, 0xb0, 0x14, 0xb0, 0xac, 0x09, 0x50, 0x68, 0x54, 0x12, 0xdc, - 0x9c, 0x68, 0x5c, 0x38, 0x51, 0xc7, 0xe1, 0x1a, 0x90, 0x8c, 0xaa, 0x8a, 0x44, 0x76, 0x73, 0xd4, 0x64, 0x5f, 0x89, - 0x0b, 0xb4, 0xc1, 0xdf, 0xaf, 0xd7, 0x0e, 0x4a, 0x0c, 0xb9, 0xd5, 0xa9, 0x31, 0xc6, 0x01, 0x58, 0xb0, 0x24, 0x8e, - 0x19, 0xb6, 0xac, 0xcf, 0x26, 0x70, 0xca, 0x76, 0xf7, 0x09, 0x91, 0x15, 0x6c, 0x6a, 0x4c, 0xa5, 0xe7, 0xae, 0x24, - 0xc2, 0xd4, 0xb3, 0xa5, 0x45, 0x35, 0x71, 0x42, 0x22, 0xaf, 0x9d, 0x88, 0x7a, 0xcb, 0x9a, 0x70, 0x98, 0x06, 0xc5, - 0xd6, 0x29, 0x10, 0xd5, 0x62, 0x17, 0xbc, 0x77, 0x61, 0x4d, 0xad, 0x9d, 0x00, 0xe2, 0x45, 0x0d, 0xe2, 0x01, 0x68, - 0xa5, 0x25, 0x5e, 0x72, 0x40, 0x68, 0xbd, 0x72, 0xcc, 0x70, 0x61, 0x17, 0x62, 0x0b, 0x8a, 0x9b, 0xec, 0xa7, 0xc1, - 0x42, 0x90, 0x65, 0x15, 0xf0, 0x77, 0xe1, 0x11, 0x11, 0xc3, 0xe0, 0xc5, 0x6a, 0xb5, 0x85, 0x76, 0x3b, 0xb9, 0x50, - 0x94, 0x54, 0xd2, 0xe1, 0x6a, 0xf5, 0x4a, 0xa2, 0xd8, 0xf1, 0xbf, 0x98, 0xa1, 0x9e, 0x27, 0xba, 0x0f, 0x5f, 0x42, - 0x29, 0xc3, 0x8e, 0x56, 0x29, 0xa5, 0xe0, 0x50, 0xc7, 0xda, 0xfa, 0x42, 0xe9, 0x80, 0x72, 0x3f, 0xde, 0x22, 0x60, - 0x26, 0xd1, 0x9d, 0xd4, 0xd5, 0x94, 0x1f, 0xbb, 0xa6, 0x05, 0x42, 0x28, 0x55, 0x46, 0x96, 0xd9, 0xdf, 0x25, 0x5f, - 0x1e, 0x1c, 0xa8, 0xa0, 0xa1, 0xeb, 0x92, 0x52, 0x7c, 0x8e, 0xe1, 0x54, 0x56, 0x77, 0xc2, 0xb0, 0x2f, 0x9f, 0xfd, - 0x39, 0xb4, 0x25, 0x9d, 0xb6, 0xba, 0x20, 0x98, 0xd3, 0x1b, 0xca, 0xf5, 0x5e, 0xd9, 0x8a, 0x15, 0xcc, 0x63, 0x86, - 0x96, 0x8e, 0xdb, 0x48, 0x0a, 0x06, 0xfc, 0x23, 0x90, 0x05, 0xcf, 0x45, 0x5b, 0xc4, 0xcf, 0xa6, 0x0c, 0x54, 0xd9, - 0x9e, 0x91, 0x28, 0xc5, 0xc3, 0x7d, 0x77, 0x90, 0xb8, 0x86, 0x77, 0x8f, 0x7d, 0xbd, 0x59, 0xbd, 0x26, 0x0d, 0xcc, - 0x59, 0x31, 0x96, 0xc5, 0xcc, 0xe7, 0xad, 0x37, 0xbe, 0x1d, 0x71, 0xe4, 0xe3, 0x78, 0x67, 0xdb, 0x4e, 0x04, 0xe8, - 0x6e, 0xc8, 0xde, 0x95, 0xd4, 0x5e, 0x3b, 0x4d, 0xcb, 0x03, 0xd8, 0x2a, 0x08, 0x3d, 0x66, 0xaa, 0x50, 0xca, 0x77, - 0xea, 0xd5, 0xae, 0xd5, 0x9d, 0xec, 0xb7, 0xbb, 0xa5, 0xe4, 0xe7, 0xb1, 0xa1, 0x6b, 0x75, 0x1c, 0xee, 0x54, 0x95, - 0x8b, 0x7c, 0xe4, 0x06, 0x2b, 0x10, 0x66, 0x0e, 0x8f, 0x6e, 0x78, 0x9e, 0x57, 0xa9, 0xff, 0x09, 0x69, 0x57, 0x8e, - 0xb4, 0x4b, 0x4f, 0xda, 0x81, 0x54, 0x00, 0x69, 0xb7, 0xcd, 0x55, 0xd5, 0xe5, 0xd6, 0xf6, 0x94, 0x96, 0xa8, 0x2b, - 0x23, 0x4e, 0x43, 0x7f, 0x0b, 0x3f, 0x02, 0x54, 0x32, 0x5f, 0x5f, 0x62, 0xa7, 0x8f, 0x01, 0x31, 0xd0, 0xea, 0x34, - 0x59, 0xa8, 0xa9, 0xf8, 0x12, 0x23, 0xac, 0xd6, 0xac, 0xc4, 0xec, 0x87, 0x4f, 0x41, 0x69, 0x17, 0x4c, 0x07, 0xce, - 0x31, 0x93, 0xfc, 0x1f, 0xf1, 0x51, 0x7e, 0x76, 0xc2, 0xcd, 0x4e, 0xf9, 0xd9, 0x01, 0xad, 0xaf, 0x66, 0x37, 0xfa, - 0x3e, 0xb5, 0x37, 0xd3, 0x13, 0xe5, 0xf4, 0xaa, 0xf5, 0x5e, 0xad, 0xe2, 0x8d, 0x14, 0xd0, 0xe8, 0x3b, 0x29, 0xa5, - 0x28, 0x5b, 0x07, 0x1a, 0x10, 0x42, 0x06, 0x12, 0xd6, 0x76, 0xd2, 0xe5, 0x29, 0xf7, 0xf2, 0x5f, 0xe9, 0x79, 0x8c, - 0xe2, 0xde, 0xd6, 0x7f, 0x2c, 0x67, 0x73, 0x60, 0xc8, 0x36, 0x50, 0x7a, 0xc2, 0x5c, 0x87, 0x55, 0xfe, 0x7a, 0x47, - 0x5a, 0xad, 0x8e, 0xd9, 0x8f, 0x35, 0x6c, 0x2a, 0xa5, 0xe6, 0xfd, 0xd6, 0x7a, 0x51, 0x26, 0x95, 0x84, 0x63, 0x97, - 0x6e, 0xe5, 0xf1, 0xa6, 0x66, 0xc6, 0x67, 0xbc, 0x8e, 0x85, 0xa5, 0xc3, 0x02, 0x68, 0x5d, 0x40, 0x7e, 0x3c, 0xba, - 0x87, 0xeb, 0xbf, 0xae, 0x80, 0xb3, 0x5c, 0x6f, 0x80, 0x6f, 0xb9, 0x5e, 0xbf, 0xd7, 0x4e, 0xd2, 0xc6, 0xef, 0x77, - 0xc8, 0xbd, 0x25, 0xf4, 0xaa, 0x4c, 0x27, 0x33, 0xf6, 0x07, 0x90, 0xb6, 0xc5, 0x42, 0x92, 0xe5, 0x4c, 0x8e, 0x58, - 0x1a, 0xc9, 0x39, 0x13, 0xd1, 0x1a, 0xf4, 0xac, 0x0e, 0x01, 0xfe, 0x16, 0xf1, 0xf2, 0x6d, 0x5d, 0xdf, 0x9a, 0xbe, - 0xd7, 0x6b, 0x50, 0x85, 0xbd, 0xe4, 0x3b, 0x94, 0xb1, 0xef, 0x59, 0xa1, 0x0c, 0x4f, 0x5a, 0xb2, 0xb7, 0x2f, 0x79, - 0x75, 0x40, 0xbd, 0xe4, 0xe9, 0xb7, 0xab, 0x54, 0x02, 0x49, 0xd4, 0x4e, 0xce, 0x92, 0xe3, 0x08, 0x19, 0x8d, 0xf1, - 0x33, 0xaf, 0x31, 0x5e, 0x94, 0x1a, 0xe3, 0xe7, 0x9a, 0x2c, 0x36, 0x34, 0xc6, 0x7f, 0x08, 0xf2, 0x5c, 0xf7, 0x9e, - 0x7b, 0x6d, 0xfa, 0x1b, 0x99, 0xf3, 0xec, 0x2e, 0x8e, 0x72, 0xae, 0x9b, 0x70, 0x9b, 0x18, 0xe1, 0xa5, 0xcd, 0x00, - 0x55, 0xa3, 0xd1, 0x77, 0xaf, 0xbd, 0xfc, 0x87, 0x85, 0x20, 0xd1, 0xbd, 0x9c, 0xeb, 0x7b, 0x11, 0x9e, 0x6a, 0xf2, - 0x09, 0x7e, 0xdd, 0x5b, 0xc6, 0xbf, 0x52, 0x3d, 0x4d, 0x0a, 0x2a, 0x46, 0x72, 0x16, 0xa3, 0x46, 0x14, 0xa1, 0x44, - 0x19, 0x21, 0xe4, 0x01, 0x5a, 0xdf, 0xfb, 0x84, 0xff, 0x91, 0x24, 0xea, 0x45, 0x8d, 0xa9, 0xc6, 0x9a, 0x92, 0x4f, - 0x17, 0xf7, 0x96, 0xff, 0xc8, 0xf5, 0xe5, 0x27, 0xfc, 0x54, 0x97, 0x6a, 0x7d, 0x7c, 0xcb, 0x48, 0x8c, 0xc8, 0xe5, - 0x53, 0x3f, 0xa4, 0xc7, 0x72, 0x66, 0x15, 0xfc, 0x11, 0xc2, 0x5f, 0x41, 0xaf, 0x7b, 0xc9, 0x2b, 0x22, 0xe4, 0xee, - 0x60, 0xf6, 0x49, 0x24, 0x8d, 0xf2, 0x20, 0x3a, 0x38, 0x08, 0xd2, 0x4a, 0x16, 0x02, 0x7f, 0x96, 0xa4, 0x26, 0xaa, - 0x63, 0x46, 0xa1, 0xa5, 0xcf, 0x32, 0xe6, 0xc8, 0x37, 0x13, 0x7b, 0x4d, 0xb5, 0xdb, 0xb1, 0xbc, 0x6f, 0x75, 0x0f, - 0x09, 0xd7, 0xac, 0xa0, 0x5a, 0x16, 0x03, 0x14, 0xb2, 0x25, 0xf8, 0x15, 0x27, 0x9f, 0xfa, 0x7b, 0xff, 0xcf, 0xff, - 0xf8, 0x6b, 0xfc, 0x57, 0x31, 0xf8, 0x84, 0x05, 0x23, 0x47, 0x17, 0x71, 0x2f, 0x8d, 0xf7, 0x9b, 0xcd, 0xd5, 0x5f, - 0x47, 0xfd, 0xff, 0xa6, 0xcd, 0x7f, 0x1e, 0x36, 0xff, 0x1c, 0xa0, 0x55, 0xfc, 0xd7, 0x51, 0xaf, 0xef, 0xbe, 0xfa, - 0xff, 0x7d, 0xf9, 0x97, 0x1a, 0x1c, 0xda, 0xc4, 0x7b, 0x08, 0x1d, 0x4d, 0xf0, 0x2f, 0x82, 0x1c, 0x35, 0x9b, 0x97, - 0x47, 0x13, 0xfc, 0x93, 0x20, 0x47, 0xf0, 0xf7, 0x4e, 0x93, 0xb7, 0x6c, 0xf2, 0xf4, 0x76, 0x1e, 0x7f, 0xba, 0x5c, - 0xdd, 0x5b, 0xbe, 0xe2, 0x6b, 0x68, 0xb7, 0xff, 0xdf, 0x7f, 0xfd, 0xa5, 0xa2, 0x1f, 0x2f, 0xc9, 0xd1, 0xa0, 0x81, - 0x62, 0x93, 0x7c, 0x48, 0xec, 0x9f, 0xb8, 0x97, 0xf6, 0xff, 0xdb, 0x0d, 0x25, 0xfa, 0xf1, 0xaf, 0x4f, 0x17, 0x97, - 0x64, 0xb0, 0x8a, 0xa3, 0xd5, 0x8f, 0x68, 0x85, 0xd0, 0xea, 0x1e, 0xfa, 0x84, 0xa3, 0x49, 0x84, 0xf0, 0x6f, 0x82, - 0x1c, 0xfd, 0x78, 0x34, 0xc1, 0x7f, 0x0a, 0x72, 0x14, 0x1d, 0x4d, 0xf0, 0x23, 0x49, 0x8e, 0xfe, 0x3b, 0xee, 0xa5, - 0x56, 0x09, 0xb7, 0x32, 0xea, 0x8f, 0x15, 0xdc, 0x84, 0xd0, 0x82, 0xd1, 0x95, 0xe6, 0x3a, 0x67, 0xe8, 0xde, 0x11, - 0xc7, 0xef, 0x25, 0x00, 0x2b, 0xd6, 0xa0, 0xa4, 0x31, 0x97, 0xb0, 0xcb, 0x6b, 0x58, 0x78, 0xc0, 0xa0, 0x7b, 0x29, - 0xc7, 0x56, 0x4f, 0xa0, 0x52, 0x6d, 0x6f, 0x6f, 0x15, 0x5c, 0xdf, 0xe2, 0xc7, 0xe4, 0xbd, 0x8c, 0xdb, 0x08, 0x73, - 0x0a, 0x3f, 0x3a, 0x08, 0x7f, 0xd0, 0xee, 0xc2, 0x13, 0xb6, 0xb9, 0xc5, 0x30, 0x21, 0x2d, 0x3f, 0x13, 0x21, 0xfc, - 0x72, 0x47, 0xa6, 0x9e, 0x82, 0xfa, 0x01, 0xe1, 0x9f, 0x6b, 0xd7, 0xa3, 0xf8, 0xb1, 0x26, 0x25, 0x72, 0xbc, 0x2b, - 0x18, 0xfb, 0x40, 0xf3, 0x2f, 0xac, 0x88, 0x9f, 0x6a, 0xdc, 0xee, 0x3c, 0xc0, 0x46, 0x55, 0xbd, 0xdf, 0x46, 0xdd, - 0xf2, 0x76, 0xeb, 0xb9, 0xb4, 0xf7, 0x09, 0x70, 0x0a, 0xd7, 0xf5, 0x35, 0xb0, 0xf6, 0xfb, 0x7c, 0x4b, 0xa9, 0x55, - 0xd0, 0x9b, 0x08, 0xd5, 0xaf, 0x52, 0xb9, 0xf8, 0x4a, 0x73, 0x3e, 0xda, 0xd3, 0x6c, 0x36, 0xcf, 0xa9, 0x66, 0x7b, - 0x6e, 0xce, 0x7b, 0x14, 0x1a, 0x8a, 0x4a, 0x9e, 0xe2, 0x0f, 0x51, 0x6d, 0xda, 0x3f, 0x44, 0x52, 0xed, 0x9d, 0x18, - 0xee, 0xb3, 0x1c, 0x5f, 0x22, 0x68, 0x79, 0x5d, 0xb6, 0x79, 0x23, 0xd8, 0x6c, 0x83, 0xb2, 0x6c, 0x60, 0xce, 0x6f, - 0x85, 0xe1, 0x7e, 0x93, 0x90, 0x4e, 0x2f, 0xba, 0x50, 0x5f, 0x27, 0x97, 0x11, 0xdc, 0xe4, 0x14, 0x44, 0x30, 0xa3, - 0x3c, 0x82, 0x12, 0x94, 0xb4, 0xba, 0xf4, 0x82, 0x75, 0x69, 0xa3, 0xe1, 0xd9, 0xec, 0x8c, 0xf0, 0x3e, 0xb5, 0xf5, - 0x73, 0x3c, 0xc5, 0x23, 0xd2, 0x6c, 0xe3, 0x05, 0x69, 0x99, 0x2a, 0xdd, 0xc5, 0x45, 0xe6, 0xfa, 0x39, 0x38, 0x88, - 0x8b, 0x24, 0xa7, 0x4a, 0xbf, 0x00, 0x8d, 0x00, 0x59, 0xe0, 0x29, 0x29, 0x12, 0x76, 0xcb, 0xb2, 0x38, 0x43, 0x78, - 0xea, 0x68, 0x10, 0xea, 0xa2, 0x05, 0x09, 0x8a, 0x81, 0x9c, 0x41, 0x04, 0xeb, 0x4d, 0xfb, 0xed, 0x01, 0x21, 0x24, - 0xda, 0x6f, 0x36, 0xa3, 0x5e, 0x41, 0x7e, 0x11, 0x29, 0xa4, 0x04, 0xec, 0x34, 0xf9, 0x09, 0x92, 0x3a, 0x41, 0x52, - 0xfc, 0x48, 0x26, 0x9a, 0x29, 0x1d, 0x43, 0x32, 0x28, 0x09, 0x94, 0xc7, 0xf0, 0xe8, 0xe2, 0x28, 0x6a, 0x40, 0xaa, - 0x41, 0x51, 0x84, 0x0b, 0x72, 0xa7, 0x51, 0x3a, 0xed, 0x1f, 0x0f, 0xc2, 0x33, 0xc2, 0xa6, 0x42, 0xff, 0x77, 0xba, - 0x37, 0xed, 0xb7, 0x4c, 0xff, 0x97, 0x51, 0x2f, 0x2e, 0x88, 0xb2, 0x6c, 0x5c, 0x4f, 0xa5, 0x82, 0x99, 0xf9, 0xa2, - 0xd4, 0x0d, 0xd0, 0xf5, 0x3d, 0x22, 0xcd, 0x4e, 0x1a, 0x8f, 0xc2, 0x99, 0x34, 0xa1, 0x43, 0x07, 0x0a, 0x9c, 0x13, - 0x28, 0x8f, 0x0b, 0x02, 0x9d, 0x56, 0xd5, 0xee, 0x74, 0xea, 0x12, 0x7e, 0x8c, 0x7e, 0xec, 0xfd, 0x29, 0xd2, 0xdf, - 0x84, 0x1d, 0xc1, 0x9f, 0x62, 0xb5, 0x82, 0xbf, 0xbf, 0x89, 0x1e, 0x0c, 0xcb, 0xa4, 0xfd, 0xe2, 0xd2, 0x7e, 0x82, - 0x34, 0xc1, 0x52, 0x33, 0x60, 0xac, 0x4a, 0x7e, 0xcc, 0x2e, 0xce, 0x98, 0xd8, 0x19, 0x1c, 0x1c, 0xf0, 0x3e, 0x6d, - 0xb4, 0x07, 0x70, 0x23, 0x50, 0x68, 0xf5, 0x81, 0xeb, 0x69, 0x1c, 0x1d, 0x5d, 0x46, 0xa8, 0x17, 0xed, 0xc1, 0x2a, - 0x77, 0x65, 0x83, 0x38, 0x58, 0x67, 0x0d, 0x4d, 0xd3, 0xd1, 0x25, 0x69, 0xf5, 0x62, 0x61, 0x89, 0x7c, 0x8e, 0x70, - 0xe6, 0x68, 0x6a, 0x0b, 0x8f, 0x50, 0x43, 0x88, 0x86, 0xff, 0x1e, 0xa1, 0xc6, 0x54, 0x37, 0xc6, 0x28, 0xcd, 0xe0, - 0x6f, 0x3c, 0x22, 0x84, 0x34, 0x3b, 0x65, 0x45, 0x7f, 0x58, 0x52, 0x94, 0x8e, 0xbd, 0x7a, 0xb4, 0x6f, 0x36, 0x87, - 0x6c, 0xc4, 0xbc, 0xcf, 0x06, 0xab, 0x55, 0x74, 0xd1, 0xbb, 0x8c, 0x50, 0x23, 0xf6, 0x68, 0x77, 0xe4, 0xf1, 0x0e, - 0x21, 0x2c, 0x06, 0x6b, 0x77, 0x03, 0x75, 0xc3, 0x6a, 0xb7, 0x4d, 0xcb, 0x6a, 0xff, 0x07, 0x64, 0x81, 0xad, 0x4b, - 0xb9, 0xc7, 0xf2, 0xb7, 0x73, 0x98, 0xaa, 0xc7, 0x6d, 0x49, 0x5a, 0xb8, 0x20, 0x5e, 0xdd, 0x4d, 0x89, 0xae, 0xf0, - 0x3f, 0x23, 0x55, 0x71, 0xdc, 0xcf, 0xf1, 0x74, 0x40, 0x04, 0x35, 0xf2, 0x4b, 0xd7, 0x2b, 0xd3, 0x59, 0x4e, 0x6e, - 0xd8, 0xc6, 0xfd, 0x6f, 0x0e, 0x77, 0x32, 0x8f, 0x75, 0x92, 0x2d, 0x8a, 0x82, 0x09, 0xfd, 0x4a, 0x8e, 0x1c, 0x63, - 0xc7, 0x72, 0x90, 0xad, 0xe0, 0x62, 0x17, 0x03, 0x57, 0xd7, 0xf1, 0x3b, 0x65, 0xb4, 0x95, 0xbd, 0x20, 0x23, 0xcb, - 0x70, 0x99, 0xeb, 0xde, 0xee, 0xc2, 0x89, 0xd2, 0x31, 0xc2, 0x23, 0x77, 0x0f, 0x1c, 0x27, 0x49, 0xb2, 0x48, 0x32, - 0xc8, 0x86, 0x0e, 0x14, 0x5a, 0x9b, 0x7d, 0x15, 0x2b, 0xf2, 0x58, 0x27, 0x82, 0xdd, 0x9a, 0x6e, 0x63, 0x54, 0x1d, - 0xe2, 0x7e, 0xbf, 0x5d, 0xd0, 0xae, 0x21, 0x40, 0x2a, 0x11, 0x72, 0xc4, 0x00, 0x42, 0x70, 0xf7, 0xef, 0x92, 0xa6, - 0x54, 0x85, 0x37, 0x5b, 0xd5, 0x00, 0xfb, 0xa1, 0xca, 0x7b, 0x01, 0x7a, 0x62, 0xc3, 0x9e, 0x95, 0x85, 0xad, 0xf2, - 0x1c, 0x21, 0x3e, 0x8e, 0x17, 0x09, 0xdc, 0x08, 0x1a, 0x4c, 0x12, 0x02, 0xad, 0x56, 0x8b, 0x10, 0xb7, 0xa6, 0x95, - 0x62, 0x7a, 0x4c, 0xa6, 0xfd, 0xa2, 0xd1, 0x30, 0xca, 0xeb, 0x91, 0xc5, 0x8b, 0x05, 0xc2, 0xe3, 0x72, 0xaf, 0xf9, - 0x72, 0x73, 0x52, 0xef, 0x2a, 0x1e, 0xd7, 0x95, 0xc0, 0x0d, 0x21, 0x90, 0xd1, 0x2f, 0x6a, 0x68, 0x1d, 0x4f, 0xc8, - 0x51, 0xdc, 0x4f, 0x7a, 0xff, 0x73, 0x80, 0x7a, 0x71, 0x72, 0x88, 0x8e, 0x2c, 0x2d, 0x19, 0xa3, 0x6e, 0x66, 0xfb, - 0x58, 0x9a, 0xdb, 0xcf, 0x36, 0x36, 0x0a, 0xc8, 0x54, 0x62, 0x41, 0x67, 0x2c, 0x9d, 0xc0, 0xae, 0xf7, 0xc8, 0x33, - 0xc7, 0x80, 0x4c, 0xe9, 0xc4, 0xd1, 0x96, 0x24, 0xea, 0x49, 0x5a, 0x7e, 0xf5, 0xa2, 0x1e, 0xad, 0xbe, 0xfe, 0x67, - 0xd4, 0xcb, 0x68, 0xfa, 0x98, 0xaf, 0x9d, 0x92, 0xbc, 0xd6, 0xc7, 0x99, 0xef, 0x63, 0x6d, 0x17, 0x27, 0x00, 0xde, - 0x08, 0x6d, 0x6b, 0x47, 0x16, 0x68, 0xcd, 0xc7, 0x25, 0x75, 0x52, 0x89, 0xa6, 0x13, 0x80, 0x6a, 0xb0, 0x08, 0x2a, - 0xb4, 0x0d, 0x08, 0xa6, 0x0c, 0xd8, 0xe2, 0x91, 0x16, 0xa0, 0xb9, 0xb8, 0x6c, 0xa1, 0x65, 0xad, 0xb0, 0xe3, 0xac, - 0xea, 0x77, 0xf1, 0x25, 0xf1, 0x1e, 0x03, 0x55, 0xbe, 0x58, 0x74, 0xc7, 0x8d, 0x06, 0x52, 0x1e, 0xbf, 0x46, 0xfd, - 0xf1, 0x00, 0xdf, 0x02, 0x0a, 0xe1, 0x1a, 0x46, 0xe1, 0xda, 0x1c, 0x3b, 0x6e, 0x8e, 0x8d, 0x86, 0x5c, 0xa3, 0x6e, - 0x50, 0x79, 0xe1, 0x2a, 0xaf, 0xd7, 0x16, 0x32, 0x9b, 0x18, 0x77, 0x8e, 0x4c, 0x0a, 0x18, 0x82, 0x11, 0x42, 0xfe, - 0x91, 0x68, 0x67, 0xb3, 0xd0, 0x28, 0x54, 0x37, 0xbb, 0x17, 0x28, 0xaa, 0x3d, 0x3d, 0x62, 0x80, 0x05, 0x54, 0x2d, - 0xd5, 0xc8, 0x53, 0x8d, 0x47, 0x8d, 0xb6, 0x41, 0xf7, 0x66, 0xbb, 0x5b, 0x6f, 0xec, 0x7e, 0xd5, 0x18, 0x1e, 0x35, - 0xc8, 0xb4, 0xda, 0xe1, 0x6b, 0xd9, 0x68, 0xac, 0xeb, 0xf7, 0xa5, 0x7e, 0x13, 0xd7, 0xee, 0x2f, 0x9e, 0x6e, 0x99, - 0x78, 0xf8, 0xd3, 0xb7, 0x3a, 0x6f, 0x45, 0xc2, 0x85, 0x60, 0x05, 0x9c, 0xb0, 0x44, 0x63, 0xb1, 0x5e, 0x97, 0xa7, - 0xfe, 0xef, 0xda, 0xda, 0x8c, 0x11, 0x0e, 0x74, 0xc8, 0x48, 0x6d, 0x58, 0xe2, 0x02, 0x53, 0x43, 0x45, 0x08, 0x21, - 0x1f, 0xb4, 0x37, 0x8f, 0xd1, 0x86, 0x24, 0x65, 0x24, 0x38, 0xbb, 0x63, 0x45, 0x58, 0x72, 0x7d, 0xef, 0xb1, 0xfc, - 0xae, 0x48, 0xd7, 0x17, 0x83, 0xd4, 0x14, 0xcb, 0x1d, 0x21, 0xcb, 0xc9, 0x57, 0x90, 0x73, 0xca, 0x0b, 0x96, 0xc4, - 0x10, 0xc4, 0x27, 0xbc, 0x60, 0x86, 0x71, 0xbf, 0xe7, 0xe5, 0xc6, 0xac, 0xce, 0x69, 0x66, 0xa1, 0xf6, 0x07, 0xa0, - 0x99, 0x83, 0x72, 0x48, 0x92, 0xad, 0x62, 0xd7, 0xf7, 0x1e, 0xbe, 0xde, 0x25, 0x43, 0xaf, 0x56, 0x4e, 0x7a, 0xce, - 0x80, 0xf5, 0xc1, 0x79, 0x35, 0xd4, 0xcc, 0xfd, 0x48, 0xe3, 0xcc, 0x30, 0x51, 0x79, 0xcc, 0x01, 0x99, 0xae, 0xef, - 0x3d, 0x7c, 0x17, 0x73, 0xa3, 0x9b, 0x42, 0x38, 0x9c, 0x77, 0x5c, 0x90, 0x98, 0x12, 0x86, 0xec, 0xe4, 0x4b, 0x3a, - 0x56, 0x04, 0xa7, 0x7b, 0x4a, 0x4d, 0x26, 0x88, 0x1d, 0x7d, 0x31, 0x20, 0x99, 0x03, 0x01, 0xc9, 0x10, 0xce, 0x6a, - 0x72, 0x1d, 0x31, 0x6b, 0x60, 0x3a, 0xbb, 0x82, 0xc5, 0x48, 0x2c, 0x7b, 0x88, 0x70, 0x66, 0xba, 0xd5, 0x6b, 0x7b, - 0x9c, 0x28, 0xba, 0x69, 0xe8, 0x56, 0xc9, 0xb3, 0xef, 0x41, 0xf0, 0xf2, 0x1f, 0xaf, 0x5c, 0xdb, 0x65, 0xc2, 0x13, - 0x6f, 0x91, 0x76, 0x7d, 0xef, 0xe1, 0xaf, 0xce, 0x28, 0x6d, 0x4e, 0x3d, 0xf9, 0xdf, 0x92, 0x51, 0x1f, 0xfe, 0x9a, - 0x54, 0xb9, 0xa6, 0xf0, 0xf5, 0xbd, 0x87, 0xbf, 0xef, 0x2a, 0x06, 0xe9, 0xeb, 0x45, 0xa5, 0x24, 0x30, 0xe3, 0x5b, - 0xb2, 0x3c, 0x5d, 0xba, 0xb3, 0x22, 0x15, 0x6b, 0x6c, 0x4e, 0xa8, 0x54, 0xad, 0x4b, 0xdd, 0xca, 0x13, 0x2c, 0x89, - 0xb9, 0x4a, 0xaa, 0x2f, 0x9b, 0x43, 0x63, 0x2e, 0xc5, 0x55, 0x26, 0xe7, 0xec, 0x1b, 0xf7, 0x4b, 0x4f, 0x35, 0x4a, - 0xf8, 0x0c, 0x0c, 0x71, 0xcc, 0xd8, 0x05, 0xde, 0x6f, 0xa1, 0xee, 0xc6, 0x79, 0x26, 0x0d, 0xa2, 0x16, 0xf5, 0xc3, - 0x06, 0x53, 0xd2, 0xc2, 0x19, 0x69, 0xe1, 0x9c, 0xa8, 0x7e, 0xcb, 0x9e, 0x18, 0xdd, 0xbc, 0x6c, 0xda, 0x9e, 0x3b, - 0xb0, 0xdd, 0x73, 0xbb, 0x6f, 0xed, 0xa1, 0x3c, 0xed, 0xe6, 0x46, 0x7f, 0x69, 0x0e, 0xfa, 0xa9, 0x41, 0x8d, 0x27, - 0x2c, 0x2e, 0x70, 0x61, 0x5a, 0xbe, 0xe2, 0xc3, 0x1c, 0xec, 0x54, 0x60, 0x66, 0x58, 0xa3, 0xb4, 0x2c, 0xdb, 0x76, - 0x65, 0xf3, 0xc4, 0xac, 0x55, 0x81, 0xf3, 0x04, 0x48, 0x39, 0xce, 0x9d, 0x5d, 0x8f, 0xda, 0xae, 0x72, 0x76, 0x70, - 0x10, 0xbb, 0x4a, 0x34, 0x2e, 0x7c, 0x7e, 0x75, 0x03, 0xf8, 0xde, 0x52, 0x8d, 0x29, 0x32, 0x13, 0x68, 0x34, 0xb2, - 0xc1, 0x9a, 0xee, 0x13, 0x12, 0xe7, 0x75, 0x28, 0xfa, 0xd1, 0x1b, 0x66, 0x70, 0x03, 0x00, 0x8d, 0x46, 0x79, 0xdd, - 0xbb, 0x01, 0xb1, 0xa7, 0x1a, 0xcb, 0xf5, 0xd7, 0xb8, 0xb4, 0x26, 0x6a, 0x6d, 0xd9, 0x61, 0xf9, 0x51, 0x20, 0x11, - 0xe2, 0xae, 0xf0, 0xf3, 0x09, 0xb6, 0x86, 0x80, 0x72, 0x2f, 0x9c, 0x0d, 0x04, 0x36, 0x56, 0x5b, 0xae, 0x90, 0x27, - 0x6d, 0x1d, 0x94, 0xfa, 0x42, 0x70, 0xc1, 0x05, 0x85, 0x1a, 0x6b, 0x87, 0xe5, 0x4f, 0xd8, 0xb6, 0x39, 0x27, 0x56, - 0xc8, 0x69, 0xcb, 0xcc, 0x30, 0x0c, 0xc0, 0x3a, 0x25, 0x60, 0x9e, 0x93, 0x97, 0xdf, 0x46, 0xfd, 0x87, 0x01, 0xea, - 0x3f, 0x22, 0x2c, 0xd8, 0x06, 0x56, 0x57, 0x92, 0x48, 0xa7, 0xa0, 0x50, 0x3e, 0xeb, 0xf1, 0x9c, 0x80, 0x36, 0xae, - 0x0e, 0xd5, 0xda, 0x15, 0xe5, 0x37, 0x28, 0x4b, 0xb8, 0x53, 0x8c, 0x3e, 0x13, 0xfb, 0xfb, 0xe4, 0xb8, 0xba, 0xa0, - 0x83, 0xae, 0x77, 0x29, 0x07, 0x43, 0x52, 0xf8, 0xf0, 0xf7, 0xef, 0xdf, 0xad, 0x3e, 0x9e, 0x6f, 0xef, 0xe0, 0xc0, - 0xac, 0x14, 0x66, 0x1d, 0x6c, 0xe0, 0xba, 0x91, 0x29, 0xf4, 0x5f, 0xde, 0x89, 0xd7, 0xa9, 0xd0, 0xc6, 0x66, 0xf4, - 0xc7, 0x21, 0x8c, 0xb6, 0xdd, 0x36, 0x25, 0x58, 0xd0, 0x2c, 0xd0, 0x25, 0x6b, 0xdc, 0x4a, 0x8b, 0x6f, 0x90, 0x91, - 0x87, 0xa6, 0x00, 0x13, 0xa3, 0xdd, 0xd9, 0x8f, 0xd6, 0x0e, 0x4f, 0xec, 0xd0, 0xd0, 0xd2, 0x10, 0x42, 0x8b, 0xf7, - 0x80, 0x39, 0xf6, 0x88, 0x00, 0x10, 0xbd, 0x34, 0x90, 0xaa, 0x40, 0x16, 0x45, 0x95, 0x22, 0xff, 0xf9, 0x3e, 0x21, - 0x2f, 0x2b, 0x45, 0xe6, 0xdb, 0xca, 0x98, 0x0b, 0x10, 0x03, 0xa5, 0x70, 0x91, 0x50, 0x26, 0xd8, 0xcb, 0xd0, 0x0f, - 0xda, 0x97, 0x37, 0xd2, 0x66, 0x52, 0x71, 0xe3, 0xc1, 0x4d, 0xa9, 0x51, 0xf1, 0xd9, 0x7c, 0x0f, 0x89, 0x8d, 0xdc, - 0x7b, 0x90, 0xcb, 0xa8, 0x19, 0x24, 0x7c, 0xbf, 0x33, 0xa5, 0x7d, 0xbb, 0xeb, 0x2f, 0x9b, 0x16, 0x31, 0x1b, 0xeb, - 0x92, 0x70, 0xa1, 0x58, 0xa1, 0x1f, 0xb1, 0xb1, 0x2c, 0xe0, 0xfe, 0xa3, 0x04, 0x0b, 0x5a, 0xdf, 0x0b, 0x74, 0x80, - 0x66, 0x82, 0xc1, 0xa5, 0xc3, 0xc6, 0x0c, 0xcd, 0xaf, 0x2f, 0xe6, 0x0e, 0xfc, 0x7a, 0xb3, 0xd6, 0xcb, 0x83, 0x83, - 0xaf, 0xac, 0x02, 0x94, 0x1b, 0xa6, 0x19, 0x46, 0x40, 0xbc, 0x2c, 0x97, 0xe3, 0x6e, 0x86, 0xef, 0xc5, 0x95, 0xca, - 0xc0, 0x13, 0x8e, 0x90, 0x08, 0x3d, 0x27, 0x7a, 0x3d, 0xd9, 0xa4, 0xf7, 0x4e, 0x9b, 0x21, 0x42, 0xb1, 0x06, 0xc8, - 0x3d, 0xc8, 0xe5, 0x56, 0xc9, 0xa4, 0x2a, 0x5b, 0xdb, 0x72, 0x10, 0x8f, 0x01, 0x5c, 0xb1, 0x11, 0x52, 0x02, 0x34, - 0xdc, 0x2d, 0xb4, 0x3c, 0x97, 0xc0, 0xfe, 0x63, 0x95, 0x80, 0x48, 0x8b, 0x6a, 0x1b, 0x17, 0x21, 0x6c, 0x4d, 0x7d, - 0x02, 0xe3, 0x84, 0x87, 0xcf, 0x77, 0x69, 0xa8, 0x3d, 0x6a, 0x33, 0x73, 0x06, 0x41, 0x09, 0x89, 0xca, 0x0a, 0xc9, - 0xd7, 0x58, 0x38, 0x6e, 0xce, 0xdf, 0xc3, 0x01, 0x29, 0x56, 0x34, 0xb6, 0x77, 0x5b, 0x70, 0x7c, 0x14, 0xc9, 0x22, - 0xae, 0x75, 0xdd, 0x2d, 0x4c, 0x35, 0xec, 0x40, 0x47, 0x43, 0x38, 0x15, 0xe6, 0x9e, 0xf0, 0x71, 0x45, 0x52, 0x7f, - 0xb6, 0x26, 0xda, 0xda, 0x13, 0xc3, 0xca, 0x34, 0x25, 0x98, 0xff, 0xcf, 0xd6, 0xea, 0xba, 0x2c, 0x84, 0x99, 0x19, - 0xc6, 0x8d, 0x5d, 0x05, 0xb6, 0x06, 0x1c, 0x5b, 0x7e, 0x96, 0xc1, 0xa2, 0x7a, 0xa5, 0xb8, 0xe9, 0x34, 0x60, 0x02, - 0xde, 0x82, 0xf5, 0xcc, 0xe6, 0xd6, 0x7f, 0x6e, 0x0e, 0x46, 0x81, 0x55, 0x8d, 0xc0, 0x4b, 0x43, 0xe0, 0x11, 0x30, - 0x6e, 0xde, 0xb4, 0xbc, 0xe7, 0x8c, 0x68, 0x84, 0x3f, 0xf1, 0x1c, 0x9e, 0x59, 0x96, 0x7b, 0xeb, 0x63, 0x63, 0x45, - 0x52, 0x41, 0xc0, 0xb6, 0x08, 0x3b, 0x22, 0x2f, 0x11, 0x56, 0x8d, 0x46, 0x57, 0x5d, 0xb0, 0x4a, 0xab, 0x52, 0x0d, - 0x53, 0xc0, 0x2d, 0x31, 0xe0, 0x7d, 0xed, 0x44, 0x05, 0x43, 0x02, 0x6f, 0xfd, 0xad, 0x40, 0x7d, 0xff, 0xf0, 0x6d, - 0x1c, 0xd2, 0xb7, 0xb0, 0x6c, 0x79, 0x11, 0x0b, 0x53, 0x8a, 0xab, 0x3b, 0x9c, 0x37, 0xdf, 0x37, 0x1b, 0x81, 0x71, - 0xef, 0xb7, 0x31, 0xd8, 0xb8, 0xa1, 0xae, 0xb6, 0xa4, 0xa1, 0xdc, 0x84, 0x5d, 0x54, 0xd9, 0x3b, 0x86, 0x9d, 0x75, - 0x75, 0x25, 0xed, 0x6a, 0xa2, 0xd6, 0x6b, 0xc5, 0x2a, 0xa3, 0x81, 0x0d, 0xc3, 0x4e, 0x73, 0xcc, 0x6c, 0x2b, 0xf0, - 0x1f, 0xcf, 0x89, 0xc6, 0x01, 0xb2, 0xbe, 0xf9, 0xd6, 0x75, 0x4a, 0x35, 0x4c, 0xd8, 0xde, 0xee, 0x7c, 0x7c, 0xcc, - 0x77, 0x9d, 0x8f, 0x58, 0xba, 0xad, 0x6f, 0xce, 0xc6, 0xf6, 0xbf, 0x71, 0x36, 0x3a, 0xb5, 0xbd, 0x3f, 0x1e, 0x81, - 0x3b, 0xa9, 0x1d, 0x8f, 0xf5, 0x35, 0x25, 0x12, 0x0b, 0xb7, 0x1c, 0x97, 0x9d, 0xd5, 0x4a, 0xf4, 0x5b, 0xa0, 0x76, - 0x8a, 0x22, 0xf8, 0xd9, 0xb6, 0x3f, 0x03, 0x92, 0x6c, 0x75, 0xc8, 0xb1, 0x28, 0x45, 0x19, 0x94, 0x80, 0x01, 0x75, - 0x6c, 0x6c, 0xbd, 0x0c, 0x62, 0x3b, 0x1c, 0x72, 0x58, 0x4e, 0x44, 0x79, 0x75, 0x05, 0x23, 0x36, 0xc7, 0x86, 0x13, - 0x30, 0xe3, 0x9d, 0x56, 0x85, 0x5e, 0xfc, 0xfc, 0xd7, 0xcc, 0x69, 0xed, 0x88, 0xb1, 0x9c, 0x44, 0xcd, 0x8a, 0xc1, - 0x8d, 0xc0, 0x31, 0x8c, 0xfb, 0x46, 0x42, 0xad, 0x4e, 0x75, 0x54, 0x3b, 0x92, 0x70, 0x0b, 0xd4, 0x6e, 0xfb, 0xe6, - 0x5c, 0x5a, 0xad, 0x76, 0x1e, 0x2c, 0xb8, 0x08, 0x70, 0xfb, 0x39, 0xd1, 0x35, 0x92, 0x42, 0x89, 0x93, 0xa0, 0x70, - 0x6e, 0x50, 0x55, 0x13, 0xd9, 0x6f, 0x0d, 0x80, 0x27, 0xed, 0x66, 0x17, 0xb2, 0x12, 0x92, 0xb3, 0x46, 0x03, 0xe5, - 0x65, 0xc7, 0xb4, 0x2f, 0x1a, 0xd9, 0x00, 0x33, 0x9c, 0x59, 0x81, 0x05, 0x4e, 0xaf, 0x38, 0xaf, 0xba, 0xee, 0x67, - 0x03, 0x84, 0x8b, 0xd5, 0x2a, 0xb6, 0x43, 0xcb, 0xd1, 0x6a, 0x95, 0x87, 0x43, 0x33, 0xf9, 0x50, 0xf1, 0x65, 0x4f, - 0x93, 0x97, 0xe6, 0x3c, 0x7c, 0x09, 0x83, 0x6c, 0x90, 0x38, 0x77, 0x2a, 0xc1, 0x1c, 0x34, 0x57, 0x0d, 0xd9, 0xcf, - 0x1a, 0xed, 0x41, 0x40, 0xc3, 0xfa, 0xd9, 0x80, 0xe4, 0x6b, 0xb0, 0x9c, 0x55, 0xee, 0xc0, 0xfc, 0x0c, 0x07, 0xdb, - 0x67, 0x73, 0xce, 0xd8, 0x06, 0xc3, 0x35, 0xd9, 0x54, 0x19, 0x94, 0x78, 0xe5, 0x16, 0xd7, 0x97, 0xab, 0x19, 0x58, - 0x94, 0x85, 0xb0, 0xbb, 0x66, 0xee, 0x81, 0xf0, 0x5f, 0x62, 0xbb, 0xa4, 0xa5, 0x11, 0xf7, 0x06, 0xe2, 0x7b, 0xdb, - 0xed, 0x24, 0x49, 0x68, 0x31, 0x31, 0x57, 0x22, 0xfe, 0x86, 0xd7, 0xec, 0x81, 0x63, 0x37, 0xce, 0xa0, 0xe7, 0x7e, - 0xd9, 0xd9, 0x80, 0xd8, 0xf1, 0x7b, 0x66, 0xc7, 0x3b, 0xae, 0x14, 0x74, 0xb7, 0x2e, 0xc2, 0x0e, 0x86, 0xfe, 0x2f, - 0x0f, 0xe6, 0xc4, 0x0d, 0xc6, 0xa2, 0xc9, 0x06, 0xdc, 0xbe, 0x01, 0x8f, 0x82, 0x6e, 0xc0, 0xed, 0xdb, 0xf0, 0xf5, - 0xd0, 0xca, 0xbe, 0x39, 0xc0, 0x80, 0x4c, 0xd8, 0x91, 0x56, 0x09, 0xc1, 0x30, 0x4f, 0x37, 0x39, 0x32, 0x4b, 0x56, - 0xe1, 0x70, 0xd5, 0x24, 0x16, 0x1b, 0x7b, 0xa1, 0x62, 0x52, 0x03, 0xc1, 0x58, 0xa4, 0x2f, 0x51, 0xa8, 0x34, 0xa8, - 0x1b, 0xc7, 0x00, 0x56, 0x39, 0x6d, 0xfd, 0xcb, 0x83, 0x03, 0x10, 0x1a, 0x80, 0xb5, 0x4b, 0x32, 0x3a, 0xd7, 0x8b, - 0x02, 0xf8, 0x2b, 0xe5, 0x7f, 0x43, 0x32, 0xb8, 0x9d, 0x98, 0x34, 0xf8, 0x01, 0x09, 0x73, 0xaa, 0x14, 0xff, 0x6a, - 0xd3, 0xdc, 0x6f, 0x5c, 0x10, 0x8f, 0xd1, 0xca, 0x72, 0x8a, 0x12, 0x75, 0xa5, 0x43, 0xd7, 0x3a, 0xe4, 0x9e, 0x7e, - 0x65, 0x42, 0xbf, 0xe4, 0x4a, 0x33, 0x01, 0x00, 0xa8, 0x10, 0x0f, 0xa6, 0xa4, 0x10, 0x6c, 0xdd, 0x5a, 0x2d, 0x3a, - 0x1a, 0x7d, 0xb7, 0x8a, 0xae, 0xb3, 0x45, 0x53, 0x2a, 0x46, 0xb9, 0xed, 0x24, 0xb4, 0x99, 0xf4, 0x76, 0xa2, 0x65, - 0xc9, 0xd0, 0x62, 0xa7, 0x62, 0x3f, 0x0c, 0xad, 0x8f, 0x05, 0xf1, 0xe7, 0x82, 0x3f, 0x4b, 0xbf, 0xcb, 0xc7, 0xc0, - 0x95, 0xfa, 0x37, 0x56, 0x21, 0x9c, 0x09, 0xd6, 0x01, 0x79, 0x4d, 0xea, 0xe3, 0xf4, 0xa8, 0x93, 0x6f, 0x29, 0x17, - 0x4a, 0xa3, 0xb0, 0x8d, 0x93, 0xc2, 0x60, 0xca, 0xd9, 0xb7, 0x25, 0xae, 0x5f, 0xfd, 0x31, 0xe2, 0x8f, 0x0e, 0xf1, - 0xef, 0x52, 0x69, 0xb4, 0x2c, 0x11, 0x0c, 0xf9, 0x1d, 0xa9, 0x15, 0x5c, 0xc5, 0xe6, 0x5c, 0x3f, 0xd7, 0xb3, 0x7c, - 0xc3, 0x13, 0xa7, 0xab, 0x55, 0x29, 0x15, 0xa8, 0xf8, 0x86, 0xe1, 0x27, 0x0c, 0xee, 0x8d, 0x9f, 0xf1, 0xa0, 0xca, - 0xf6, 0x7d, 0xf1, 0xb3, 0xe0, 0xbe, 0xf8, 0x19, 0x4f, 0xb7, 0x8b, 0x06, 0xf7, 0xc4, 0x9d, 0xe4, 0x3c, 0x69, 0x45, - 0x9e, 0x8f, 0x9a, 0xd2, 0xca, 0xbf, 0xd2, 0x6e, 0x0d, 0x5c, 0xd9, 0xc4, 0x81, 0x71, 0x5e, 0x5d, 0x84, 0x62, 0xce, - 0x9c, 0xd1, 0x72, 0xf8, 0xdf, 0x5a, 0x27, 0x77, 0xf2, 0x48, 0x2b, 0x85, 0xbc, 0xa1, 0x85, 0xbe, 0x07, 0x1b, 0xae, - 0xd8, 0xf2, 0x01, 0xa4, 0x04, 0x94, 0x6d, 0xff, 0x5e, 0x17, 0x81, 0x38, 0xae, 0xac, 0xf3, 0x51, 0xd8, 0x3e, 0x29, - 0x4a, 0xae, 0xae, 0x2e, 0x84, 0xdc, 0x1a, 0x2d, 0x01, 0xc2, 0xd4, 0xbb, 0xe6, 0x31, 0x47, 0x93, 0x59, 0xba, 0x5c, - 0x97, 0xaa, 0x83, 0xc2, 0x72, 0x75, 0x1c, 0xe1, 0x62, 0x6d, 0x6e, 0xd0, 0xff, 0xe1, 0xf8, 0x33, 0x77, 0x34, 0xf2, - 0xe7, 0x92, 0x02, 0xbd, 0xdf, 0xed, 0x6b, 0xb3, 0x83, 0x44, 0xda, 0x39, 0x94, 0x96, 0x02, 0x80, 0xd5, 0x06, 0x5f, - 0xd7, 0x1e, 0xa7, 0x9e, 0x48, 0x37, 0x9b, 0x6f, 0x1a, 0xc2, 0x62, 0x56, 0x5a, 0xf0, 0x98, 0x6e, 0x76, 0x58, 0x8e, - 0x7a, 0x59, 0x5c, 0x97, 0x7b, 0xac, 0xd6, 0x2f, 0xfa, 0x06, 0x28, 0x2b, 0x43, 0xb4, 0xd5, 0x2a, 0xae, 0xc3, 0x9b, - 0x88, 0xe0, 0x1a, 0x04, 0x61, 0x11, 0x18, 0x70, 0xd4, 0x18, 0x6f, 0x5b, 0x27, 0x46, 0x9b, 0xf6, 0x4b, 0x9e, 0x75, - 0xaf, 0x8d, 0x23, 0x54, 0x34, 0xd8, 0xea, 0xa1, 0xe6, 0x01, 0xdb, 0xd9, 0x95, 0x1d, 0x05, 0x10, 0x9a, 0x52, 0x6f, - 0x9c, 0x5b, 0x59, 0xd1, 0xee, 0x80, 0x2f, 0xfa, 0x8e, 0x79, 0xae, 0x03, 0xdd, 0x76, 0x7e, 0x60, 0xdb, 0xf4, 0x44, - 0x7e, 0xcb, 0xb6, 0xa9, 0xc6, 0x09, 0xef, 0xb7, 0xd0, 0xf7, 0x0d, 0x61, 0x6d, 0x5f, 0xbb, 0x8b, 0xfc, 0x2f, 0x74, - 0xd7, 0x06, 0xf4, 0xb4, 0x60, 0xf6, 0x34, 0xe6, 0x83, 0x5e, 0xaf, 0x7f, 0x2e, 0xfd, 0x17, 0x8c, 0xad, 0xd0, 0xcf, - 0x76, 0x17, 0x38, 0xb1, 0xd2, 0x38, 0x04, 0xc7, 0xff, 0x70, 0x32, 0xc9, 0xe5, 0x90, 0xe6, 0xef, 0xa0, 0xc7, 0x2a, - 0xf7, 0xf9, 0xdd, 0xa8, 0xa0, 0x9a, 0x39, 0x5a, 0x53, 0x8d, 0xe2, 0x1f, 0x1e, 0x0c, 0xe3, 0x1f, 0x6e, 0x29, 0x77, - 0xd5, 0x02, 0x5e, 0xbe, 0x2c, 0x9b, 0x48, 0x7f, 0x5e, 0x97, 0x32, 0x98, 0xda, 0xdd, 0xcb, 0x26, 0x49, 0x63, 0x25, - 0x49, 0x63, 0x2a, 0xde, 0x6c, 0x2a, 0x8e, 0x3f, 0x7f, 0x63, 0xb0, 0xdb, 0x64, 0xee, 0x73, 0x40, 0xe6, 0x3e, 0xf3, - 0xf4, 0xbb, 0xb5, 0x02, 0x8a, 0x77, 0x9c, 0x1c, 0x1b, 0xcb, 0x18, 0x3b, 0xea, 0xb7, 0x1a, 0x0c, 0x1a, 0x34, 0xb9, - 0x0c, 0xbc, 0x1d, 0xaa, 0xd3, 0xcb, 0xdb, 0x1f, 0xc5, 0xd9, 0x42, 0x69, 0x39, 0x73, 0x8d, 0x2a, 0xe7, 0xe3, 0x64, - 0x32, 0x41, 0x81, 0x6d, 0xee, 0xf0, 0xd3, 0xba, 0x1b, 0xd9, 0xf2, 0x0b, 0x17, 0xa3, 0x54, 0x61, 0x77, 0xb6, 0xa8, - 0x54, 0xae, 0x89, 0x37, 0x73, 0xde, 0xce, 0xc3, 0x63, 0x2e, 0xb8, 0x9a, 0xb2, 0x22, 0x2e, 0xd0, 0xf2, 0x5b, 0x9d, - 0x15, 0x70, 0x9b, 0x63, 0x3b, 0xc3, 0xa3, 0xd2, 0x72, 0x40, 0x27, 0xd0, 0x1a, 0xe8, 0x8c, 0x66, 0x4c, 0x4f, 0xe5, - 0x08, 0x0c, 0x5f, 0x92, 0x51, 0xe9, 0x4e, 0x75, 0x70, 0xb0, 0x1f, 0x47, 0x46, 0x7f, 0x01, 0x3e, 0xe8, 0x61, 0x0e, - 0xea, 0x2d, 0xc1, 0x31, 0xa8, 0xea, 0x9a, 0xa1, 0x25, 0xdb, 0xf4, 0xa1, 0xd1, 0xc9, 0x17, 0x76, 0x87, 0x39, 0x5a, - 0xaf, 0x53, 0x3b, 0xea, 0x68, 0xcc, 0x59, 0x3e, 0x8a, 0xf0, 0x17, 0x76, 0x97, 0x96, 0x6e, 0xeb, 0xc6, 0xcb, 0xda, - 0x2c, 0x62, 0x24, 0x6f, 0x44, 0x84, 0xab, 0x4e, 0xd2, 0xe5, 0x1a, 0xcb, 0x82, 0x4f, 0x00, 0x47, 0x7f, 0x61, 0x77, - 0xa9, 0x6b, 0x2f, 0x70, 0x15, 0x44, 0x4b, 0x0f, 0xfa, 0x24, 0x48, 0x0e, 0x97, 0xc1, 0x09, 0x1c, 0x7d, 0x53, 0x77, - 0x40, 0x6a, 0xe5, 0x2a, 0x11, 0x12, 0xa1, 0xf5, 0xbf, 0x3b, 0x15, 0xbc, 0x08, 0xcf, 0x39, 0x5d, 0xb3, 0xb8, 0xdd, - 0xa8, 0xc4, 0xa0, 0x42, 0x65, 0x41, 0xf2, 0x31, 0xe6, 0x7e, 0xf7, 0x39, 0xef, 0x87, 0x40, 0x67, 0xb6, 0xa0, 0xae, - 0xd1, 0x74, 0x64, 0x7e, 0xa1, 0xea, 0x0e, 0x6a, 0xa6, 0xab, 0x8a, 0x7b, 0x1f, 0x63, 0x00, 0x3c, 0x58, 0xcb, 0x50, - 0xe3, 0x10, 0xba, 0xf6, 0x66, 0xaa, 0x63, 0x4a, 0xe2, 0xa5, 0x9f, 0x43, 0xca, 0x43, 0x30, 0xea, 0x35, 0xa0, 0xa1, - 0x43, 0x30, 0x6b, 0x79, 0xc8, 0xc7, 0xb1, 0xd8, 0x3a, 0x43, 0xa5, 0x39, 0x43, 0x93, 0x00, 0xe4, 0xdf, 0x38, 0x33, - 0x99, 0x81, 0x86, 0xe1, 0x2d, 0xcd, 0x01, 0xe8, 0x56, 0xd7, 0xe1, 0x50, 0xb8, 0xa2, 0xa5, 0xf3, 0x9e, 0x5d, 0x74, - 0x59, 0x1b, 0x56, 0x6c, 0xda, 0x41, 0xeb, 0x14, 0xa6, 0xc4, 0x6c, 0x81, 0xb5, 0xd7, 0xfb, 0x70, 0x6f, 0x57, 0x1b, - 0x17, 0x89, 0x9f, 0x16, 0xf1, 0x30, 0x89, 0x29, 0x5a, 0xf2, 0x98, 0x62, 0x09, 0x76, 0x90, 0xc5, 0xba, 0x1c, 0x3f, - 0x0b, 0x97, 0xa3, 0x66, 0x25, 0xbd, 0xdb, 0xc1, 0x10, 0xb8, 0x7c, 0x0d, 0xb6, 0xa1, 0x98, 0x7b, 0xc2, 0xc2, 0x63, - 0xe3, 0xe9, 0x17, 0xac, 0xdb, 0xdc, 0x2e, 0x88, 0x5f, 0x81, 0x31, 0x8d, 0x97, 0xc1, 0x2c, 0x42, 0xa7, 0x72, 0xe7, - 0x70, 0xe8, 0xae, 0x09, 0x2b, 0xe3, 0xd5, 0x58, 0x91, 0x8d, 0xa3, 0xe7, 0xfb, 0x36, 0x9e, 0x7f, 0x2f, 0x58, 0x71, - 0x77, 0xc5, 0xc0, 0xc6, 0x5a, 0x82, 0xbb, 0x71, 0xb5, 0x0c, 0x95, 0x81, 0x7c, 0x4f, 0x1a, 0xd6, 0x65, 0x8d, 0xbf, - 0x1b, 0x15, 0x63, 0x6d, 0xee, 0x29, 0x03, 0x6d, 0x8d, 0xdd, 0x2e, 0xec, 0x9b, 0xae, 0x9b, 0xac, 0x6b, 0x14, 0x71, - 0x15, 0xa4, 0xdd, 0xdd, 0x02, 0x2e, 0x42, 0x7f, 0xd8, 0xbe, 0x1a, 0x6c, 0xaa, 0x6e, 0x20, 0x09, 0xae, 0xfd, 0xe4, - 0xb7, 0xa7, 0xba, 0xcb, 0x5a, 0xf7, 0xdb, 0x53, 0xad, 0x5d, 0x16, 0x1a, 0x43, 0x22, 0xec, 0xfa, 0x29, 0xfd, 0xa7, - 0xc5, 0x7a, 0x8d, 0xd6, 0x30, 0xbc, 0x47, 0xbc, 0x1b, 0xc7, 0x8f, 0xbc, 0x85, 0x62, 0x02, 0x17, 0xb9, 0x57, 0xb9, - 0xf4, 0x84, 0xbc, 0x1a, 0xc1, 0x23, 0xbe, 0x35, 0x84, 0x47, 0x3c, 0x70, 0x7a, 0x05, 0xa9, 0x69, 0x22, 0xd8, 0xc8, - 0xd3, 0x4f, 0x64, 0x91, 0xd0, 0xf0, 0x71, 0xaf, 0x39, 0x11, 0xfa, 0x53, 0x0a, 0xfc, 0x17, 0x1e, 0x2e, 0xb4, 0x96, - 0x02, 0x73, 0x31, 0x5f, 0x68, 0xac, 0xcc, 0xe8, 0x97, 0x63, 0x29, 0x74, 0x73, 0x4c, 0x67, 0x3c, 0xbf, 0x4b, 0x17, - 0xbc, 0x39, 0x93, 0x42, 0xaa, 0x39, 0xcd, 0x18, 0x56, 0x77, 0x4a, 0xb3, 0x59, 0x73, 0xc1, 0xf1, 0x73, 0x96, 0x7f, - 0x65, 0x9a, 0x67, 0x14, 0xbf, 0x95, 0x43, 0xa9, 0x25, 0x7e, 0x7d, 0x7b, 0x37, 0x61, 0x02, 0xff, 0x3e, 0x5c, 0x08, - 0xbd, 0xc0, 0x8a, 0x0a, 0xd5, 0x54, 0xac, 0xe0, 0xe3, 0x6e, 0xb3, 0x39, 0x2f, 0xf8, 0x8c, 0x16, 0x77, 0xcd, 0x4c, - 0xe6, 0xb2, 0x48, 0xff, 0xab, 0x75, 0x4c, 0x1f, 0x8c, 0x4f, 0xba, 0xba, 0xa0, 0x42, 0x71, 0x58, 0x98, 0x94, 0xe6, - 0xf9, 0xde, 0xf1, 0x69, 0x6b, 0xa6, 0xf6, 0xed, 0x85, 0x1f, 0x15, 0x7a, 0xfd, 0x09, 0x7f, 0x90, 0x30, 0xca, 0x64, - 0xa8, 0x85, 0x1b, 0xe4, 0x32, 0x5b, 0x14, 0x4a, 0x16, 0xe9, 0x5c, 0x72, 0xa1, 0x59, 0xd1, 0x1d, 0xca, 0x62, 0xc4, - 0x8a, 0x66, 0x41, 0x47, 0x7c, 0xa1, 0xd2, 0x93, 0xf9, 0x6d, 0xb7, 0xde, 0x83, 0xcd, 0x4f, 0x85, 0x14, 0xac, 0x0b, - 0xfc, 0xc6, 0xa4, 0x90, 0x0b, 0x31, 0x72, 0xc3, 0x58, 0x08, 0xc5, 0x74, 0x77, 0x4e, 0x47, 0x60, 0x07, 0x9c, 0x9e, - 0xcf, 0x6f, 0xbb, 0x66, 0xd6, 0x37, 0x8c, 0x4f, 0xa6, 0x3a, 0x3d, 0x6d, 0xb5, 0xec, 0xb7, 0xe2, 0xff, 0xb0, 0xb4, - 0xdd, 0x49, 0x3a, 0xa7, 0xf3, 0x5b, 0xe0, 0xe0, 0x35, 0x2b, 0x9a, 0x00, 0x0b, 0xa8, 0xd4, 0x4e, 0x5a, 0x0f, 0x8e, - 0xef, 0x43, 0x06, 0xd8, 0x38, 0x34, 0xcd, 0x84, 0xc0, 0xd8, 0x3d, 0x5d, 0xcc, 0xe7, 0xac, 0x00, 0x2f, 0xfa, 0xee, - 0x8c, 0x16, 0x13, 0x2e, 0x9a, 0x85, 0x69, 0xb4, 0x79, 0x3e, 0xbf, 0x5d, 0xc3, 0x7c, 0x52, 0x6b, 0xb6, 0xea, 0xa6, - 0xe5, 0xbe, 0x96, 0xc1, 0x10, 0x4d, 0x4c, 0x9a, 0xb4, 0x98, 0x0c, 0x69, 0xdc, 0xee, 0xdc, 0xc7, 0xfe, 0x7f, 0x49, - 0x07, 0x05, 0x60, 0x6b, 0x8e, 0x16, 0x85, 0xb9, 0x45, 0x4d, 0xdb, 0xca, 0x36, 0x3b, 0x95, 0x5f, 0x59, 0xe1, 0x5b, - 0x35, 0x1f, 0xcb, 0xad, 0x79, 0xff, 0x47, 0x8d, 0x52, 0xdb, 0xd6, 0x0b, 0x75, 0x05, 0x34, 0x7a, 0xbb, 0xb1, 0xff, - 0xea, 0x9c, 0xd3, 0xfb, 0x27, 0xa7, 0x1e, 0xee, 0xe3, 0xf1, 0xb8, 0x06, 0x74, 0x0f, 0xdd, 0x76, 0x6b, 0x7e, 0xbb, - 0xd7, 0x69, 0x79, 0x18, 0x5b, 0x98, 0x9e, 0xcd, 0x6f, 0x77, 0xac, 0x60, 0x80, 0x15, 0x9b, 0xbd, 0xed, 0x25, 0xc7, - 0x6a, 0x8f, 0x51, 0xc5, 0xd6, 0x9f, 0xf0, 0x84, 0x02, 0x6e, 0x18, 0xa4, 0xed, 0x1b, 0x39, 0x15, 0x56, 0x60, 0xb0, - 0xbc, 0xe1, 0x23, 0x3d, 0x4d, 0xdb, 0xad, 0xd6, 0x0f, 0x15, 0x26, 0x75, 0xa7, 0x76, 0x49, 0xdb, 0x05, 0x9b, 0xd5, - 0xf0, 0x6b, 0x46, 0xcb, 0x5d, 0xb0, 0x9c, 0x4b, 0xd7, 0x69, 0xc1, 0x72, 0x13, 0xe5, 0x66, 0xed, 0xb6, 0xc2, 0xd6, - 0x94, 0xb9, 0x98, 0xb2, 0x82, 0xeb, 0x6e, 0xfd, 0xab, 0xea, 0x78, 0x7b, 0x4e, 0x6b, 0x2b, 0x1f, 0x2f, 0x6d, 0x0d, - 0x77, 0x19, 0xfb, 0x18, 0x3e, 0xb6, 0xb1, 0xf2, 0x2b, 0x2d, 0xe2, 0x8d, 0x0d, 0x83, 0xc3, 0x1a, 0x68, 0x1d, 0xcc, - 0xb9, 0x00, 0x53, 0xd1, 0x01, 0xfe, 0x06, 0x14, 0x32, 0x9a, 0x67, 0x31, 0x8c, 0x68, 0xaf, 0xb9, 0x77, 0x5c, 0xb0, - 0x19, 0xf2, 0x80, 0x48, 0xee, 0x9f, 0x16, 0x6c, 0xb6, 0x4e, 0x4c, 0xf5, 0xa5, 0x41, 0x5d, 0x9a, 0xf3, 0x89, 0x48, - 0x33, 0x06, 0xdb, 0x6a, 0x9d, 0x30, 0xa1, 0xb9, 0xbe, 0x6b, 0x16, 0xf2, 0x66, 0x39, 0xe2, 0x6a, 0x9e, 0xd3, 0xbb, - 0x74, 0x9c, 0xb3, 0xdb, 0xae, 0x29, 0xd5, 0xe4, 0x9a, 0xcd, 0x94, 0x2b, 0xdb, 0x85, 0xf4, 0xe6, 0xc8, 0x9a, 0x73, - 0x00, 0xf4, 0xe4, 0xcd, 0xe6, 0xbe, 0xf6, 0x8b, 0xd6, 0x94, 0x0b, 0xbd, 0xd7, 0x52, 0xdd, 0x19, 0x17, 0x4d, 0x37, - 0x90, 0x13, 0xc0, 0x88, 0x6d, 0xc8, 0x07, 0xfd, 0x27, 0xec, 0x76, 0x4e, 0xc5, 0x88, 0x8d, 0x96, 0x41, 0xb5, 0x0e, - 0xd4, 0x0b, 0x4b, 0xa5, 0x42, 0x4f, 0x9b, 0xc6, 0x06, 0x2d, 0xee, 0x08, 0xf4, 0x0d, 0x94, 0x7f, 0xd0, 0xc2, 0xf6, - 0xff, 0x93, 0x36, 0x0a, 0x2b, 0xef, 0x41, 0x38, 0x28, 0x3e, 0xbe, 0x6b, 0xc2, 0xdf, 0x25, 0xf8, 0x3c, 0xf1, 0x8c, - 0xe6, 0x0e, 0x22, 0x33, 0x3e, 0x1a, 0xe5, 0xb5, 0x11, 0x5d, 0x06, 0x9d, 0xb5, 0xd1, 0x12, 0xe6, 0x9f, 0xb6, 0xf6, - 0x5a, 0x7b, 0x66, 0x2e, 0x6e, 0x1b, 0x9c, 0x9c, 0xdc, 0x3f, 0x7e, 0xc0, 0xba, 0x39, 0x17, 0xac, 0x36, 0xd5, 0xef, - 0x82, 0x3a, 0x6c, 0xb8, 0xe3, 0x1a, 0x6e, 0xef, 0xb5, 0xf7, 0x4e, 0x5a, 0x3f, 0x78, 0x2a, 0x92, 0xb3, 0xb1, 0xb6, - 0xfb, 0xa6, 0x46, 0x56, 0xce, 0x7d, 0xd3, 0x37, 0x05, 0x9d, 0xa7, 0x42, 0xc2, 0x9f, 0x2e, 0x6c, 0xfe, 0x71, 0x2e, - 0x6f, 0xd2, 0x29, 0x1f, 0x8d, 0x98, 0xb0, 0x05, 0xca, 0x44, 0x96, 0xe7, 0x7c, 0xae, 0xb8, 0x5d, 0x0d, 0x87, 0xbb, - 0xa7, 0x1b, 0x50, 0x0d, 0x07, 0x74, 0x1c, 0x0c, 0xe8, 0xb4, 0x1a, 0x50, 0xd5, 0x7f, 0x38, 0xc2, 0xce, 0xc6, 0x5c, - 0x4d, 0xa9, 0x6e, 0x0d, 0x93, 0x3e, 0x2f, 0x94, 0x06, 0x98, 0x7b, 0xe3, 0x11, 0x73, 0xba, 0x34, 0x87, 0x4c, 0xdf, - 0x30, 0x26, 0xbe, 0x3d, 0x88, 0xcb, 0x54, 0x8a, 0xfc, 0xce, 0x7e, 0x2e, 0xc3, 0x2e, 0xe9, 0x42, 0xcb, 0x75, 0x32, - 0xe4, 0x82, 0x16, 0x77, 0xd7, 0x8a, 0x09, 0x25, 0x8b, 0x6b, 0x39, 0x1e, 0x2f, 0xbf, 0x45, 0xf2, 0xee, 0xa3, 0x75, - 0xa2, 0xb8, 0x98, 0xe4, 0xcc, 0x12, 0x38, 0x83, 0x08, 0xee, 0x90, 0xb1, 0xed, 0x9a, 0x26, 0x6b, 0x83, 0x5e, 0x27, - 0x59, 0xce, 0x67, 0x54, 0x33, 0x03, 0xe7, 0x80, 0xd4, 0xb8, 0xc9, 0x5b, 0x2a, 0xd7, 0xda, 0xb3, 0x7f, 0xaa, 0xd2, - 0xb0, 0x8d, 0x82, 0xc2, 0xbe, 0x49, 0x2e, 0x0c, 0x7e, 0x18, 0x70, 0x98, 0x5d, 0x64, 0x56, 0xcf, 0xac, 0x5d, 0x00, - 0x3b, 0x98, 0x5d, 0xad, 0xa9, 0x4b, 0x47, 0x97, 0x6c, 0x8b, 0xa7, 0xad, 0x1f, 0xea, 0xb9, 0x39, 0x1d, 0xb2, 0x7c, - 0x69, 0x37, 0xaa, 0x07, 0xae, 0xdb, 0xaa, 0xe1, 0x32, 0x07, 0x24, 0xc3, 0x80, 0x68, 0x90, 0xa6, 0xcd, 0x1b, 0x36, - 0xfc, 0xc2, 0xb5, 0xdd, 0x32, 0x4d, 0x75, 0x03, 0x4e, 0x45, 0x66, 0x4c, 0x73, 0x56, 0x2c, 0x3d, 0x21, 0x6f, 0xd5, - 0x08, 0xe8, 0x95, 0x30, 0x07, 0xb4, 0xa6, 0xc3, 0x26, 0x84, 0x58, 0x63, 0xc5, 0x72, 0xd7, 0xe4, 0x66, 0xf4, 0xd6, - 0xa1, 0xd8, 0x83, 0xd6, 0x0f, 0xb5, 0x43, 0xf6, 0xa4, 0xd5, 0xf2, 0x47, 0x44, 0xd3, 0xd6, 0x48, 0xdb, 0xc9, 0x29, - 0x9b, 0x95, 0x89, 0x5a, 0xce, 0xd3, 0x5a, 0xc2, 0x50, 0x6a, 0x2d, 0x67, 0x36, 0x6d, 0x07, 0x35, 0xaa, 0x93, 0xde, - 0x76, 0x67, 0x7e, 0xbb, 0x67, 0xfe, 0x69, 0xed, 0xb5, 0xb6, 0x49, 0xed, 0x36, 0x56, 0x1c, 0x23, 0x8f, 0xc7, 0xd0, - 0x71, 0x9b, 0xcd, 0xba, 0x0b, 0x05, 0xc7, 0xbd, 0x81, 0xb8, 0x39, 0xd1, 0xd6, 0x66, 0xb2, 0x00, 0x58, 0xca, 0x05, - 0x9c, 0xae, 0xf6, 0xb0, 0x83, 0x3e, 0x94, 0x04, 0x73, 0xf8, 0x9d, 0x8d, 0xd6, 0x87, 0xd5, 0xda, 0xab, 0x06, 0x06, - 0xff, 0xac, 0x3f, 0x55, 0xfc, 0xf9, 0x0b, 0x16, 0xc8, 0x47, 0xbc, 0x91, 0x9c, 0xae, 0x5a, 0x4e, 0x26, 0x1a, 0xe9, - 0x4a, 0x54, 0x33, 0x1e, 0x25, 0x33, 0x7a, 0x6b, 0x5d, 0x4b, 0x66, 0x5c, 0x80, 0xe1, 0x1a, 0xc2, 0x3a, 0x30, 0xf1, - 0x9f, 0x86, 0x0d, 0x8d, 0x74, 0x0c, 0x0d, 0x1f, 0x76, 0x92, 0xd3, 0x53, 0x84, 0x5b, 0xb8, 0x73, 0x7a, 0x1a, 0xc8, - 0x64, 0x63, 0xbd, 0xab, 0xe8, 0xae, 0x92, 0x72, 0x47, 0xc9, 0x23, 0xd3, 0xe8, 0x51, 0xbb, 0xd5, 0xc2, 0xc6, 0x7d, - 0xbe, 0x2c, 0xcc, 0xd5, 0x8e, 0x66, 0xdb, 0xad, 0x16, 0x34, 0x0b, 0x7f, 0xdc, 0xbc, 0x7e, 0x21, 0xcb, 0x56, 0xda, - 0xc2, 0xed, 0xb4, 0x8d, 0x3b, 0x69, 0x07, 0x1f, 0xa7, 0xc7, 0xf8, 0x24, 0x3d, 0xc1, 0xa7, 0xe9, 0x29, 0x3e, 0x4b, - 0xcf, 0xf0, 0xfd, 0xf4, 0x3e, 0x3e, 0x4f, 0xcf, 0xf1, 0x83, 0xf4, 0x01, 0x7e, 0x98, 0xb6, 0x5b, 0xf8, 0x51, 0xda, - 0x6e, 0xe3, 0xc7, 0x69, 0xbb, 0x83, 0x9f, 0xa4, 0xed, 0x63, 0xfc, 0x34, 0x6d, 0x9f, 0xe0, 0x67, 0x69, 0xfb, 0x14, - 0x53, 0xc8, 0x1d, 0x42, 0x6e, 0x06, 0xb9, 0x23, 0xc8, 0x65, 0x90, 0x3b, 0x4e, 0xdb, 0xa7, 0x6b, 0xac, 0x6c, 0xc8, - 0x8d, 0xa8, 0xd5, 0xee, 0x1c, 0x9f, 0x9c, 0x9e, 0xdd, 0x3f, 0x7f, 0xf0, 0xf0, 0xd1, 0xe3, 0x27, 0x4f, 0x9f, 0x45, - 0x03, 0x3c, 0x34, 0x9e, 0x2f, 0x4a, 0xf4, 0xf9, 0x41, 0xfb, 0x74, 0x80, 0xaf, 0xfd, 0x67, 0xcc, 0x0f, 0x3a, 0x27, - 0x2d, 0x74, 0x79, 0x79, 0x32, 0x68, 0x94, 0xb9, 0x8f, 0x8c, 0xc3, 0x4d, 0x95, 0x45, 0x08, 0x89, 0x21, 0x07, 0xe1, - 0x3b, 0x53, 0xef, 0x11, 0x8b, 0x79, 0x52, 0xa0, 0x83, 0x03, 0xf3, 0x63, 0xe2, 0x7f, 0x0c, 0xfd, 0x0f, 0x1a, 0x2c, - 0xd2, 0x2d, 0x8d, 0x9d, 0xc7, 0xb5, 0x2e, 0xfd, 0x1d, 0x4a, 0x53, 0xa2, 0x3d, 0xee, 0x8c, 0xfa, 0xff, 0x2b, 0xb2, - 0x46, 0x3b, 0xe4, 0xc4, 0x2a, 0xc6, 0x4e, 0x7b, 0x8c, 0x2c, 0x8b, 0xb4, 0x73, 0x7a, 0x7a, 0xf0, 0x4b, 0x9f, 0xf7, - 0xdb, 0x83, 0xc1, 0x61, 0xfb, 0x3e, 0x9e, 0x94, 0x09, 0x1d, 0x9b, 0x30, 0x2c, 0x13, 0x8e, 0x6d, 0x02, 0x4d, 0x6d, - 0x6d, 0x48, 0x3a, 0x31, 0x49, 0x50, 0x62, 0x9d, 0x9a, 0xb6, 0xef, 0xdb, 0xb6, 0x1f, 0x80, 0x35, 0x99, 0x69, 0xde, - 0x35, 0x7d, 0x71, 0x71, 0xb2, 0x72, 0x8d, 0xe2, 0x49, 0xea, 0x5a, 0xf3, 0x89, 0x27, 0x83, 0x01, 0x1e, 0x9a, 0xc4, - 0xd3, 0x2a, 0xf1, 0x6c, 0x30, 0x70, 0x5d, 0x3d, 0x30, 0x5d, 0xdd, 0xaf, 0xb2, 0xce, 0x07, 0x03, 0xd3, 0x25, 0x72, - 0xb1, 0x03, 0x94, 0xde, 0xfb, 0x5a, 0xea, 0x6f, 0xf8, 0x45, 0xe7, 0xf4, 0xb4, 0x07, 0x18, 0x66, 0x6c, 0x82, 0x3d, - 0x8c, 0x6e, 0x02, 0x18, 0xdd, 0xc1, 0xef, 0xde, 0x90, 0xa6, 0xd7, 0xb4, 0x04, 0x52, 0x2f, 0xfa, 0xaf, 0xa8, 0xa1, - 0x0d, 0xcc, 0xcd, 0x9f, 0x89, 0xfd, 0x33, 0x44, 0x8d, 0xaf, 0x14, 0xc0, 0x0d, 0x1a, 0x29, 0xaf, 0x52, 0x36, 0x3d, - 0x7e, 0xa1, 0xe0, 0xe2, 0x33, 0x55, 0x39, 0xed, 0xad, 0xa6, 0x37, 0xc3, 0xd5, 0x54, 0x7d, 0x45, 0x7f, 0xc5, 0x7f, - 0xa9, 0xc3, 0xb8, 0xdf, 0x6c, 0x24, 0xec, 0xaf, 0x11, 0xf8, 0x12, 0xf5, 0xd2, 0x11, 0x9b, 0xa0, 0x5e, 0xff, 0x2f, - 0x85, 0x07, 0x8d, 0x20, 0xe3, 0x87, 0xed, 0x14, 0xf0, 0x34, 0xda, 0x4c, 0x8c, 0x7f, 0x40, 0x3d, 0xd4, 0xfb, 0x4b, - 0x1d, 0xfe, 0x85, 0xee, 0x1d, 0x55, 0x73, 0xf9, 0x5d, 0xba, 0x2d, 0x5c, 0x85, 0x1f, 0x3a, 0x2c, 0xb7, 0x30, 0xc3, - 0xed, 0x26, 0x83, 0x60, 0x6d, 0xe0, 0x8a, 0x4e, 0x62, 0xd9, 0xe0, 0x47, 0xc7, 0x2d, 0xf4, 0x43, 0xbb, 0x03, 0xca, - 0x95, 0xa6, 0x38, 0xdc, 0xde, 0xf4, 0x45, 0xf3, 0x18, 0x3f, 0x68, 0x16, 0xb8, 0x8d, 0x70, 0xb3, 0xed, 0xb5, 0xde, - 0x7d, 0x15, 0xb7, 0x10, 0x56, 0xf1, 0x39, 0xfc, 0x73, 0x82, 0x06, 0xd5, 0x86, 0xbc, 0xa2, 0x9b, 0xbd, 0x83, 0xdf, - 0x2c, 0x89, 0x55, 0x83, 0x1f, 0x9d, 0xb5, 0xd0, 0x0f, 0x67, 0xa6, 0x23, 0x76, 0xa8, 0x77, 0x74, 0x25, 0xf1, 0x49, - 0x53, 0x42, 0x47, 0xad, 0xb2, 0x1f, 0x11, 0x9f, 0x22, 0x2c, 0xe2, 0x63, 0xf8, 0xa7, 0x1d, 0xf6, 0xf3, 0xeb, 0x56, - 0x3f, 0x66, 0xde, 0x6d, 0x9c, 0x9c, 0x5a, 0x37, 0x5c, 0x65, 0xef, 0xc4, 0x1b, 0xec, 0xb2, 0x6d, 0x2e, 0xf3, 0xda, - 0x47, 0xf0, 0x81, 0xb0, 0x3e, 0x24, 0x0a, 0xb3, 0x43, 0xf0, 0xdf, 0x05, 0xb3, 0x15, 0x75, 0x71, 0xdc, 0x55, 0x8d, - 0x06, 0x12, 0x7d, 0x35, 0x38, 0x24, 0xed, 0xa6, 0x6e, 0x32, 0x0c, 0xbf, 0x1b, 0xa4, 0x0c, 0x0a, 0x27, 0xaa, 0x5e, - 0x1f, 0xbb, 0x5e, 0xed, 0xcd, 0xbf, 0xc7, 0x0e, 0x42, 0x88, 0xea, 0xc5, 0xba, 0xc9, 0xd0, 0x91, 0x68, 0xc4, 0xfa, - 0x82, 0xf5, 0xce, 0xd2, 0x16, 0x32, 0xd8, 0xa9, 0x7a, 0x31, 0x6b, 0x72, 0x48, 0xef, 0xa4, 0x31, 0x6f, 0x6a, 0xf8, - 0x75, 0x12, 0xcc, 0x42, 0x00, 0xde, 0x55, 0xde, 0x48, 0xc5, 0x51, 0xe7, 0xf4, 0x14, 0x0b, 0xc2, 0x93, 0x89, 0xf9, - 0xa5, 0x08, 0x4f, 0x86, 0xe6, 0x97, 0x24, 0x25, 0xbc, 0x6c, 0xef, 0xb8, 0x20, 0xc1, 0xaa, 0x9a, 0x14, 0x0a, 0x0b, - 0x5a, 0xa0, 0xa3, 0x8e, 0x37, 0x0b, 0xc0, 0x53, 0x3f, 0x07, 0x50, 0x83, 0x14, 0xc6, 0x22, 0x54, 0x36, 0x0b, 0x9c, - 0x13, 0x7a, 0x99, 0x9c, 0xf6, 0xa6, 0x47, 0x71, 0xa7, 0x29, 0x9b, 0x05, 0x4a, 0xa7, 0x47, 0xa6, 0x26, 0xce, 0xc8, - 0x63, 0x6a, 0x5b, 0xc3, 0x53, 0xb8, 0xcb, 0xcd, 0x48, 0x76, 0x78, 0xd6, 0x6a, 0x24, 0xa7, 0x08, 0xf7, 0xb3, 0x55, - 0x0b, 0xe7, 0xab, 0x55, 0x0b, 0xd3, 0x60, 0x19, 0x1e, 0x0b, 0x0f, 0x90, 0x52, 0x53, 0xb7, 0x19, 0x9b, 0xa7, 0xc7, - 0x63, 0x0d, 0x76, 0x09, 0x1a, 0xbc, 0x7d, 0x34, 0xf8, 0x21, 0xa5, 0xdc, 0x5d, 0x08, 0x22, 0x13, 0x9d, 0x70, 0x1c, - 0xea, 0xee, 0x5e, 0x0b, 0xbf, 0xae, 0xde, 0xb2, 0x54, 0xc4, 0xbf, 0x4b, 0x6c, 0xd3, 0x82, 0x62, 0x74, 0xbb, 0xd8, - 0xaf, 0x74, 0xab, 0xd8, 0x9b, 0x1d, 0xc5, 0xae, 0xb6, 0x8b, 0x7d, 0x94, 0x81, 0xa6, 0x91, 0xff, 0x70, 0x7c, 0xd6, - 0x6a, 0x1c, 0x03, 0xb2, 0x1e, 0x9f, 0xb5, 0xaa, 0x42, 0xf7, 0x68, 0xb5, 0x56, 0x9a, 0x7c, 0xa1, 0xd6, 0xd7, 0x82, - 0x7b, 0xa7, 0x6f, 0xb3, 0x70, 0xd6, 0xe5, 0xbc, 0xf4, 0x2f, 0xef, 0x9f, 0x82, 0x2d, 0x8b, 0x30, 0xd4, 0x4e, 0xf7, - 0xcf, 0x06, 0xbd, 0x29, 0x8b, 0x1b, 0x90, 0x8a, 0xd2, 0xb1, 0x76, 0xbf, 0x50, 0x79, 0xa5, 0xfd, 0x51, 0x42, 0x52, - 0x67, 0x80, 0xb0, 0x24, 0x0d, 0xdd, 0x3f, 0x1e, 0x98, 0xf3, 0xae, 0x80, 0xdf, 0x27, 0xe6, 0x77, 0xa9, 0x50, 0x72, - 0x0e, 0x19, 0xd3, 0x9b, 0x61, 0xd4, 0x13, 0xe4, 0x35, 0x8d, 0x8d, 0x8d, 0x3d, 0x4a, 0xcb, 0x0c, 0xf5, 0x15, 0x32, - 0xde, 0x94, 0x19, 0x82, 0xbc, 0x16, 0xee, 0x37, 0x5e, 0x16, 0x29, 0xd8, 0xdb, 0xe0, 0x49, 0x0a, 0xb6, 0x36, 0x78, - 0x98, 0x0a, 0xf0, 0x07, 0xa1, 0x29, 0x0b, 0xac, 0xf8, 0x1f, 0x3a, 0x0d, 0x9e, 0xb9, 0x75, 0x26, 0x06, 0x4b, 0xbb, - 0x0c, 0x4e, 0x8a, 0x8f, 0x32, 0x86, 0xbf, 0x0d, 0x8d, 0x30, 0x83, 0x36, 0x19, 0xc2, 0x3c, 0x29, 0x08, 0xa4, 0x61, - 0x9e, 0x4c, 0x08, 0x83, 0x26, 0x79, 0x32, 0x24, 0xac, 0xdf, 0x09, 0xd0, 0xe4, 0xa9, 0x81, 0x1d, 0x00, 0x87, 0xd7, - 0x2f, 0xf2, 0xb5, 0x6d, 0x1c, 0x2c, 0x04, 0xa0, 0x09, 0x41, 0xb8, 0x8a, 0x61, 0x16, 0xb0, 0x39, 0xcd, 0xcf, 0x4e, - 0x15, 0xfe, 0x92, 0x27, 0xd4, 0x50, 0xef, 0x4f, 0x40, 0x56, 0xe3, 0x7b, 0x4b, 0xb6, 0xc6, 0x7b, 0xf7, 0x96, 0x62, - 0xfd, 0x03, 0xfc, 0x51, 0xf6, 0x0f, 0x30, 0x0f, 0x09, 0x45, 0x6b, 0xf4, 0x29, 0x85, 0x62, 0x3b, 0x4a, 0xa1, 0x4f, - 0xde, 0x1d, 0x50, 0x91, 0xe5, 0x6d, 0x1a, 0x8d, 0x68, 0xf1, 0x25, 0xc2, 0x7f, 0xa6, 0x51, 0x0e, 0xdc, 0x62, 0x84, - 0x3f, 0xa6, 0x51, 0xc1, 0x22, 0xfc, 0x47, 0x1a, 0x0d, 0xf3, 0x45, 0x84, 0x3f, 0xa4, 0xd1, 0xa4, 0x88, 0xf0, 0x7b, - 0x50, 0xd6, 0x8e, 0xf8, 0x62, 0x16, 0xe1, 0xdf, 0xd3, 0x48, 0x19, 0x6f, 0x08, 0xfc, 0x30, 0x8d, 0x18, 0x8b, 0xf0, - 0xbb, 0x34, 0x92, 0x79, 0x84, 0xaf, 0xd2, 0x48, 0x16, 0x11, 0x7e, 0x94, 0x46, 0x05, 0x8d, 0xf0, 0xe3, 0x34, 0x82, - 0x42, 0x93, 0x08, 0x3f, 0x49, 0x23, 0x68, 0x59, 0x45, 0xf8, 0x6d, 0x1a, 0x71, 0x11, 0xe1, 0xdf, 0xd2, 0x48, 0x2f, - 0x8a, 0xbf, 0x17, 0x92, 0xab, 0x08, 0x3f, 0x4d, 0xa3, 0x29, 0x8f, 0xf0, 0x9b, 0x34, 0x2a, 0x64, 0x84, 0x5f, 0xa7, - 0x11, 0xcd, 0x23, 0xfc, 0x2a, 0x8d, 0x72, 0x16, 0xe1, 0x5f, 0xd3, 0x68, 0xc4, 0x22, 0xfc, 0x32, 0x8d, 0xee, 0x58, - 0x9e, 0xcb, 0x08, 0x3f, 0x4b, 0x23, 0x26, 0x22, 0xfc, 0x4b, 0x1a, 0x65, 0xd3, 0x08, 0xff, 0x94, 0x46, 0xb4, 0xf8, - 0xa2, 0x22, 0xfc, 0x3c, 0x8d, 0x18, 0x8d, 0xf0, 0x0b, 0xdb, 0xd1, 0x24, 0xc2, 0x3f, 0xa7, 0xd1, 0xcd, 0x34, 0x5a, - 0x63, 0xa5, 0xc8, 0xf2, 0x35, 0xcf, 0xd8, 0x1f, 0x2c, 0x8d, 0xc6, 0xad, 0xf1, 0xf9, 0x78, 0x1c, 0x61, 0x2a, 0x34, - 0xff, 0x7b, 0xc1, 0x6e, 0x9e, 0x6a, 0x48, 0xa4, 0x6c, 0x38, 0xba, 0x1f, 0x61, 0xfa, 0xf7, 0x82, 0xa6, 0xd1, 0x78, - 0x6c, 0x0a, 0xfc, 0xbd, 0xa0, 0x33, 0x5a, 0xbc, 0x65, 0x69, 0x74, 0x7f, 0x3c, 0x1e, 0x8f, 0x4e, 0x22, 0x4c, 0xff, - 0x59, 0x7c, 0x34, 0x2d, 0x98, 0x02, 0x43, 0xc6, 0x27, 0x50, 0xf7, 0x74, 0x7c, 0x3a, 0xca, 0x22, 0x3c, 0xe4, 0xea, - 0xef, 0x05, 0x7c, 0x8f, 0xd9, 0x49, 0x76, 0x12, 0xe1, 0x61, 0x4e, 0xb3, 0x2f, 0x69, 0xd4, 0x32, 0xbf, 0xc4, 0x2f, - 0x6c, 0xf4, 0x7a, 0x26, 0xcd, 0x55, 0xc6, 0x98, 0x0d, 0xb3, 0x51, 0x84, 0xcd, 0x60, 0xc6, 0xf0, 0xf7, 0x2b, 0x7f, - 0xc7, 0x74, 0x1a, 0x9d, 0xd3, 0xce, 0x90, 0x75, 0x22, 0x3c, 0x7c, 0x73, 0x23, 0xd2, 0x88, 0x9e, 0x76, 0x68, 0x87, - 0x46, 0x78, 0xb8, 0x28, 0xf2, 0xbb, 0x1b, 0x29, 0x47, 0x00, 0x84, 0xe1, 0xf9, 0xf9, 0xfd, 0x08, 0x67, 0xf4, 0x57, - 0x0d, 0xb5, 0x4f, 0xc7, 0x0f, 0x18, 0x6d, 0x45, 0xf8, 0x17, 0x5a, 0xe8, 0x8f, 0x0b, 0xe5, 0x06, 0xda, 0x82, 0x14, - 0x99, 0xbd, 0x03, 0x35, 0x7f, 0x34, 0xea, 0x9c, 0x3d, 0x68, 0xb3, 0x08, 0x67, 0x57, 0xaf, 0xa1, 0xb7, 0xfb, 0xe3, - 0xd3, 0x16, 0x7c, 0x08, 0x90, 0x4b, 0x59, 0x01, 0x8d, 0x9c, 0x9d, 0x3c, 0x38, 0x65, 0x23, 0x93, 0xa8, 0x78, 0xfe, - 0xc5, 0xcc, 0xfe, 0x1c, 0xe6, 0x93, 0x15, 0x7c, 0xa6, 0xa4, 0x48, 0xa3, 0x51, 0xd6, 0x3e, 0x39, 0x86, 0x84, 0x3b, - 0x2a, 0x3c, 0x70, 0x6e, 0xa1, 0xea, 0xf9, 0x30, 0xc2, 0xb7, 0x36, 0xf5, 0x7c, 0x68, 0x3e, 0x26, 0xef, 0x7e, 0x15, - 0x6f, 0x46, 0x69, 0x34, 0x3c, 0x3f, 0x3f, 0x6b, 0x41, 0xc2, 0x07, 0x7a, 0x97, 0x46, 0xf4, 0x01, 0xfc, 0x07, 0xd9, - 0x1f, 0x9f, 0x41, 0x87, 0x30, 0xc2, 0xdb, 0xc9, 0xc7, 0x30, 0xe7, 0xcb, 0x94, 0x7e, 0xe1, 0x69, 0x34, 0x1c, 0x0d, - 0xef, 0x9f, 0x41, 0xbd, 0x19, 0x9d, 0x3c, 0xd3, 0x14, 0xda, 0x6d, 0xb5, 0x4c, 0xcb, 0xef, 0xf8, 0x57, 0x66, 0xaa, - 0x9f, 0x9e, 0x9e, 0x0d, 0x3b, 0x30, 0x82, 0x2b, 0x50, 0xa8, 0xc0, 0x78, 0xce, 0x33, 0xd3, 0xe0, 0x55, 0xf6, 0x74, - 0x94, 0x46, 0x0f, 0x1e, 0x1c, 0x77, 0xb2, 0x2c, 0xc2, 0xb7, 0x1f, 0x47, 0xb6, 0xb6, 0xc9, 0x53, 0x00, 0xfb, 0x34, - 0x62, 0x0f, 0x1e, 0x9c, 0xdd, 0xa7, 0xf0, 0xfd, 0xdc, 0xb4, 0x75, 0x3e, 0x1e, 0x66, 0xe7, 0xd0, 0xd6, 0xef, 0x30, - 0x9d, 0x93, 0xf3, 0xe3, 0x91, 0xe9, 0xeb, 0x77, 0x33, 0xea, 0xce, 0xf8, 0x64, 0x7c, 0x62, 0x32, 0xcd, 0x50, 0xcb, - 0xcf, 0xdf, 0x58, 0x1a, 0x65, 0x6c, 0xd4, 0x8e, 0xf0, 0xad, 0x5b, 0xb8, 0x07, 0x27, 0xad, 0xd6, 0xe8, 0x38, 0xc2, - 0xa3, 0x87, 0xf3, 0xf9, 0x5b, 0x03, 0xc1, 0xf6, 0xc9, 0x03, 0xfb, 0xad, 0xbe, 0xdc, 0x41, 0xd3, 0x43, 0x03, 0xb4, - 0x11, 0x9f, 0x99, 0x96, 0xcf, 0x1e, 0xc0, 0x7f, 0xe6, 0xdb, 0x34, 0x5d, 0x7e, 0xcb, 0xd1, 0xc4, 0x2e, 0x4a, 0x9b, - 0x3d, 0x68, 0x41, 0x8d, 0x31, 0xff, 0x38, 0x2c, 0x38, 0xa0, 0xd1, 0xb0, 0x03, 0xff, 0x17, 0xe1, 0x71, 0x7e, 0xf5, - 0xda, 0xe1, 0xec, 0x78, 0x4c, 0xc7, 0xad, 0x08, 0x8f, 0xe5, 0x47, 0xa5, 0x3f, 0x3c, 0x14, 0x69, 0xd4, 0xe9, 0x9c, - 0x0f, 0x4d, 0x99, 0xc5, 0x2f, 0x8a, 0x1b, 0x3c, 0x6e, 0x99, 0x56, 0x26, 0xf4, 0xad, 0x1a, 0x5e, 0x49, 0x58, 0x49, - 0xf8, 0x2f, 0xc2, 0x13, 0xd0, 0xc2, 0xb9, 0x56, 0xce, 0xed, 0x76, 0x98, 0xbc, 0x33, 0xa8, 0x39, 0xba, 0x0f, 0xf0, - 0xf2, 0xcb, 0x38, 0xa2, 0xf4, 0xb4, 0xd3, 0x8a, 0xb0, 0x19, 0xf5, 0x79, 0x0b, 0xfe, 0x8b, 0xb0, 0x85, 0x9c, 0x81, - 0xeb, 0xe4, 0xe3, 0xb3, 0x97, 0x37, 0x69, 0x44, 0x47, 0xe3, 0x31, 0x2c, 0x89, 0x99, 0x8c, 0x2f, 0x36, 0x95, 0x82, - 0xdd, 0xfd, 0x7a, 0xe3, 0xb6, 0x8b, 0x49, 0xd0, 0x0e, 0x3a, 0x67, 0x0f, 0x86, 0x27, 0x11, 0x7e, 0x3b, 0xe2, 0x54, - 0xc0, 0x2a, 0x65, 0xa3, 0xd3, 0xec, 0x34, 0x33, 0x09, 0x13, 0x99, 0x46, 0x27, 0xb0, 0xe4, 0x9d, 0x08, 0xf3, 0xaf, - 0x57, 0x77, 0x16, 0xdd, 0xa0, 0xb6, 0x43, 0x90, 0x71, 0x8b, 0x9d, 0x9d, 0x67, 0x11, 0xce, 0xe9, 0xd7, 0x67, 0xbf, - 0x16, 0x69, 0xc4, 0xce, 0xd8, 0xd9, 0x98, 0xfa, 0xef, 0x3f, 0xd4, 0xd4, 0xd4, 0x68, 0x8d, 0x4f, 0x21, 0xe9, 0x46, - 0x98, 0xb1, 0xde, 0xcf, 0xc6, 0x06, 0x43, 0x5e, 0xcd, 0xa4, 0xc8, 0x9e, 0x8e, 0xc7, 0xd2, 0x62, 0x31, 0x85, 0x4d, - 0xf8, 0x27, 0x40, 0x9b, 0x8e, 0x46, 0xe7, 0xec, 0x2c, 0xc2, 0x7f, 0xda, 0x5d, 0xe2, 0x26, 0xf0, 0xa7, 0xc5, 0x6c, - 0xe6, 0x76, 0xfb, 0x9f, 0x16, 0x28, 0x30, 0xdf, 0x31, 0x1d, 0xd3, 0x51, 0x27, 0xc2, 0x7f, 0x1a, 0xb8, 0x8c, 0x8e, - 0xe1, 0x3f, 0x28, 0x00, 0x9d, 0x3d, 0x68, 0x31, 0xf6, 0xa0, 0x65, 0xbe, 0xc2, 0x3c, 0x37, 0xf3, 0xe1, 0x59, 0xd6, - 0x8e, 0xf0, 0x9f, 0x0e, 0x1d, 0xc7, 0x63, 0xda, 0x02, 0x74, 0xfc, 0xd3, 0xa1, 0x63, 0xa7, 0x35, 0xec, 0x50, 0xf3, - 0x6d, 0xb1, 0xe6, 0xfc, 0x7e, 0xc6, 0x60, 0x72, 0x7f, 0x5a, 0x84, 0xbc, 0x7f, 0xff, 0xfc, 0xfc, 0xc1, 0x03, 0xf8, - 0x34, 0x6d, 0x97, 0x9f, 0x4a, 0x3f, 0xcc, 0x0d, 0x92, 0xb5, 0xb2, 0x13, 0xa0, 0x93, 0x7f, 0x9a, 0x31, 0x8e, 0xc7, - 0x63, 0xd6, 0x8a, 0x70, 0xce, 0x67, 0xcc, 0x62, 0x82, 0xfd, 0x6d, 0x3a, 0x3a, 0xee, 0x64, 0xa3, 0xe3, 0x4e, 0x84, - 0xf3, 0xb7, 0xcf, 0xcc, 0x6c, 0x5a, 0x30, 0x7b, 0xbf, 0xe5, 0x3c, 0xd6, 0xcc, 0xe8, 0x1b, 0x18, 0x24, 0xac, 0x34, - 0x54, 0x7e, 0x1f, 0xd0, 0xc3, 0xb3, 0xb3, 0x6c, 0x04, 0x03, 0x7d, 0x0f, 0xdd, 0x02, 0x18, 0xdf, 0xdb, 0xcd, 0x37, - 0xa4, 0xa7, 0xa7, 0x30, 0xdd, 0xf7, 0xf3, 0x45, 0x31, 0x7f, 0x95, 0x46, 0x0f, 0x8e, 0xef, 0xb7, 0x46, 0xc3, 0x08, - 0xbf, 0x77, 0x13, 0x3c, 0xce, 0x86, 0xc7, 0xf7, 0xdb, 0x11, 0x7e, 0x6f, 0xf6, 0xdb, 0xfd, 0xe1, 0xd9, 0x39, 0x9c, - 0x1b, 0xef, 0xd5, 0xbc, 0x78, 0x3b, 0x31, 0x05, 0xc6, 0xf4, 0x01, 0x34, 0xfb, 0x9b, 0xd9, 0x8d, 0xa3, 0x36, 0x6c, - 0xe4, 0xf7, 0x66, 0x93, 0x19, 0x3c, 0xb9, 0xdf, 0x3e, 0x3d, 0x3f, 0x8d, 0xf0, 0x8c, 0x8f, 0x04, 0x10, 0x78, 0xb3, - 0x51, 0x1e, 0xb4, 0x1f, 0xdc, 0x6f, 0x45, 0x78, 0xf6, 0x56, 0x67, 0x1f, 0xe9, 0xcc, 0x50, 0xe3, 0x31, 0xc0, 0x6c, - 0xc6, 0x95, 0xbe, 0x7b, 0xa3, 0x1c, 0x3d, 0x66, 0xed, 0x08, 0xcf, 0x64, 0x96, 0x51, 0xf5, 0xd6, 0x26, 0x0c, 0x4f, - 0x23, 0x2c, 0xe8, 0x57, 0xfa, 0x59, 0xfa, 0xcd, 0x34, 0x62, 0x74, 0x64, 0xd2, 0x0c, 0x0e, 0x47, 0xf8, 0xdd, 0x08, - 0x2e, 0x23, 0xd3, 0x68, 0x3c, 0x1a, 0x9f, 0x02, 0x78, 0x80, 0x00, 0x59, 0xec, 0x06, 0x68, 0xc0, 0xd7, 0xe8, 0xd1, - 0x30, 0x8d, 0xce, 0x86, 0xe7, 0xac, 0x73, 0x1c, 0xe1, 0x92, 0x1a, 0xd1, 0x53, 0xc8, 0x37, 0x9f, 0x1f, 0xcd, 0x96, - 0x3a, 0xb1, 0x09, 0x06, 0x40, 0x23, 0x7a, 0xbf, 0x35, 0x3a, 0x8b, 0xf0, 0xfc, 0x35, 0xf3, 0x7b, 0x8c, 0x31, 0x76, - 0x0e, 0xb0, 0x84, 0x24, 0x83, 0x40, 0xe7, 0xe3, 0xe1, 0x83, 0x73, 0xf3, 0x0d, 0x60, 0xa0, 0x63, 0xc6, 0x00, 0x48, - 0xf3, 0xd7, 0xac, 0x04, 0xc4, 0x68, 0x78, 0xbf, 0x05, 0xf4, 0x65, 0x4e, 0xe7, 0xf4, 0x8e, 0xde, 0x3c, 0x9d, 0x9b, - 0x39, 0x8d, 0x47, 0xa7, 0x11, 0x9e, 0x3f, 0xff, 0x65, 0xbe, 0x18, 0x8f, 0xcd, 0x84, 0xe8, 0xf0, 0x41, 0x84, 0xe7, - 0xac, 0x58, 0xc0, 0x1a, 0x9d, 0x9f, 0x1e, 0x8f, 0x23, 0xec, 0xd0, 0x30, 0x6b, 0x65, 0x43, 0xb8, 0x6d, 0x5d, 0xcc, - 0xd2, 0x68, 0x34, 0xa2, 0xad, 0x11, 0xdc, 0xbd, 0xca, 0x9b, 0x5f, 0x0b, 0x8b, 0x46, 0xcc, 0xe0, 0x83, 0x5b, 0x43, - 0x98, 0x2f, 0xc0, 0xe3, 0xe3, 0x90, 0x65, 0x19, 0x75, 0x89, 0x67, 0x67, 0xc7, 0xc7, 0x80, 0x7b, 0x76, 0x86, 0x16, - 0x41, 0xde, 0xa8, 0xbb, 0x61, 0x21, 0xe1, 0xe8, 0x02, 0xa2, 0x0a, 0x64, 0xf5, 0xcd, 0xdd, 0x6b, 0x43, 0x57, 0xdb, - 0x67, 0x0f, 0x60, 0x01, 0x14, 0x1d, 0x8d, 0x5e, 0xd9, 0xc3, 0xed, 0x7c, 0x78, 0x72, 0xda, 0x3e, 0x8e, 0xb0, 0xdf, - 0x08, 0xf4, 0xbc, 0x75, 0xbf, 0x03, 0x25, 0xc4, 0xe8, 0xce, 0x96, 0x18, 0x9f, 0xd0, 0x93, 0xb3, 0x56, 0x84, 0xfd, - 0xd6, 0x60, 0xe7, 0xc3, 0xd3, 0xfb, 0xf0, 0xa9, 0xa6, 0x2c, 0xcf, 0x0d, 0x7e, 0x9f, 0x02, 0x5c, 0x14, 0x7f, 0x26, - 0x68, 0x1a, 0xd1, 0xd6, 0x69, 0xa7, 0x33, 0x82, 0xcf, 0xfc, 0x2b, 0x2b, 0xd2, 0x28, 0x6b, 0xc1, 0x7f, 0x11, 0x0e, - 0x76, 0x12, 0x1b, 0x46, 0xd8, 0xe0, 0xdd, 0x19, 0x3d, 0x35, 0x7b, 0xdf, 0xed, 0xaa, 0xd6, 0x79, 0x0b, 0x36, 0xac, - 0xdb, 0x54, 0xee, 0x4b, 0x09, 0x79, 0xe3, 0x48, 0x2c, 0x8d, 0x70, 0x80, 0xa0, 0xe3, 0xfb, 0xe3, 0x08, 0xfb, 0x1d, - 0x77, 0x72, 0x76, 0xde, 0x01, 0x52, 0xa6, 0x81, 0x50, 0x8c, 0x3a, 0xc3, 0x13, 0x20, 0x4d, 0x9a, 0xbd, 0xb6, 0x78, - 0x12, 0x61, 0xfd, 0x54, 0xe9, 0x57, 0x69, 0x34, 0x3a, 0x1f, 0x8e, 0x47, 0xe7, 0x11, 0xd6, 0x72, 0x46, 0xb5, 0x34, - 0x14, 0xf0, 0xf8, 0xe4, 0x7e, 0x84, 0x0d, 0x9a, 0xb7, 0x58, 0x6b, 0xd4, 0x8a, 0xb0, 0x3b, 0x4a, 0x18, 0x3b, 0xef, - 0xc0, 0xb4, 0x7e, 0x7e, 0xae, 0x01, 0x97, 0x47, 0x6c, 0x78, 0x1c, 0xe1, 0x92, 0xde, 0x1b, 0x42, 0x04, 0x5f, 0x6a, - 0x26, 0xbf, 0x38, 0xd6, 0x03, 0x48, 0x9d, 0xdf, 0xf0, 0xb0, 0x0c, 0x2f, 0x6f, 0x2c, 0x1a, 0x51, 0xb3, 0xc5, 0x83, - 0xdb, 0xe8, 0x27, 0x34, 0xf6, 0x6c, 0x3b, 0x27, 0xcb, 0x35, 0x2e, 0x83, 0xbc, 0x7e, 0x61, 0x77, 0x2a, 0x56, 0xca, - 0x70, 0xb2, 0x41, 0x0a, 0x38, 0x62, 0x38, 0xb7, 0x06, 0xe7, 0xb9, 0x0a, 0x82, 0xa4, 0x20, 0xad, 0xae, 0xb8, 0xf0, - 0xde, 0xb4, 0x5d, 0x01, 0xa1, 0x1f, 0x20, 0xbd, 0x20, 0x94, 0x68, 0x88, 0x90, 0x63, 0x85, 0x49, 0xef, 0x64, 0x60, - 0x64, 0x4a, 0x69, 0xdd, 0x16, 0x28, 0xa1, 0x3e, 0x36, 0x3e, 0x5c, 0x95, 0x43, 0xf4, 0x28, 0xd4, 0x95, 0xc4, 0x44, - 0xba, 0x7e, 0x21, 0x74, 0xac, 0x54, 0xbf, 0x18, 0xe0, 0xf6, 0x19, 0xc2, 0x10, 0x43, 0x82, 0xf4, 0xe5, 0xe5, 0x65, - 0xfb, 0xec, 0xc0, 0x08, 0x7d, 0x97, 0x97, 0xe7, 0xf6, 0x07, 0xfc, 0x3b, 0xa8, 0xe2, 0x76, 0xc3, 0xf8, 0xde, 0xb3, - 0x40, 0xa3, 0x67, 0xf8, 0xeb, 0xf7, 0x6c, 0xb5, 0x8a, 0xdf, 0x33, 0x02, 0x33, 0xc6, 0xef, 0x59, 0x62, 0xee, 0x48, - 0xac, 0x87, 0x10, 0xe9, 0x83, 0xe6, 0xac, 0x85, 0x21, 0x9a, 0xbc, 0xe7, 0xbc, 0xdf, 0xb3, 0x3e, 0xaf, 0x7b, 0x97, - 0x57, 0x21, 0x9c, 0x0f, 0x0e, 0x96, 0x45, 0xaa, 0xad, 0x98, 0xa0, 0xad, 0x98, 0xa0, 0xad, 0x98, 0xa0, 0xab, 0x20, - 0xfa, 0x27, 0x3d, 0x90, 0x52, 0x8c, 0xb2, 0xc5, 0xf1, 0xd4, 0xef, 0x40, 0xed, 0x01, 0xda, 0xc9, 0x5e, 0xa5, 0xec, - 0x28, 0x75, 0x15, 0x3b, 0x15, 0x18, 0x3b, 0x13, 0x9d, 0xb6, 0xe3, 0xe8, 0xdf, 0x51, 0x77, 0xbc, 0xac, 0x89, 0x65, - 0xef, 0x76, 0x8a, 0x65, 0xb0, 0x92, 0x46, 0x34, 0xdb, 0xb7, 0xf1, 0x48, 0x74, 0xff, 0xbe, 0x11, 0xcc, 0xaa, 0x20, - 0x79, 0x0d, 0x48, 0xea, 0x82, 0x14, 0x72, 0x6e, 0xa4, 0xb4, 0x02, 0xa5, 0x23, 0x1d, 0x17, 0xa0, 0xa1, 0xf4, 0x0a, - 0xca, 0x32, 0x96, 0x6b, 0xc3, 0x00, 0x44, 0x59, 0x19, 0xcd, 0xca, 0x6a, 0xa7, 0x20, 0xba, 0x80, 0x26, 0xcc, 0x48, - 0x2c, 0xd0, 0x80, 0x30, 0x0d, 0x08, 0x57, 0x19, 0xc4, 0x19, 0x97, 0x7d, 0x62, 0xb2, 0x95, 0xc9, 0x56, 0x65, 0xb6, - 0xf4, 0xd9, 0x56, 0x48, 0x94, 0x26, 0x5b, 0x96, 0xd9, 0x20, 0xb3, 0xe1, 0x49, 0xaa, 0xf0, 0x30, 0x95, 0x56, 0x54, - 0xab, 0x64, 0xab, 0xb7, 0x34, 0xd4, 0xe6, 0x1e, 0x1c, 0xc4, 0xa5, 0x9c, 0x64, 0xd4, 0xc4, 0xf7, 0x96, 0x3c, 0x29, - 0x8c, 0x0c, 0xc4, 0x93, 0x89, 0xfb, 0x3b, 0x5c, 0x6f, 0xca, 0x4a, 0xc5, 0x64, 0xf8, 0x8d, 0x92, 0xe8, 0x93, 0x57, - 0xa2, 0xbe, 0xe7, 0x26, 0x0a, 0xd0, 0x05, 0x49, 0x5a, 0xad, 0xe3, 0xf6, 0x71, 0xeb, 0xbc, 0xc7, 0x0f, 0xdb, 0x9d, - 0xe4, 0x41, 0x27, 0x35, 0x8a, 0x88, 0xb9, 0xbc, 0x01, 0x05, 0xcc, 0x51, 0x27, 0x39, 0x41, 0x87, 0xed, 0xa4, 0x75, - 0x7a, 0xda, 0x84, 0x7f, 0xf0, 0x23, 0x5d, 0x56, 0x3b, 0x69, 0x9d, 0x9c, 0xf6, 0xf8, 0xd1, 0x46, 0xa5, 0x98, 0x37, - 0xa0, 0x20, 0x3a, 0x32, 0x95, 0x30, 0xd4, 0xaf, 0x96, 0xf7, 0xd9, 0x96, 0x9e, 0xe7, 0x91, 0x8e, 0xa5, 0x55, 0xc5, - 0x01, 0x54, 0xfd, 0xd7, 0xc4, 0x00, 0xd1, 0x7f, 0x0d, 0xcb, 0x48, 0xbd, 0xcb, 0x02, 0x44, 0xed, 0xf7, 0x3c, 0x16, - 0x0d, 0x76, 0x18, 0xdb, 0x7c, 0x0d, 0x75, 0x9b, 0x10, 0x3d, 0x0f, 0x4f, 0x5c, 0xae, 0x0a, 0x73, 0x27, 0x08, 0x35, - 0x15, 0xe4, 0x0e, 0x5d, 0xae, 0x0c, 0x73, 0x87, 0x08, 0x35, 0x25, 0xe4, 0xd2, 0x94, 0x27, 0x14, 0x72, 0x74, 0x42, - 0x9b, 0x06, 0x92, 0xd5, 0xa2, 0x3c, 0x67, 0x7e, 0xd8, 0x7c, 0x0c, 0xcb, 0x63, 0x08, 0x8a, 0x13, 0xa4, 0x05, 0xbc, - 0xb0, 0x52, 0x6a, 0x73, 0x5a, 0xb8, 0x54, 0xe3, 0x40, 0x46, 0x03, 0xfe, 0x39, 0x64, 0xe6, 0xd9, 0x8d, 0x56, 0xef, - 0xf8, 0xac, 0x95, 0xb6, 0xc1, 0x55, 0x1c, 0x64, 0x6d, 0x61, 0x65, 0x6d, 0xe1, 0x65, 0x6d, 0xe1, 0x65, 0x6d, 0x10, - 0xe0, 0x83, 0xbe, 0xff, 0x96, 0x35, 0xf3, 0x1b, 0x5e, 0xda, 0xf2, 0x58, 0x63, 0x8d, 0x58, 0xaf, 0x56, 0xcb, 0x35, - 0x58, 0x5a, 0x55, 0x2a, 0x77, 0x55, 0xa9, 0x3f, 0x97, 0x45, 0xda, 0xc2, 0x93, 0x14, 0xb4, 0xdc, 0x2d, 0x4c, 0xcd, - 0xe6, 0xf6, 0x54, 0x61, 0x33, 0x8a, 0x4f, 0xcf, 0xab, 0x93, 0x2f, 0xc9, 0xb1, 0xd1, 0x1e, 0x2f, 0x8b, 0x94, 0x5b, - 0x9a, 0xc1, 0x2d, 0xcd, 0xe0, 0x96, 0x66, 0x40, 0x23, 0xb8, 0x2c, 0x6c, 0xca, 0x26, 0x94, 0xc0, 0x95, 0x40, 0xff, - 0x78, 0x00, 0x41, 0x0c, 0x63, 0x4d, 0xcc, 0xa8, 0x37, 0x3a, 0x6f, 0x43, 0xd0, 0x36, 0x5b, 0x52, 0x27, 0xd4, 0xf8, - 0xae, 0x97, 0x63, 0x7e, 0x55, 0x43, 0xfb, 0x04, 0x5e, 0xd4, 0x79, 0xa8, 0xe3, 0x16, 0x98, 0xae, 0x44, 0x45, 0xd4, - 0x33, 0x64, 0x21, 0x35, 0x3a, 0x1b, 0x67, 0x92, 0xfe, 0x65, 0xc3, 0x13, 0xd8, 0x52, 0x82, 0xf0, 0x1d, 0x89, 0x2f, - 0xac, 0x0a, 0x4d, 0x50, 0x5a, 0xdc, 0x3a, 0x73, 0x39, 0x7b, 0x24, 0x74, 0xc1, 0x6c, 0xde, 0xc7, 0xbc, 0xea, 0x09, - 0x22, 0x95, 0x71, 0xda, 0x24, 0x55, 0xd4, 0x66, 0x70, 0x62, 0x26, 0xb7, 0xd4, 0xb8, 0xf4, 0xbc, 0xb0, 0x7f, 0x5e, - 0xd1, 0xc0, 0xe7, 0xb1, 0x98, 0x0c, 0xbd, 0xab, 0xf0, 0xb5, 0x89, 0x6d, 0x44, 0xf6, 0xf7, 0xad, 0x45, 0xbb, 0xf9, - 0xda, 0x34, 0x69, 0x37, 0x89, 0x26, 0x1b, 0x76, 0xa8, 0x5f, 0xa3, 0xbf, 0xbd, 0xc7, 0x5e, 0x31, 0x19, 0xa2, 0x80, - 0x66, 0x1b, 0xb0, 0xca, 0x0a, 0x58, 0xca, 0xd5, 0x2b, 0x1d, 0x39, 0xa1, 0x77, 0x33, 0xe6, 0x75, 0x31, 0x19, 0xee, - 0x7c, 0x7a, 0xc5, 0xf6, 0xd8, 0x7b, 0x4b, 0x83, 0x1e, 0xbc, 0x6a, 0x7b, 0xca, 0x6e, 0xbf, 0x57, 0xe7, 0x66, 0x67, - 0x1d, 0x95, 0x7f, 0xaf, 0xce, 0xd3, 0x5d, 0x75, 0x66, 0xfc, 0x36, 0xf6, 0x7b, 0x47, 0x07, 0x6a, 0x6c, 0x63, 0x26, - 0x35, 0x19, 0x42, 0xac, 0x7c, 0xf8, 0x6b, 0x23, 0xda, 0x74, 0x3d, 0x09, 0x87, 0x55, 0x90, 0xbd, 0xe4, 0x34, 0x65, - 0x98, 0x92, 0xce, 0x61, 0x61, 0x62, 0xda, 0x88, 0x84, 0x36, 0x55, 0x42, 0x71, 0x4e, 0xe2, 0x98, 0x1e, 0x66, 0x10, - 0x99, 0xa7, 0xdd, 0xa3, 0x69, 0x4c, 0x1b, 0x19, 0x3a, 0x8a, 0xdb, 0x0d, 0x7a, 0x98, 0x21, 0xd4, 0x68, 0x83, 0xce, - 0x54, 0x92, 0x76, 0x33, 0x87, 0x58, 0x9d, 0x86, 0x14, 0xe7, 0x87, 0x22, 0x29, 0x1a, 0xf2, 0x50, 0x25, 0x45, 0x23, - 0x39, 0xc5, 0x22, 0x99, 0x94, 0xc9, 0x13, 0x93, 0x3c, 0xb1, 0xc9, 0xc3, 0x32, 0x79, 0x68, 0x92, 0x87, 0x36, 0x99, - 0x92, 0xe2, 0x50, 0x24, 0xb4, 0x11, 0xb7, 0x9b, 0x05, 0x3a, 0x84, 0x11, 0xf8, 0xd1, 0x13, 0x11, 0x86, 0x48, 0x5f, - 0x1b, 0x1b, 0xa3, 0xb9, 0xcc, 0x5d, 0xd0, 0xd2, 0x0a, 0x48, 0xa5, 0xe3, 0x17, 0xd4, 0x79, 0x16, 0x80, 0x09, 0x6b, - 0xfb, 0xc7, 0x87, 0xe4, 0x5b, 0x67, 0xb9, 0x14, 0x81, 0x63, 0x1b, 0xd8, 0xe2, 0x7f, 0x71, 0xee, 0x3c, 0x00, 0xd5, - 0x35, 0xcd, 0xe7, 0x53, 0xba, 0xe5, 0x3d, 0x5c, 0x4c, 0x86, 0x6e, 0x67, 0x95, 0xcd, 0x30, 0x5a, 0xd8, 0x50, 0xd7, - 0x75, 0x3f, 0x4f, 0x00, 0xb5, 0xf7, 0x2d, 0x4d, 0xa8, 0x51, 0x92, 0xdb, 0x1a, 0x93, 0x82, 0xdd, 0xa9, 0x8c, 0xe6, - 0x2c, 0xae, 0x0e, 0xe0, 0x6a, 0x98, 0x8c, 0xbc, 0x00, 0x8f, 0x80, 0xe2, 0x30, 0x39, 0x6e, 0xe8, 0x64, 0x72, 0x98, - 0x9c, 0x3e, 0x68, 0xe8, 0x64, 0x78, 0x98, 0xb4, 0xdb, 0x15, 0xce, 0x26, 0x05, 0xd1, 0xc9, 0x84, 0x68, 0xd0, 0x18, - 0xda, 0x46, 0xe5, 0x9c, 0x82, 0x89, 0xdb, 0xbf, 0x31, 0x8c, 0x86, 0x1b, 0x86, 0x60, 0x13, 0x1b, 0xf5, 0x73, 0x6b, - 0x0c, 0x61, 0x37, 0x9d, 0xd3, 0xd3, 0xa6, 0x4e, 0x0a, 0xac, 0xed, 0x4a, 0x36, 0x75, 0x32, 0xc1, 0xda, 0x2e, 0x5f, - 0x53, 0x27, 0x43, 0xdb, 0x94, 0xd1, 0x01, 0x32, 0x11, 0x00, 0xeb, 0x39, 0x0b, 0x20, 0xdf, 0xf1, 0x4e, 0x3a, 0x6b, - 0xd0, 0x1a, 0x7e, 0xaf, 0x5c, 0xd3, 0x17, 0x54, 0x54, 0x83, 0xa9, 0x13, 0xfb, 0x56, 0xd1, 0x76, 0xd5, 0x24, 0xfb, - 0xd7, 0x65, 0xcb, 0x66, 0x0b, 0xa9, 0xeb, 0x05, 0x1f, 0xd6, 0x30, 0xc4, 0x95, 0x72, 0x07, 0xf7, 0x3f, 0x94, 0xc4, - 0x10, 0xdb, 0xcf, 0x9c, 0x42, 0x9c, 0x78, 0x3d, 0x32, 0x24, 0xf1, 0x46, 0x63, 0x8d, 0xe2, 0xe0, 0xbc, 0x7d, 0x1a, - 0x52, 0xd5, 0xad, 0x80, 0x7f, 0x84, 0x44, 0x0b, 0x61, 0x4d, 0x42, 0x47, 0x51, 0xc0, 0x82, 0x38, 0xed, 0x6e, 0xed, - 0x80, 0x38, 0x38, 0xd8, 0x3c, 0x2f, 0xfc, 0xd3, 0x0b, 0x5b, 0xcf, 0x2d, 0x54, 0xf6, 0x84, 0xfe, 0x41, 0x28, 0x6b, - 0x69, 0xcc, 0x03, 0x44, 0xf1, 0xa1, 0xb7, 0xee, 0x1b, 0x0a, 0xdf, 0xaf, 0xe2, 0x0e, 0xba, 0x9c, 0xe6, 0x99, 0xc9, - 0x30, 0x7d, 0x0d, 0x82, 0xb1, 0xbd, 0x09, 0x27, 0x54, 0xda, 0x4a, 0xfe, 0xcb, 0x8e, 0x83, 0x4e, 0xdc, 0x83, 0x35, - 0x61, 0xa3, 0x9f, 0x43, 0xcb, 0xe4, 0x0a, 0x36, 0xce, 0x27, 0x7d, 0xb5, 0xaa, 0x3d, 0x4f, 0x64, 0x1f, 0xc1, 0x41, - 0x07, 0x07, 0x5c, 0x3d, 0x03, 0x63, 0x6a, 0x16, 0x37, 0xc2, 0xc3, 0xf7, 0xef, 0xda, 0x69, 0xfd, 0xd9, 0x9c, 0xab, - 0x69, 0x70, 0xd0, 0x3d, 0xac, 0xe5, 0xef, 0x5c, 0x89, 0x9e, 0x4e, 0xb9, 0x5b, 0xeb, 0xcf, 0x95, 0xa9, 0xfa, 0xd6, - 0x43, 0x59, 0x07, 0x07, 0xbc, 0x0a, 0x57, 0x15, 0xfd, 0x10, 0xa1, 0x9e, 0x91, 0x41, 0x9e, 0xe5, 0x92, 0xc2, 0x8d, - 0x28, 0x5c, 0x31, 0xa4, 0x0d, 0x7e, 0xa4, 0xf1, 0x1f, 0xf2, 0xff, 0x53, 0x23, 0x87, 0x3a, 0x6d, 0xf0, 0x40, 0x00, - 0x0b, 0x59, 0xa1, 0x2a, 0x50, 0xa4, 0x81, 0x74, 0x68, 0x79, 0x8e, 0xca, 0xc3, 0x9c, 0xce, 0xe7, 0xf9, 0x9d, 0x79, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcc, 0xbd, 0x6d, 0x7b, 0x1b, 0x37, 0xb2, 0x20, 0xfa, + 0xf9, 0xee, 0xaf, 0x90, 0xfa, 0x38, 0x4a, 0x43, 0x04, 0x5b, 0x24, 0x25, 0xca, 0x72, 0x53, 0x10, 0xd7, 0xaf, 0x63, + 0x27, 0x8e, 0xed, 0x58, 0xb6, 0x33, 0x0e, 0xc3, 0xe3, 0x80, 0x4d, 0x90, 0x84, 0xdd, 0x04, 0x98, 0x06, 0x68, 0x49, + 0x21, 0xf9, 0xdf, 0xef, 0x53, 0x78, 0xe9, 0x46, 0x93, 0xb4, 0x67, 0x66, 0xef, 0xee, 0x7d, 0xf6, 0xe4, 0x8c, 0xc5, + 0xc6, 0x3b, 0x0a, 0x85, 0x42, 0x55, 0xa1, 0xaa, 0x70, 0x79, 0x38, 0x96, 0x99, 0xbe, 0x5b, 0xb0, 0x83, 0x99, 0x9e, + 0xe7, 0x57, 0x97, 0xee, 0x5f, 0x46, 0xc7, 0x57, 0x97, 0x39, 0x17, 0x5f, 0x0e, 0x0a, 0x96, 0x13, 0x9e, 0x49, 0x71, + 0x30, 0x2b, 0xd8, 0x84, 0x8c, 0xa9, 0xa6, 0x29, 0x9f, 0xd3, 0x29, 0x3b, 0x38, 0xb9, 0xba, 0x9c, 0x33, 0x4d, 0x0f, + 0xb2, 0x19, 0x2d, 0x14, 0xd3, 0xe4, 0xfd, 0xbb, 0x67, 0xcd, 0x8b, 0xab, 0x4b, 0x95, 0x15, 0x7c, 0xa1, 0x0f, 0xa0, + 0x49, 0x32, 0x97, 0xe3, 0x65, 0xce, 0xae, 0x4e, 0x4e, 0x6e, 0x6e, 0x6e, 0x92, 0xcf, 0xea, 0x7f, 0x7c, 0xa5, 0xc5, + 0xc1, 0x3f, 0x0a, 0xf2, 0x7a, 0xf4, 0x99, 0x65, 0x3a, 0x19, 0xb3, 0x09, 0x17, 0xec, 0x4d, 0x21, 0x17, 0xac, 0xd0, + 0x77, 0x3d, 0xc8, 0xfc, 0xb5, 0x20, 0x31, 0xc7, 0x1a, 0x33, 0x44, 0xae, 0xf4, 0x01, 0x17, 0x07, 0xbc, 0xff, 0x8f, + 0xc2, 0xa4, 0xac, 0x98, 0x58, 0xce, 0x59, 0x41, 0x47, 0x39, 0x4b, 0x0f, 0x5b, 0x38, 0x93, 0x62, 0xc2, 0xa7, 0xcb, + 0xf2, 0xfb, 0xa6, 0xe0, 0xda, 0xff, 0xfe, 0x4a, 0xf3, 0x25, 0x4b, 0xd9, 0x06, 0xa5, 0x7c, 0xa0, 0x87, 0x84, 0x99, + 0x96, 0xbf, 0x54, 0x0d, 0xc7, 0xbf, 0x9a, 0x26, 0xef, 0x16, 0x4c, 0x4e, 0x0e, 0xf4, 0x21, 0x89, 0xd4, 0xdd, 0x7c, + 0x24, 0xf3, 0xa8, 0xaf, 0x1b, 0x51, 0x94, 0x42, 0x19, 0xcc, 0x50, 0x2f, 0x93, 0x42, 0xe9, 0x03, 0xc1, 0xc9, 0x0d, + 0x17, 0x63, 0x79, 0x83, 0x6f, 0x04, 0x11, 0x3c, 0xb9, 0x9e, 0xd1, 0xb1, 0xbc, 0x79, 0x2b, 0xa5, 0x3e, 0x3a, 0x8a, + 0xdd, 0xf7, 0xdd, 0xe3, 0xeb, 0x6b, 0x42, 0xc8, 0x57, 0xc9, 0xc7, 0x07, 0xad, 0xf5, 0x3a, 0x48, 0x4d, 0x04, 0xd5, + 0xfc, 0x2b, 0xb3, 0x95, 0xd0, 0xd1, 0x51, 0x44, 0xc7, 0x72, 0xa1, 0xd9, 0xf8, 0x5a, 0xdf, 0xe5, 0xec, 0x7a, 0xc6, + 0x98, 0x56, 0x11, 0x17, 0x07, 0x4f, 0x64, 0xb6, 0x9c, 0x33, 0xa1, 0x93, 0x45, 0x21, 0xb5, 0x84, 0x81, 0x1d, 0x1d, + 0x45, 0x05, 0x5b, 0xe4, 0x34, 0x63, 0x90, 0xff, 0xf8, 0xfa, 0xba, 0xaa, 0x51, 0x15, 0xc2, 0x5f, 0x04, 0xb9, 0x36, + 0x43, 0x8f, 0x11, 0xfe, 0x4d, 0x10, 0xc1, 0x6e, 0x0e, 0x7e, 0x63, 0xf4, 0xcb, 0x2f, 0x74, 0xd1, 0xcb, 0x72, 0xaa, + 0xd4, 0xc1, 0x2b, 0xb9, 0x32, 0xd3, 0x28, 0x96, 0x99, 0x96, 0x45, 0xac, 0x31, 0xc3, 0x02, 0xad, 0xf8, 0x24, 0xd6, + 0x33, 0xae, 0x92, 0x4f, 0xf7, 0x32, 0xa5, 0xde, 0x32, 0xb5, 0xcc, 0xf5, 0x3d, 0x72, 0xd8, 0xc2, 0xe2, 0x90, 0x90, + 0x2f, 0x02, 0xe9, 0x59, 0x21, 0x6f, 0x0e, 0x9e, 0x16, 0x85, 0x2c, 0xe2, 0xe8, 0xf1, 0xf5, 0xb5, 0x2d, 0x71, 0xc0, + 0xd5, 0x81, 0x90, 0xfa, 0xa0, 0x6c, 0x0f, 0xa0, 0x9d, 0x1c, 0xbc, 0x57, 0xec, 0xe0, 0xcf, 0xa5, 0x50, 0x74, 0xc2, + 0x1e, 0x5f, 0x5f, 0xff, 0x79, 0x20, 0x8b, 0x83, 0x3f, 0x33, 0xa5, 0xfe, 0x3c, 0xe0, 0x42, 0x69, 0x46, 0xc7, 0x49, + 0x84, 0x7a, 0xa6, 0xb3, 0x4c, 0xa9, 0x77, 0xec, 0x56, 0x13, 0x8d, 0xcd, 0xa7, 0x26, 0x6c, 0x33, 0x65, 0xfa, 0x40, + 0x95, 0xf3, 0x8a, 0xd1, 0x2a, 0x67, 0xfa, 0x40, 0x13, 0x93, 0x2f, 0x1d, 0xfc, 0x99, 0xfd, 0xd4, 0x3d, 0x3e, 0x89, + 0x6f, 0xc4, 0xd1, 0x91, 0x2e, 0x01, 0x8d, 0x56, 0x6e, 0x85, 0x08, 0x3b, 0xf4, 0x69, 0x47, 0x47, 0x2c, 0xc9, 0x99, + 0x98, 0xea, 0x19, 0x21, 0xa4, 0xdd, 0x13, 0x47, 0x47, 0xb1, 0x26, 0xbf, 0x89, 0x64, 0xca, 0x74, 0xcc, 0x10, 0xc2, + 0x55, 0xed, 0xa3, 0xa3, 0xd8, 0x02, 0x41, 0x12, 0x6d, 0x00, 0x57, 0x83, 0x31, 0x4a, 0x1c, 0xf4, 0xaf, 0xef, 0x44, + 0x16, 0x87, 0xe3, 0x47, 0x58, 0x1c, 0x1d, 0xfd, 0x26, 0x12, 0x05, 0x2d, 0x62, 0x8d, 0xd0, 0xa6, 0x60, 0x7a, 0x59, + 0x88, 0x03, 0xbd, 0xd1, 0xf2, 0x5a, 0x17, 0x5c, 0x4c, 0x63, 0xb4, 0xf2, 0x69, 0x41, 0xc5, 0xcd, 0xc6, 0x0e, 0xf7, + 0xf7, 0x82, 0x70, 0x72, 0x05, 0x3d, 0xbe, 0x92, 0xb1, 0xc3, 0x41, 0x4e, 0x48, 0xa4, 0x4c, 0xdd, 0xa8, 0xcf, 0x53, + 0xde, 0x88, 0x22, 0x6c, 0x47, 0x89, 0xbf, 0x08, 0x84, 0x85, 0x06, 0xd4, 0x4d, 0x92, 0x44, 0x23, 0x72, 0xb5, 0xf2, + 0x60, 0xe1, 0xc1, 0x44, 0xfb, 0x7c, 0xd0, 0x1a, 0xa6, 0x3a, 0x29, 0xd8, 0x78, 0x99, 0xb1, 0x38, 0x16, 0x58, 0x61, + 0x89, 0xc8, 0x95, 0x68, 0xc4, 0x05, 0xb9, 0x82, 0xf5, 0x2e, 0xea, 0x8b, 0x4d, 0xc8, 0x61, 0x0b, 0xb9, 0x41, 0x16, + 0x7e, 0x84, 0x00, 0x62, 0x37, 0xa0, 0x82, 0x90, 0x48, 0x2c, 0xe7, 0x23, 0x56, 0x44, 0x65, 0xb1, 0x5e, 0x0d, 0x2f, + 0x96, 0x8a, 0x1d, 0x64, 0x4a, 0x1d, 0x4c, 0x96, 0x22, 0xd3, 0x5c, 0x8a, 0x83, 0xa8, 0x51, 0x34, 0x22, 0x8b, 0x0f, + 0x25, 0x3a, 0x44, 0x68, 0x83, 0x62, 0x85, 0x1a, 0x7c, 0x20, 0x1b, 0xed, 0x21, 0x86, 0x51, 0xa2, 0x9e, 0x6b, 0xcf, + 0x41, 0x80, 0x61, 0x0e, 0x93, 0xdc, 0xe0, 0x9f, 0xec, 0xce, 0x87, 0x29, 0xde, 0x88, 0x3e, 0x4f, 0x76, 0x77, 0x0a, + 0xd1, 0xc9, 0x9c, 0x2e, 0x62, 0x46, 0xae, 0x98, 0xc1, 0x2e, 0x2a, 0x32, 0x18, 0x6b, 0x6d, 0xe1, 0xfa, 0x2c, 0x65, + 0x49, 0x85, 0x53, 0x28, 0xd5, 0xc9, 0x44, 0x16, 0x4f, 0x69, 0x36, 0x83, 0x7a, 0x25, 0xc6, 0x8c, 0xfd, 0x86, 0xcb, + 0x0a, 0x46, 0x35, 0x7b, 0x9a, 0x33, 0xf8, 0x8a, 0x23, 0x53, 0x33, 0x42, 0x58, 0xc1, 0x56, 0xcf, 0xb9, 0x7e, 0x25, + 0x45, 0xc6, 0x7a, 0x2a, 0xc0, 0x2f, 0xb3, 0xf2, 0x0f, 0xb5, 0x2e, 0xf8, 0x68, 0xa9, 0x59, 0x1c, 0x09, 0x28, 0x11, + 0x61, 0x85, 0xb0, 0x48, 0x34, 0xbb, 0xd5, 0x8f, 0xa5, 0xd0, 0x4c, 0x68, 0xc2, 0x3c, 0x54, 0x31, 0x4f, 0xe8, 0x62, + 0xc1, 0xc4, 0xf8, 0xf1, 0x8c, 0xe7, 0xe3, 0x58, 0xa0, 0x0d, 0xda, 0xe0, 0x8f, 0x82, 0xc0, 0x24, 0xc9, 0x15, 0x4f, + 0xe1, 0x9f, 0x6f, 0x4f, 0x27, 0xd6, 0xe4, 0xca, 0x6c, 0x0b, 0x46, 0xa2, 0xa8, 0x37, 0x91, 0x45, 0xec, 0xa6, 0x70, + 0x00, 0xa4, 0x0b, 0xfa, 0x78, 0xbb, 0xcc, 0x99, 0x42, 0xac, 0x41, 0x44, 0xb9, 0x8e, 0x0e, 0xc2, 0xbf, 0x17, 0x31, + 0x83, 0x05, 0xe0, 0x28, 0xe5, 0x86, 0x04, 0xbe, 0xe4, 0x6e, 0x53, 0x8d, 0x4b, 0xa2, 0xf6, 0x97, 0x20, 0x63, 0x9e, + 0xe8, 0x62, 0xa9, 0x34, 0x1b, 0xbf, 0xbb, 0x5b, 0x30, 0x85, 0x35, 0x25, 0x7f, 0x89, 0xfe, 0x5f, 0x22, 0x61, 0xf3, + 0x85, 0xbe, 0xbb, 0x36, 0xd4, 0x3c, 0x8d, 0x22, 0xfc, 0x4f, 0x53, 0xb4, 0x60, 0x34, 0x03, 0x92, 0xe6, 0x40, 0xf6, + 0x46, 0xe6, 0x77, 0x13, 0x9e, 0xe7, 0xd7, 0xcb, 0xc5, 0x42, 0x16, 0x1a, 0x6b, 0x41, 0x56, 0x5a, 0x56, 0xf0, 0x81, + 0x15, 0x5d, 0xa9, 0x1b, 0xae, 0xb3, 0x59, 0xac, 0xd1, 0x2a, 0xa3, 0x8a, 0x1d, 0x3c, 0x92, 0x32, 0x67, 0x54, 0xa4, + 0x9c, 0xf0, 0xbe, 0xa6, 0xa9, 0x58, 0xe6, 0x79, 0x6f, 0x54, 0x30, 0xfa, 0xa5, 0x67, 0xb2, 0xed, 0xe1, 0x90, 0x9a, + 0xdf, 0x0f, 0x8b, 0x82, 0xde, 0x41, 0x41, 0x42, 0xa0, 0x58, 0x9f, 0xa7, 0x3f, 0x5d, 0xbf, 0x7e, 0x95, 0xd8, 0xbd, + 0xc2, 0x27, 0x77, 0x31, 0x2f, 0xf7, 0x1f, 0xdf, 0xe0, 0x49, 0x21, 0xe7, 0x5b, 0x5d, 0x5b, 0xd0, 0xf1, 0xde, 0x37, + 0x86, 0xc0, 0x08, 0x3f, 0xb4, 0x4d, 0x87, 0x23, 0x78, 0x65, 0x30, 0x1f, 0x32, 0x89, 0xeb, 0x17, 0xfe, 0x49, 0x6d, + 0x72, 0xcc, 0xd1, 0xf7, 0x47, 0xab, 0x8b, 0xbb, 0x15, 0x23, 0x66, 0x9c, 0x0b, 0x38, 0x18, 0x61, 0x8c, 0x19, 0xd5, + 0xd9, 0x6c, 0xc5, 0x4c, 0x63, 0x1b, 0x3f, 0x62, 0xb6, 0xd9, 0xe0, 0xbf, 0xa5, 0xc7, 0x7a, 0x7d, 0x48, 0x08, 0x37, + 0xf4, 0x8a, 0xe8, 0xf5, 0x9a, 0x13, 0xc2, 0x11, 0x7e, 0xcb, 0xc9, 0x8a, 0xfa, 0x09, 0xc1, 0xc9, 0x06, 0xdb, 0x33, + 0xb5, 0x54, 0x06, 0x4e, 0xc0, 0xaf, 0xac, 0xd0, 0xac, 0x48, 0xb5, 0xc0, 0x05, 0x9b, 0xe4, 0x30, 0x8e, 0xc3, 0x36, + 0x9e, 0x51, 0xf5, 0x78, 0x46, 0xc5, 0x94, 0x8d, 0xd3, 0xbf, 0xe5, 0x06, 0x33, 0x41, 0xa2, 0x09, 0x17, 0x34, 0xe7, + 0x7f, 0xb3, 0x71, 0xe4, 0xce, 0x85, 0x0f, 0xfa, 0x80, 0xdd, 0x6a, 0x26, 0xc6, 0xea, 0xe0, 0xf9, 0xbb, 0x5f, 0x5e, + 0xba, 0xc5, 0xac, 0x9d, 0x15, 0x68, 0xa5, 0x96, 0x0b, 0x56, 0xc4, 0x08, 0xbb, 0xb3, 0xe2, 0x29, 0x37, 0x74, 0xf2, + 0x17, 0xba, 0xb0, 0x29, 0x5c, 0xbd, 0x5f, 0x8c, 0xa9, 0x66, 0x6f, 0x98, 0x18, 0x73, 0x31, 0x25, 0x87, 0x6d, 0x9b, + 0x3e, 0xa3, 0x2e, 0x63, 0x5c, 0x26, 0x7d, 0xba, 0xf7, 0x34, 0x37, 0x73, 0x2f, 0x3f, 0x97, 0x31, 0xda, 0x28, 0x4d, + 0x35, 0xcf, 0x0e, 0xe8, 0x78, 0xfc, 0x42, 0x70, 0xcd, 0xcd, 0x08, 0x0b, 0x58, 0x22, 0xc0, 0x55, 0x66, 0x4f, 0x0d, + 0x3f, 0xf2, 0x18, 0xe1, 0x38, 0x76, 0x67, 0xc1, 0x0c, 0xb9, 0x35, 0x3b, 0x3a, 0xaa, 0x28, 0x7f, 0x9f, 0xa5, 0x36, + 0x93, 0x0c, 0x86, 0x28, 0x59, 0x2c, 0x15, 0x2c, 0xb6, 0xef, 0x02, 0x0e, 0x1a, 0x39, 0x52, 0xac, 0xf8, 0xca, 0xc6, + 0x25, 0x82, 0xa8, 0x18, 0xad, 0xb6, 0xfa, 0x70, 0xdb, 0x43, 0x93, 0xc1, 0xb0, 0x17, 0x92, 0x70, 0xe6, 0x90, 0xdd, + 0x72, 0x2a, 0x9c, 0xa9, 0x92, 0xa8, 0xc4, 0x70, 0xa0, 0x96, 0x84, 0x45, 0x11, 0x3f, 0xbf, 0x45, 0x2c, 0x80, 0x87, + 0x08, 0x29, 0x87, 0x3f, 0x73, 0x9f, 0x7e, 0x35, 0x87, 0x87, 0xc2, 0x02, 0x61, 0x6d, 0x47, 0xaa, 0x10, 0xda, 0x20, + 0xac, 0xfd, 0x70, 0x2d, 0x51, 0xf2, 0x7c, 0x11, 0x9c, 0xda, 0xe4, 0x2d, 0x37, 0xc7, 0x36, 0xd0, 0x36, 0xaa, 0xd9, + 0xd1, 0x51, 0xcc, 0x92, 0x12, 0x31, 0xc8, 0x61, 0xdb, 0x2d, 0x52, 0x00, 0xad, 0x6f, 0x8c, 0x1b, 0x7a, 0x36, 0x0c, + 0xce, 0x21, 0x4b, 0x84, 0x7c, 0x98, 0x65, 0x4c, 0x29, 0x59, 0x1c, 0x1d, 0x1d, 0x9a, 0xf2, 0x25, 0x67, 0x01, 0x8b, + 0xf8, 0xfa, 0x46, 0x54, 0x43, 0x40, 0xd5, 0x69, 0xeb, 0xf9, 0x26, 0x52, 0xf1, 0x4d, 0x9e, 0x09, 0x49, 0xa3, 0x4f, + 0x9f, 0xa2, 0x86, 0xc6, 0x0e, 0x0e, 0x53, 0xe6, 0xbb, 0xbe, 0x7b, 0xc2, 0x2c, 0x5b, 0x68, 0x98, 0x90, 0x1d, 0xd0, + 0xec, 0xe5, 0x07, 0xe3, 0xfa, 0x90, 0xb0, 0xc6, 0x0a, 0x6d, 0x82, 0x15, 0xdd, 0xdb, 0xb4, 0xe1, 0x6f, 0xec, 0xd2, + 0xad, 0xa6, 0x86, 0xa7, 0x08, 0xd6, 0x71, 0xc0, 0x86, 0x1b, 0x6c, 0x60, 0xef, 0x67, 0x23, 0xcd, 0x40, 0x07, 0x7a, + 0xd8, 0x73, 0xf9, 0x44, 0x59, 0xc8, 0x15, 0xec, 0xaf, 0x25, 0x53, 0xda, 0x22, 0x72, 0xac, 0xb1, 0xc4, 0x70, 0x46, + 0x6d, 0x33, 0x9d, 0x35, 0x96, 0x74, 0xdf, 0xd8, 0x5e, 0x2f, 0xe0, 0x6c, 0x54, 0x80, 0xd4, 0xdf, 0xc7, 0x27, 0x18, + 0xab, 0x46, 0xeb, 0xf5, 0x5b, 0xee, 0x5b, 0xa9, 0xd6, 0xb2, 0xe4, 0xd7, 0xb6, 0x16, 0x85, 0x09, 0xe4, 0x0e, 0xe7, + 0xc3, 0xb6, 0x1b, 0xbf, 0x18, 0x92, 0xc3, 0x56, 0x89, 0xc5, 0x0e, 0xac, 0x76, 0x3c, 0x16, 0x8a, 0xaf, 0x6d, 0x53, + 0xc8, 0x9c, 0xf5, 0x35, 0x7c, 0x49, 0x66, 0x3b, 0xb8, 0x3a, 0x23, 0x03, 0xe0, 0x3a, 0x92, 0xd9, 0xf0, 0x5b, 0xf8, + 0xe4, 0x29, 0x42, 0xac, 0x77, 0xf3, 0x2a, 0xc2, 0xf1, 0xb5, 0x4e, 0x38, 0xb6, 0xa6, 0x11, 0x2d, 0xca, 0x2a, 0x51, + 0x89, 0x66, 0x6e, 0xab, 0x57, 0x59, 0x58, 0x98, 0xc1, 0x54, 0x53, 0x0a, 0x9a, 0x78, 0x45, 0xe7, 0x4c, 0xc5, 0x0c, + 0xe1, 0x6f, 0x15, 0xb0, 0xf8, 0x09, 0x45, 0x86, 0xc1, 0x19, 0xaa, 0xe0, 0x0c, 0x05, 0x76, 0x17, 0x98, 0xb4, 0xfa, + 0x96, 0x53, 0x98, 0x0d, 0xd4, 0xb0, 0xe2, 0xed, 0x82, 0xc9, 0x9b, 0xc3, 0xd9, 0x21, 0xb8, 0x87, 0x9f, 0x4d, 0xb3, + 0x40, 0x33, 0x2c, 0x84, 0x42, 0xf8, 0xb0, 0xb5, 0xbd, 0x92, 0xbe, 0x54, 0x35, 0xc7, 0xc1, 0x10, 0xd6, 0xc1, 0x1c, + 0x1b, 0x09, 0x57, 0xe6, 0x6f, 0x6d, 0xab, 0x01, 0xd8, 0xae, 0x01, 0x33, 0x92, 0x49, 0x4e, 0x75, 0xdc, 0x3e, 0x69, + 0x01, 0x63, 0xfa, 0x95, 0xc1, 0xa9, 0x82, 0xd0, 0xee, 0x54, 0x58, 0xb2, 0x14, 0x6a, 0xc6, 0x27, 0x3a, 0xfe, 0x28, + 0x0c, 0x51, 0x61, 0xb9, 0x62, 0x20, 0xe1, 0x04, 0xec, 0xb1, 0x21, 0x38, 0x1f, 0x05, 0xf4, 0xd3, 0x2b, 0x0f, 0x22, + 0x37, 0x52, 0x43, 0xb8, 0x80, 0x3c, 0x54, 0xac, 0x75, 0x45, 0x66, 0x4a, 0xc6, 0x0d, 0xb8, 0xc7, 0x76, 0xdf, 0xb6, + 0x98, 0x3a, 0x6a, 0x20, 0x02, 0x0e, 0x56, 0xa4, 0x21, 0x89, 0x70, 0x89, 0x3a, 0xd1, 0xf2, 0xa5, 0xbc, 0x61, 0xc5, + 0x63, 0x0a, 0x83, 0x4f, 0x6d, 0xf5, 0x8d, 0x3d, 0x0a, 0x0c, 0xc5, 0xd7, 0x3d, 0x8f, 0x2f, 0x9f, 0xcc, 0xc4, 0xdf, + 0x14, 0x72, 0xce, 0x15, 0x03, 0xbe, 0xcd, 0xc2, 0x5f, 0xc0, 0x46, 0x33, 0x3b, 0x12, 0x8e, 0x1b, 0x56, 0xe2, 0xd7, + 0xc3, 0x97, 0x75, 0xfc, 0xfa, 0x74, 0xef, 0xe9, 0xd4, 0x53, 0xc0, 0xfa, 0x3e, 0x46, 0x38, 0x76, 0xe2, 0x45, 0x70, + 0xd2, 0x25, 0x33, 0xe4, 0x8e, 0xf9, 0xf5, 0x5a, 0x07, 0x62, 0x5c, 0x8d, 0x73, 0x64, 0x76, 0xdb, 0xa0, 0x0d, 0x1d, + 0x8f, 0x81, 0xc5, 0x2b, 0x64, 0x9e, 0x07, 0x87, 0x15, 0x16, 0xbd, 0xf2, 0x78, 0xfa, 0x74, 0xef, 0xe9, 0xf5, 0xf7, + 0x4e, 0x28, 0xc8, 0x0f, 0x0f, 0x29, 0x3f, 0x50, 0x31, 0x66, 0x05, 0xc8, 0x95, 0xc1, 0x6a, 0xb9, 0x73, 0xf6, 0xb1, + 0x14, 0x82, 0x65, 0x9a, 0x8d, 0x41, 0x68, 0x11, 0x44, 0x27, 0x33, 0xa9, 0x74, 0x99, 0x58, 0x8d, 0x5e, 0x84, 0x42, + 0x68, 0x92, 0xd1, 0x3c, 0x8f, 0xad, 0x80, 0x32, 0x97, 0x5f, 0xd9, 0x9e, 0x51, 0xf7, 0x6a, 0x43, 0x2e, 0x9b, 0x61, + 0x41, 0x33, 0x2c, 0x51, 0x8b, 0x9c, 0x67, 0xac, 0x3c, 0xbc, 0xae, 0x13, 0x2e, 0xc6, 0xec, 0x16, 0xe8, 0x08, 0xba, + 0xba, 0xba, 0x6a, 0xe1, 0x36, 0xda, 0x58, 0x80, 0xaf, 0x76, 0x00, 0xfb, 0x9d, 0x63, 0xd3, 0x0a, 0xe2, 0xab, 0xbd, + 0x64, 0x0d, 0x05, 0x67, 0x25, 0xf7, 0x82, 0x96, 0x25, 0xcf, 0x08, 0x8f, 0x59, 0xce, 0x34, 0xf3, 0xe4, 0x1c, 0x98, + 0x69, 0xbb, 0x75, 0xdf, 0x96, 0xf0, 0x2b, 0xd1, 0xc9, 0xef, 0x32, 0xbf, 0xe6, 0xaa, 0x14, 0xdd, 0xab, 0xe5, 0xa9, + 0xa0, 0xdd, 0xd7, 0x76, 0x79, 0xa8, 0xd6, 0x34, 0x9b, 0x59, 0x89, 0x3d, 0xde, 0x99, 0x52, 0xd5, 0x86, 0x23, 0xed, + 0xe5, 0x26, 0xfa, 0xa9, 0x70, 0xc3, 0xdc, 0x07, 0x82, 0x6b, 0x47, 0x14, 0x18, 0x08, 0x81, 0x76, 0xd9, 0x1e, 0xd3, + 0x3c, 0x1f, 0xd1, 0xec, 0x4b, 0x1d, 0xfb, 0x2b, 0x34, 0x20, 0xdb, 0xd4, 0x38, 0xc8, 0x0a, 0x48, 0x56, 0x38, 0x6f, + 0x4f, 0xa5, 0x6b, 0x1b, 0x25, 0x3e, 0x6c, 0x55, 0x68, 0x5f, 0x5f, 0xe8, 0x6f, 0x62, 0xbb, 0x19, 0x91, 0x70, 0x33, + 0x8b, 0x81, 0x0a, 0xfc, 0x4b, 0x8c, 0xf3, 0xf4, 0xc0, 0xe1, 0x1d, 0x08, 0x1e, 0x9b, 0xad, 0x81, 0x68, 0xb4, 0xda, + 0x8c, 0xb9, 0xfa, 0x36, 0x04, 0xfe, 0xb7, 0x8c, 0xf2, 0x49, 0xd0, 0xc3, 0xbf, 0x3b, 0xd0, 0x92, 0xc6, 0x39, 0xc6, + 0xb9, 0x1c, 0x99, 0x63, 0x28, 0x3c, 0xa1, 0xf9, 0x19, 0x98, 0x17, 0x83, 0xef, 0xaf, 0x6d, 0x96, 0xe1, 0xcb, 0x60, + 0x18, 0xaa, 0x17, 0x32, 0x14, 0x35, 0x14, 0x70, 0x44, 0x55, 0x98, 0x33, 0x57, 0xd6, 0x44, 0x49, 0xc7, 0xb5, 0x5b, + 0x71, 0xdc, 0xd1, 0xdc, 0x82, 0xc4, 0x71, 0xac, 0x40, 0x9a, 0xf3, 0xfc, 0x7d, 0x35, 0x0b, 0xb5, 0x33, 0x0b, 0x95, + 0x04, 0xd2, 0x16, 0xaa, 0x90, 0x39, 0xa8, 0x9e, 0x6a, 0x81, 0xc2, 0x52, 0xc0, 0xb2, 0x26, 0x40, 0xa1, 0x51, 0x49, + 0x70, 0x73, 0xa2, 0x71, 0xe1, 0x44, 0x1d, 0x87, 0x6b, 0x40, 0x32, 0xaa, 0x2a, 0x12, 0xd9, 0xcd, 0x51, 0x93, 0x7d, + 0x25, 0x2e, 0xd0, 0x16, 0x7f, 0xbf, 0xd9, 0x38, 0x28, 0x31, 0xe4, 0x56, 0xa7, 0xc6, 0x18, 0x07, 0x60, 0xc1, 0x92, + 0x38, 0x66, 0xd8, 0xb2, 0x3e, 0xdb, 0xc0, 0x29, 0xdb, 0x3d, 0x24, 0x44, 0x56, 0xb0, 0xa9, 0x31, 0x95, 0x9e, 0xbb, + 0x92, 0x08, 0x53, 0xcf, 0x96, 0x16, 0xd5, 0xc4, 0x09, 0x89, 0xbc, 0x76, 0x22, 0xea, 0xaf, 0x6a, 0xc2, 0x61, 0x1a, + 0x14, 0xdb, 0xa4, 0x40, 0x54, 0x8b, 0x7d, 0xf0, 0xde, 0x87, 0x35, 0xb5, 0x76, 0x02, 0x88, 0x17, 0x35, 0x88, 0x07, + 0xa0, 0x95, 0x96, 0x78, 0xc9, 0x21, 0xa1, 0xf5, 0xca, 0x31, 0xc3, 0x85, 0x5d, 0x88, 0x1d, 0x28, 0x6e, 0xb3, 0x9f, + 0x06, 0x0b, 0x41, 0x96, 0x55, 0xc0, 0xdf, 0x85, 0x47, 0x44, 0x0c, 0x83, 0x17, 0xeb, 0xf5, 0x0e, 0xda, 0xed, 0xe5, + 0x42, 0x51, 0x52, 0x49, 0x87, 0xeb, 0xf5, 0xdf, 0x12, 0xc5, 0x8e, 0xff, 0xc5, 0x0c, 0xf5, 0x3d, 0xd1, 0x7d, 0xf8, + 0x12, 0x4a, 0x19, 0x76, 0xb4, 0x4a, 0x29, 0x05, 0x87, 0x3a, 0xd6, 0xd6, 0x17, 0x4a, 0x07, 0x94, 0xfb, 0xf1, 0x0e, + 0x01, 0x33, 0x89, 0xee, 0xa4, 0xae, 0xa6, 0xfc, 0xd8, 0x35, 0x2d, 0x10, 0x42, 0xa9, 0x32, 0xb2, 0xcc, 0xe1, 0x3e, + 0xf9, 0xf2, 0xe8, 0x48, 0x05, 0x0d, 0x7d, 0x2a, 0x29, 0xc5, 0xe7, 0x18, 0x4e, 0x65, 0x75, 0x27, 0x0c, 0xfb, 0xf2, + 0xd9, 0x9f, 0x43, 0x3b, 0xd2, 0x69, 0xab, 0x07, 0x82, 0x39, 0xbd, 0xa1, 0x5c, 0x1f, 0x94, 0xad, 0x58, 0xc1, 0x3c, + 0x66, 0x68, 0xe5, 0xb8, 0x8d, 0xa4, 0x60, 0xc0, 0x3f, 0x02, 0x59, 0xf0, 0x5c, 0xb4, 0x45, 0xfc, 0x6c, 0xc6, 0x40, + 0x95, 0xed, 0x19, 0x89, 0x52, 0x3c, 0x3c, 0x74, 0x07, 0x89, 0x6b, 0x78, 0xff, 0xd8, 0x37, 0xdb, 0xd5, 0x6b, 0xd2, + 0xc0, 0x82, 0x15, 0x13, 0x59, 0xcc, 0x7d, 0xde, 0x66, 0xeb, 0xdb, 0x11, 0x47, 0x3e, 0x89, 0xf7, 0xb6, 0xed, 0x44, + 0x80, 0xde, 0x96, 0xec, 0x5d, 0x49, 0xed, 0xb5, 0xd3, 0xb4, 0x3c, 0x80, 0xad, 0x82, 0xd0, 0x63, 0xa6, 0x0a, 0xa5, + 0x7c, 0xa7, 0x5e, 0xed, 0x59, 0xdd, 0xc9, 0x61, 0xbb, 0x57, 0x4a, 0x7e, 0x1e, 0x1b, 0x7a, 0x56, 0xc7, 0xe1, 0x4e, + 0x55, 0xb9, 0xcc, 0xc7, 0x6e, 0xb0, 0x02, 0x61, 0xe6, 0xf0, 0xe8, 0x86, 0xe7, 0x79, 0x95, 0xfa, 0x9f, 0x90, 0x76, + 0xe5, 0x48, 0xbb, 0xf4, 0xa4, 0x1d, 0x48, 0x05, 0x90, 0x76, 0xdb, 0x5c, 0x55, 0x5d, 0xee, 0x6c, 0x4f, 0x69, 0x89, + 0xba, 0x32, 0xe2, 0x34, 0xf4, 0xb7, 0xf4, 0x23, 0x40, 0x25, 0xf3, 0xf5, 0x25, 0x76, 0xfa, 0x18, 0x10, 0x03, 0xad, + 0x4e, 0x93, 0x85, 0x9a, 0x8a, 0x2f, 0x31, 0xc2, 0x6a, 0xc3, 0x4a, 0xcc, 0x7e, 0xf8, 0x14, 0x94, 0x76, 0xc1, 0x74, + 0xe0, 0x1c, 0x33, 0xc9, 0xff, 0x11, 0x1f, 0xe5, 0x67, 0x27, 0xdc, 0xec, 0x94, 0x9f, 0x1d, 0xd0, 0xfa, 0x6a, 0x76, + 0xe3, 0xef, 0x53, 0x7b, 0x33, 0x3d, 0x51, 0x4e, 0xaf, 0x5a, 0xef, 0xf5, 0x3a, 0xde, 0x4a, 0x01, 0x8d, 0xbe, 0x93, + 0x52, 0x8a, 0xb2, 0x75, 0xa0, 0x01, 0x21, 0x64, 0x20, 0x61, 0x63, 0x27, 0x5d, 0x9e, 0x72, 0x2f, 0xff, 0x95, 0x9e, + 0xc7, 0x28, 0xee, 0x6d, 0xfd, 0xc7, 0x72, 0xbe, 0x00, 0x86, 0x6c, 0x0b, 0xa5, 0xa7, 0xcc, 0x75, 0x58, 0xe5, 0x6f, + 0xf6, 0xa4, 0xd5, 0xea, 0x98, 0xfd, 0x58, 0xc3, 0xa6, 0x52, 0x6a, 0x3e, 0x6c, 0x6d, 0x96, 0x65, 0x52, 0x49, 0x38, + 0xf6, 0xe9, 0x56, 0x1e, 0x6f, 0x6b, 0x66, 0x7c, 0xc6, 0xeb, 0x58, 0x58, 0x3a, 0x2c, 0x80, 0xd6, 0x05, 0xe4, 0xc7, + 0xa3, 0x7b, 0xb8, 0xfe, 0x9b, 0x0a, 0x38, 0xab, 0xcd, 0x16, 0xf8, 0x56, 0x9b, 0xcd, 0x07, 0xed, 0x24, 0x6d, 0xfc, + 0x61, 0x8f, 0xdc, 0x5b, 0x42, 0xaf, 0xca, 0x74, 0x32, 0xe3, 0x60, 0x08, 0x69, 0x3b, 0x2c, 0x24, 0x59, 0xcd, 0xe5, + 0x98, 0xa5, 0x91, 0x5c, 0x30, 0x11, 0x6d, 0x40, 0xcf, 0xea, 0x10, 0xe0, 0x9f, 0x22, 0x5e, 0xbd, 0xad, 0xeb, 0x5b, + 0xd3, 0x0f, 0x7a, 0x03, 0xaa, 0xb0, 0x97, 0x7c, 0x8f, 0x32, 0xf6, 0x03, 0x2b, 0x94, 0xe1, 0x49, 0x4b, 0xf6, 0xf6, + 0x25, 0xaf, 0x0e, 0xa8, 0x97, 0x3c, 0xfd, 0x76, 0x95, 0x4a, 0x20, 0x89, 0xda, 0xc9, 0x79, 0x72, 0x1a, 0x21, 0xa3, + 0x31, 0x7e, 0xe6, 0x35, 0xc6, 0xcb, 0x52, 0x63, 0xfc, 0x5c, 0x93, 0xe5, 0x96, 0xc6, 0xf8, 0x67, 0x41, 0x9e, 0xeb, + 0xfe, 0x73, 0xaf, 0x4d, 0x7f, 0x23, 0x73, 0x9e, 0xdd, 0xc5, 0x51, 0xce, 0x75, 0x13, 0x6e, 0x13, 0x23, 0xbc, 0xb2, + 0x19, 0xa0, 0x6a, 0x34, 0xfa, 0xee, 0x8d, 0x97, 0xff, 0xb0, 0x10, 0x24, 0xba, 0x97, 0x73, 0x7d, 0x2f, 0xc2, 0x33, + 0x4d, 0xfe, 0x84, 0x5f, 0xf7, 0x56, 0xf1, 0x2f, 0x54, 0xcf, 0x92, 0x82, 0x8a, 0xb1, 0x9c, 0xc7, 0xa8, 0x11, 0x45, + 0x28, 0x51, 0x46, 0x08, 0x79, 0x80, 0x36, 0xf7, 0xfe, 0xc4, 0x9f, 0x25, 0x89, 0xfa, 0x51, 0x63, 0xa6, 0x31, 0xa3, + 0xe4, 0xcf, 0xcb, 0x7b, 0xab, 0xcf, 0x72, 0x73, 0xf5, 0x27, 0x7e, 0xaa, 0x4b, 0xb5, 0x3e, 0xbe, 0x65, 0x24, 0x46, + 0xe4, 0xea, 0xa9, 0x1f, 0xd2, 0x63, 0x39, 0xb7, 0x0a, 0xfe, 0x08, 0xe1, 0xaf, 0xa0, 0xd7, 0xbd, 0xe2, 0x15, 0x11, + 0x72, 0x77, 0x30, 0x87, 0x24, 0x92, 0x46, 0x79, 0x10, 0x1d, 0x1d, 0x05, 0x69, 0x25, 0x0b, 0x81, 0x1f, 0x49, 0x52, + 0x13, 0xd5, 0x31, 0xa7, 0xd0, 0xd2, 0x23, 0x19, 0x73, 0xe4, 0x9b, 0x89, 0xbd, 0xa6, 0xda, 0xed, 0x58, 0x3e, 0xb0, + 0xba, 0x87, 0x84, 0x6b, 0x56, 0x50, 0x2d, 0x8b, 0x21, 0x0a, 0xd9, 0x12, 0xfc, 0x8a, 0x93, 0x3f, 0x07, 0x07, 0xff, + 0xcf, 0xff, 0xf8, 0x63, 0xf2, 0x47, 0x31, 0xfc, 0x13, 0x0b, 0x46, 0x4e, 0x2e, 0xe3, 0x7e, 0x1a, 0x1f, 0x36, 0x9b, + 0xeb, 0x3f, 0x4e, 0x06, 0xff, 0x4d, 0x9b, 0x7f, 0x3f, 0x6c, 0xfe, 0x3e, 0x44, 0xeb, 0xf8, 0x8f, 0x93, 0xfe, 0xc0, + 0x7d, 0x0d, 0xfe, 0xfb, 0xea, 0x0f, 0x35, 0x3c, 0xb6, 0x89, 0xf7, 0x10, 0x3a, 0x99, 0xe2, 0x7f, 0x08, 0x72, 0xd2, + 0x6c, 0x5e, 0x9d, 0x4c, 0xf1, 0xaf, 0x82, 0x9c, 0xc0, 0xdf, 0x3b, 0x4d, 0xde, 0xb2, 0xe9, 0xd3, 0xdb, 0x45, 0xfc, + 0xe7, 0xd5, 0xfa, 0xde, 0xea, 0x15, 0xdf, 0x40, 0xbb, 0x83, 0xff, 0xfe, 0xe3, 0x0f, 0x15, 0xfd, 0x78, 0x45, 0x4e, + 0x86, 0x0d, 0x14, 0x9b, 0xe4, 0x63, 0x62, 0xff, 0xc4, 0xfd, 0x74, 0xf0, 0xdf, 0x6e, 0x28, 0xd1, 0x8f, 0x7f, 0xfc, + 0x79, 0x79, 0x45, 0x86, 0xeb, 0x38, 0x5a, 0xff, 0x88, 0xd6, 0x08, 0xad, 0xef, 0xa1, 0x3f, 0x71, 0x34, 0x8d, 0x10, + 0xfe, 0x5d, 0x90, 0x93, 0x1f, 0x4f, 0xa6, 0xf8, 0x27, 0x41, 0x4e, 0xa2, 0x93, 0x29, 0xfe, 0x20, 0xc9, 0xc9, 0x7f, + 0xc7, 0xfd, 0xd4, 0x2a, 0xe1, 0xd6, 0x46, 0xfd, 0xb1, 0x86, 0x9b, 0x10, 0x5a, 0x30, 0xba, 0xd6, 0x5c, 0xe7, 0x0c, + 0xdd, 0x3b, 0xe1, 0xf8, 0xb9, 0x04, 0x60, 0xc5, 0x1a, 0x94, 0x34, 0xe6, 0x12, 0x76, 0xf5, 0x09, 0x16, 0x1e, 0x30, + 0xe8, 0x5e, 0xca, 0xb1, 0xd5, 0x13, 0xa8, 0x54, 0xdb, 0xdb, 0x5b, 0x05, 0xd7, 0xb7, 0xf8, 0x31, 0x79, 0x2e, 0xe3, + 0x36, 0xc2, 0x82, 0xc2, 0x8f, 0x0e, 0xc2, 0xef, 0xb5, 0xbb, 0xf0, 0x84, 0x6d, 0x6e, 0x31, 0x4c, 0x48, 0xcb, 0xcf, + 0x44, 0x08, 0xbf, 0xdc, 0x93, 0xa9, 0x67, 0xa0, 0x7e, 0x40, 0x58, 0xab, 0xf0, 0x7a, 0x14, 0x3f, 0xd6, 0xa4, 0x44, + 0x8e, 0x77, 0x05, 0x63, 0xbf, 0xd1, 0xfc, 0x0b, 0x2b, 0xe2, 0xa7, 0x1a, 0xb7, 0x3b, 0x0f, 0xb0, 0x51, 0x55, 0x1f, + 0xb6, 0x51, 0xaf, 0xbc, 0xdd, 0x7a, 0x2f, 0xed, 0x7d, 0x02, 0x9c, 0xc2, 0x75, 0x7d, 0x0d, 0xac, 0xfd, 0x21, 0xdf, + 0x51, 0x6a, 0x15, 0xf4, 0x26, 0x42, 0xf5, 0xab, 0x54, 0x2e, 0xbe, 0xd2, 0x9c, 0x8f, 0x0f, 0x34, 0x9b, 0x2f, 0x72, + 0xaa, 0xd9, 0x81, 0x9b, 0xf3, 0x01, 0x85, 0x86, 0xa2, 0x92, 0xa7, 0xf8, 0x59, 0x54, 0x9b, 0xf6, 0x67, 0x91, 0x54, + 0x7b, 0x27, 0x86, 0xfb, 0x2c, 0xc7, 0x97, 0x28, 0x5a, 0x5e, 0x97, 0x6d, 0xdf, 0x08, 0x36, 0xdb, 0xa0, 0x2c, 0x1b, + 0x9a, 0xf3, 0x5b, 0x61, 0xb8, 0xdf, 0x24, 0xa4, 0xd3, 0x8f, 0x2e, 0xd5, 0xd7, 0xe9, 0x55, 0x04, 0x37, 0x39, 0x05, + 0x11, 0xcc, 0x28, 0x8f, 0xa0, 0x04, 0x25, 0xad, 0x1e, 0xbd, 0x64, 0x3d, 0xda, 0x68, 0x78, 0x36, 0x3b, 0x23, 0x7c, + 0x40, 0x6d, 0xfd, 0x1c, 0xcf, 0xf0, 0x98, 0x34, 0xdb, 0x78, 0x49, 0x5a, 0xa6, 0x4a, 0x6f, 0x79, 0x99, 0xb9, 0x7e, + 0x8e, 0x8e, 0xe2, 0x22, 0xc9, 0xa9, 0xd2, 0x2f, 0x40, 0x23, 0x40, 0x96, 0x78, 0x46, 0x8a, 0x84, 0xdd, 0xb2, 0x2c, + 0xce, 0x10, 0x9e, 0x39, 0x1a, 0x84, 0x7a, 0x68, 0x49, 0x82, 0x62, 0x20, 0x67, 0x10, 0xc1, 0xfa, 0xb3, 0x41, 0x7b, + 0x48, 0x08, 0x89, 0x0e, 0x9b, 0xcd, 0xa8, 0x5f, 0x90, 0x7f, 0x88, 0x14, 0x52, 0x02, 0x76, 0x9a, 0xfc, 0x0a, 0x49, + 0x9d, 0x20, 0x29, 0xfe, 0x20, 0x13, 0xcd, 0x94, 0x8e, 0x21, 0x19, 0x94, 0x04, 0xca, 0x63, 0x78, 0x74, 0x79, 0x12, + 0x35, 0x20, 0xd5, 0xa0, 0x28, 0xc2, 0x05, 0xb9, 0xd3, 0x28, 0x9d, 0x0d, 0x4e, 0x87, 0xe1, 0x19, 0x61, 0x53, 0xa1, + 0xff, 0x3b, 0xdd, 0x9f, 0x0d, 0x5a, 0xa6, 0xff, 0xab, 0xa8, 0x1f, 0x17, 0x44, 0x59, 0x36, 0xae, 0xaf, 0x52, 0xc1, + 0xcc, 0x7c, 0x51, 0xea, 0x06, 0xe8, 0xfa, 0x1e, 0x93, 0x66, 0x27, 0x8d, 0xc7, 0xe1, 0x4c, 0x9a, 0xd0, 0xa1, 0x03, + 0x05, 0xce, 0x09, 0x94, 0xc7, 0x05, 0x81, 0x4e, 0xab, 0x6a, 0x77, 0x3a, 0x75, 0x09, 0x3f, 0x46, 0x3f, 0xf6, 0x7f, + 0x12, 0xe9, 0xef, 0xc2, 0x8e, 0xe0, 0x27, 0xb1, 0x5e, 0xc3, 0xdf, 0xdf, 0x45, 0x1f, 0x86, 0x65, 0xd2, 0xfe, 0xe1, + 0xd2, 0x7e, 0x85, 0x34, 0xc1, 0x52, 0x33, 0x60, 0xac, 0x4a, 0x7e, 0xcc, 0x2e, 0xce, 0x84, 0xd8, 0x19, 0x1c, 0x1d, + 0xf1, 0x01, 0x6d, 0xb4, 0x87, 0x70, 0x23, 0x50, 0x68, 0xf5, 0x1b, 0xd7, 0xb3, 0x38, 0x3a, 0xb9, 0x8a, 0x50, 0x3f, + 0x3a, 0x80, 0x55, 0xee, 0xc9, 0x06, 0x71, 0xb0, 0xce, 0x1a, 0x8c, 0xa6, 0xe3, 0x2b, 0xd2, 0xea, 0xc7, 0xc2, 0x12, + 0xf9, 0x1c, 0xe1, 0xcc, 0xd1, 0xd4, 0x16, 0x1e, 0xa3, 0x86, 0x10, 0x0d, 0xff, 0x3d, 0x46, 0x8d, 0x99, 0x6e, 0x4c, + 0x50, 0x9a, 0xc1, 0xdf, 0x78, 0x4c, 0x08, 0x69, 0x76, 0xca, 0x8a, 0xfe, 0xb0, 0xa4, 0x28, 0x9d, 0x78, 0xf5, 0xe8, + 0xc0, 0x6c, 0x0e, 0xd9, 0x88, 0xf9, 0x80, 0x0d, 0xd7, 0xeb, 0xe8, 0xb2, 0x7f, 0x15, 0xa1, 0x46, 0xec, 0xd1, 0xee, + 0xc4, 0xe3, 0x1d, 0x42, 0x58, 0x0c, 0x37, 0xee, 0x06, 0xea, 0x86, 0xd5, 0x6e, 0x9b, 0x56, 0xd5, 0xfe, 0x0f, 0xc8, + 0x02, 0xdb, 0x94, 0x72, 0x8f, 0xe5, 0x6f, 0x17, 0x30, 0x55, 0x8f, 0xdb, 0x92, 0xb4, 0x70, 0x41, 0xbc, 0xba, 0x9b, + 0x12, 0x5d, 0xe1, 0x7f, 0x46, 0xaa, 0xe2, 0x78, 0x90, 0xe3, 0xd9, 0x90, 0x28, 0x6a, 0xe4, 0x97, 0x9e, 0x57, 0xa6, + 0xb3, 0x9c, 0xdc, 0xb0, 0xad, 0xfb, 0xdf, 0x1c, 0xee, 0x64, 0x1e, 0xeb, 0x24, 0x5b, 0x16, 0x05, 0x13, 0xfa, 0x95, + 0x1c, 0x3b, 0xc6, 0x8e, 0xe5, 0x20, 0x5b, 0xc1, 0xc5, 0x2e, 0x06, 0xae, 0xae, 0xe3, 0x77, 0xca, 0x78, 0x27, 0x7b, + 0x49, 0xc6, 0x96, 0xe1, 0x32, 0xd7, 0xbd, 0xbd, 0xa5, 0x13, 0xa5, 0x63, 0x84, 0xc7, 0xee, 0x1e, 0x38, 0x4e, 0x92, + 0x64, 0x99, 0x64, 0x90, 0x0d, 0x1d, 0x28, 0xb4, 0x31, 0xfb, 0x2a, 0x56, 0xe4, 0xb1, 0x4e, 0x04, 0xbb, 0x35, 0xdd, + 0xc6, 0xa8, 0x3a, 0xc4, 0xfd, 0x7e, 0xbb, 0xa4, 0x3d, 0x43, 0x80, 0x54, 0x22, 0xe4, 0x98, 0x01, 0x84, 0xe0, 0xee, + 0xdf, 0x25, 0xcd, 0xa8, 0x0a, 0x6f, 0xb6, 0xaa, 0x01, 0x0e, 0x42, 0x95, 0xf7, 0x12, 0xf4, 0xc4, 0x86, 0x3d, 0x2b, + 0x0b, 0x5b, 0xe5, 0x39, 0x42, 0x7c, 0x12, 0x2f, 0x13, 0xb8, 0x11, 0x34, 0x98, 0x24, 0x04, 0x5a, 0xaf, 0x97, 0x21, + 0x6e, 0xcd, 0x2a, 0xc5, 0xf4, 0x84, 0xcc, 0x06, 0x45, 0xa3, 0x61, 0x94, 0xd7, 0x63, 0x8b, 0x17, 0x4b, 0x84, 0x27, + 0xe5, 0x5e, 0xf3, 0xe5, 0x16, 0xa4, 0xde, 0x55, 0x3c, 0xa9, 0x2b, 0x81, 0x1b, 0x42, 0x20, 0xa3, 0x5f, 0xd4, 0xd0, + 0x3a, 0x9e, 0x92, 0x93, 0x78, 0x90, 0xf4, 0xff, 0xe7, 0x10, 0xf5, 0xe3, 0xe4, 0x18, 0x9d, 0x58, 0x5a, 0x32, 0x41, + 0xbd, 0xcc, 0xf6, 0xb1, 0x32, 0xb7, 0x9f, 0x6d, 0x6c, 0x14, 0x90, 0xa9, 0xc4, 0x82, 0xce, 0x59, 0x3a, 0x85, 0x5d, + 0xef, 0x91, 0x67, 0x81, 0x01, 0x99, 0xd2, 0xa9, 0xa3, 0x2d, 0x49, 0xd4, 0x2f, 0x68, 0xf9, 0xd5, 0x8f, 0xfa, 0x59, + 0xf5, 0xf5, 0x3f, 0xa3, 0x7e, 0x4e, 0xd3, 0xc7, 0x7c, 0xe3, 0x94, 0xe4, 0xb5, 0x3e, 0xce, 0x7d, 0x1f, 0x1b, 0xbb, + 0x38, 0x01, 0xf0, 0xc6, 0x68, 0x57, 0x3b, 0xb2, 0x44, 0x1b, 0x3e, 0x29, 0xa9, 0x93, 0x4a, 0x34, 0x9d, 0x02, 0x54, + 0x83, 0x45, 0x50, 0xa1, 0x6d, 0x40, 0x30, 0x65, 0xc0, 0x16, 0x8f, 0xb4, 0x00, 0xcd, 0xe5, 0x55, 0x0b, 0xad, 0x6a, + 0x85, 0x1d, 0x67, 0x55, 0xbf, 0x8b, 0x2f, 0x89, 0xf7, 0x04, 0xa8, 0xf2, 0xe5, 0xb2, 0x37, 0x69, 0x34, 0x90, 0xf2, + 0xf8, 0x35, 0x1e, 0x4c, 0x86, 0xf8, 0x16, 0x50, 0x08, 0xd7, 0x30, 0x0a, 0xd7, 0xe6, 0xd8, 0x71, 0x73, 0x6c, 0x34, + 0xe4, 0x06, 0xf5, 0x82, 0xca, 0x4b, 0x57, 0x79, 0xb3, 0xb1, 0x90, 0xd9, 0xc6, 0xb8, 0x0b, 0x64, 0x52, 0xc0, 0x10, + 0x8c, 0x10, 0xf2, 0x59, 0xa2, 0xbd, 0xcd, 0x42, 0xa3, 0x50, 0xdd, 0xec, 0x5e, 0xa0, 0xa8, 0xf6, 0xf4, 0x88, 0x01, + 0x16, 0x50, 0xb5, 0x54, 0x23, 0xcf, 0x34, 0x1e, 0x37, 0xda, 0x06, 0xdd, 0x9b, 0xed, 0x5e, 0xbd, 0xb1, 0xfb, 0x55, + 0x63, 0x78, 0xdc, 0x20, 0xb3, 0x6a, 0x87, 0x6f, 0x64, 0xa3, 0xb1, 0xa9, 0xdf, 0x97, 0xfa, 0x4d, 0x5c, 0xbb, 0xbf, + 0x78, 0xba, 0x63, 0xe2, 0xe1, 0x4f, 0xdf, 0xea, 0xbc, 0x15, 0x09, 0x17, 0x82, 0x15, 0x70, 0xc2, 0x12, 0x8d, 0xc5, + 0x66, 0x53, 0x9e, 0xfa, 0xbf, 0x69, 0x6b, 0x33, 0x46, 0x38, 0xd0, 0x21, 0x23, 0xb5, 0x61, 0x89, 0x0b, 0x4c, 0x0d, + 0x15, 0x21, 0x84, 0xbc, 0xd7, 0xde, 0x3c, 0x46, 0x1b, 0x92, 0x94, 0x91, 0xe0, 0xec, 0x8e, 0x15, 0x61, 0xc9, 0xa7, + 0x7b, 0x8f, 0xe5, 0x77, 0x45, 0xba, 0x81, 0x18, 0xa6, 0xa6, 0x58, 0xee, 0x08, 0x59, 0x4e, 0xbe, 0x82, 0x9c, 0x53, + 0x5e, 0xb0, 0x24, 0x86, 0x20, 0x3e, 0xe1, 0x05, 0x33, 0x8c, 0xfb, 0x3d, 0x2f, 0x37, 0x66, 0x75, 0x4e, 0x33, 0x0b, + 0xb5, 0x3f, 0x00, 0xcd, 0x1c, 0x94, 0x43, 0x92, 0xec, 0x14, 0xfb, 0x74, 0xef, 0xe1, 0xeb, 0x7d, 0x32, 0xf4, 0x7a, + 0xed, 0xa4, 0xe7, 0x0c, 0x58, 0x1f, 0x9c, 0x57, 0x43, 0xcd, 0xdc, 0x8f, 0x34, 0xce, 0x0c, 0x13, 0x95, 0xc7, 0x1c, + 0x90, 0xe9, 0xd3, 0xbd, 0x87, 0xef, 0x62, 0x6e, 0x74, 0x53, 0x08, 0x87, 0xf3, 0x8e, 0x0b, 0x12, 0x53, 0xc2, 0x90, + 0x9d, 0x7c, 0x49, 0xc7, 0x8a, 0xe0, 0x74, 0x4f, 0xa9, 0xc9, 0x04, 0xb1, 0x63, 0x20, 0x86, 0x24, 0x73, 0x20, 0x20, + 0x19, 0xc2, 0x59, 0x4d, 0xae, 0x23, 0x66, 0x0d, 0x4c, 0x67, 0xd7, 0xb0, 0x18, 0x89, 0x65, 0x0f, 0x11, 0xce, 0x4c, + 0xb7, 0x7a, 0x63, 0x8f, 0x13, 0x49, 0xb7, 0x0d, 0xdd, 0x2a, 0x79, 0xf6, 0x03, 0x08, 0x5e, 0xfe, 0xe3, 0x95, 0x6b, + 0xbb, 0x4c, 0x78, 0xe2, 0x2d, 0xd2, 0x3e, 0xdd, 0x7b, 0xf8, 0x8b, 0x33, 0x4a, 0x5b, 0x50, 0x4f, 0xfe, 0x77, 0x64, + 0xd4, 0x87, 0xbf, 0x24, 0x55, 0xae, 0x29, 0xfc, 0xe9, 0xde, 0xc3, 0xf7, 0xfb, 0x8a, 0x41, 0xfa, 0x66, 0x59, 0x29, + 0x09, 0xcc, 0xf8, 0x56, 0x2c, 0x4f, 0x57, 0xee, 0xac, 0x48, 0xc5, 0x06, 0x9b, 0x13, 0x2a, 0x55, 0x9b, 0x52, 0xb7, + 0xf2, 0x04, 0x4b, 0x62, 0xae, 0x92, 0xea, 0xcb, 0xe6, 0xd0, 0x98, 0x4b, 0x71, 0x9d, 0xc9, 0x05, 0xfb, 0xc6, 0xfd, + 0xd2, 0x53, 0x8d, 0x12, 0x3e, 0x07, 0x43, 0x1c, 0x33, 0x76, 0x81, 0x0f, 0x5b, 0xa8, 0xb7, 0x75, 0x9e, 0x49, 0x83, + 0xa8, 0x45, 0xfd, 0xb0, 0xc1, 0x94, 0xb4, 0x70, 0x46, 0x5a, 0x38, 0x27, 0x6a, 0xd0, 0xb2, 0x27, 0x46, 0x2f, 0x2f, + 0x9b, 0xb6, 0xe7, 0x0e, 0x6c, 0xf7, 0xdc, 0xee, 0x5b, 0x7b, 0x28, 0xcf, 0x7a, 0xb9, 0xd1, 0x5f, 0x9a, 0x83, 0x7e, + 0x66, 0x50, 0xe3, 0x05, 0x8b, 0x0b, 0x5c, 0x98, 0x96, 0xaf, 0xf9, 0x28, 0x07, 0x3b, 0x15, 0x98, 0x19, 0xd6, 0x28, + 0x2d, 0xcb, 0xb6, 0x5d, 0xd9, 0x3c, 0x31, 0x6b, 0x55, 0xe0, 0x3c, 0x01, 0x52, 0x8e, 0x73, 0x67, 0xd7, 0xa3, 0x76, + 0xab, 0x9c, 0x1f, 0x1d, 0xc5, 0xb6, 0xd2, 0x8c, 0xc6, 0x85, 0xcf, 0xaf, 0x6e, 0x00, 0x3f, 0x58, 0xaa, 0x31, 0x43, + 0x66, 0x02, 0x8d, 0x46, 0x36, 0xdc, 0xd0, 0x43, 0x42, 0xe2, 0xbc, 0x0e, 0x45, 0x3f, 0x7a, 0xc3, 0x0c, 0x6e, 0x01, + 0xa0, 0xd1, 0x28, 0xaf, 0x7b, 0xb7, 0x20, 0xf6, 0x54, 0x63, 0xb9, 0xf9, 0x1a, 0x97, 0xd6, 0x44, 0xad, 0x1d, 0x3b, + 0x2c, 0x3f, 0x0a, 0x24, 0x42, 0xdc, 0x15, 0x7e, 0x3e, 0xc1, 0xd6, 0x10, 0x50, 0xee, 0x85, 0xb3, 0x81, 0xc0, 0xc6, + 0x6a, 0xcb, 0x15, 0xf2, 0xa4, 0xad, 0x83, 0x52, 0x5f, 0x08, 0x2e, 0xb8, 0xa0, 0x50, 0x63, 0xe3, 0xb0, 0xfc, 0x05, + 0xdb, 0x35, 0xe7, 0xc4, 0x0a, 0x39, 0x6d, 0x99, 0x19, 0x86, 0x01, 0x58, 0xa7, 0x04, 0xcc, 0x73, 0xf2, 0xf2, 0xdb, + 0xa8, 0xff, 0x30, 0x40, 0xfd, 0x47, 0x84, 0x05, 0xdb, 0xc0, 0xea, 0x4a, 0x12, 0xe9, 0x14, 0x14, 0xca, 0x67, 0x3d, + 0x5e, 0x10, 0xd0, 0xc6, 0xd5, 0xa1, 0x5a, 0xbb, 0xa2, 0xfc, 0x06, 0x65, 0x09, 0x77, 0x8a, 0xd1, 0x67, 0x62, 0x7f, + 0x9f, 0x1c, 0x57, 0x17, 0x74, 0xd0, 0xf5, 0x3e, 0xe5, 0x60, 0x48, 0x0a, 0x1f, 0xbe, 0xff, 0xfe, 0xdd, 0xea, 0xe3, + 0xc5, 0xee, 0x0e, 0x0e, 0xcc, 0x4a, 0x61, 0xd6, 0xc1, 0x06, 0xae, 0x1b, 0x99, 0x42, 0xff, 0xe5, 0x9d, 0x78, 0x9d, + 0x0a, 0x6d, 0x6d, 0x46, 0x7f, 0x1c, 0xc2, 0x68, 0xdb, 0x6d, 0x53, 0x82, 0x05, 0xcd, 0x02, 0x5d, 0xb2, 0xc6, 0xad, + 0xb4, 0xf8, 0x06, 0x19, 0x79, 0x68, 0x0a, 0x30, 0x31, 0xde, 0x9f, 0xfd, 0x68, 0xe3, 0xf0, 0xc4, 0x0e, 0x0d, 0xad, + 0x0c, 0x21, 0xb4, 0x78, 0x0f, 0x98, 0x63, 0x8f, 0x08, 0x00, 0xd1, 0x4b, 0x03, 0xa9, 0x0a, 0x64, 0x51, 0x54, 0x29, + 0xf2, 0x9f, 0x1f, 0x12, 0xf2, 0xb2, 0x52, 0x64, 0xbe, 0xad, 0x8c, 0xb9, 0x00, 0x31, 0x50, 0x0a, 0x17, 0x09, 0x65, + 0x82, 0xbd, 0x0c, 0x7d, 0xaf, 0x7d, 0x79, 0x23, 0x6d, 0x26, 0x15, 0x37, 0x1e, 0xdc, 0x94, 0x1a, 0x15, 0x9f, 0xcd, + 0xf7, 0x90, 0xd8, 0xca, 0xbd, 0x07, 0xb9, 0x9c, 0x9a, 0x41, 0xc2, 0xf7, 0x3b, 0x53, 0xda, 0xb7, 0xbb, 0xf9, 0xb2, + 0x6d, 0x11, 0xb3, 0xb5, 0x2e, 0x09, 0x17, 0x8a, 0x15, 0xfa, 0x11, 0x9b, 0xc8, 0x02, 0xee, 0x3f, 0x4a, 0xb0, 0xa0, + 0xcd, 0xbd, 0x40, 0x07, 0x68, 0x26, 0x18, 0x5c, 0x3a, 0x6c, 0xcd, 0xd0, 0xfc, 0xfa, 0x62, 0xee, 0xc0, 0x3f, 0x6d, + 0xd7, 0x7a, 0x79, 0x74, 0xf4, 0x95, 0x55, 0x80, 0x72, 0xc3, 0x34, 0xc3, 0x08, 0x88, 0x97, 0xe5, 0x72, 0xdc, 0xcd, + 0xf0, 0xbd, 0xb8, 0x52, 0x19, 0x78, 0xc2, 0x11, 0x12, 0xa1, 0xe7, 0x44, 0x6f, 0xa6, 0xdb, 0xf4, 0xde, 0x69, 0x33, + 0x44, 0x28, 0xd6, 0x00, 0xb9, 0x07, 0xb9, 0xdc, 0x2a, 0x99, 0x54, 0x65, 0x6b, 0x5b, 0x0e, 0xe2, 0x31, 0x80, 0x2b, + 0x36, 0x42, 0x4a, 0x80, 0x86, 0xfb, 0x85, 0x96, 0xf7, 0x12, 0xd8, 0x7f, 0xac, 0x12, 0x10, 0x69, 0x51, 0x6d, 0xe3, + 0x22, 0x84, 0xad, 0xa9, 0x4f, 0x60, 0x9c, 0xf0, 0xf0, 0xf9, 0x3e, 0x0d, 0xb5, 0x47, 0x6d, 0x66, 0xce, 0x20, 0x28, + 0x21, 0x51, 0x59, 0x21, 0xf9, 0x1a, 0x0b, 0xc7, 0xcd, 0xf9, 0x7b, 0x38, 0x20, 0xc5, 0x92, 0xc6, 0xf6, 0x6e, 0x0b, + 0x8e, 0x8f, 0x22, 0x59, 0xc6, 0xb5, 0xae, 0x7b, 0x85, 0xa9, 0x86, 0x1d, 0xe8, 0x68, 0x08, 0xa7, 0xc2, 0xdc, 0x13, + 0x3e, 0xae, 0x48, 0xaa, 0x76, 0x16, 0x50, 0x9e, 0x18, 0x56, 0xa6, 0x29, 0xc1, 0xfc, 0xb5, 0x33, 0x5f, 0x2b, 0x8f, + 0x09, 0x66, 0x86, 0x71, 0x63, 0x57, 0x81, 0x6d, 0x00, 0xc7, 0x56, 0x8f, 0x64, 0xb0, 0xa8, 0x5e, 0x29, 0x6e, 0x3a, + 0x0d, 0x98, 0x80, 0xb7, 0x60, 0x3d, 0xb3, 0xbd, 0xf5, 0x9f, 0x9b, 0x83, 0x51, 0x60, 0x55, 0x23, 0xf0, 0xd2, 0x10, + 0x78, 0x04, 0x8c, 0x9b, 0x37, 0x2d, 0xef, 0x3b, 0x23, 0x1a, 0xe1, 0x4f, 0x3c, 0x87, 0x67, 0x96, 0xe5, 0xde, 0xf9, + 0xd8, 0x5a, 0x91, 0x54, 0x10, 0xb0, 0x2d, 0xc2, 0x8e, 0xc8, 0x4b, 0x84, 0x55, 0xa3, 0xd1, 0x53, 0x97, 0xac, 0xd2, + 0xaa, 0x54, 0xc3, 0x14, 0x70, 0x4b, 0x0c, 0x78, 0x5f, 0x3b, 0x51, 0xc1, 0x90, 0xc0, 0x5b, 0x7f, 0x2b, 0x50, 0xdf, + 0x3f, 0x7c, 0x1b, 0x87, 0xf4, 0x2d, 0x2c, 0x5b, 0x5e, 0xc4, 0xc2, 0x94, 0xe2, 0xea, 0x0e, 0xe7, 0xcd, 0xf7, 0xcd, + 0x46, 0x60, 0xdc, 0x87, 0x6d, 0x0c, 0x36, 0x6e, 0xa8, 0xa7, 0x2d, 0x69, 0x28, 0x37, 0x61, 0x0f, 0x55, 0xf6, 0x8e, + 0x61, 0x67, 0x3d, 0x5d, 0x49, 0xbb, 0x9a, 0xa8, 0xcd, 0x46, 0xb1, 0xca, 0x68, 0x60, 0xcb, 0xb0, 0xd3, 0x1c, 0x33, + 0xbb, 0x0a, 0xfc, 0xc7, 0x0b, 0xa2, 0x71, 0x80, 0xac, 0x6f, 0xbe, 0x75, 0x9d, 0x52, 0x0d, 0x13, 0xb6, 0xb7, 0x3b, + 0x1f, 0x1f, 0xf3, 0x7d, 0xe7, 0x23, 0x96, 0x6e, 0xeb, 0x9b, 0xb3, 0xb1, 0xfd, 0x6f, 0x9c, 0x8d, 0x4e, 0x6d, 0xef, + 0x8f, 0x47, 0xe0, 0x4e, 0x6a, 0xc7, 0x63, 0x7d, 0x4d, 0x89, 0xc4, 0xc2, 0x2d, 0xc7, 0x55, 0x67, 0xbd, 0x16, 0x83, + 0x16, 0xa8, 0x9d, 0xa2, 0x08, 0x7e, 0xb6, 0xed, 0xcf, 0x80, 0x24, 0x5b, 0x1d, 0x72, 0x2c, 0x4a, 0x51, 0x06, 0x25, + 0x60, 0x40, 0x1d, 0x1b, 0x5b, 0x2f, 0x83, 0xd8, 0x0e, 0x87, 0x1c, 0x96, 0x13, 0x51, 0x5e, 0x5d, 0xc1, 0x88, 0xcd, + 0xb1, 0xe1, 0x04, 0xcc, 0x78, 0xaf, 0x55, 0xa1, 0x17, 0x3f, 0xff, 0x35, 0x73, 0x5a, 0x3b, 0x62, 0x2c, 0x27, 0x51, + 0xb3, 0x62, 0x70, 0x23, 0x70, 0x0c, 0xe3, 0xa1, 0x91, 0x50, 0xab, 0x53, 0x1d, 0xd5, 0x8e, 0x24, 0xdc, 0x02, 0xb5, + 0xdb, 0xa1, 0x39, 0x97, 0xd6, 0xeb, 0xbd, 0x07, 0x0b, 0x2e, 0x02, 0xdc, 0x7e, 0x4e, 0x74, 0x8d, 0xa4, 0x50, 0xe2, + 0x24, 0x28, 0x9c, 0x1b, 0x54, 0xd5, 0x44, 0x0e, 0x5a, 0x43, 0xe0, 0x49, 0x7b, 0xd9, 0xa5, 0xac, 0x84, 0xe4, 0xac, + 0xd1, 0x40, 0x79, 0xd9, 0x31, 0x1d, 0x88, 0x46, 0x36, 0xc4, 0x0c, 0x67, 0x56, 0x60, 0x81, 0xd3, 0x2b, 0xce, 0xab, + 0xae, 0x07, 0xd9, 0x10, 0xe1, 0x62, 0xbd, 0x8e, 0xed, 0xd0, 0x72, 0xb4, 0x5e, 0xe7, 0xe1, 0xd0, 0x4c, 0x3e, 0x54, + 0x7c, 0xd9, 0xd7, 0xe4, 0xa5, 0x39, 0x0f, 0x5f, 0xc2, 0x20, 0x1b, 0x24, 0xce, 0x9d, 0x4a, 0x30, 0x07, 0xcd, 0x55, + 0x43, 0x0e, 0xb2, 0x46, 0x7b, 0x18, 0xd0, 0xb0, 0x41, 0x36, 0x24, 0xf9, 0x06, 0x2c, 0x67, 0x95, 0x3b, 0x30, 0x3f, + 0xc3, 0xc1, 0xf6, 0xd9, 0x9c, 0x33, 0xb6, 0xc1, 0x70, 0x4d, 0xb6, 0x55, 0x06, 0x25, 0x5e, 0xb9, 0xc5, 0xf5, 0xe5, + 0x6a, 0x06, 0x16, 0x65, 0x21, 0xec, 0xae, 0x99, 0xfb, 0x20, 0xfc, 0x97, 0xd8, 0x5e, 0xd0, 0xd2, 0x88, 0x7b, 0x0b, + 0xf1, 0xbd, 0xed, 0x76, 0x92, 0x24, 0xb4, 0x98, 0x9a, 0x2b, 0x11, 0x7f, 0xc3, 0x6b, 0xf6, 0xc0, 0xa9, 0x1b, 0x67, + 0xd0, 0xf3, 0xa0, 0xec, 0x6c, 0x48, 0xec, 0xf8, 0x3d, 0xb3, 0xe3, 0x1d, 0x57, 0x28, 0xdd, 0xaf, 0x8b, 0xb0, 0x83, + 0xc9, 0xfe, 0x97, 0x07, 0x73, 0xe6, 0x06, 0x63, 0xd1, 0x64, 0x0b, 0x6e, 0xdf, 0x80, 0x07, 0xa5, 0x5b, 0x70, 0xfb, + 0x36, 0x7c, 0x3d, 0xb4, 0xf2, 0x6f, 0x0e, 0x30, 0x20, 0x13, 0x76, 0xa4, 0x55, 0x42, 0x30, 0xcc, 0xee, 0x36, 0x47, + 0x66, 0xc9, 0x2a, 0x1c, 0xae, 0x9a, 0xc4, 0x62, 0x6b, 0x2f, 0x54, 0x4c, 0x6a, 0x20, 0x18, 0x8b, 0xf4, 0x25, 0x0a, + 0x95, 0x06, 0x75, 0xe3, 0x18, 0xc0, 0x2a, 0xa7, 0xad, 0x7f, 0x79, 0x74, 0x04, 0x42, 0x03, 0xb0, 0x76, 0x49, 0x46, + 0x17, 0x7a, 0x59, 0x00, 0x7f, 0xa5, 0xfc, 0x6f, 0x48, 0x06, 0xb7, 0x13, 0x93, 0x06, 0x3f, 0x20, 0x61, 0x41, 0x95, + 0xe2, 0x5f, 0x6d, 0x9a, 0xfb, 0x8d, 0x0b, 0xe2, 0x31, 0x5a, 0x59, 0x4e, 0x51, 0xa2, 0x9e, 0x74, 0xe8, 0x5a, 0x87, + 0xdc, 0xd3, 0xaf, 0x4c, 0xe8, 0x97, 0x5c, 0x69, 0x26, 0x00, 0x00, 0x15, 0xe2, 0xc1, 0x94, 0x14, 0x82, 0xad, 0x5b, + 0xab, 0x45, 0xc7, 0xe3, 0xef, 0x56, 0xd1, 0x75, 0xb6, 0x68, 0x46, 0xc5, 0x38, 0xb7, 0x9d, 0x84, 0x36, 0x93, 0xde, + 0x4e, 0xb4, 0x2c, 0x19, 0x5a, 0xec, 0x54, 0xec, 0x87, 0xa1, 0xf5, 0xb1, 0x20, 0xfe, 0x5c, 0xf0, 0x67, 0xe9, 0x77, + 0xf9, 0x18, 0xb8, 0x52, 0xff, 0xc6, 0x2a, 0x84, 0x33, 0xc1, 0x3a, 0x20, 0xaf, 0x49, 0x7d, 0x9c, 0x1e, 0x75, 0x66, + 0x3b, 0xca, 0x85, 0xd2, 0x28, 0x6c, 0xeb, 0xa4, 0x30, 0x98, 0x72, 0xfe, 0x6d, 0x89, 0xeb, 0x17, 0x7f, 0x8c, 0xf8, + 0xa3, 0x43, 0xfc, 0xbb, 0x54, 0x1a, 0xad, 0x4a, 0x04, 0x43, 0x7e, 0x47, 0x32, 0x05, 0x57, 0xb1, 0x39, 0xd7, 0xcf, + 0xf5, 0x3c, 0xdf, 0xf2, 0xc4, 0xe9, 0x31, 0x55, 0x42, 0x47, 0xc5, 0x37, 0x0c, 0xbf, 0x60, 0x70, 0x6f, 0xfc, 0x8c, + 0x07, 0x55, 0x76, 0xef, 0x8b, 0x9f, 0x05, 0xf7, 0xc5, 0xcf, 0x78, 0xba, 0x5b, 0x34, 0xb8, 0x27, 0xee, 0x24, 0x17, + 0x49, 0x2b, 0xf2, 0x7c, 0xd4, 0x98, 0x56, 0xfe, 0x95, 0x76, 0x6b, 0xe0, 0xca, 0x26, 0x0e, 0x8c, 0xf3, 0xea, 0x22, + 0x14, 0x73, 0xe6, 0x8c, 0x96, 0xc3, 0xff, 0xd6, 0x3a, 0xb9, 0x93, 0x47, 0x5a, 0x29, 0xe4, 0x0d, 0x2d, 0xf4, 0x3d, + 0xd8, 0x70, 0xc5, 0x8e, 0x0f, 0x20, 0x25, 0xa0, 0x6c, 0xfb, 0xf7, 0xba, 0x08, 0xc4, 0x71, 0x65, 0x9d, 0x8f, 0xc2, + 0xf6, 0x49, 0x51, 0x72, 0x75, 0x75, 0x21, 0xe4, 0xd6, 0x68, 0x09, 0x10, 0xa6, 0xde, 0x35, 0x8f, 0x39, 0x9a, 0xcc, + 0xd2, 0xd5, 0xa6, 0x54, 0x1d, 0x14, 0x96, 0xab, 0xe3, 0x08, 0x17, 0x1b, 0x73, 0x83, 0xfe, 0x37, 0xc7, 0x9f, 0xb9, + 0xa3, 0x91, 0x3f, 0x95, 0x14, 0xe8, 0xc3, 0x7e, 0x5f, 0x9b, 0x3d, 0x24, 0xd2, 0xce, 0xa1, 0xb4, 0x14, 0x00, 0xac, + 0x36, 0xf8, 0xba, 0xf1, 0x38, 0xf5, 0x44, 0xba, 0xd9, 0x7c, 0xd3, 0x10, 0x16, 0xb3, 0xd2, 0x82, 0xc7, 0x74, 0xb3, + 0xc7, 0x72, 0xd4, 0xcb, 0xe2, 0xba, 0xdc, 0x63, 0xb5, 0x7e, 0xd1, 0x37, 0x40, 0x59, 0x19, 0xa2, 0xad, 0xd7, 0x71, + 0x1d, 0xde, 0x44, 0x04, 0xd7, 0x20, 0x08, 0x8b, 0xc0, 0x80, 0xa3, 0xc6, 0x78, 0xdb, 0x3a, 0x31, 0xda, 0xb6, 0x5f, + 0xf2, 0xac, 0x7b, 0x6d, 0x1c, 0xa1, 0xa2, 0xc1, 0x56, 0x0f, 0x35, 0x0f, 0xd8, 0xce, 0xae, 0xec, 0x28, 0x80, 0xd0, + 0x98, 0x7a, 0xe3, 0xdc, 0xca, 0x8a, 0x76, 0x0f, 0x7c, 0xd1, 0x77, 0xcc, 0x73, 0x1d, 0xe8, 0x76, 0xf3, 0x03, 0xdb, + 0xa6, 0x27, 0xf2, 0x5b, 0xb6, 0x4d, 0x35, 0x4e, 0xf8, 0xb0, 0x85, 0xbe, 0x6f, 0x08, 0x6b, 0xfb, 0xda, 0x5f, 0xe4, + 0x7f, 0xa1, 0xbb, 0x36, 0xa0, 0xa7, 0x05, 0xb3, 0xa7, 0x31, 0xef, 0xf5, 0x66, 0xf3, 0x53, 0xe9, 0xbf, 0x60, 0x6c, + 0x85, 0x7e, 0xb2, 0xbb, 0xc0, 0x89, 0x95, 0xc6, 0x21, 0x38, 0xfe, 0x9b, 0x93, 0x69, 0x2e, 0x47, 0x34, 0x7f, 0x07, + 0x3d, 0x56, 0xb9, 0xcf, 0xef, 0xc6, 0x05, 0xd5, 0xcc, 0xd1, 0x9a, 0x6a, 0x14, 0x7f, 0xf3, 0x60, 0x18, 0x7f, 0x73, + 0x4b, 0xb9, 0xab, 0x16, 0xf0, 0xea, 0x65, 0xd9, 0x44, 0xfa, 0xd3, 0xc6, 0xd3, 0x0e, 0xae, 0xf6, 0xf7, 0xb2, 0x4d, + 0xd2, 0x78, 0x49, 0xd2, 0xb8, 0x8a, 0xb7, 0x9b, 0x8a, 0xe3, 0xcf, 0xdf, 0x18, 0xec, 0x2e, 0x99, 0xfb, 0x1c, 0x90, + 0xb9, 0xcf, 0x3c, 0xfd, 0x6e, 0xad, 0x80, 0xe2, 0x9d, 0x26, 0xa7, 0xc6, 0x32, 0xc6, 0x8e, 0xfa, 0xad, 0x06, 0x83, + 0x06, 0x4d, 0xae, 0x02, 0x6f, 0x87, 0xea, 0xf4, 0xf2, 0xf6, 0x47, 0x71, 0xb6, 0x54, 0x5a, 0xce, 0x5d, 0xa3, 0xca, + 0xf9, 0x38, 0x99, 0x4c, 0x50, 0x60, 0x9b, 0x3b, 0xfc, 0xb4, 0xee, 0x46, 0xb6, 0xfa, 0xc2, 0xc5, 0x38, 0x55, 0xd8, + 0x9d, 0x2d, 0x2a, 0x95, 0x1b, 0xe2, 0xcd, 0x9c, 0x77, 0xf3, 0xf0, 0x84, 0x0b, 0xae, 0x66, 0xac, 0x88, 0x0b, 0xb4, + 0xfa, 0x56, 0x67, 0x05, 0xdc, 0xe6, 0xd8, 0xce, 0xf0, 0xb2, 0xb4, 0x1c, 0xd0, 0x09, 0xb4, 0x06, 0x3a, 0xa3, 0x39, + 0xd3, 0x33, 0x39, 0x06, 0xc3, 0x97, 0x64, 0x5c, 0xba, 0x53, 0x1d, 0x1d, 0x1d, 0xc6, 0x91, 0xd1, 0x5f, 0x80, 0x0f, + 0x7a, 0x98, 0x83, 0xfa, 0x2b, 0x70, 0x0c, 0xaa, 0xba, 0x66, 0x68, 0xc5, 0xb6, 0x7d, 0x68, 0x74, 0xf2, 0x85, 0xdd, + 0x61, 0x8e, 0x36, 0x9b, 0xd4, 0x8e, 0x3a, 0x9a, 0x70, 0x96, 0x8f, 0x23, 0xfc, 0x85, 0xdd, 0xa5, 0xa5, 0xdb, 0xba, + 0xf1, 0xb2, 0x36, 0x8b, 0x18, 0xc9, 0x1b, 0x11, 0xe1, 0xaa, 0x93, 0x74, 0xb5, 0xc1, 0xb2, 0xe0, 0x53, 0xc0, 0xd1, + 0x9f, 0xd9, 0x5d, 0xea, 0xda, 0x0b, 0x5c, 0x05, 0xd1, 0xca, 0x83, 0x3e, 0x09, 0x92, 0xc3, 0x65, 0x70, 0x02, 0xc7, + 0xc0, 0xd4, 0x1d, 0x92, 0x5a, 0xb9, 0x4a, 0x84, 0x44, 0x68, 0xf3, 0xef, 0x4e, 0x05, 0x4f, 0xc2, 0x73, 0x4e, 0xd7, + 0x2c, 0x6e, 0xb7, 0x2a, 0x31, 0xa8, 0x50, 0x59, 0x90, 0x7c, 0x8c, 0xb9, 0xdf, 0x7d, 0xce, 0xfb, 0x21, 0xd0, 0x99, + 0x4d, 0xa8, 0x6b, 0x34, 0x5d, 0x9a, 0x5f, 0xa8, 0xba, 0x83, 0x9a, 0xeb, 0xaa, 0xe2, 0xc1, 0xc7, 0x18, 0x00, 0x0f, + 0xd6, 0x32, 0xd4, 0x38, 0x84, 0x6e, 0xbc, 0x99, 0xea, 0x82, 0x92, 0x78, 0xe5, 0xe7, 0x90, 0xf2, 0x10, 0x8c, 0x7a, + 0x03, 0x68, 0xe8, 0x10, 0xcc, 0x5a, 0x1e, 0xf2, 0x49, 0x2c, 0x76, 0xce, 0x50, 0x69, 0xce, 0xd0, 0x24, 0x00, 0xf9, + 0x37, 0xce, 0x4c, 0x66, 0xa0, 0x61, 0x78, 0x4b, 0x73, 0x00, 0xba, 0xd5, 0x75, 0x38, 0x14, 0xae, 0x68, 0xe9, 0xbc, + 0x67, 0x17, 0x5d, 0xd6, 0x86, 0x15, 0x9b, 0x76, 0xd0, 0x26, 0x85, 0x29, 0x31, 0x5b, 0x60, 0xe3, 0xf5, 0x3e, 0xdc, + 0xdb, 0xd5, 0xc6, 0x45, 0xe2, 0xa7, 0x45, 0x3c, 0x4c, 0x62, 0x8a, 0x56, 0x3c, 0xa6, 0x58, 0x82, 0x1d, 0x64, 0xb1, + 0x29, 0xc7, 0xcf, 0xc2, 0xe5, 0xa8, 0x59, 0x49, 0xef, 0x77, 0x30, 0x04, 0x2e, 0x5f, 0x83, 0x6d, 0x28, 0xe6, 0x25, + 0x61, 0x89, 0x8d, 0xa7, 0x5f, 0xb0, 0x6e, 0x53, 0xbb, 0x20, 0x7e, 0x05, 0x16, 0x34, 0x5e, 0x05, 0xb3, 0x08, 0x9d, + 0xca, 0x9d, 0xc3, 0xa1, 0xbb, 0x26, 0xac, 0x8c, 0x57, 0x63, 0x45, 0xb6, 0x8e, 0x9e, 0xef, 0xdb, 0x78, 0xfe, 0xb5, + 0x64, 0xc5, 0xdd, 0x35, 0x03, 0x1b, 0x6b, 0x09, 0xee, 0xc6, 0xd5, 0x32, 0x54, 0x06, 0xf2, 0x7d, 0x69, 0x58, 0x97, + 0x0d, 0xfe, 0x6e, 0x54, 0x8c, 0x8d, 0xb9, 0xa7, 0x0c, 0xb4, 0x35, 0x76, 0xbb, 0xb0, 0x6f, 0xba, 0x6e, 0xb2, 0x9e, + 0x89, 0x95, 0x50, 0x41, 0xda, 0xdd, 0x2d, 0xe0, 0x22, 0xf4, 0x87, 0x1d, 0xa8, 0xe1, 0xb6, 0xea, 0x06, 0x92, 0xe0, + 0xda, 0x4f, 0x7e, 0x7b, 0xaa, 0xfb, 0xac, 0x75, 0xbf, 0x3d, 0xd5, 0xda, 0x65, 0xa1, 0x31, 0x24, 0xc2, 0xae, 0x9f, + 0xd2, 0x7f, 0x5a, 0x6c, 0x36, 0x68, 0x03, 0xc3, 0x7b, 0xc4, 0x7b, 0x71, 0xfc, 0xc8, 0x5b, 0x28, 0x26, 0x70, 0x91, + 0x7b, 0x9d, 0x4b, 0x4f, 0xc8, 0xab, 0x11, 0x3c, 0xe2, 0x3b, 0x43, 0x78, 0xc4, 0x03, 0xa7, 0x57, 0x90, 0x9a, 0xa6, + 0x82, 0x8d, 0x3d, 0xfd, 0x44, 0x16, 0x09, 0x0d, 0x1f, 0xf7, 0x9a, 0x13, 0xa1, 0xff, 0x4c, 0x81, 0xff, 0xc2, 0xa3, + 0xa5, 0xd6, 0x52, 0x60, 0x2e, 0x16, 0x4b, 0x8d, 0x95, 0x19, 0xfd, 0x6a, 0x22, 0x85, 0x6e, 0x4e, 0xe8, 0x9c, 0xe7, + 0x77, 0xe9, 0x92, 0x37, 0xe7, 0x52, 0x48, 0xb5, 0xa0, 0x19, 0xc3, 0xea, 0x4e, 0x69, 0x36, 0x6f, 0x2e, 0x39, 0x7e, + 0xce, 0xf2, 0xaf, 0x4c, 0xf3, 0x8c, 0xe2, 0xb7, 0x72, 0x24, 0xb5, 0xc4, 0xaf, 0x6f, 0xef, 0xa6, 0x4c, 0xe0, 0xf7, + 0xa3, 0xa5, 0xd0, 0x4b, 0xac, 0xa8, 0x50, 0x4d, 0xc5, 0x0a, 0x3e, 0xe9, 0x35, 0x9b, 0x8b, 0x82, 0xcf, 0x69, 0x71, + 0xd7, 0xcc, 0x64, 0x2e, 0x8b, 0xf4, 0xbf, 0x5a, 0xa7, 0xf4, 0xc1, 0xe4, 0xac, 0xa7, 0x0b, 0x2a, 0x14, 0x87, 0x85, + 0x49, 0x69, 0x9e, 0x1f, 0x9c, 0x76, 0x5b, 0x73, 0x75, 0x68, 0x2f, 0xfc, 0xa8, 0xd0, 0x9b, 0x3f, 0xf1, 0x6f, 0x12, + 0x46, 0x99, 0x8c, 0xb4, 0x70, 0x83, 0x5c, 0x65, 0xcb, 0x42, 0xc9, 0x22, 0x5d, 0x48, 0x2e, 0x34, 0x2b, 0x7a, 0x23, + 0x59, 0x8c, 0x59, 0xd1, 0x2c, 0xe8, 0x98, 0x2f, 0x55, 0x7a, 0xb6, 0xb8, 0xed, 0xd5, 0x7b, 0xb0, 0xf9, 0xa9, 0x90, + 0x82, 0xf5, 0x80, 0xdf, 0x98, 0x16, 0x72, 0x29, 0xc6, 0x6e, 0x18, 0x4b, 0xa1, 0x98, 0xee, 0x2d, 0xe8, 0x18, 0xec, + 0x80, 0xd3, 0x8b, 0xc5, 0x6d, 0xcf, 0xcc, 0xfa, 0x86, 0xf1, 0xe9, 0x4c, 0xa7, 0xdd, 0x56, 0xcb, 0x7e, 0x2b, 0xfe, + 0x37, 0x4b, 0xdb, 0x9d, 0xa4, 0xd3, 0x5d, 0xdc, 0x02, 0x07, 0xaf, 0x59, 0xd1, 0x04, 0x58, 0x40, 0xa5, 0x76, 0xd2, + 0x7a, 0x70, 0x7a, 0x1f, 0x32, 0xc0, 0xc6, 0xa1, 0x69, 0x26, 0x04, 0xc6, 0xee, 0xe9, 0x72, 0xb1, 0x60, 0x05, 0x78, + 0xd1, 0xf7, 0xe6, 0xb4, 0x98, 0x72, 0xd1, 0x2c, 0x4c, 0xa3, 0xcd, 0x8b, 0xc5, 0xed, 0x06, 0xe6, 0x93, 0x5a, 0xb3, + 0x55, 0x37, 0x2d, 0xf7, 0xb5, 0x0a, 0x86, 0x68, 0x62, 0xd2, 0xa4, 0xc5, 0x74, 0x44, 0xe3, 0x76, 0xe7, 0x3e, 0xf6, + 0xff, 0x4b, 0x3a, 0x28, 0x00, 0x5b, 0x73, 0xbc, 0x2c, 0xcc, 0x2d, 0x6a, 0xda, 0x56, 0xb6, 0xd9, 0x99, 0xfc, 0xca, + 0x0a, 0xdf, 0xaa, 0xf9, 0x58, 0xed, 0xcc, 0xfb, 0x3f, 0x6a, 0x94, 0xda, 0xb6, 0x5e, 0xa8, 0x6b, 0xa0, 0xd1, 0xbb, + 0x8d, 0xfd, 0x57, 0xe7, 0x82, 0xde, 0x3f, 0xeb, 0x7a, 0xb8, 0x4f, 0x26, 0x93, 0x1a, 0xd0, 0x3d, 0x74, 0xdb, 0xad, + 0xc5, 0xed, 0x41, 0xa7, 0xe5, 0x61, 0x6c, 0x61, 0x7a, 0xbe, 0xb8, 0xdd, 0xb3, 0x82, 0x01, 0x56, 0x6c, 0xf7, 0x76, + 0x90, 0x9c, 0xaa, 0x03, 0x46, 0x15, 0xdb, 0xfc, 0x89, 0xe7, 0x14, 0x70, 0xc3, 0x20, 0xed, 0xc0, 0xc8, 0xa9, 0xb0, + 0x02, 0xc3, 0xd5, 0x0d, 0x1f, 0xeb, 0x59, 0xda, 0x6e, 0xb5, 0x7e, 0xa8, 0x30, 0xa9, 0x37, 0xb3, 0x4b, 0xda, 0x2e, + 0xd8, 0xbc, 0x86, 0x5f, 0x23, 0x5a, 0xee, 0x82, 0xd5, 0x42, 0xba, 0x4e, 0x0b, 0x96, 0x9b, 0x28, 0x37, 0x1b, 0xb7, + 0x15, 0x76, 0xa6, 0xcc, 0xc5, 0x8c, 0x15, 0x5c, 0xf7, 0xea, 0x5f, 0x55, 0xc7, 0xbb, 0x73, 0xda, 0x58, 0xf9, 0x78, + 0x65, 0x6b, 0xb8, 0xcb, 0xd8, 0xc7, 0xf0, 0xb1, 0x8b, 0x95, 0x5f, 0x69, 0x11, 0x6f, 0x6d, 0x18, 0x1c, 0xd6, 0x40, + 0x9b, 0x60, 0xce, 0x05, 0x98, 0x8a, 0x0e, 0xf1, 0x37, 0xa0, 0x90, 0xd1, 0x3c, 0x8b, 0x61, 0x44, 0x07, 0xcd, 0x83, + 0xd3, 0x82, 0xcd, 0x91, 0x07, 0x44, 0x72, 0xbf, 0x5b, 0xb0, 0xf9, 0x26, 0x31, 0xd5, 0x57, 0x06, 0x75, 0x69, 0xce, + 0xa7, 0x22, 0xcd, 0x18, 0x6c, 0xab, 0x4d, 0xc2, 0x84, 0xe6, 0xfa, 0xae, 0x59, 0xc8, 0x9b, 0xd5, 0x98, 0xab, 0x45, + 0x4e, 0xef, 0xd2, 0x49, 0xce, 0x6e, 0x7b, 0xa6, 0x54, 0x93, 0x6b, 0x36, 0x57, 0xae, 0x6c, 0x0f, 0xd2, 0x9b, 0x63, + 0x6b, 0xce, 0x01, 0xd0, 0x93, 0x37, 0xdb, 0xfb, 0xda, 0x2f, 0x5a, 0x53, 0x2e, 0xf5, 0x41, 0x4b, 0xf5, 0xe6, 0x5c, + 0x34, 0xdd, 0x40, 0xce, 0x00, 0x23, 0x76, 0x21, 0x1f, 0xf4, 0x9f, 0xb0, 0xdb, 0x05, 0x15, 0x63, 0x36, 0x5e, 0x05, + 0xd5, 0x3a, 0x50, 0x2f, 0x2c, 0x95, 0x0a, 0x3d, 0x6b, 0x1a, 0x1b, 0xb4, 0xb8, 0x23, 0xd0, 0x37, 0x50, 0xfe, 0x41, + 0x0b, 0xdb, 0xff, 0x4f, 0xda, 0x28, 0xac, 0x7c, 0x00, 0xe1, 0xa0, 0xf8, 0xe4, 0xae, 0x09, 0x7f, 0x57, 0xe0, 0xf3, + 0xc4, 0x33, 0x9a, 0x3b, 0x88, 0xcc, 0xf9, 0x78, 0x9c, 0xd7, 0x46, 0x74, 0x15, 0x74, 0xd6, 0x46, 0x2b, 0x98, 0x7f, + 0xda, 0x3a, 0x68, 0x1d, 0x98, 0xb9, 0xb8, 0x6d, 0x70, 0x76, 0x76, 0xff, 0xf4, 0x01, 0xeb, 0xe5, 0x5c, 0xb0, 0xda, + 0x54, 0xbf, 0x0b, 0xea, 0xb0, 0xe1, 0x8e, 0x6b, 0xb8, 0x7d, 0xd0, 0x3e, 0x38, 0x6b, 0xfd, 0xe0, 0xa9, 0x48, 0xce, + 0x26, 0xda, 0xee, 0x9b, 0x1a, 0x59, 0xb9, 0xf0, 0x4d, 0xdf, 0x14, 0x74, 0x91, 0x0a, 0x09, 0x7f, 0x7a, 0xb0, 0xf9, + 0x27, 0xb9, 0xbc, 0x49, 0x67, 0x7c, 0x3c, 0x66, 0xc2, 0x16, 0x28, 0x13, 0x59, 0x9e, 0xf3, 0x85, 0xe2, 0x76, 0x35, + 0x1c, 0xee, 0x76, 0xb7, 0xa0, 0x1a, 0x0e, 0xe8, 0x34, 0x18, 0x50, 0xb7, 0x1a, 0x50, 0xd5, 0x7f, 0x38, 0xc2, 0xce, + 0xd6, 0x5c, 0x4d, 0xa9, 0x5e, 0x0d, 0x93, 0x3e, 0x2f, 0x95, 0x06, 0x98, 0x7b, 0xe3, 0x11, 0x73, 0xba, 0x34, 0x47, + 0x4c, 0xdf, 0x30, 0x26, 0xbe, 0x3d, 0x88, 0xab, 0x54, 0x8a, 0xfc, 0xce, 0x7e, 0xae, 0xc2, 0x2e, 0xe9, 0x52, 0xcb, + 0x4d, 0x32, 0xe2, 0x82, 0x16, 0x77, 0x9f, 0x14, 0x13, 0x4a, 0x16, 0x9f, 0xe4, 0x64, 0xb2, 0xfa, 0x16, 0xc9, 0xbb, + 0x8f, 0x36, 0x89, 0xe2, 0x62, 0x9a, 0x33, 0x4b, 0xe0, 0x0c, 0x22, 0xb8, 0x43, 0xc6, 0xb6, 0x6b, 0x9a, 0xac, 0x0d, + 0x7a, 0x93, 0x64, 0x39, 0x9f, 0x53, 0xcd, 0x0c, 0x9c, 0x03, 0x52, 0xe3, 0x26, 0x6f, 0xa9, 0x5c, 0xeb, 0xc0, 0xfe, + 0xa9, 0x4a, 0xc3, 0x36, 0x0a, 0x0a, 0xfb, 0x26, 0xb9, 0x30, 0xf8, 0x61, 0xc0, 0x61, 0x76, 0x91, 0x59, 0x3d, 0xb3, + 0x76, 0x01, 0xec, 0x60, 0x76, 0xb5, 0xa6, 0xae, 0x1c, 0x5d, 0xb2, 0x2d, 0x76, 0x5b, 0x3f, 0xd4, 0x73, 0x73, 0x3a, + 0x62, 0xf9, 0xca, 0x6e, 0x54, 0x0f, 0x5c, 0xb7, 0x55, 0xc3, 0x65, 0x0e, 0x48, 0x86, 0x01, 0xd1, 0x30, 0x4d, 0x9b, + 0x37, 0x6c, 0xf4, 0x85, 0x6b, 0xbb, 0x65, 0x9a, 0xea, 0x06, 0x9c, 0x8a, 0xcc, 0x98, 0x16, 0xac, 0x58, 0x79, 0x42, + 0xde, 0xaa, 0x11, 0xd0, 0x6b, 0x61, 0x0e, 0x68, 0x4d, 0x47, 0x4d, 0x08, 0xb1, 0xc6, 0x8a, 0xd5, 0xbe, 0xc9, 0xcd, + 0xe9, 0xad, 0x43, 0xb1, 0x07, 0xad, 0x1f, 0x6a, 0x87, 0xec, 0x59, 0xab, 0xe5, 0x8f, 0x88, 0xa6, 0xad, 0x91, 0xb6, + 0x93, 0x2e, 0x9b, 0x97, 0x89, 0x5a, 0x2e, 0xd2, 0x5a, 0xc2, 0x48, 0x6a, 0x2d, 0xe7, 0x36, 0x6d, 0x0f, 0x35, 0xaa, + 0x93, 0xde, 0x76, 0x67, 0x71, 0x7b, 0x60, 0xfe, 0x69, 0x1d, 0xb4, 0x76, 0x49, 0xed, 0x2e, 0x56, 0x9c, 0x22, 0x8f, + 0xc7, 0xd0, 0x71, 0x9b, 0xcd, 0x7b, 0x4b, 0x05, 0xc7, 0xbd, 0x81, 0xb8, 0x39, 0xd1, 0x36, 0x66, 0xb2, 0x00, 0x58, + 0xca, 0x05, 0x9c, 0xae, 0xf6, 0xb0, 0x83, 0x3e, 0x94, 0x04, 0x73, 0xf8, 0xbd, 0x8d, 0xd6, 0x87, 0xd5, 0x3a, 0xa8, + 0x06, 0x06, 0xff, 0x6c, 0xfe, 0xac, 0xf8, 0xf3, 0x27, 0x2c, 0x90, 0x8f, 0x78, 0x23, 0xe9, 0xae, 0x5b, 0x4e, 0x26, + 0x1a, 0xeb, 0x4a, 0x54, 0x33, 0x1e, 0x25, 0x73, 0x7a, 0x6b, 0x5d, 0x4b, 0xe6, 0x5c, 0x80, 0xe1, 0x1a, 0xc2, 0x3a, + 0x30, 0xf1, 0x9f, 0x85, 0x0d, 0x8d, 0x75, 0x0c, 0x0d, 0x1f, 0x77, 0x92, 0x6e, 0x17, 0xe1, 0x16, 0xee, 0x74, 0xbb, + 0x81, 0x4c, 0x36, 0xd1, 0xfb, 0x8a, 0xee, 0x2b, 0x29, 0xf7, 0x94, 0x3c, 0x31, 0x8d, 0x9e, 0xb4, 0x5b, 0x2d, 0x6c, + 0xdc, 0xe7, 0xcb, 0xc2, 0x42, 0xed, 0x69, 0xb6, 0xdd, 0x6a, 0x41, 0xb3, 0xf0, 0xc7, 0xcd, 0xeb, 0x67, 0xb2, 0x6a, + 0xa5, 0x2d, 0xdc, 0x4e, 0xdb, 0xb8, 0x93, 0x76, 0xf0, 0x69, 0x7a, 0x8a, 0xcf, 0xd2, 0x33, 0xdc, 0x4d, 0xbb, 0xf8, + 0x3c, 0x3d, 0xc7, 0xf7, 0xd3, 0xfb, 0xf8, 0x22, 0xbd, 0xc0, 0x0f, 0xd2, 0x07, 0xf8, 0x61, 0xda, 0x6e, 0xe1, 0x47, + 0x69, 0xbb, 0x8d, 0x1f, 0xa7, 0xed, 0x0e, 0x7e, 0x92, 0xb6, 0x4f, 0xf1, 0xd3, 0xb4, 0x7d, 0x86, 0x9f, 0xa5, 0xed, + 0x2e, 0xa6, 0x90, 0x3b, 0x82, 0xdc, 0x0c, 0x72, 0xc7, 0x90, 0xcb, 0x20, 0x77, 0x92, 0xb6, 0xbb, 0x1b, 0xac, 0x6c, + 0xc8, 0x8d, 0xa8, 0xd5, 0xee, 0x9c, 0x9e, 0x75, 0xcf, 0xef, 0x5f, 0x3c, 0x78, 0xf8, 0xe8, 0xf1, 0x93, 0xa7, 0xcf, + 0xa2, 0x21, 0xfe, 0x64, 0x3c, 0x5f, 0x94, 0x18, 0xf0, 0xa3, 0x76, 0x77, 0x88, 0xef, 0xfc, 0x67, 0xcc, 0x8f, 0x3a, + 0x67, 0x2d, 0x74, 0x75, 0x75, 0x36, 0x6c, 0x94, 0xb9, 0x8f, 0x8c, 0xc3, 0x4d, 0x95, 0x45, 0x08, 0x89, 0x21, 0x07, + 0xe1, 0x5b, 0xeb, 0x40, 0xc3, 0x62, 0x9e, 0x14, 0xe8, 0xe8, 0xc8, 0xfc, 0x98, 0xfa, 0x1f, 0x23, 0xff, 0x83, 0x06, + 0x8b, 0xf4, 0x95, 0xc6, 0xce, 0xe3, 0x5a, 0x97, 0xfe, 0x0e, 0xa5, 0x29, 0xd1, 0x01, 0x77, 0x46, 0xfd, 0xff, 0x15, + 0x59, 0xa3, 0x1d, 0x72, 0x66, 0x15, 0x63, 0xdd, 0x3e, 0x23, 0xab, 0x22, 0xed, 0x74, 0xbb, 0x47, 0x3f, 0x0f, 0xf8, + 0xa0, 0x3d, 0x1c, 0x1e, 0xb7, 0xef, 0xe3, 0x69, 0x99, 0xd0, 0xb1, 0x09, 0xa3, 0x32, 0xe1, 0xd4, 0x26, 0xd0, 0xd4, + 0xd6, 0x86, 0xa4, 0x33, 0x93, 0x04, 0x25, 0x36, 0xa9, 0x69, 0xfb, 0xbe, 0x6d, 0xfb, 0x01, 0x58, 0x93, 0x99, 0xe6, + 0x5d, 0xd3, 0x97, 0x97, 0x67, 0x6b, 0xd7, 0x28, 0x9e, 0xa6, 0xae, 0x35, 0x9f, 0x78, 0x36, 0x1c, 0xe2, 0x91, 0x49, + 0xec, 0x56, 0x89, 0xe7, 0xc3, 0xa1, 0xeb, 0xea, 0x81, 0xe9, 0xea, 0x7e, 0x95, 0x75, 0x31, 0x1c, 0x9a, 0x2e, 0x91, + 0x8b, 0x1d, 0xa0, 0xf4, 0xc1, 0x4d, 0xa9, 0xbf, 0xe1, 0x97, 0x9d, 0x6e, 0xb7, 0x0f, 0x18, 0x66, 0x6c, 0x82, 0x3d, + 0x8c, 0xbe, 0x04, 0x30, 0xba, 0x85, 0xdf, 0xfd, 0x4f, 0x34, 0xbd, 0xa3, 0x25, 0x90, 0xfa, 0xd1, 0x7f, 0x45, 0x0d, + 0x6d, 0x60, 0x6e, 0xfe, 0x4c, 0xed, 0x9f, 0x11, 0x6a, 0xdc, 0x50, 0x00, 0x37, 0x68, 0xa4, 0xbc, 0x4a, 0xd9, 0xf4, + 0x78, 0x4d, 0xc1, 0xc5, 0x67, 0xa6, 0x72, 0xda, 0x5f, 0xcf, 0x6e, 0x46, 0xeb, 0x99, 0xfa, 0x8a, 0xfe, 0x88, 0xff, + 0x50, 0xc7, 0xf1, 0xa0, 0xd9, 0x48, 0xd8, 0x1f, 0x63, 0xf0, 0x25, 0xea, 0xa7, 0x63, 0x36, 0x45, 0xfd, 0xc1, 0x1f, + 0x0a, 0x0f, 0x1b, 0x41, 0xc6, 0x0f, 0xbb, 0x29, 0xe0, 0x69, 0xb4, 0x9d, 0x18, 0xff, 0x80, 0xfa, 0xa8, 0xff, 0x87, + 0x3a, 0xfe, 0x03, 0xdd, 0x3b, 0x09, 0xb4, 0x26, 0xd2, 0x6d, 0xe1, 0x2a, 0xfc, 0xd0, 0x71, 0xb9, 0x85, 0x19, 0x6e, + 0x37, 0x19, 0x04, 0x6b, 0x03, 0x57, 0x74, 0x12, 0xcb, 0x06, 0x3f, 0x39, 0x6d, 0xa1, 0x1f, 0xda, 0x1d, 0x50, 0xae, + 0x34, 0xc5, 0xf1, 0xee, 0xa6, 0x2f, 0x9a, 0xa7, 0xf8, 0x41, 0xb3, 0xc0, 0x6d, 0x84, 0x9b, 0x6d, 0xaf, 0xf5, 0x1e, + 0xa8, 0xb8, 0x85, 0xb0, 0x8a, 0x2f, 0xe0, 0x9f, 0x33, 0x34, 0xac, 0x36, 0xe4, 0x2f, 0x74, 0xbb, 0x77, 0xf0, 0x9b, + 0x25, 0xb1, 0x6a, 0xf0, 0x93, 0xf3, 0x16, 0xfa, 0xe1, 0xdc, 0x74, 0xc4, 0x8e, 0xf5, 0x9e, 0xae, 0x24, 0x3e, 0x6b, + 0x4a, 0xe8, 0xa8, 0x55, 0xf6, 0x23, 0xe2, 0x2e, 0xc2, 0x22, 0x3e, 0x85, 0x7f, 0xda, 0x61, 0x3f, 0x8f, 0x77, 0xfa, + 0x31, 0xf3, 0x6e, 0xe3, 0xa4, 0x6b, 0xdd, 0x70, 0x95, 0xbd, 0x13, 0x6f, 0xb0, 0xab, 0xb6, 0xb9, 0xcc, 0x6b, 0x9f, + 0xc0, 0x07, 0xc2, 0xfa, 0x98, 0x28, 0xcc, 0x8e, 0xc1, 0x7f, 0x17, 0xcc, 0x56, 0xd4, 0xe5, 0x69, 0x4f, 0x35, 0x1a, + 0x48, 0x0c, 0xd4, 0xf0, 0x98, 0xb4, 0x9b, 0xba, 0xc9, 0x30, 0xfc, 0x6e, 0x90, 0x32, 0x28, 0x9c, 0xa8, 0x7a, 0x7d, + 0xed, 0x7a, 0xb5, 0x37, 0xff, 0x1e, 0x3b, 0x08, 0x21, 0xaa, 0x1f, 0xeb, 0x26, 0x43, 0x27, 0xa2, 0x11, 0xeb, 0x4b, + 0xd6, 0x3f, 0x4f, 0x5b, 0xc8, 0x60, 0xa7, 0xea, 0xc7, 0xac, 0xc9, 0x21, 0xbd, 0x93, 0xc6, 0xbc, 0xa9, 0xe1, 0xd7, + 0x59, 0x00, 0x2d, 0x01, 0x78, 0x57, 0x79, 0x23, 0x15, 0x27, 0x9d, 0x6e, 0x17, 0x0b, 0xc2, 0x93, 0xa9, 0xf9, 0xa5, + 0x08, 0x4f, 0x46, 0xe6, 0x97, 0x24, 0x25, 0xbc, 0x6c, 0xef, 0xb8, 0x20, 0xc1, 0xaa, 0x9a, 0x14, 0x0a, 0x0b, 0x5a, + 0xa0, 0x93, 0x8e, 0x37, 0x0b, 0xc0, 0x33, 0x3f, 0x07, 0x50, 0x83, 0x14, 0xc6, 0x22, 0x54, 0x36, 0x0b, 0x9c, 0x13, + 0x7a, 0x95, 0x74, 0xfb, 0xb3, 0x93, 0xb8, 0xd3, 0x94, 0xcd, 0x02, 0xa5, 0xb3, 0x13, 0x53, 0x13, 0x67, 0xe4, 0x35, + 0xb5, 0xad, 0xe1, 0x19, 0xdc, 0xe5, 0x66, 0x24, 0x3b, 0x3e, 0x6f, 0x35, 0x92, 0x2e, 0xc2, 0x83, 0x6c, 0xdd, 0xc2, + 0xf9, 0x7a, 0xdd, 0xc2, 0x34, 0x5c, 0x06, 0xe1, 0x01, 0x52, 0x6a, 0xea, 0xb6, 0x63, 0xf3, 0xf4, 0x79, 0xac, 0xc1, + 0x2e, 0x41, 0x83, 0xb7, 0x8f, 0x06, 0x3f, 0xa4, 0x94, 0xbb, 0x0b, 0x41, 0x64, 0xa2, 0x13, 0x4e, 0x42, 0xdd, 0xdd, + 0x6b, 0xe1, 0xd7, 0xd5, 0x5b, 0x96, 0x8a, 0xf8, 0xa3, 0xc4, 0x36, 0xad, 0x2a, 0xf6, 0x86, 0xee, 0x16, 0x7b, 0x4c, + 0x77, 0x8a, 0xdd, 0xdb, 0x53, 0xec, 0x97, 0xdd, 0x62, 0x7f, 0xc9, 0x40, 0xd3, 0xc8, 0x7f, 0x38, 0x3d, 0x6f, 0x35, + 0x4e, 0x01, 0x59, 0x4f, 0xcf, 0x5b, 0x55, 0xa1, 0x87, 0xb4, 0x5a, 0x2b, 0x4d, 0xae, 0xa9, 0xf5, 0xb5, 0xe0, 0xde, + 0xe9, 0xdb, 0x2c, 0x9c, 0x75, 0x39, 0x2f, 0xfd, 0xcb, 0x07, 0x5d, 0xb0, 0x65, 0x11, 0x86, 0xda, 0xe9, 0xc1, 0xf9, + 0xb0, 0x3f, 0x63, 0x71, 0x03, 0x52, 0x51, 0x3a, 0xd1, 0xee, 0x17, 0x2a, 0xaf, 0xb4, 0xff, 0x92, 0x90, 0xd4, 0x19, + 0x22, 0x2c, 0x49, 0x43, 0x0f, 0x4e, 0x87, 0xe6, 0xbc, 0x2b, 0xe0, 0xf7, 0x99, 0xf9, 0x5d, 0x2a, 0x94, 0x9c, 0x43, + 0xc6, 0xec, 0x66, 0x14, 0xf5, 0x05, 0x79, 0x43, 0x63, 0x63, 0x63, 0x8f, 0xd2, 0x32, 0x43, 0x7d, 0x85, 0x8c, 0x7b, + 0x65, 0x86, 0x20, 0xaf, 0x85, 0xfb, 0x8d, 0x57, 0x45, 0x0a, 0xf6, 0x36, 0x78, 0x9a, 0x82, 0xad, 0x0d, 0x1e, 0xa5, + 0x02, 0xfc, 0x41, 0x68, 0xca, 0x02, 0x2b, 0xfe, 0xa7, 0x4e, 0x83, 0x67, 0x6e, 0x9d, 0x89, 0xc1, 0xd2, 0x1e, 0x83, + 0x93, 0xe2, 0x2f, 0x19, 0xc3, 0xdf, 0x86, 0x46, 0x98, 0x41, 0x9b, 0x0c, 0x61, 0x9e, 0x14, 0x04, 0xd2, 0x30, 0x4f, + 0xa6, 0x84, 0x41, 0x93, 0x3c, 0x19, 0x11, 0x36, 0xe8, 0x04, 0x68, 0xf2, 0xc2, 0xc0, 0x0e, 0x80, 0xc3, 0xeb, 0x17, + 0xf9, 0xda, 0x36, 0x0e, 0x16, 0x02, 0xd0, 0x84, 0x20, 0x10, 0x73, 0x61, 0x00, 0x66, 0x23, 0xca, 0xfe, 0xec, 0x54, + 0xe1, 0x2f, 0x79, 0x42, 0x0d, 0xf5, 0xfe, 0x13, 0xc8, 0x6a, 0x7c, 0x6f, 0xc5, 0x36, 0xf8, 0xe0, 0xde, 0x4a, 0x6c, + 0x7e, 0x80, 0x3f, 0xca, 0xfe, 0x01, 0xe6, 0x21, 0xa1, 0x68, 0x83, 0xfe, 0x4c, 0xa1, 0xd8, 0x9e, 0x52, 0xe8, 0x4f, + 0xef, 0x0e, 0xa8, 0xc8, 0xea, 0x36, 0x8d, 0xc6, 0xb4, 0xf8, 0x12, 0xe1, 0xdf, 0xd3, 0x28, 0x07, 0x6e, 0x31, 0xc2, + 0x1f, 0xd3, 0xa8, 0x60, 0x11, 0xfe, 0x67, 0x1a, 0x8d, 0xf2, 0x65, 0x84, 0x7f, 0x4b, 0xa3, 0x69, 0x11, 0xe1, 0x0f, + 0xa0, 0xac, 0x1d, 0xf3, 0xe5, 0x3c, 0xc2, 0xef, 0xd3, 0x48, 0x19, 0x6f, 0x08, 0xfc, 0x30, 0x8d, 0x18, 0x8b, 0xf0, + 0xbb, 0x34, 0x92, 0x79, 0x84, 0xaf, 0xd3, 0x48, 0x16, 0x11, 0x7e, 0x94, 0x46, 0x05, 0x8d, 0xf0, 0xe3, 0x34, 0x82, + 0x42, 0xd3, 0x08, 0x3f, 0x49, 0x23, 0x68, 0x59, 0x45, 0xf8, 0x6d, 0x1a, 0x71, 0x11, 0xe1, 0x5f, 0xd3, 0x48, 0x2f, + 0x8b, 0xbf, 0x96, 0x92, 0xab, 0x08, 0x3f, 0x4d, 0xa3, 0x19, 0x8f, 0xf0, 0x9b, 0x34, 0x2a, 0x64, 0x84, 0x5f, 0xa7, + 0x11, 0xcd, 0x23, 0xfc, 0x2a, 0x8d, 0x72, 0x16, 0xe1, 0x5f, 0xd2, 0x68, 0xcc, 0x22, 0xfc, 0x32, 0x8d, 0xee, 0x58, + 0x9e, 0xcb, 0x08, 0x3f, 0x4b, 0x23, 0x26, 0x22, 0xfc, 0x73, 0x1a, 0x65, 0xb3, 0x08, 0xff, 0x23, 0x8d, 0x68, 0xf1, + 0x45, 0x45, 0xf8, 0x79, 0x1a, 0x31, 0x1a, 0xe1, 0x17, 0xb6, 0xa3, 0x69, 0x84, 0x7f, 0x4a, 0xa3, 0x9b, 0x59, 0xb4, + 0xc1, 0x52, 0x91, 0xd5, 0x6b, 0x9e, 0xb1, 0x7f, 0xb2, 0x34, 0x9a, 0xb4, 0x26, 0x17, 0x93, 0x49, 0x84, 0xa9, 0xd0, + 0xfc, 0xaf, 0x25, 0xbb, 0x79, 0xaa, 0x21, 0x91, 0xb2, 0xd1, 0xf8, 0x7e, 0x84, 0xe9, 0x5f, 0x4b, 0x9a, 0x46, 0x93, + 0x89, 0x29, 0xf0, 0xd7, 0x92, 0xce, 0x69, 0xf1, 0x96, 0xa5, 0xd1, 0xfd, 0xc9, 0x64, 0x32, 0x3e, 0x8b, 0x30, 0xfd, + 0x7b, 0xf9, 0xd1, 0xb4, 0x60, 0x0a, 0x8c, 0x18, 0x9f, 0x42, 0xdd, 0xee, 0xa4, 0x3b, 0xce, 0x22, 0x3c, 0xe2, 0xea, + 0xaf, 0x25, 0x7c, 0x4f, 0xd8, 0x59, 0x76, 0x16, 0xe1, 0x51, 0x4e, 0xb3, 0x2f, 0x69, 0xd4, 0x32, 0xbf, 0xc4, 0xcf, + 0x6c, 0xfc, 0x7a, 0x2e, 0xcd, 0x55, 0xc6, 0x84, 0x8d, 0xb2, 0x71, 0x84, 0xcd, 0x60, 0x26, 0xf0, 0xf7, 0x2b, 0x7f, + 0xc7, 0x74, 0x1a, 0x5d, 0xd0, 0xce, 0x88, 0x75, 0x22, 0x3c, 0x7a, 0x73, 0x23, 0xd2, 0x88, 0x76, 0x3b, 0xb4, 0x43, + 0x23, 0x3c, 0x5a, 0x16, 0xf9, 0xdd, 0x8d, 0x94, 0x63, 0x00, 0xc2, 0xe8, 0xe2, 0xe2, 0x7e, 0x84, 0x33, 0xfa, 0x8b, + 0x86, 0xda, 0xdd, 0xc9, 0x03, 0x46, 0x5b, 0x11, 0xfe, 0x99, 0x16, 0xfa, 0xe3, 0x52, 0xb9, 0x81, 0xb6, 0x20, 0x45, + 0x66, 0xef, 0x40, 0xcd, 0x1f, 0x8d, 0x3b, 0xe7, 0x0f, 0xda, 0x2c, 0xc2, 0xd9, 0xf5, 0x6b, 0xe8, 0xed, 0xfe, 0xa4, + 0xdb, 0x82, 0x0f, 0x01, 0x72, 0x29, 0x2b, 0xa0, 0x91, 0xf3, 0xb3, 0x07, 0x5d, 0x36, 0x36, 0x89, 0x8a, 0xe7, 0x5f, + 0xcc, 0xec, 0x2f, 0x60, 0x3e, 0x59, 0xc1, 0xe7, 0x4a, 0x8a, 0x34, 0x1a, 0x67, 0xed, 0xb3, 0x53, 0x48, 0xb8, 0xa3, + 0xc2, 0x03, 0xe7, 0x16, 0xaa, 0x5e, 0x8c, 0x22, 0x7c, 0x6b, 0x53, 0x2f, 0x46, 0xe6, 0x63, 0xfa, 0xee, 0x17, 0xf1, + 0x66, 0x9c, 0x46, 0xa3, 0x8b, 0x8b, 0xf3, 0x16, 0x24, 0xfc, 0x46, 0xef, 0xd2, 0x88, 0x3e, 0x80, 0xff, 0x20, 0xfb, + 0xe3, 0x33, 0xe8, 0x10, 0x46, 0x78, 0x3b, 0xfd, 0x18, 0xe6, 0x7c, 0x99, 0xd1, 0x2f, 0x3c, 0x8d, 0x46, 0xe3, 0xd1, + 0xfd, 0x73, 0xa8, 0x37, 0xa7, 0xd3, 0x67, 0x9a, 0x42, 0xbb, 0xad, 0x96, 0x69, 0xf9, 0x1d, 0xff, 0xca, 0x4c, 0xf5, + 0x6e, 0xf7, 0x7c, 0xd4, 0x81, 0x11, 0x5c, 0x83, 0x42, 0x05, 0xc6, 0x73, 0x91, 0x99, 0x06, 0xaf, 0xb3, 0xa7, 0xe3, + 0x34, 0x7a, 0xf0, 0xe0, 0xb4, 0x93, 0x65, 0x11, 0xbe, 0xfd, 0x38, 0xb6, 0xb5, 0x4d, 0x9e, 0x02, 0xd8, 0xa7, 0x11, + 0x7b, 0xf0, 0xe0, 0xfc, 0x3e, 0x85, 0xef, 0xe7, 0xa6, 0xad, 0x8b, 0xc9, 0x28, 0xbb, 0x80, 0xb6, 0xde, 0xc3, 0x74, + 0xce, 0x2e, 0x4e, 0xc7, 0xa6, 0xaf, 0xf7, 0x66, 0xd4, 0x9d, 0xc9, 0xd9, 0xe4, 0xcc, 0x64, 0x9a, 0xa1, 0x96, 0x9f, + 0xbf, 0xb2, 0x34, 0xca, 0xd8, 0xb8, 0x1d, 0xe1, 0x5b, 0xb7, 0x70, 0x0f, 0xce, 0x5a, 0xad, 0xf1, 0x69, 0x84, 0xc7, + 0x0f, 0x17, 0x8b, 0xb7, 0x06, 0x82, 0xed, 0xb3, 0x07, 0xf6, 0x5b, 0x7d, 0xb9, 0x83, 0xa6, 0x47, 0x06, 0x68, 0x63, + 0x3e, 0x37, 0x2d, 0x9f, 0x3f, 0x80, 0xff, 0xcc, 0xb7, 0x69, 0xba, 0xfc, 0x96, 0xe3, 0xa9, 0x5d, 0x94, 0x36, 0x7b, + 0xd0, 0x82, 0x1a, 0x13, 0xfe, 0x71, 0x54, 0x70, 0x40, 0xa3, 0x51, 0x07, 0xfe, 0x2f, 0xc2, 0x93, 0xfc, 0xfa, 0xb5, + 0xc3, 0xd9, 0xc9, 0x84, 0x4e, 0x5a, 0x11, 0x9e, 0xc8, 0x8f, 0x4a, 0xff, 0xf6, 0x50, 0xa4, 0x51, 0xa7, 0x73, 0x31, + 0x32, 0x65, 0x96, 0x3f, 0x2b, 0x6e, 0xf0, 0xb8, 0x65, 0x5a, 0x99, 0xd2, 0xb7, 0x6a, 0x74, 0x2d, 0x61, 0x25, 0xe1, + 0xbf, 0x08, 0x4f, 0x41, 0x0b, 0xe7, 0x5a, 0xb9, 0xb0, 0xdb, 0x61, 0xfa, 0xce, 0xa0, 0xe6, 0xf8, 0x3e, 0xc0, 0xcb, + 0x2f, 0xe3, 0x98, 0xd2, 0x6e, 0xa7, 0x15, 0x61, 0x33, 0xea, 0x8b, 0x16, 0xfc, 0x17, 0x61, 0x0b, 0x39, 0x03, 0xd7, + 0xe9, 0xc7, 0x67, 0x2f, 0x6f, 0xd2, 0x88, 0x8e, 0x27, 0x13, 0x58, 0x12, 0x33, 0x19, 0x5f, 0x6c, 0x26, 0x05, 0xbb, + 0xfb, 0xe5, 0xc6, 0x6d, 0x17, 0x93, 0xa0, 0x1d, 0x74, 0xce, 0x1f, 0x8c, 0xce, 0x22, 0xfc, 0x76, 0xcc, 0xa9, 0x80, + 0x55, 0xca, 0xc6, 0xdd, 0xac, 0x9b, 0x99, 0x84, 0xa9, 0x4c, 0xa3, 0x33, 0x58, 0xf2, 0x4e, 0x84, 0xf9, 0xd7, 0xeb, + 0x3b, 0x8b, 0x6e, 0x50, 0xdb, 0x21, 0xc8, 0xa4, 0xc5, 0xce, 0x2f, 0xb2, 0x08, 0xe7, 0xf4, 0xeb, 0xb3, 0x5f, 0x8a, + 0x34, 0x62, 0xe7, 0xec, 0x7c, 0x42, 0xfd, 0xf7, 0x3f, 0xd5, 0xcc, 0xd4, 0x68, 0x4d, 0xba, 0x90, 0x74, 0x23, 0xcc, + 0x58, 0xef, 0x67, 0x13, 0x83, 0x21, 0xaf, 0xe6, 0x52, 0x64, 0x4f, 0x27, 0x13, 0x69, 0xb1, 0x98, 0xc2, 0x26, 0xfc, + 0x1d, 0xa0, 0x4d, 0xc7, 0xe3, 0x0b, 0x76, 0x1e, 0xe1, 0xdf, 0xed, 0x2e, 0x71, 0x13, 0xf8, 0xdd, 0x62, 0x36, 0x73, + 0xbb, 0xfd, 0x77, 0x0b, 0x14, 0x98, 0xef, 0x84, 0x4e, 0xe8, 0xb8, 0x13, 0xe1, 0xdf, 0x0d, 0x5c, 0xc6, 0xa7, 0xf0, + 0x1f, 0x14, 0x80, 0xce, 0x1e, 0xb4, 0x18, 0x7b, 0xd0, 0x32, 0x5f, 0x61, 0x9e, 0x9b, 0xf9, 0xe8, 0x3c, 0x6b, 0x47, + 0xf8, 0x77, 0x87, 0x8e, 0x93, 0x09, 0x6d, 0x01, 0x3a, 0xfe, 0xee, 0xd0, 0xb1, 0xd3, 0x1a, 0x75, 0xa8, 0xf9, 0xb6, + 0x58, 0x73, 0x71, 0x3f, 0x63, 0x30, 0xb9, 0xdf, 0x2d, 0x42, 0xde, 0xbf, 0x7f, 0x71, 0xf1, 0xe0, 0x01, 0x7c, 0x9a, + 0xb6, 0xcb, 0x4f, 0xa5, 0x1f, 0xe6, 0x06, 0xc9, 0x5a, 0xd9, 0x19, 0xd0, 0xc9, 0xdf, 0xcd, 0x18, 0x27, 0x93, 0x09, + 0x6b, 0x45, 0x38, 0xe7, 0x73, 0x66, 0x31, 0xc1, 0xfe, 0x36, 0x1d, 0x9d, 0x76, 0xb2, 0xf1, 0x69, 0x27, 0xc2, 0xf9, + 0xdb, 0x67, 0x66, 0x36, 0x2d, 0x98, 0xbd, 0xdf, 0x72, 0x1e, 0x6b, 0xe6, 0xf4, 0x0d, 0x0c, 0x12, 0x56, 0x1a, 0x2a, + 0x7f, 0x08, 0xe8, 0xe1, 0xf9, 0x79, 0x36, 0x86, 0x81, 0x7e, 0x80, 0x6e, 0x01, 0x8c, 0x1f, 0xec, 0xe6, 0x1b, 0xd1, + 0x6e, 0x17, 0xa6, 0xfb, 0x61, 0xb1, 0x2c, 0x16, 0xaf, 0xd2, 0xe8, 0xc1, 0xe9, 0xfd, 0xd6, 0x78, 0x14, 0xe1, 0x0f, + 0x6e, 0x82, 0xa7, 0xd9, 0xe8, 0xf4, 0x7e, 0x3b, 0xc2, 0x1f, 0xcc, 0x7e, 0xbb, 0x3f, 0x3a, 0xbf, 0x80, 0x73, 0xe3, + 0x83, 0x5a, 0x14, 0x6f, 0xa7, 0xa6, 0xc0, 0x84, 0x3e, 0x80, 0x66, 0x7f, 0x35, 0xbb, 0x71, 0xdc, 0x86, 0x8d, 0xfc, + 0xc1, 0x6c, 0x32, 0x83, 0x27, 0xf7, 0xdb, 0xdd, 0x8b, 0x6e, 0x84, 0xe7, 0x7c, 0x2c, 0x80, 0xc0, 0x9b, 0x8d, 0xf2, + 0xa0, 0xfd, 0xe0, 0x7e, 0x2b, 0xc2, 0xf3, 0xb7, 0x3a, 0xfb, 0x48, 0xe7, 0x86, 0x1a, 0x4f, 0x00, 0x66, 0x73, 0xae, + 0xf4, 0xdd, 0x1b, 0xe5, 0xe8, 0x31, 0x6b, 0x47, 0x78, 0x2e, 0xb3, 0x8c, 0xaa, 0xb7, 0x36, 0x61, 0xd4, 0x8d, 0xb0, + 0xa0, 0x5f, 0xe9, 0x67, 0xe9, 0x37, 0xd3, 0x98, 0xd1, 0xb1, 0x49, 0x33, 0x38, 0x1c, 0xe1, 0x77, 0x63, 0xb8, 0x8c, + 0x4c, 0xa3, 0xc9, 0x78, 0xd2, 0x05, 0xf0, 0x00, 0x01, 0xb2, 0xd8, 0x0d, 0xd0, 0x80, 0xaf, 0xf1, 0xa3, 0x51, 0x1a, + 0x9d, 0x8f, 0x2e, 0x58, 0xe7, 0x34, 0xc2, 0x25, 0x35, 0xa2, 0x5d, 0xc8, 0x37, 0x9f, 0x1f, 0xcd, 0x96, 0x3a, 0xb3, + 0x09, 0x06, 0x40, 0x63, 0x7a, 0xbf, 0x35, 0x3e, 0x8f, 0xf0, 0xe2, 0x35, 0xf3, 0x7b, 0x8c, 0x31, 0x76, 0x01, 0xb0, + 0x84, 0x24, 0x83, 0x40, 0x17, 0x93, 0xd1, 0x83, 0x0b, 0xf3, 0x0d, 0x60, 0xa0, 0x13, 0xc6, 0x00, 0x48, 0x8b, 0xd7, + 0xac, 0x04, 0xc4, 0x78, 0x74, 0xbf, 0x05, 0xf4, 0x65, 0x41, 0x17, 0xf4, 0x8e, 0xde, 0x3c, 0x5d, 0x98, 0x39, 0x4d, + 0xc6, 0xdd, 0x08, 0x2f, 0x9e, 0xff, 0xbc, 0x58, 0x4e, 0x26, 0x66, 0x42, 0x74, 0xf4, 0x20, 0xc2, 0x0b, 0x56, 0x2c, + 0x61, 0x8d, 0x2e, 0xba, 0xa7, 0x93, 0x08, 0x3b, 0x34, 0xcc, 0x5a, 0xd9, 0x08, 0x6e, 0x5b, 0x97, 0xf3, 0x34, 0x1a, + 0x8f, 0x69, 0x6b, 0x0c, 0x77, 0xaf, 0xf2, 0xe6, 0x97, 0xc2, 0xa2, 0x11, 0x33, 0xf8, 0xe0, 0xd6, 0x10, 0xe6, 0x0b, + 0xf0, 0xf8, 0x38, 0x62, 0x59, 0x46, 0x5d, 0xe2, 0xf9, 0xf9, 0xe9, 0x29, 0xe0, 0x9e, 0x9d, 0xa1, 0x45, 0x90, 0x37, + 0xea, 0x6e, 0x54, 0x48, 0x38, 0xba, 0x80, 0xa8, 0x02, 0x59, 0x7d, 0x73, 0xf7, 0xda, 0xd0, 0xd5, 0xf6, 0xf9, 0x03, + 0x58, 0x00, 0x45, 0xc7, 0xe3, 0x57, 0xf6, 0x70, 0xbb, 0x18, 0x9d, 0x75, 0xdb, 0xa7, 0x11, 0xf6, 0x1b, 0x81, 0x5e, + 0xb4, 0xee, 0x77, 0xa0, 0x84, 0x18, 0xdf, 0xd9, 0x12, 0x93, 0x33, 0x7a, 0x76, 0xde, 0x8a, 0xb0, 0xdf, 0x1a, 0xec, + 0x62, 0xd4, 0xbd, 0x0f, 0x9f, 0x6a, 0xc6, 0xf2, 0xdc, 0xe0, 0x77, 0x17, 0xe0, 0xa2, 0xf8, 0x33, 0x41, 0xd3, 0x88, + 0xb6, 0xba, 0x9d, 0xce, 0x18, 0x3e, 0xf3, 0xaf, 0xac, 0x48, 0xa3, 0xac, 0x05, 0xff, 0x45, 0x38, 0xd8, 0x49, 0x6c, + 0x14, 0x61, 0x83, 0x77, 0xe7, 0xb4, 0x6b, 0xf6, 0xbe, 0xdb, 0x55, 0xad, 0x8b, 0x16, 0x6c, 0x58, 0xb7, 0xa9, 0xdc, + 0x97, 0x12, 0xf2, 0xc6, 0x91, 0x58, 0x1a, 0xe1, 0x00, 0x41, 0x27, 0xf7, 0x27, 0x11, 0xf6, 0x3b, 0xee, 0xec, 0xfc, + 0xa2, 0x03, 0xa4, 0x4c, 0x03, 0xa1, 0x18, 0x77, 0x46, 0x67, 0x40, 0x9a, 0x34, 0x7b, 0x6d, 0xf1, 0x24, 0xc2, 0xfa, + 0xa9, 0xd2, 0xaf, 0xd2, 0x68, 0x7c, 0x31, 0x9a, 0x8c, 0x2f, 0x22, 0xac, 0xe5, 0x9c, 0x6a, 0x69, 0x28, 0xe0, 0xe9, + 0xd9, 0xfd, 0x08, 0x1b, 0x34, 0x6f, 0xb1, 0xd6, 0xb8, 0x15, 0x61, 0x77, 0x94, 0x30, 0x76, 0xd1, 0x81, 0x69, 0xfd, + 0xf4, 0x5c, 0x03, 0x2e, 0x8f, 0xd9, 0xe8, 0x34, 0xc2, 0x25, 0xbd, 0x37, 0x84, 0x08, 0xbe, 0xd4, 0x5c, 0x7e, 0x71, + 0xac, 0x07, 0x90, 0x3a, 0xbf, 0xe1, 0x61, 0x19, 0x5e, 0xde, 0x58, 0x34, 0xa2, 0x66, 0x8b, 0x07, 0xb7, 0xd1, 0x4f, + 0x68, 0xec, 0xd9, 0x76, 0x4e, 0x56, 0x1b, 0x5c, 0x06, 0x79, 0xfd, 0xc2, 0xee, 0x54, 0x2c, 0x95, 0xe1, 0x64, 0x83, + 0x14, 0xa5, 0x90, 0x77, 0x6b, 0x70, 0x9e, 0xab, 0x20, 0x48, 0x0a, 0xd2, 0xea, 0x89, 0x4b, 0xef, 0x4d, 0xdb, 0x13, + 0x10, 0xfa, 0x01, 0xd2, 0x0b, 0x42, 0x89, 0x86, 0x08, 0x39, 0x56, 0x98, 0xf4, 0x4e, 0x06, 0x46, 0xa6, 0x94, 0xd6, + 0x6d, 0x81, 0x12, 0xea, 0x63, 0xe3, 0xc7, 0x12, 0x2b, 0x88, 0x1e, 0x85, 0x7a, 0x92, 0x98, 0x48, 0xd7, 0x2f, 0x84, + 0x8e, 0xa5, 0x1a, 0x14, 0x43, 0xdc, 0x3e, 0x47, 0x18, 0x62, 0x48, 0x90, 0x81, 0xbc, 0xba, 0x6a, 0x9f, 0x1f, 0x19, + 0xa1, 0xef, 0xea, 0xea, 0xc2, 0xfe, 0x80, 0x7f, 0x87, 0x55, 0xdc, 0x6e, 0x18, 0xdf, 0x07, 0x56, 0xcd, 0xf1, 0x9d, + 0xe1, 0xaf, 0x3f, 0xb0, 0xf5, 0x3a, 0xfe, 0xc0, 0x08, 0xcc, 0x18, 0x7f, 0x60, 0x89, 0xb9, 0x23, 0xb1, 0x1e, 0x42, + 0x64, 0x00, 0x9a, 0xb3, 0x16, 0x86, 0x68, 0xf2, 0x9e, 0xf3, 0xfe, 0xc0, 0x06, 0xbc, 0xee, 0x5d, 0x5e, 0x85, 0x70, + 0x3e, 0x3a, 0x5a, 0x15, 0xa9, 0xb6, 0x62, 0x82, 0xb6, 0x62, 0x82, 0xb6, 0x62, 0x82, 0xae, 0x82, 0xe8, 0x9f, 0xf5, + 0x41, 0x4a, 0x31, 0xca, 0x16, 0xc7, 0x53, 0xbf, 0x04, 0xb5, 0x07, 0x68, 0x27, 0xfb, 0x95, 0xb2, 0xa3, 0xd4, 0x55, + 0xec, 0x55, 0x60, 0xec, 0x4d, 0x74, 0xda, 0x8e, 0x93, 0x7f, 0x47, 0xdd, 0xf1, 0xb6, 0x26, 0x96, 0xbd, 0xdc, 0x2b, + 0x96, 0xc1, 0x4a, 0x1a, 0xd1, 0xec, 0xd0, 0xc6, 0x23, 0xd1, 0x83, 0xfb, 0x46, 0x30, 0xab, 0x82, 0xe4, 0x35, 0x20, + 0xa9, 0x07, 0x52, 0xc8, 0x85, 0x91, 0xd2, 0x0a, 0x94, 0x8e, 0x75, 0x5c, 0x80, 0x86, 0xd2, 0x2b, 0x28, 0xcb, 0x58, + 0xae, 0x0d, 0x03, 0x10, 0x65, 0x65, 0x34, 0x2b, 0xab, 0x75, 0x41, 0x74, 0x01, 0x4d, 0x98, 0x91, 0x58, 0xa0, 0x01, + 0x61, 0x1a, 0x10, 0xae, 0x32, 0x88, 0x33, 0x2e, 0xfb, 0xcc, 0x64, 0x2b, 0x93, 0xad, 0xca, 0x6c, 0xe9, 0xb3, 0xad, + 0x90, 0x28, 0x4d, 0xb6, 0x2c, 0xb3, 0x41, 0x66, 0xc3, 0xd3, 0x54, 0xe1, 0x51, 0x2a, 0xad, 0xa8, 0x56, 0xc9, 0x56, + 0xcf, 0x68, 0xa8, 0xcd, 0x3d, 0x3a, 0x8a, 0x4b, 0x39, 0xc9, 0xa8, 0x89, 0xef, 0xad, 0x78, 0x52, 0x18, 0x19, 0x88, + 0x27, 0x53, 0xf7, 0x77, 0xb4, 0xd9, 0x96, 0x95, 0x8a, 0xe9, 0xe8, 0x1b, 0x25, 0xd1, 0x9f, 0x5e, 0x89, 0xfa, 0x81, + 0x9b, 0x28, 0x40, 0x97, 0x24, 0x69, 0xb5, 0x4e, 0xdb, 0xa7, 0xad, 0x8b, 0x3e, 0x3f, 0x6e, 0x77, 0x92, 0x07, 0x9d, + 0xd4, 0x28, 0x22, 0x16, 0xf2, 0x06, 0x14, 0x30, 0x27, 0x9d, 0xe4, 0x0c, 0x1d, 0xb7, 0x93, 0x56, 0xb7, 0xdb, 0x84, + 0x7f, 0xf0, 0x23, 0x5d, 0x56, 0x3b, 0x6b, 0x9d, 0x75, 0xfb, 0xfc, 0x64, 0xab, 0x52, 0xcc, 0x1b, 0x50, 0x10, 0x9d, + 0x98, 0x4a, 0x18, 0xea, 0x57, 0xcb, 0xfb, 0x6a, 0x47, 0xcf, 0xf3, 0x48, 0xc7, 0xd2, 0xaa, 0xe2, 0x00, 0xaa, 0xfe, + 0x6b, 0x6a, 0x80, 0xe8, 0xbf, 0x46, 0x65, 0xa4, 0xde, 0x55, 0x01, 0xa2, 0xf6, 0x07, 0x1e, 0x8b, 0x06, 0x3b, 0x8e, + 0x6d, 0xbe, 0x86, 0xba, 0x4d, 0x88, 0x9e, 0x87, 0xa7, 0x2e, 0x57, 0x85, 0xb9, 0x53, 0x84, 0x9a, 0x0a, 0x72, 0x47, + 0x2e, 0x57, 0x86, 0xb9, 0x23, 0x84, 0x9a, 0x12, 0x72, 0x69, 0xca, 0x13, 0x0a, 0x39, 0x3a, 0xa1, 0x4d, 0x03, 0xc9, + 0x6a, 0x51, 0x9e, 0x33, 0x3f, 0x6c, 0x3e, 0x81, 0xe5, 0x31, 0x04, 0xc5, 0x09, 0xd2, 0x02, 0x5e, 0x58, 0x29, 0xb5, + 0x39, 0x2d, 0x5c, 0xaa, 0x71, 0x20, 0xa3, 0x01, 0xff, 0x1c, 0x33, 0xf3, 0xec, 0x46, 0xab, 0x7f, 0x7a, 0xde, 0x4a, + 0xdb, 0xe0, 0x2a, 0x0e, 0xb2, 0xb6, 0xb0, 0xb2, 0xb6, 0xf0, 0xb2, 0xb6, 0xf0, 0xb2, 0x36, 0x08, 0xf0, 0x41, 0xdf, + 0xff, 0x94, 0x35, 0xf3, 0x1b, 0x5e, 0xda, 0xf2, 0x58, 0x63, 0x8d, 0x58, 0xaf, 0xd7, 0xab, 0x0d, 0x58, 0x5a, 0x95, + 0x35, 0x0a, 0x55, 0xa9, 0x3f, 0x57, 0x45, 0xda, 0xc2, 0xd3, 0x14, 0xb4, 0xdc, 0x2d, 0x4c, 0xcd, 0xe6, 0xf6, 0x54, + 0x61, 0x3b, 0x8a, 0x4f, 0xdf, 0xab, 0x93, 0xaf, 0xc8, 0xa9, 0xd1, 0x1e, 0xaf, 0x8a, 0x94, 0x5b, 0x9a, 0xc1, 0x2d, + 0xcd, 0xe0, 0x96, 0x66, 0x40, 0x23, 0xb8, 0x2c, 0x6c, 0xca, 0x26, 0x94, 0xc0, 0x95, 0xc0, 0xe0, 0x74, 0x08, 0x41, + 0x0c, 0x63, 0x4d, 0xcc, 0xa8, 0xb7, 0x3a, 0x6f, 0x43, 0xd0, 0x36, 0x5b, 0x52, 0x27, 0xd4, 0xf8, 0xae, 0x97, 0x63, + 0xfe, 0xbb, 0x86, 0xf6, 0x09, 0xbc, 0xa8, 0xf3, 0x50, 0xc7, 0x2d, 0x30, 0x5d, 0x89, 0x8a, 0xa8, 0x6f, 0xc8, 0x42, + 0x6a, 0x74, 0x36, 0xce, 0x24, 0xfd, 0xcb, 0x96, 0x27, 0xb0, 0xa5, 0x04, 0xe1, 0x3b, 0x12, 0x5f, 0x58, 0x15, 0x9a, + 0xa0, 0xb4, 0xb8, 0x75, 0xe6, 0x72, 0xf6, 0x48, 0xe8, 0x81, 0xd9, 0xbc, 0x8f, 0x79, 0xd5, 0x17, 0xa4, 0x80, 0x98, + 0x8f, 0xa9, 0x49, 0x74, 0x51, 0x9b, 0xc1, 0x89, 0x99, 0x7c, 0xa5, 0xc6, 0xa5, 0xe7, 0x9d, 0xfd, 0xf3, 0x37, 0x0d, + 0x7c, 0x1e, 0x8b, 0xe9, 0xc8, 0xbb, 0x0a, 0x7f, 0x32, 0xb1, 0x8d, 0xc8, 0xe1, 0xa1, 0xb5, 0x68, 0x37, 0x5f, 0xdb, + 0x26, 0xed, 0x26, 0xd1, 0x64, 0xc3, 0x0e, 0xf5, 0x6b, 0xf4, 0x4f, 0xef, 0xb1, 0x57, 0x4c, 0x47, 0x28, 0xa0, 0xd9, + 0x06, 0xac, 0xb2, 0x02, 0x96, 0x72, 0xf5, 0x4a, 0x47, 0x4e, 0xe8, 0xdd, 0x8c, 0x79, 0x53, 0x4c, 0x47, 0x7b, 0x9f, + 0x5e, 0xb1, 0x3d, 0xf6, 0x9f, 0xd1, 0xa0, 0x07, 0xaf, 0xda, 0x9e, 0xb1, 0xdb, 0xef, 0xd5, 0xf9, 0xb2, 0xb7, 0x8e, + 0xca, 0xbf, 0x57, 0xe7, 0xc5, 0xbe, 0x3a, 0x73, 0x7e, 0x1b, 0xfb, 0xbd, 0xa3, 0x03, 0x35, 0xb6, 0x31, 0x93, 0x9a, + 0x8e, 0x20, 0x56, 0x3e, 0xfc, 0xb5, 0x11, 0x6d, 0x7a, 0x9e, 0x84, 0xc3, 0x2a, 0xc8, 0x7e, 0xd2, 0x4d, 0x19, 0xa6, + 0xa4, 0x73, 0x5c, 0x98, 0x98, 0x36, 0x22, 0xa1, 0x4d, 0x95, 0x50, 0x9c, 0x93, 0x38, 0xa6, 0xc7, 0x19, 0x44, 0xe6, + 0x69, 0xf7, 0x69, 0x1a, 0xd3, 0x46, 0x86, 0x4e, 0xe2, 0x76, 0x83, 0x1e, 0x67, 0x08, 0x35, 0xda, 0xa0, 0x33, 0x95, + 0xa4, 0xdd, 0xcc, 0x21, 0x56, 0xa7, 0x21, 0xc5, 0xf9, 0xb1, 0x48, 0x8a, 0x86, 0x3c, 0x56, 0x49, 0xd1, 0x48, 0xba, + 0x58, 0x24, 0xd3, 0x32, 0x79, 0x6a, 0x92, 0xa7, 0x36, 0x79, 0x54, 0x26, 0x8f, 0x4c, 0xf2, 0xc8, 0x26, 0x53, 0x52, + 0x1c, 0x8b, 0x84, 0x36, 0xe2, 0x76, 0xb3, 0x40, 0xc7, 0x30, 0x02, 0x3f, 0x7a, 0x22, 0xc2, 0x10, 0xe9, 0x1b, 0x63, + 0x63, 0xb4, 0x90, 0xb9, 0x0b, 0x5a, 0x5a, 0x01, 0xa9, 0x74, 0xfc, 0x82, 0x3a, 0xaf, 0x02, 0x30, 0x61, 0x6d, 0xff, + 0xf8, 0x90, 0x7c, 0x9b, 0x2c, 0x97, 0x22, 0x70, 0x6c, 0x03, 0x5b, 0xfc, 0x2f, 0xce, 0x9d, 0x07, 0xa0, 0xba, 0xa1, + 0xf9, 0x62, 0x46, 0x77, 0xbc, 0x87, 0x8b, 0xe9, 0xc8, 0xed, 0xac, 0xb2, 0x19, 0x46, 0x0b, 0x1b, 0xea, 0xba, 0xee, + 0xe7, 0x09, 0xa0, 0xf6, 0xbe, 0xa5, 0x09, 0x35, 0x4a, 0x72, 0x5b, 0x63, 0x5a, 0xb0, 0x3b, 0x95, 0xd1, 0x9c, 0xc5, + 0xd5, 0x01, 0x5c, 0x0d, 0x93, 0x91, 0x27, 0xe0, 0x11, 0x50, 0x1c, 0x27, 0xa7, 0x0d, 0x9d, 0x4c, 0x8f, 0x93, 0xee, + 0x83, 0x86, 0x4e, 0x46, 0xc7, 0x49, 0xbb, 0x5d, 0xe1, 0x6c, 0x52, 0x10, 0x9d, 0x4c, 0x89, 0x06, 0x8d, 0xa1, 0x6d, + 0x54, 0x2e, 0x28, 0x98, 0xb8, 0xfd, 0x1b, 0xc3, 0x68, 0xb8, 0x61, 0x08, 0x36, 0xb5, 0x51, 0x3f, 0x77, 0xc6, 0x10, + 0x76, 0xd3, 0xe9, 0x76, 0x9b, 0x3a, 0x29, 0xb0, 0xb6, 0x2b, 0xd9, 0xd4, 0xc9, 0x14, 0x6b, 0xbb, 0x7c, 0x4d, 0x9d, + 0x8c, 0x6c, 0x53, 0x46, 0x07, 0xc8, 0x44, 0x00, 0xac, 0xe7, 0x2c, 0x80, 0x7c, 0xc7, 0x3b, 0xe9, 0x6c, 0x40, 0x6b, + 0xf8, 0xbd, 0x72, 0x4d, 0x5f, 0x50, 0x51, 0x0d, 0xa6, 0x4e, 0xec, 0x5b, 0x45, 0xdb, 0x55, 0x93, 0xec, 0x5f, 0x97, + 0x2d, 0x9b, 0x2d, 0xa4, 0xae, 0x17, 0x7c, 0x5a, 0xc3, 0x10, 0x57, 0xca, 0x1d, 0xdc, 0x9f, 0x29, 0x89, 0x21, 0xb6, + 0x9f, 0x39, 0x85, 0x38, 0xf1, 0x7a, 0x64, 0x48, 0xe2, 0x8d, 0xc6, 0x06, 0xc5, 0xc1, 0x79, 0xfb, 0x34, 0xa4, 0xaa, + 0x3b, 0x01, 0xff, 0x08, 0x89, 0x96, 0xc2, 0x9a, 0x84, 0x8e, 0xa3, 0x8a, 0x16, 0xbf, 0x75, 0xda, 0xdd, 0xda, 0x01, + 0x71, 0x74, 0xb4, 0x7d, 0x5e, 0xf8, 0xa7, 0x17, 0x76, 0x9e, 0x5b, 0xa8, 0xec, 0x09, 0xfd, 0x83, 0x50, 0xd6, 0xd2, + 0x98, 0x07, 0x88, 0xe2, 0x43, 0x6f, 0xdd, 0x37, 0x14, 0x7e, 0x50, 0xc5, 0x1d, 0x74, 0x39, 0xcd, 0x73, 0x93, 0x61, + 0xfa, 0x1a, 0x06, 0x63, 0x7b, 0x13, 0x4e, 0xa8, 0xb4, 0x95, 0xfc, 0x97, 0x1d, 0x07, 0x9d, 0xb8, 0x07, 0x6b, 0xc2, + 0x46, 0x3f, 0x87, 0x96, 0xc9, 0x15, 0x6c, 0x9c, 0x4f, 0xfa, 0x7a, 0x5d, 0x7b, 0x9e, 0xc8, 0x3e, 0x82, 0x83, 0x8e, + 0x8e, 0xb8, 0x7a, 0x06, 0xc6, 0xd4, 0x2c, 0x6e, 0x84, 0x87, 0xef, 0x5f, 0xb5, 0xd3, 0xfa, 0xb3, 0x39, 0x57, 0xd3, + 0xe0, 0xa0, 0x7b, 0x58, 0xcb, 0xdf, 0xbb, 0x12, 0x7d, 0x9d, 0x72, 0xb7, 0xd6, 0x8f, 0x2a, 0x53, 0xf5, 0x9d, 0x87, + 0xb2, 0x8e, 0x8e, 0x78, 0x15, 0xae, 0x2a, 0xfa, 0x21, 0x42, 0x7d, 0x23, 0x83, 0x3c, 0xcb, 0x25, 0x85, 0x1b, 0x51, + 0xb8, 0x62, 0x48, 0x1b, 0xfc, 0x44, 0xe3, 0x9f, 0xe5, 0xff, 0xa7, 0x46, 0x8e, 0x75, 0xda, 0xe0, 0x15, 0x4a, 0xbd, + 0x08, 0x59, 0xa1, 0x2a, 0x50, 0xa4, 0x81, 0x74, 0x68, 0x79, 0x8e, 0xca, 0xc3, 0x9c, 0x2e, 0x16, 0xf9, 0x9d, 0x79, 0x2b, 0x2c, 0xe0, 0xa8, 0xaa, 0x8b, 0x26, 0x17, 0xa5, 0x0f, 0x17, 0xc0, 0xd3, 0x03, 0xee, 0x21, 0xe3, 0x65, 0x5b, - 0x5e, 0x6e, 0x0b, 0x04, 0x92, 0x99, 0x22, 0xb2, 0xd9, 0xee, 0xaa, 0x4b, 0x90, 0xcb, 0x9a, 0x4d, 0xa4, 0x5d, 0xf0, - 0x72, 0xcc, 0x41, 0x26, 0x53, 0xd6, 0x93, 0x76, 0xcf, 0x16, 0x04, 0xc9, 0x4d, 0x1a, 0x91, 0x6d, 0x77, 0x29, 0x3e, - 0x8e, 0x01, 0x8d, 0x90, 0x15, 0xf8, 0x42, 0x61, 0x91, 0x03, 0xd7, 0x59, 0xf8, 0x8e, 0xbf, 0xd1, 0x52, 0xd1, 0x57, - 0x83, 0x01, 0x2e, 0xcc, 0xf3, 0x18, 0xe5, 0x7c, 0x0a, 0x15, 0x3c, 0xb7, 0x14, 0x88, 0x28, 0x7c, 0xb5, 0xda, 0x87, - 0xd7, 0x8c, 0x5c, 0x9b, 0xe0, 0x7a, 0xeb, 0x7e, 0x56, 0x2f, 0x97, 0xc0, 0x38, 0x18, 0x69, 0x99, 0x8b, 0x42, 0x27, - 0x6f, 0xb2, 0x0b, 0xd1, 0x6d, 0x34, 0x98, 0x09, 0x34, 0x45, 0x20, 0xaa, 0x1c, 0xf8, 0x45, 0xc2, 0x1f, 0x1b, 0x3b, - 0x4a, 0x31, 0x1b, 0x81, 0x0f, 0x42, 0x83, 0xd7, 0x12, 0x56, 0x2b, 0x65, 0x23, 0xbc, 0x98, 0x1c, 0x1b, 0xeb, 0xa5, - 0xec, 0xa7, 0x0c, 0x25, 0x5b, 0x99, 0x71, 0x70, 0xb7, 0xd5, 0xdf, 0x56, 0xfb, 0x79, 0x8f, 0xdb, 0x6b, 0x3c, 0x6e, - 0xe2, 0x26, 0x18, 0x40, 0x2d, 0x37, 0x36, 0xb8, 0xb5, 0xf3, 0x8f, 0xad, 0x51, 0x32, 0xdb, 0x84, 0xa0, 0x28, 0xe3, - 0x04, 0xd8, 0x9b, 0x5b, 0x1f, 0x37, 0x51, 0x99, 0x39, 0x29, 0xa4, 0xfb, 0x20, 0x47, 0x0f, 0x08, 0x74, 0x6e, 0x7f, - 0x56, 0x74, 0xa1, 0x92, 0x89, 0xcb, 0x31, 0xfe, 0x12, 0xdc, 0xe6, 0xf5, 0xa3, 0xeb, 0x6b, 0xb3, 0xc9, 0xaf, 0xaf, - 0x23, 0x1c, 0x1a, 0xd7, 0x47, 0x01, 0x2f, 0x18, 0x0d, 0xca, 0xd0, 0x5a, 0x66, 0xe3, 0x37, 0xdb, 0x55, 0x63, 0x8f, - 0x68, 0x85, 0x77, 0xb0, 0x3c, 0xa6, 0xf1, 0x2d, 0x67, 0xd4, 0x3e, 0x07, 0x78, 0xb3, 0x3e, 0x1f, 0x74, 0xdf, 0xc4, - 0x0a, 0x1d, 0x1c, 0xbc, 0x89, 0x25, 0xea, 0x5d, 0x31, 0x73, 0xe7, 0x06, 0xde, 0xe8, 0x7d, 0x6e, 0x86, 0x2f, 0x03, - 0x04, 0xb8, 0x62, 0x9b, 0x92, 0xcd, 0x5b, 0x13, 0xfb, 0x23, 0x85, 0xd8, 0xe2, 0x10, 0xe1, 0xd8, 0x81, 0x04, 0x7a, - 0x7d, 0x13, 0x42, 0xbb, 0xcb, 0x08, 0x03, 0x16, 0xbe, 0xf4, 0x15, 0x64, 0xc9, 0x8c, 0x15, 0x13, 0x56, 0xac, 0x56, - 0x8f, 0xa8, 0xf5, 0xff, 0xdb, 0x08, 0x55, 0xa9, 0xba, 0x8d, 0x06, 0x35, 0xe3, 0x07, 0xf1, 0x81, 0x0e, 0xf0, 0xfe, - 0x9b, 0xb8, 0x40, 0x08, 0x2c, 0x8c, 0xb8, 0x58, 0x78, 0x5f, 0xb7, 0xac, 0xb6, 0x2e, 0x05, 0x2a, 0x1b, 0xc9, 0x49, - 0x0b, 0x4f, 0x49, 0x56, 0xae, 0xd1, 0xc5, 0xb4, 0xdb, 0x68, 0xe4, 0x48, 0xc6, 0x59, 0x3f, 0x1f, 0x60, 0x8e, 0x0b, - 0xb8, 0x4c, 0xdd, 0x5e, 0x87, 0x39, 0xab, 0x51, 0x2e, 0x37, 0xdf, 0xa5, 0x1d, 0x6b, 0xfa, 0x9e, 0xae, 0x03, 0x60, - 0xbc, 0xa7, 0x01, 0x91, 0xd8, 0x05, 0x64, 0x61, 0x81, 0xac, 0x3c, 0x90, 0x85, 0x01, 0xb2, 0x42, 0xbd, 0x39, 0x04, - 0x6d, 0x52, 0x28, 0xdd, 0xa2, 0xe8, 0xf5, 0xf0, 0xa2, 0xce, 0x75, 0x05, 0x73, 0x13, 0xe1, 0xc2, 0x2d, 0x07, 0xb8, - 0xb1, 0x38, 0x6f, 0x48, 0x45, 0x96, 0x51, 0x64, 0x22, 0xed, 0xe2, 0x5b, 0xf3, 0x27, 0xb9, 0xc5, 0x77, 0xf6, 0xc7, - 0x5d, 0xa0, 0x4c, 0x7a, 0x5e, 0xd3, 0x36, 0x70, 0x17, 0x97, 0x2e, 0x4a, 0x22, 0x40, 0x6b, 0x17, 0x64, 0x51, 0xd4, - 0xdf, 0x9d, 0x53, 0x36, 0x1c, 0x86, 0x68, 0x10, 0x85, 0x45, 0x40, 0x3a, 0xff, 0xfa, 0x2b, 0x42, 0x3d, 0x01, 0xd1, - 0x8c, 0xdc, 0xc9, 0xd6, 0x6c, 0xa3, 0x46, 0x94, 0x44, 0x69, 0xec, 0x83, 0x65, 0xc0, 0xce, 0x88, 0xa2, 0xe0, 0xcd, - 0x99, 0x72, 0x18, 0x1f, 0x6a, 0xc3, 0x30, 0x83, 0xaa, 0xc2, 0x7f, 0x5c, 0x2e, 0x37, 0x83, 0x2d, 0x19, 0xa8, 0x0a, - 0x13, 0xe9, 0x06, 0xd9, 0x87, 0xd8, 0x18, 0x61, 0x07, 0x07, 0xac, 0x2f, 0x06, 0xc1, 0xcb, 0x6a, 0xd5, 0x75, 0xb8, - 0x0e, 0x17, 0x2e, 0xa6, 0x10, 0xed, 0x7e, 0xb5, 0xb2, 0x7f, 0xc9, 0x07, 0x23, 0xcd, 0xc0, 0x13, 0x79, 0xc1, 0x19, - 0x2b, 0x76, 0xcb, 0x62, 0x89, 0x96, 0xbf, 0x83, 0x65, 0x9f, 0x8b, 0x5d, 0xc8, 0xdd, 0x54, 0xdb, 0x1e, 0xea, 0x73, - 0xa3, 0x51, 0x08, 0x22, 0x07, 0x57, 0x47, 0x1a, 0x9e, 0xeb, 0x30, 0xaf, 0x16, 0x01, 0x38, 0x53, 0x65, 0x20, 0x57, - 0x38, 0x52, 0x12, 0xb0, 0xf4, 0x36, 0x74, 0x12, 0x7e, 0xd4, 0xa9, 0xa4, 0x63, 0x21, 0x01, 0x0a, 0x1c, 0x99, 0xcb, - 0x79, 0x13, 0xa8, 0x9f, 0xa1, 0x1d, 0x44, 0x2e, 0x30, 0xa1, 0xa9, 0xcb, 0x96, 0x2e, 0xa2, 0x56, 0x34, 0x93, 0x0b, - 0xc5, 0x16, 0x73, 0x38, 0xdf, 0xcb, 0xb4, 0x2c, 0xe7, 0xd9, 0x97, 0x7a, 0x0a, 0x18, 0x44, 0xde, 0xea, 0x19, 0x13, - 0x8b, 0xc8, 0xcd, 0xf3, 0x95, 0x15, 0xf7, 0xdf, 0xbc, 0xc0, 0xef, 0x49, 0xe7, 0xf0, 0x15, 0xfe, 0x48, 0xc9, 0xfb, - 0xc6, 0x2b, 0x3c, 0xe1, 0xc4, 0xf2, 0x06, 0xc9, 0x9b, 0xd7, 0x57, 0x2f, 0xde, 0xbd, 0x78, 0xff, 0xf4, 0xfa, 0xc5, - 0xab, 0x67, 0x2f, 0x5e, 0xbd, 0x78, 0xf7, 0x11, 0xff, 0x4d, 0xc9, 0xab, 0xa3, 0xf6, 0x79, 0x0b, 0x7f, 0x20, 0xaf, - 0x8e, 0x3a, 0xf8, 0x56, 0x93, 0x57, 0x47, 0x27, 0x38, 0x57, 0xe4, 0xd5, 0x61, 0xe7, 0xe8, 0x18, 0x2f, 0xb4, 0x6d, - 0x32, 0x97, 0x93, 0x76, 0x0b, 0xff, 0xed, 0xbe, 0x40, 0xbc, 0xaf, 0x66, 0x31, 0x61, 0x1b, 0xc6, 0x0f, 0xa6, 0x0c, - 0x1d, 0x2a, 0x63, 0x88, 0x72, 0x11, 0xa0, 0xd3, 0x54, 0x85, 0xe8, 0x64, 0x43, 0x49, 0x83, 0x0d, 0x23, 0xa0, 0x15, - 0x27, 0xae, 0x1d, 0x7e, 0xd4, 0x66, 0xc7, 0x40, 0x9f, 0x78, 0x29, 0x1c, 0x97, 0x2a, 0x9c, 0xb6, 0xd3, 0x62, 0x8c, - 0x73, 0x29, 0x8b, 0x78, 0x01, 0x8c, 0x80, 0xd1, 0x5a, 0xf0, 0xa3, 0x32, 0x66, 0x95, 0xb8, 0x20, 0xed, 0x5e, 0x3b, - 0x15, 0x17, 0xa4, 0xd3, 0xeb, 0xc0, 0x9f, 0xd3, 0xde, 0x69, 0xda, 0x6e, 0xa1, 0xc3, 0x60, 0x1c, 0x7f, 0xd4, 0xd0, - 0xba, 0x3f, 0xc0, 0xae, 0x0b, 0xf5, 0x77, 0xa1, 0xbd, 0x4a, 0x4f, 0x38, 0x75, 0x6c, 0xbb, 0x2b, 0x2e, 0x98, 0xd1, - 0xc3, 0xf2, 0x1f, 0x00, 0xb5, 0x8d, 0x5b, 0x4d, 0xb9, 0x71, 0xdc, 0x2f, 0x7e, 0x24, 0x50, 0x2d, 0x30, 0x4e, 0xcc, - 0x56, 0x2d, 0x04, 0x4c, 0xa3, 0xc9, 0x06, 0x73, 0xa0, 0x44, 0xc9, 0x42, 0xfb, 0xe0, 0xfe, 0xaa, 0x29, 0x51, 0x32, - 0x97, 0xf3, 0xb8, 0xa6, 0x6a, 0xf8, 0x35, 0x30, 0x73, 0xdc, 0xe7, 0xea, 0x15, 0x7d, 0x15, 0xd7, 0x78, 0x9e, 0x90, - 0xb5, 0x0b, 0xb7, 0xc5, 0x2f, 0xce, 0x8a, 0xa2, 0x06, 0xae, 0x12, 0xb0, 0x7e, 0x54, 0x4d, 0x7d, 0x01, 0xaf, 0x18, - 0xb2, 0x86, 0xbe, 0x24, 0x01, 0xf5, 0xfc, 0xa9, 0x34, 0xe3, 0x2a, 0x95, 0xd1, 0x5e, 0x11, 0x6d, 0xcc, 0x82, 0xbc, - 0x22, 0xfa, 0x42, 0x19, 0x20, 0x48, 0xc2, 0xfb, 0x62, 0x00, 0x07, 0xbe, 0x1d, 0xa0, 0x34, 0x74, 0x0e, 0xd4, 0x4a, - 0x95, 0x99, 0x90, 0xf9, 0x34, 0x21, 0x1a, 0x40, 0xf3, 0x54, 0xa9, 0xa0, 0xcc, 0x27, 0x96, 0x28, 0x18, 0xfa, 0x9f, - 0xe1, 0x06, 0x38, 0x8c, 0x0d, 0x2a, 0x06, 0xd9, 0xf7, 0x44, 0x3d, 0xbf, 0x7d, 0xde, 0x3a, 0x7a, 0x15, 0xe4, 0x8f, - 0x94, 0xb7, 0xf7, 0xf8, 0x1c, 0x50, 0x72, 0x1b, 0x54, 0xac, 0x8d, 0x7d, 0x3c, 0xb8, 0x6e, 0x08, 0x90, 0x43, 0x8d, - 0x8e, 0xcc, 0x83, 0x8e, 0x5d, 0xa4, 0x0f, 0x49, 0xbb, 0x05, 0x41, 0xdc, 0x76, 0x50, 0xbe, 0x9f, 0x36, 0x60, 0xaa, - 0x93, 0xdb, 0x26, 0xd0, 0x6a, 0x78, 0xe3, 0xe9, 0xae, 0xc9, 0x93, 0x3b, 0xac, 0x02, 0x9c, 0x61, 0x87, 0xac, 0x21, - 0x0e, 0x05, 0x72, 0xc1, 0x6f, 0xed, 0x06, 0xd0, 0x54, 0x74, 0xec, 0x5b, 0x83, 0xde, 0x38, 0xea, 0xa2, 0x99, 0x9c, - 0x1e, 0xbe, 0x3a, 0x38, 0x88, 0x65, 0x83, 0xbc, 0x47, 0x78, 0x49, 0xc1, 0x66, 0x1b, 0x7c, 0xef, 0xb8, 0x65, 0xe2, - 0x53, 0x15, 0x50, 0xc7, 0x85, 0xaa, 0x1d, 0x6b, 0x55, 0x67, 0xe5, 0x6e, 0xf0, 0x63, 0xea, 0xa0, 0x46, 0x90, 0x66, - 0x47, 0xd7, 0x09, 0xa1, 0xfc, 0x5b, 0xcd, 0x51, 0x0e, 0xb6, 0x65, 0xe3, 0x23, 0x45, 0x3f, 0xbc, 0x6f, 0xbe, 0x0a, - 0xca, 0xd4, 0x4c, 0x93, 0xde, 0x37, 0xde, 0xa3, 0x1f, 0xde, 0x07, 0xae, 0x8e, 0xbc, 0x62, 0x4f, 0x3c, 0x37, 0xf2, - 0x9b, 0xe5, 0x4a, 0x7f, 0x03, 0xc9, 0xbe, 0x20, 0xbf, 0x01, 0x96, 0x53, 0xf2, 0x5b, 0x2c, 0x9b, 0x10, 0x02, 0x92, - 0xfc, 0x16, 0x17, 0xf0, 0x23, 0x27, 0xbf, 0xc5, 0x80, 0xed, 0x78, 0x6a, 0x7e, 0x14, 0x25, 0x30, 0xc0, 0xbd, 0x4e, - 0x5a, 0x2f, 0xbb, 0x62, 0xb5, 0x12, 0x07, 0x07, 0xd2, 0xfe, 0xa2, 0x97, 0xd9, 0xc1, 0x41, 0x7e, 0x31, 0x0d, 0x6c, - 0x6f, 0xf5, 0x2e, 0xfa, 0x62, 0x10, 0x0a, 0x07, 0xa6, 0x69, 0xbc, 0x86, 0x57, 0x35, 0xca, 0x0a, 0x0d, 0x34, 0x8f, - 0x3b, 0xf7, 0xcf, 0xce, 0x31, 0xfc, 0x7b, 0x3f, 0x28, 0xf8, 0x73, 0xc9, 0x77, 0x91, 0x36, 0x6b, 0x9e, 0x55, 0x75, - 0x2e, 0x03, 0x7c, 0xc6, 0x0c, 0x35, 0xc5, 0xc1, 0x01, 0xbf, 0x08, 0x70, 0x19, 0x33, 0xd4, 0x08, 0x2c, 0xf6, 0x1e, - 0x96, 0xf6, 0x64, 0x86, 0x6b, 0x82, 0xc7, 0x7d, 0x79, 0xbf, 0x18, 0x5c, 0x68, 0x47, 0x4d, 0xc2, 0x10, 0xe0, 0x8a, - 0xb4, 0xdc, 0x26, 0xeb, 0x8a, 0xa6, 0xba, 0x6c, 0x77, 0x91, 0x24, 0xaa, 0x21, 0x2e, 0x2f, 0xdb, 0x18, 0x54, 0xf2, - 0x3d, 0x45, 0x64, 0x2a, 0x88, 0x77, 0x53, 0x5c, 0xe6, 0x32, 0x55, 0x78, 0xca, 0x53, 0xe1, 0xe5, 0xec, 0xd7, 0xde, - 0x7a, 0xda, 0x38, 0x8e, 0x9a, 0x9e, 0x19, 0x16, 0x3d, 0x55, 0x3a, 0x3c, 0xc2, 0x26, 0x55, 0x03, 0x78, 0x3b, 0xb1, - 0xc4, 0x3c, 0x66, 0xbd, 0xfc, 0x18, 0xc4, 0xa6, 0x56, 0x8d, 0x36, 0x64, 0xc2, 0xe7, 0x3a, 0x55, 0x30, 0x50, 0x53, - 0xf8, 0x02, 0xc8, 0x54, 0x56, 0x19, 0x66, 0xfb, 0x86, 0xa1, 0x80, 0x80, 0x02, 0x97, 0x84, 0x05, 0x12, 0x3c, 0xdc, - 0x7e, 0x04, 0x84, 0xa3, 0x4e, 0x2e, 0xec, 0xe4, 0x2e, 0x14, 0x74, 0x27, 0x06, 0x17, 0xba, 0x8b, 0x44, 0xa3, 0xe1, - 0xb8, 0xed, 0x4b, 0x61, 0x06, 0xd1, 0x6c, 0x0f, 0x2e, 0x59, 0x17, 0xa9, 0x66, 0xb3, 0x34, 0x80, 0xbc, 0x6c, 0xad, - 0x56, 0xea, 0xc2, 0x37, 0xd2, 0xf3, 0xe7, 0xb8, 0xe1, 0xbb, 0xbc, 0xe0, 0xf9, 0x9b, 0x24, 0xfd, 0x08, 0xa8, 0x2a, - 0xf0, 0xd9, 0x72, 0x1e, 0xe1, 0xc8, 0x3c, 0xab, 0x07, 0x7f, 0xcd, 0x73, 0x68, 0x11, 0x8e, 0xdc, 0x4b, 0x7b, 0xd1, - 0xa0, 0x1a, 0x2c, 0xcf, 0xca, 0x20, 0xf1, 0x3c, 0xb9, 0x06, 0xc6, 0x41, 0x7f, 0x56, 0x68, 0x59, 0xfd, 0x4e, 0x72, - 0x17, 0x2e, 0x45, 0xf9, 0xc7, 0xdf, 0xdc, 0xa8, 0xd6, 0xbb, 0x1d, 0x54, 0x39, 0x8e, 0x7c, 0x55, 0x78, 0x44, 0xe1, - 0x3b, 0xaf, 0x4f, 0xb6, 0xdd, 0xa3, 0xe7, 0xcb, 0xb2, 0x07, 0xe0, 0xbc, 0xd7, 0x6b, 0x84, 0x7f, 0x93, 0x3b, 0x5f, - 0x40, 0x8e, 0xae, 0xa5, 0x78, 0x42, 0x35, 0x8d, 0x1a, 0x6f, 0x8c, 0xe1, 0x9b, 0x95, 0xb3, 0xba, 0xdf, 0x1a, 0x07, - 0xfb, 0xb7, 0xba, 0x87, 0x00, 0x16, 0xb5, 0xc7, 0x9a, 0xac, 0xec, 0x6b, 0xc2, 0x96, 0xc8, 0xc0, 0xf4, 0x6d, 0x07, - 0x3c, 0xfc, 0x18, 0x29, 0xb8, 0x55, 0x5b, 0x3e, 0x89, 0x42, 0x64, 0xd8, 0x9a, 0x33, 0x37, 0xa4, 0xd8, 0x3e, 0x8c, - 0xe3, 0xef, 0x1a, 0x85, 0x5c, 0xf7, 0x58, 0xd5, 0x89, 0x69, 0xd5, 0x8d, 0x91, 0x3a, 0xd8, 0x26, 0x0b, 0xce, 0xaa, - 0xde, 0x8d, 0x84, 0x52, 0xbd, 0x6b, 0x67, 0xde, 0x26, 0x6d, 0xb6, 0xcd, 0x63, 0xcf, 0xf6, 0xf5, 0x3b, 0x05, 0x86, - 0xbc, 0xfb, 0x65, 0xd0, 0xae, 0x4b, 0x38, 0x76, 0xe3, 0x00, 0xb2, 0x92, 0x5c, 0x2e, 0xdd, 0xcb, 0x74, 0xbc, 0x2f, - 0x07, 0xeb, 0xf2, 0x9d, 0xba, 0x00, 0x0f, 0xaa, 0x91, 0x8a, 0x2c, 0xe4, 0x0c, 0xfc, 0x23, 0x8f, 0x35, 0xfd, 0x10, - 0xff, 0x07, 0x0e, 0xf8, 0x0a, 0x49, 0x53, 0xab, 0x7e, 0x82, 0xf7, 0xa3, 0x40, 0xe1, 0x6d, 0xeb, 0xfe, 0x29, 0x43, - 0x47, 0xdd, 0xba, 0x4e, 0xc5, 0xfa, 0xc2, 0xd6, 0x15, 0x2b, 0x65, 0xe1, 0x80, 0x6a, 0xc5, 0x68, 0x9d, 0x3a, 0xbf, - 0x59, 0xf7, 0xe8, 0xd4, 0x43, 0x01, 0xbe, 0x31, 0x5c, 0x8a, 0x67, 0x05, 0x44, 0x11, 0x0b, 0xf5, 0x69, 0x3f, 0xcb, - 0xf0, 0x55, 0xe5, 0x3e, 0xdc, 0x13, 0x96, 0x3c, 0x67, 0xf9, 0x12, 0x38, 0x2c, 0x90, 0x02, 0x0a, 0xa5, 0xb0, 0x58, - 0xad, 0x62, 0x01, 0x81, 0x24, 0xfe, 0x74, 0xa1, 0x85, 0xdd, 0x1b, 0x22, 0x46, 0x7f, 0x07, 0x75, 0xb1, 0x57, 0x8f, - 0x18, 0x13, 0x56, 0x14, 0x5e, 0x3a, 0xa9, 0x2c, 0xe8, 0x6b, 0x57, 0x1f, 0xa2, 0x9a, 0x72, 0x2f, 0x36, 0xfa, 0xde, - 0x77, 0x7c, 0xc6, 0xe4, 0x02, 0x1e, 0x6f, 0xc2, 0x8c, 0x28, 0xa6, 0xfd, 0x37, 0x50, 0x10, 0x78, 0x01, 0x88, 0x87, - 0xf8, 0x08, 0x7c, 0x95, 0xa7, 0x75, 0x32, 0xf3, 0x4f, 0x82, 0x44, 0x26, 0x64, 0x67, 0xd4, 0x8b, 0xc0, 0x8b, 0x08, - 0x44, 0x28, 0x42, 0x22, 0x26, 0x46, 0x51, 0x2f, 0x32, 0x2e, 0x59, 0x11, 0x58, 0x8d, 0x81, 0x92, 0x3b, 0xc2, 0x73, - 0x55, 0x11, 0xb1, 0xb0, 0xa6, 0x0e, 0x2a, 0xb1, 0xd4, 0x98, 0x69, 0x1f, 0x75, 0x2a, 0x10, 0x16, 0xd9, 0xa6, 0xa0, - 0xac, 0x37, 0xd4, 0x05, 0x58, 0x12, 0x63, 0x7a, 0xcb, 0x93, 0x6b, 0xe0, 0xe6, 0xd8, 0xc8, 0x15, 0x5d, 0xf2, 0x2b, - 0x50, 0x4f, 0xa7, 0x05, 0xbe, 0x36, 0x0c, 0xdb, 0x28, 0xa5, 0x6b, 0xc2, 0x71, 0x46, 0x8a, 0x84, 0xde, 0x42, 0x6c, - 0x8d, 0x19, 0x17, 0x69, 0x8e, 0x67, 0xf4, 0x36, 0x9d, 0xe2, 0x19, 0x17, 0x4f, 0xec, 0xb2, 0xa7, 0x23, 0x48, 0xf2, - 0x1f, 0x8b, 0x35, 0x31, 0x4f, 0x83, 0xfd, 0xae, 0x58, 0xf1, 0x08, 0x78, 0x15, 0x15, 0xa3, 0xee, 0xc8, 0xd8, 0x94, - 0x73, 0x5d, 0x19, 0xaf, 0xbf, 0xd6, 0x31, 0xc5, 0x19, 0xce, 0x51, 0x92, 0x4b, 0xcc, 0x7a, 0x22, 0x7d, 0x0d, 0x71, - 0xb5, 0x33, 0x6c, 0x9f, 0x15, 0xe3, 0xb7, 0x2c, 0x7f, 0x26, 0x8b, 0xf7, 0x66, 0xcb, 0xe7, 0x08, 0x0a, 0x81, 0x8b, - 0x8a, 0x68, 0xc2, 0xed, 0xde, 0xa2, 0x27, 0xab, 0xa6, 0xe8, 0xad, 0x6d, 0xca, 0x0d, 0x71, 0x0a, 0x01, 0x89, 0x93, - 0x29, 0x6f, 0xb4, 0x31, 0xeb, 0xb5, 0xbe, 0xd3, 0xe8, 0x14, 0x95, 0x25, 0x11, 0x86, 0xb5, 0x6a, 0xaa, 0x54, 0x12, - 0xd1, 0x54, 0x4e, 0xc2, 0x5b, 0x1a, 0x60, 0xa7, 0x0a, 0x67, 0x72, 0x21, 0x74, 0x2a, 0x03, 0xbc, 0xa1, 0xd5, 0xe6, - 0x5a, 0xde, 0x5a, 0x88, 0x69, 0x7c, 0x67, 0x7f, 0x30, 0x7c, 0x6d, 0x54, 0xfc, 0x6f, 0xc1, 0xb0, 0x47, 0xa5, 0x02, - 0xe0, 0x07, 0x86, 0xb3, 0x00, 0x39, 0xcb, 0x4f, 0xde, 0x02, 0xf8, 0x2c, 0x0b, 0x79, 0x07, 0xa9, 0xcc, 0xa4, 0xde, - 0x41, 0x2a, 0x83, 0x54, 0xe3, 0x51, 0xbf, 0x2f, 0x2a, 0x65, 0x51, 0xd8, 0x20, 0x51, 0xb8, 0x54, 0x07, 0x4b, 0x22, - 0x12, 0x68, 0xd7, 0x88, 0x72, 0x33, 0x2e, 0x20, 0xb4, 0x22, 0x34, 0x6e, 0xbf, 0xe9, 0x2d, 0x7c, 0xdf, 0xd9, 0x7c, - 0xe6, 0xf3, 0xef, 0x6c, 0xbe, 0xe9, 0xc8, 0x63, 0x7c, 0xfd, 0xb6, 0xd3, 0x58, 0xc6, 0x4b, 0x87, 0xb5, 0x1f, 0xca, - 0x87, 0x6c, 0x5a, 0xe6, 0xc1, 0x70, 0xd2, 0xc6, 0x93, 0x00, 0x29, 0x9b, 0x15, 0x0f, 0xd7, 0xc1, 0xed, 0xd6, 0x61, - 0xcc, 0x9b, 0xa4, 0x8d, 0xd0, 0xa1, 0x13, 0xae, 0x44, 0x6c, 0x24, 0xa7, 0xc3, 0xf7, 0x47, 0x70, 0xf7, 0x32, 0x53, - 0x1b, 0xbe, 0x52, 0xb6, 0x5a, 0xb3, 0xdd, 0x3a, 0xe4, 0x3b, 0xab, 0x34, 0xda, 0x78, 0xc6, 0xc8, 0x12, 0x3c, 0xd0, - 0x68, 0x61, 0x55, 0x0d, 0xe0, 0xb2, 0xfa, 0x42, 0xfc, 0xb6, 0xa0, 0x23, 0xf3, 0x7d, 0x68, 0x53, 0x5e, 0x2f, 0xb4, - 0x4f, 0x6a, 0x72, 0x18, 0x44, 0x07, 0xb9, 0x92, 0x41, 0x4e, 0xcc, 0x8f, 0x48, 0x72, 0x8a, 0x2e, 0xda, 0xbd, 0xe4, - 0xf4, 0x90, 0x1f, 0xf2, 0x14, 0x78, 0xd8, 0xb8, 0xe9, 0x2b, 0x34, 0xdb, 0xbe, 0xce, 0xe3, 0xc5, 0x90, 0x67, 0xae, - 0xf9, 0xaa, 0x83, 0x32, 0xd5, 0xce, 0x11, 0xb2, 0x00, 0xc5, 0x7c, 0x2f, 0x41, 0x76, 0xbd, 0x9b, 0x43, 0x9e, 0x42, - 0x3f, 0x50, 0xab, 0x63, 0x6b, 0x95, 0x83, 0xfb, 0x6d, 0x01, 0x08, 0xe6, 0x3b, 0xaa, 0xcd, 0xc5, 0xa6, 0x37, 0xe3, - 0xaa, 0xb3, 0x43, 0x5e, 0x8d, 0x30, 0x2c, 0xb3, 0xdd, 0x9f, 0x9f, 0x5a, 0xd5, 0xe5, 0x61, 0x00, 0x91, 0xdf, 0x16, - 0x5c, 0x84, 0x9d, 0x86, 0xdd, 0xba, 0x9c, 0xb0, 0xd3, 0xfa, 0x2c, 0x83, 0x22, 0xdb, 0xbd, 0x6e, 0xcd, 0xb4, 0x3e, - 0xdb, 0x2b, 0x70, 0x24, 0x84, 0x49, 0x99, 0x95, 0xce, 0xe0, 0x0a, 0xfd, 0xf0, 0x03, 0x72, 0xad, 0xbf, 0x5e, 0x68, - 0x9f, 0x5f, 0x22, 0x02, 0x64, 0x57, 0x5d, 0x97, 0xd5, 0xa1, 0x8f, 0xb2, 0x89, 0x57, 0x87, 0x3c, 0x58, 0xb9, 0xa7, - 0xb7, 0x73, 0x99, 0x7a, 0x7c, 0xed, 0xb5, 0xd2, 0x2d, 0xe4, 0x04, 0xe2, 0xe1, 0xba, 0x0b, 0xcb, 0x82, 0x9c, 0xdd, - 0xdc, 0x42, 0xc9, 0x70, 0xe2, 0xbe, 0xf4, 0x07, 0x66, 0xaf, 0x1b, 0xf8, 0x45, 0x72, 0x0a, 0x53, 0xdf, 0xec, 0xe1, - 0xb0, 0x03, 0x7d, 0x18, 0x38, 0x6c, 0x36, 0xe8, 0x33, 0x2b, 0x88, 0x3c, 0xe6, 0x85, 0xc5, 0xb3, 0x4b, 0xd2, 0xee, - 0xf1, 0xd4, 0x6d, 0x26, 0x23, 0x1a, 0xb5, 0x9b, 0x3c, 0x98, 0x19, 0xe0, 0x97, 0x2b, 0x1b, 0x16, 0xf1, 0xeb, 0x14, - 0x40, 0xc9, 0x17, 0xab, 0xd6, 0xa7, 0x82, 0x57, 0xbd, 0xe1, 0x74, 0x33, 0xdd, 0xaf, 0x1b, 0xdc, 0xee, 0x7a, 0x78, - 0xc2, 0x43, 0x34, 0x16, 0xad, 0xfd, 0xc4, 0x27, 0xc0, 0x01, 0x25, 0xad, 0xfb, 0xa7, 0xe0, 0x42, 0x59, 0xc2, 0x72, - 0xbb, 0xdc, 0x6c, 0xab, 0x9c, 0x85, 0xa3, 0x2d, 0x19, 0x70, 0x07, 0x9b, 0x10, 0x85, 0x0e, 0x0e, 0x3b, 0x38, 0x69, - 0xb7, 0x3b, 0xa7, 0x38, 0x39, 0x39, 0x85, 0x81, 0x36, 0x92, 0xd3, 0xc3, 0x99, 0xb2, 0x00, 0x0c, 0x72, 0xd6, 0xae, - 0xdd, 0x47, 0x10, 0xb4, 0x2a, 0x14, 0xaf, 0xf9, 0x61, 0x1c, 0xb7, 0x93, 0xfb, 0xad, 0xf6, 0xe9, 0x79, 0x03, 0x00, - 0xd4, 0x74, 0x1f, 0xae, 0xc6, 0xeb, 0x85, 0xae, 0x57, 0x29, 0x11, 0xbe, 0x5e, 0xad, 0xe1, 0xab, 0x35, 0xda, 0xeb, - 0x6a, 0x0a, 0xbe, 0xaa, 0x13, 0xce, 0x6d, 0x11, 0xaf, 0xb4, 0x09, 0xb7, 0x45, 0x6c, 0x07, 0x12, 0x83, 0x74, 0x9e, - 0x9c, 0x76, 0x4e, 0x91, 0x1d, 0x8b, 0x76, 0xf8, 0x51, 0xee, 0x93, 0xad, 0x22, 0x0d, 0x0d, 0x48, 0x52, 0xce, 0x4e, - 0x2e, 0x40, 0xa2, 0xe6, 0xe4, 0xb2, 0xdd, 0x9c, 0xb1, 0xc4, 0x4f, 0xc0, 0xa4, 0xc2, 0x72, 0x96, 0xab, 0xe0, 0x92, - 0x02, 0x40, 0x5c, 0x80, 0x71, 0xd1, 0xfd, 0xd3, 0xde, 0xfd, 0xe4, 0xf4, 0xac, 0x63, 0x89, 0x1e, 0xbf, 0xe8, 0xd4, - 0xd2, 0xcc, 0xd4, 0x93, 0x53, 0x93, 0x06, 0x5d, 0x27, 0xf7, 0x4f, 0xa1, 0x8c, 0x4b, 0x09, 0x4b, 0x41, 0xb0, 0x8d, - 0xaa, 0x18, 0x44, 0xd8, 0x48, 0x6b, 0xb9, 0x67, 0xb5, 0xec, 0xf3, 0x93, 0xe3, 0xfb, 0xa7, 0x21, 0xd4, 0xca, 0x59, - 0x98, 0x85, 0x76, 0x13, 0xf1, 0xb3, 0x83, 0xa5, 0x45, 0x87, 0xc9, 0x69, 0xba, 0x35, 0x41, 0xbb, 0x69, 0x0e, 0x0d, - 0x0e, 0x04, 0x0a, 0xc7, 0xa7, 0xc2, 0xe9, 0x4b, 0x82, 0xfb, 0xb1, 0xca, 0xd0, 0x24, 0x54, 0x38, 0xfb, 0x7b, 0xca, - 0xe0, 0x3d, 0xcd, 0xf0, 0xaa, 0xf2, 0x31, 0x15, 0x5f, 0xa9, 0x7a, 0x43, 0x21, 0x82, 0x88, 0x18, 0x44, 0x2e, 0xbe, - 0x79, 0x3d, 0xf7, 0x27, 0x70, 0x11, 0x66, 0x02, 0x2e, 0x34, 0xbd, 0x12, 0xb4, 0xe2, 0x05, 0x86, 0xa1, 0x43, 0xad, - 0x19, 0x56, 0x8f, 0xa7, 0xce, 0xa4, 0x20, 0xd4, 0x6d, 0x3d, 0xe7, 0xdf, 0x2b, 0x97, 0x94, 0x57, 0xd9, 0xc9, 0x29, - 0x4a, 0xdc, 0x65, 0x79, 0xd2, 0x46, 0x49, 0x60, 0x42, 0xe2, 0x8e, 0xe4, 0x2c, 0x23, 0xfd, 0xe8, 0x36, 0xc2, 0xd1, - 0x5d, 0x84, 0x23, 0xeb, 0xc3, 0xfc, 0x01, 0xfc, 0xb8, 0x23, 0x1c, 0x59, 0x57, 0xe6, 0x08, 0x47, 0x9a, 0x09, 0x08, - 0x2c, 0x16, 0x0d, 0x70, 0x0e, 0xa5, 0x8d, 0x67, 0x75, 0x59, 0xfa, 0xb1, 0xff, 0x2a, 0x5d, 0xaf, 0x6d, 0x4a, 0x20, - 0x65, 0x4e, 0xcd, 0x0e, 0xb5, 0x0f, 0x63, 0x47, 0xd4, 0x33, 0xeb, 0x11, 0x06, 0x01, 0x84, 0xde, 0xf9, 0x87, 0xf5, - 0xaa, 0x98, 0x24, 0xec, 0x18, 0x56, 0x1a, 0x5c, 0xd1, 0xa3, 0xf0, 0x0c, 0x8b, 0xf0, 0x58, 0xf8, 0xc2, 0x20, 0x56, - 0xf8, 0xdf, 0xb9, 0x94, 0x73, 0xff, 0x5b, 0xcb, 0xf2, 0x17, 0x3c, 0xc7, 0xe2, 0x2c, 0x5a, 0xc0, 0x72, 0xcb, 0x86, - 0x40, 0x1a, 0xb2, 0xfa, 0x08, 0xae, 0xc7, 0x2e, 0x4c, 0x1d, 0x48, 0x84, 0xd7, 0x46, 0xa0, 0xf2, 0xf2, 0xe1, 0xb5, - 0x0d, 0x99, 0x64, 0x3e, 0x21, 0x66, 0x1a, 0x84, 0x45, 0x96, 0x70, 0xa1, 0x31, 0x29, 0x98, 0x52, 0x91, 0x8d, 0x25, - 0x18, 0x49, 0xe1, 0x1f, 0x87, 0xf4, 0x29, 0x63, 0x11, 0x99, 0x0e, 0xeb, 0xb3, 0xb5, 0xe2, 0x70, 0x2e, 0x0b, 0x95, - 0xda, 0x97, 0x62, 0x3c, 0x18, 0xe7, 0xe5, 0x33, 0x8c, 0x69, 0x9e, 0xad, 0xb1, 0xbd, 0xc3, 0x2e, 0x0b, 0xb9, 0x2b, - 0xed, 0xb0, 0x54, 0x96, 0xad, 0xbf, 0x35, 0x21, 0x55, 0x9b, 0x51, 0x30, 0xd1, 0x6a, 0x40, 0x55, 0xe0, 0x0e, 0x28, - 0x6c, 0x83, 0xd2, 0xa4, 0xcb, 0xb2, 0x64, 0xba, 0x2c, 0x97, 0xe1, 0xa4, 0xd5, 0x5a, 0xaf, 0x71, 0xc1, 0x4c, 0x20, - 0x97, 0x9d, 0x25, 0x20, 0x5f, 0x4d, 0xe5, 0x4d, 0x90, 0xab, 0xd2, 0x72, 0x96, 0x66, 0x89, 0xa2, 0xc0, 0x08, 0x36, - 0x5a, 0xe3, 0xaf, 0x5c, 0x71, 0x80, 0xa7, 0x9b, 0xdd, 0x50, 0xca, 0x9c, 0x51, 0x88, 0xa1, 0x16, 0x34, 0xb9, 0xc6, - 0x53, 0x3e, 0x62, 0xbb, 0xdb, 0x04, 0x33, 0xe6, 0x7f, 0xaf, 0x45, 0x8f, 0x40, 0x96, 0xdd, 0x33, 0xa8, 0x03, 0x8b, - 0xb8, 0x82, 0x0e, 0x42, 0x19, 0x7c, 0x14, 0xe2, 0x66, 0x4e, 0xef, 0xe4, 0x42, 0x03, 0x5c, 0x16, 0x5a, 0xbe, 0x71, - 0xe1, 0x10, 0xf6, 0x5b, 0xd8, 0x47, 0x46, 0x58, 0x42, 0xc8, 0x80, 0x16, 0xb6, 0x11, 0x31, 0x5a, 0xd8, 0x05, 0x2a, - 0x68, 0x61, 0x13, 0x9e, 0xa2, 0xb5, 0x2e, 0x63, 0x9b, 0x5d, 0x97, 0x4f, 0x6a, 0x56, 0x9b, 0x60, 0xe1, 0xa4, 0x43, - 0x4d, 0x74, 0x70, 0x7b, 0xc8, 0x08, 0x6f, 0xfc, 0x7c, 0xf5, 0xfa, 0x95, 0x8b, 0x5c, 0xcd, 0xc7, 0xe0, 0xb2, 0xe9, - 0x54, 0x63, 0xd7, 0xe6, 0x2d, 0xaa, 0xb8, 0x52, 0x94, 0x5a, 0xe1, 0x14, 0x5a, 0x7e, 0x21, 0x74, 0x9e, 0xd8, 0xcb, - 0x8b, 0x67, 0xb2, 0x98, 0x51, 0x7b, 0x63, 0x84, 0xaf, 0x95, 0x7b, 0x7c, 0xde, 0xbc, 0x6f, 0x53, 0x4d, 0xf2, 0xdd, - 0xe6, 0x55, 0xc4, 0x22, 0x33, 0xf2, 0x2b, 0x68, 0x03, 0x4c, 0xe5, 0xf2, 0xed, 0xe0, 0x82, 0xb8, 0xf8, 0xff, 0x01, - 0x79, 0x79, 0x6b, 0xa9, 0x4b, 0x14, 0x35, 0xb8, 0xc1, 0x4f, 0x56, 0xf0, 0x2c, 0xb8, 0x2e, 0x34, 0xec, 0x91, 0x13, - 0x2f, 0xa2, 0x56, 0x54, 0x7f, 0x7b, 0xd7, 0xa8, 0x12, 0x7c, 0xec, 0xd8, 0x24, 0x97, 0x20, 0x7a, 0x94, 0xcf, 0xfc, - 0x71, 0x10, 0x4d, 0xfc, 0xdd, 0xf3, 0x65, 0xdb, 0xd3, 0xd9, 0xbc, 0x52, 0x27, 0x96, 0x57, 0x26, 0xe0, 0xe1, 0x68, - 0x1f, 0xd2, 0x41, 0x38, 0x48, 0x64, 0xa5, 0xf6, 0xd0, 0xe7, 0xa2, 0x6e, 0x9c, 0x5f, 0xb4, 0x59, 0xf3, 0x64, 0xb5, - 0xca, 0x2f, 0xdb, 0xac, 0x7d, 0x6a, 0x9f, 0xdd, 0x8b, 0x54, 0x06, 0x34, 0x97, 0x8f, 0x79, 0x16, 0x81, 0x76, 0x76, - 0x9c, 0x99, 0x70, 0x0a, 0x3e, 0x50, 0x34, 0x59, 0xe8, 0xaa, 0x2f, 0x09, 0xc6, 0xa5, 0xc4, 0xea, 0xf1, 0x0b, 0xd4, - 0x6b, 0xa7, 0xdb, 0xae, 0xd2, 0xcd, 0xf6, 0x61, 0x70, 0xe1, 0x52, 0x20, 0xdc, 0x81, 0x90, 0x07, 0xa0, 0xdf, 0x5d, - 0x0a, 0x30, 0x0d, 0x02, 0x54, 0x56, 0x20, 0xd2, 0xf2, 0xd9, 0x62, 0xf6, 0xac, 0xa0, 0x66, 0x19, 0x9e, 0xf0, 0x09, - 0xd7, 0x2a, 0xa5, 0x20, 0xdd, 0xee, 0x4a, 0x5f, 0xef, 0x96, 0xa0, 0xb2, 0x5a, 0xfc, 0xdd, 0x44, 0xf3, 0xec, 0x8b, - 0x72, 0x0b, 0x87, 0xb0, 0x59, 0x59, 0x81, 0x33, 0xb4, 0xc6, 0xb9, 0x9c, 0xd0, 0x82, 0xeb, 0xe9, 0xec, 0xdf, 0x5a, - 0x1d, 0xd6, 0xd7, 0x03, 0x73, 0x61, 0x05, 0x20, 0xa1, 0x62, 0xb4, 0x5a, 0xf1, 0xa3, 0xef, 0xdf, 0x27, 0x79, 0x9f, - 0xf0, 0x36, 0xee, 0xe0, 0x63, 0x7c, 0x8a, 0xdb, 0x2d, 0xdc, 0x3e, 0x85, 0xab, 0xfb, 0x2c, 0x5f, 0x8c, 0x98, 0x8a, - 0xe1, 0xfd, 0x35, 0x7d, 0x99, 0x9c, 0x1f, 0x96, 0xaf, 0x0e, 0xe8, 0x22, 0x71, 0xe8, 0x12, 0x04, 0xbf, 0x77, 0x51, - 0x03, 0xa3, 0x28, 0x0c, 0x59, 0x37, 0x0e, 0x55, 0x27, 0xa5, 0x7e, 0xe1, 0xf2, 0xb8, 0x07, 0xf6, 0xdc, 0x76, 0x65, - 0x9b, 0x60, 0xf6, 0x6d, 0x7f, 0xa6, 0xd5, 0xcf, 0xa6, 0x2e, 0x11, 0xc3, 0x43, 0xaf, 0x42, 0x0f, 0x74, 0x49, 0xda, - 0x07, 0x07, 0x60, 0x75, 0x14, 0xcc, 0x86, 0xdb, 0xe8, 0x07, 0xbc, 0x59, 0x4b, 0x83, 0x60, 0x05, 0x60, 0xdc, 0xf9, - 0x86, 0x93, 0xa5, 0x85, 0xad, 0x06, 0x2a, 0xac, 0x8b, 0x30, 0xae, 0x5e, 0x48, 0x2a, 0x8c, 0x10, 0x0d, 0x47, 0x98, - 0x0b, 0x86, 0xb2, 0xdf, 0xc2, 0x72, 0x3c, 0x56, 0x4c, 0xc3, 0xd1, 0x51, 0xb0, 0xaf, 0xac, 0x50, 0xe6, 0x14, 0x19, - 0xb2, 0x09, 0x17, 0x0f, 0xf5, 0x9f, 0xac, 0x90, 0xe6, 0xd3, 0x68, 0x30, 0xd2, 0xc8, 0xac, 0x62, 0x84, 0xb3, 0x9c, - 0xcf, 0xa1, 0xea, 0xa4, 0x00, 0xa7, 0x1f, 0xf8, 0xcb, 0x47, 0x69, 0xd8, 0x26, 0x90, 0xaf, 0x0f, 0x36, 0xa6, 0x0b, - 0x1e, 0x15, 0xf4, 0xe6, 0xb5, 0x78, 0x0c, 0x3b, 0xea, 0x61, 0xc1, 0x28, 0x64, 0x43, 0xd2, 0x3b, 0x68, 0x0a, 0x3e, - 0xa0, 0xcd, 0x97, 0x06, 0x70, 0xe9, 0xb9, 0xf9, 0xb0, 0x15, 0x7d, 0xec, 0xc6, 0xa4, 0x6c, 0xcb, 0x64, 0x9a, 0x53, - 0xba, 0xca, 0xb4, 0x51, 0xa8, 0xca, 0x29, 0xac, 0xb1, 0x8b, 0x7a, 0x12, 0x0e, 0x66, 0x44, 0xd5, 0x34, 0xed, 0x0f, - 0xcc, 0xdf, 0xd7, 0xb6, 0x64, 0x0b, 0xbb, 0x88, 0x33, 0x6b, 0x6c, 0x1e, 0x4e, 0x0d, 0xca, 0xb7, 0x31, 0xdc, 0xc3, - 0xc2, 0xeb, 0x9d, 0x35, 0xf2, 0x79, 0xe2, 0xc9, 0xe6, 0xc9, 0x7a, 0x6d, 0x06, 0xa2, 0x52, 0xd0, 0x03, 0xbd, 0xf5, - 0xdb, 0xa6, 0x05, 0xdb, 0xa3, 0xfc, 0x3a, 0x6d, 0xe1, 0x19, 0x87, 0xc7, 0x48, 0x7d, 0x7b, 0x57, 0xba, 0x90, 0x5f, - 0x1c, 0x48, 0x5a, 0x41, 0x8a, 0x9d, 0x4e, 0xd0, 0xd9, 0x31, 0x0e, 0x46, 0x0e, 0xf4, 0xfc, 0xea, 0x8b, 0x85, 0xb5, - 0xff, 0xfd, 0xa6, 0x2c, 0x68, 0xe2, 0xe9, 0x94, 0x13, 0xca, 0xfc, 0xf9, 0xf9, 0x86, 0x27, 0x15, 0x2a, 0xb8, 0x57, - 0xbc, 0x60, 0x4f, 0xdb, 0x40, 0x9f, 0x33, 0xfa, 0xd9, 0xfe, 0xb0, 0x31, 0x7c, 0x4a, 0x2d, 0x5b, 0x56, 0x48, 0xa5, - 0x1e, 0xda, 0x34, 0x7b, 0xf4, 0xc0, 0x11, 0xf9, 0x12, 0xba, 0x00, 0x5e, 0x7f, 0x54, 0xc8, 0xb9, 0x41, 0x04, 0xf7, - 0xdb, 0x8d, 0xdb, 0xf8, 0x0a, 0x80, 0xb7, 0xc3, 0x5e, 0xf5, 0x4f, 0x0b, 0xd8, 0xdf, 0xa8, 0x2c, 0xe9, 0xc7, 0xdb, - 0xb1, 0xc7, 0x7f, 0x21, 0x21, 0x6a, 0xbc, 0xc5, 0xc3, 0xc4, 0xa1, 0x53, 0xc9, 0x9a, 0x95, 0x3f, 0xb7, 0x4a, 0x02, - 0x86, 0xd5, 0x0b, 0x86, 0x6c, 0xdc, 0x56, 0x71, 0x9b, 0xf9, 0x1f, 0x54, 0x30, 0x58, 0xf0, 0xad, 0x91, 0x54, 0x2c, - 0x8b, 0xdf, 0x3e, 0x75, 0xfe, 0xab, 0xce, 0x71, 0xed, 0xeb, 0xda, 0x4b, 0xa1, 0x43, 0x13, 0xa5, 0x39, 0x42, 0x07, - 0x07, 0x1b, 0x19, 0x74, 0x0c, 0x80, 0x47, 0x8e, 0xfd, 0xf2, 0xcb, 0xe7, 0xd9, 0x31, 0xa3, 0x79, 0x2c, 0xa2, 0x90, - 0xb9, 0xf3, 0xdc, 0x9c, 0x9d, 0xc8, 0x13, 0xaa, 0xa6, 0xbe, 0x30, 0xc0, 0xf1, 0xd1, 0x56, 0x2a, 0xe0, 0x7b, 0xb4, - 0xde, 0x31, 0x81, 0x0d, 0x7e, 0xcb, 0x4e, 0x6a, 0x57, 0x41, 0xbf, 0x40, 0xcb, 0x5d, 0x4c, 0xe5, 0xc6, 0x02, 0x47, - 0x9b, 0x13, 0xd9, 0x39, 0xf4, 0x8d, 0x3a, 0x25, 0xeb, 0xf1, 0x64, 0xb7, 0xd1, 0x97, 0x14, 0xbb, 0x92, 0x2b, 0xda, - 0x36, 0x64, 0xd5, 0x6b, 0xc1, 0xba, 0x32, 0x75, 0xaa, 0xae, 0x79, 0x2b, 0x4b, 0x9b, 0xd2, 0x2e, 0xc9, 0xde, 0x6d, - 0xb1, 0xf0, 0x2a, 0xbc, 0xd1, 0x28, 0x2f, 0x42, 0xc1, 0x1e, 0x4b, 0x0c, 0xba, 0x9c, 0xc0, 0xf5, 0xc2, 0x6a, 0x15, - 0xc3, 0x9f, 0x5d, 0x63, 0xd8, 0x65, 0xba, 0xf4, 0x81, 0x6f, 0xf0, 0x2b, 0x41, 0xc0, 0x62, 0x67, 0x07, 0x09, 0xd6, - 0x5d, 0x6e, 0xd0, 0x70, 0x9c, 0xf8, 0x2f, 0x78, 0x2e, 0x5b, 0x7b, 0x97, 0x83, 0x49, 0xf6, 0x8d, 0x27, 0xf6, 0x4a, - 0xd6, 0xb2, 0x16, 0xed, 0x7e, 0x43, 0x82, 0x21, 0x76, 0x53, 0x3a, 0xc7, 0xad, 0xa4, 0x8d, 0x22, 0x57, 0xac, 0x42, - 0xff, 0x6f, 0x15, 0xc9, 0x6c, 0xe6, 0x7f, 0x9d, 0x9d, 0x9d, 0xb9, 0x14, 0x67, 0xf3, 0xa7, 0x8c, 0x07, 0x9c, 0x49, - 0x60, 0x5f, 0x79, 0xc6, 0x8c, 0x0e, 0xf9, 0x2d, 0x0c, 0x85, 0x08, 0x72, 0x29, 0x1c, 0xbb, 0x04, 0xaf, 0x3d, 0x02, - 0xe5, 0x01, 0xf6, 0xef, 0xc9, 0x46, 0x39, 0xff, 0x5c, 0x94, 0x0f, 0xa7, 0x5c, 0x36, 0xc8, 0xbe, 0x9a, 0xcf, 0xbe, - 0x35, 0x93, 0x81, 0x17, 0x12, 0x22, 0x6c, 0x7f, 0x1b, 0x96, 0xd6, 0x59, 0xca, 0xe0, 0x48, 0xcb, 0x45, 0x36, 0xb5, - 0x9a, 0x7f, 0xf7, 0x61, 0xca, 0xba, 0xa7, 0x86, 0x20, 0x72, 0x17, 0x59, 0xba, 0xa8, 0xa0, 0xd1, 0x8f, 0x65, 0x00, - 0xd0, 0xbd, 0x57, 0x6c, 0xc1, 0x7e, 0xc4, 0x7b, 0x55, 0x0a, 0x7c, 0x3c, 0x2c, 0x38, 0xcd, 0x7f, 0xc4, 0x7b, 0x55, - 0x20, 0x50, 0x70, 0x85, 0x34, 0xb1, 0x34, 0xb1, 0x79, 0x56, 0x3b, 0x8d, 0x04, 0x50, 0xd0, 0x3c, 0x32, 0x07, 0xd9, - 0x73, 0x17, 0xa3, 0x31, 0xe9, 0x60, 0x17, 0x1c, 0xcc, 0x46, 0x84, 0xb5, 0x81, 0xd4, 0x21, 0x6e, 0x5d, 0x39, 0x1b, - 0xf3, 0xf5, 0x68, 0x63, 0x41, 0x8c, 0x32, 0x99, 0x5c, 0x3e, 0xe7, 0xf1, 0xd6, 0x62, 0xa1, 0xb0, 0x5a, 0xb0, 0x40, - 0xb5, 0x2a, 0x55, 0x7a, 0x58, 0x7c, 0xbb, 0x60, 0x16, 0x14, 0x31, 0x5b, 0xef, 0xe1, 0x2d, 0x57, 0x04, 0xa4, 0x64, - 0x97, 0x04, 0x2f, 0xa3, 0x1b, 0x4c, 0x25, 0xcb, 0x99, 0x1c, 0x31, 0x4b, 0xe8, 0x99, 0xd2, 0x11, 0x36, 0x79, 0x0a, - 0x22, 0x89, 0xed, 0xb7, 0xb0, 0x63, 0x8d, 0x5e, 0x08, 0x2f, 0xa4, 0xc0, 0xb9, 0x6a, 0x9a, 0x98, 0x51, 0x6e, 0xa2, - 0x8b, 0x3d, 0x54, 0x73, 0x96, 0x69, 0x8b, 0x00, 0xfb, 0x0e, 0x0d, 0xa5, 0x78, 0x6e, 0x40, 0x61, 0x9e, 0xf4, 0x76, - 0x29, 0x8f, 0x61, 0xf1, 0x82, 0x14, 0x20, 0x6a, 0x5c, 0x4c, 0xca, 0x3a, 0xf3, 0x7c, 0x31, 0xe1, 0xa2, 0x42, 0x86, - 0x82, 0xa9, 0xb9, 0x14, 0xf0, 0xa2, 0x46, 0x59, 0xc4, 0xd0, 0xa1, 0x1a, 0xbe, 0x5b, 0x12, 0x56, 0xd6, 0x31, 0xc7, - 0x14, 0x17, 0x55, 0x0d, 0x60, 0x2e, 0x1e, 0x1a, 0x01, 0xd1, 0x87, 0x97, 0x7d, 0x2d, 0xde, 0xc9, 0x79, 0x95, 0xef, - 0x69, 0x9c, 0x0f, 0x5c, 0xef, 0xec, 0x86, 0xd1, 0xda, 0x3c, 0x7a, 0x15, 0x6c, 0xdf, 0x0f, 0xbc, 0x7a, 0x08, 0x6e, - 0x6d, 0x9e, 0xcd, 0x2a, 0xb3, 0x86, 0xac, 0x7c, 0x23, 0xa2, 0x6a, 0xaf, 0x5e, 0x55, 0x0a, 0x5b, 0x11, 0xa0, 0x52, - 0xf0, 0xd1, 0x56, 0xfe, 0x13, 0x6d, 0xf3, 0xed, 0x39, 0x54, 0x86, 0x07, 0xf2, 0x64, 0xa8, 0xea, 0x01, 0x17, 0xe5, - 0x87, 0x00, 0x16, 0x3f, 0x32, 0xf1, 0x83, 0x77, 0x5d, 0x20, 0x73, 0xa6, 0x62, 0x89, 0x97, 0x7d, 0x3a, 0x48, 0xad, - 0x3c, 0x94, 0x4a, 0xb0, 0xed, 0xb9, 0x29, 0xb8, 0xf6, 0x81, 0x8a, 0x71, 0x9f, 0x0d, 0xd2, 0x65, 0x3d, 0x98, 0xb1, - 0x0d, 0xa7, 0xec, 0xcd, 0x39, 0x4d, 0xf4, 0x5f, 0x3a, 0xc0, 0x39, 0x01, 0xdb, 0x63, 0xcf, 0x9e, 0xbe, 0x89, 0x33, - 0xd4, 0xab, 0x73, 0xf8, 0xcb, 0x35, 0xce, 0x71, 0x86, 0xd2, 0x87, 0x31, 0x5c, 0x60, 0xad, 0x31, 0x80, 0x2f, 0xb3, - 0xa4, 0x0a, 0x3c, 0x52, 0x33, 0x23, 0xb1, 0xba, 0x8b, 0x40, 0xb4, 0xd4, 0xe1, 0xed, 0x38, 0xf3, 0xe1, 0xc0, 0x0d, - 0xf7, 0xfa, 0xcc, 0x08, 0x87, 0x93, 0x2c, 0xae, 0x9d, 0x33, 0x9c, 0x5c, 0xee, 0xf3, 0xda, 0x89, 0x09, 0xd6, 0xde, - 0xe1, 0xa9, 0x02, 0x7a, 0x34, 0x38, 0x55, 0x2c, 0x0d, 0x81, 0x98, 0x09, 0xe0, 0xcd, 0x1c, 0x1e, 0x6d, 0x01, 0xce, - 0x47, 0x6b, 0x1c, 0x7c, 0xa5, 0xb5, 0xae, 0x36, 0x95, 0x28, 0xeb, 0x35, 0xee, 0x4f, 0x33, 0x3c, 0xca, 0xf0, 0x3c, - 0x1b, 0x04, 0xc7, 0xcd, 0x2c, 0x0b, 0x4d, 0xba, 0x56, 0xab, 0xa7, 0xce, 0x8c, 0x10, 0xd9, 0x9f, 0x96, 0xfe, 0xa0, - 0x1e, 0x20, 0x7c, 0x0a, 0x59, 0x40, 0x4b, 0x7a, 0xee, 0x6f, 0xc3, 0xbe, 0x16, 0x8e, 0x1a, 0x31, 0x4f, 0x2c, 0x19, - 0xe9, 0xf9, 0x1f, 0x65, 0x96, 0x6d, 0xad, 0x11, 0xcd, 0x6f, 0xf7, 0xa2, 0x86, 0x6f, 0x2f, 0xd0, 0xb2, 0x95, 0x66, - 0x3b, 0x80, 0x28, 0xd6, 0x38, 0x49, 0x07, 0x6b, 0x24, 0x57, 0xab, 0xd8, 0xa6, 0x10, 0x9e, 0xcc, 0x18, 0x55, 0x8b, - 0xc2, 0x3c, 0xa0, 0x17, 0x2b, 0x94, 0x18, 0x7e, 0x17, 0x3b, 0x1b, 0x51, 0x78, 0xaf, 0x4e, 0x82, 0xe1, 0x46, 0x2c, - 0x88, 0xac, 0x89, 0xdc, 0xc3, 0xac, 0xb2, 0x0c, 0x12, 0x44, 0x18, 0x91, 0xdf, 0x5e, 0x97, 0x0a, 0xfb, 0x44, 0x9f, - 0xfd, 0x63, 0x7c, 0x01, 0xe1, 0xe6, 0x6d, 0x42, 0x8b, 0x21, 0x9d, 0x00, 0x1b, 0x0b, 0x71, 0x08, 0xb7, 0x12, 0x56, - 0xab, 0xfe, 0xa0, 0x2b, 0x0c, 0x79, 0x76, 0x0f, 0x08, 0x96, 0x0d, 0xed, 0x6e, 0x00, 0xae, 0xba, 0x2d, 0x35, 0xd7, - 0x46, 0xf7, 0x43, 0xcd, 0x1b, 0x67, 0xdc, 0x25, 0xb9, 0x67, 0x4a, 0xaa, 0x97, 0xc8, 0x6b, 0x16, 0xe0, 0x26, 0x74, - 0x15, 0x1e, 0xe1, 0x85, 0xb5, 0xe1, 0x34, 0x0f, 0x5a, 0x51, 0xf3, 0x8e, 0x15, 0x3c, 0x9f, 0x4d, 0x58, 0x3f, 0x1b, - 0xe0, 0x91, 0x0f, 0x77, 0xbe, 0xff, 0x36, 0x1e, 0x21, 0x54, 0x10, 0x03, 0x53, 0xeb, 0xb2, 0x3d, 0xaa, 0xec, 0xf6, - 0x4d, 0xa6, 0x61, 0x18, 0x8c, 0x11, 0xf3, 0x28, 0x34, 0x62, 0xce, 0x1b, 0x0d, 0xb4, 0x20, 0x23, 0x30, 0x62, 0x5e, - 0x04, 0xad, 0x2d, 0xec, 0x63, 0xa7, 0x41, 0x7b, 0x0b, 0x84, 0xba, 0x1c, 0x68, 0x9a, 0x86, 0x67, 0x4d, 0xaa, 0x67, - 0xe5, 0xfd, 0x23, 0x5b, 0x47, 0x1d, 0x50, 0x24, 0x8c, 0x2f, 0xfd, 0x24, 0xac, 0x6b, 0xb8, 0x1d, 0xf7, 0xd8, 0x8c, - 0xdb, 0xd9, 0x36, 0xa8, 0xbe, 0xec, 0x67, 0x83, 0x41, 0x57, 0x7a, 0x2b, 0x89, 0x16, 0x1e, 0x57, 0x0f, 0xa1, 0x54, - 0x8b, 0xf7, 0x55, 0x6f, 0x5e, 0x79, 0x73, 0xff, 0xbe, 0xea, 0xe6, 0x79, 0x0c, 0x1c, 0xd0, 0x3e, 0xdc, 0x0f, 0x55, - 0xf1, 0xc1, 0x8e, 0x3a, 0x10, 0x05, 0x2d, 0x6d, 0xd5, 0x04, 0x52, 0x6b, 0x66, 0x17, 0xeb, 0xa6, 0x42, 0x87, 0x02, - 0xc2, 0x90, 0xa9, 0xaa, 0xbb, 0x3b, 0x15, 0xa8, 0x86, 0x38, 0x9c, 0xfa, 0x8f, 0xad, 0x11, 0x6b, 0x1c, 0x75, 0x46, - 0x91, 0x31, 0x92, 0xb4, 0xcb, 0x07, 0x6f, 0x1f, 0x81, 0x95, 0x80, 0x8f, 0x41, 0x6d, 0x92, 0x8c, 0x21, 0xc1, 0x5b, - 0x96, 0x69, 0xc3, 0x87, 0x70, 0x87, 0xa0, 0x3c, 0xb1, 0x41, 0x69, 0x5d, 0x25, 0x0b, 0xb9, 0xaa, 0xcb, 0xeb, 0x00, - 0x3d, 0xef, 0xca, 0xdf, 0xd8, 0x70, 0x64, 0xc1, 0xc0, 0xb2, 0xad, 0x7d, 0x02, 0x1e, 0xf9, 0xb8, 0x42, 0x10, 0xbf, - 0x14, 0x3a, 0x31, 0xf1, 0xba, 0xaf, 0x60, 0x83, 0xe2, 0x39, 0x38, 0x08, 0x3a, 0x09, 0x0e, 0x83, 0x77, 0x99, 0xd5, - 0x24, 0x1b, 0xdc, 0x9a, 0x91, 0x78, 0xbe, 0x5a, 0xb5, 0xd0, 0xe1, 0xdf, 0xe6, 0x49, 0xea, 0x71, 0xa9, 0x70, 0x1f, - 0x57, 0x0a, 0x77, 0xb0, 0x04, 0x24, 0xe3, 0x40, 0xd7, 0x8e, 0x65, 0xa8, 0x46, 0x87, 0x68, 0xe9, 0x2f, 0x20, 0x76, - 0xb6, 0x3b, 0x96, 0x40, 0xcf, 0xbe, 0x55, 0xc0, 0xea, 0xda, 0xcb, 0x12, 0xc8, 0x08, 0xee, 0x7e, 0x13, 0x18, 0x15, - 0xa2, 0xf1, 0xf9, 0x33, 0xaf, 0x5a, 0xf0, 0xc4, 0xf9, 0x73, 0xcd, 0x0c, 0xeb, 0x5e, 0xd0, 0x1b, 0xd3, 0x7c, 0x3c, - 0xc6, 0xcd, 0xb1, 0x05, 0xe7, 0x51, 0x07, 0x7e, 0x5a, 0x88, 0x1e, 0x75, 0xb0, 0x4b, 0xc5, 0xe3, 0x12, 0xc8, 0x21, - 0x7a, 0x3a, 0x03, 0x29, 0x60, 0xa5, 0x63, 0xab, 0x45, 0x9a, 0xa0, 0xd5, 0x6a, 0x72, 0x41, 0x5a, 0x08, 0x2d, 0xd5, - 0x0d, 0xd7, 0xd9, 0x14, 0x7c, 0xa4, 0x41, 0x31, 0xf0, 0x86, 0xea, 0x69, 0x8c, 0xf0, 0x18, 0x2d, 0x47, 0x6c, 0x4c, - 0x17, 0xb9, 0x4e, 0x55, 0x8f, 0x27, 0x36, 0x70, 0x2f, 0xb3, 0x91, 0xe0, 0x8e, 0x3a, 0x78, 0x62, 0xf8, 0xcb, 0xf7, - 0xc6, 0x1c, 0xa4, 0xc8, 0x4c, 0xf2, 0xc4, 0x24, 0x60, 0x9e, 0x64, 0xb9, 0x54, 0xcc, 0x36, 0xd3, 0xb5, 0xb6, 0xe5, - 0x10, 0x92, 0x3c, 0xd2, 0x05, 0x37, 0x56, 0x94, 0x51, 0x3a, 0x25, 0xaa, 0xa7, 0x8e, 0x3a, 0xe9, 0x04, 0xf3, 0x04, - 0x38, 0xbd, 0x77, 0x32, 0x66, 0x8d, 0xf2, 0x56, 0x74, 0x86, 0x0e, 0xa7, 0x58, 0x54, 0x97, 0xa8, 0x33, 0x74, 0x38, - 0x41, 0x78, 0xd6, 0x20, 0xb9, 0x02, 0x8f, 0x61, 0x2e, 0xfe, 0x8f, 0x94, 0xff, 0xe6, 0xb0, 0x21, 0xc4, 0xf4, 0x5b, - 0xd8, 0x29, 0x6c, 0x14, 0xa5, 0x39, 0x01, 0xaf, 0xc5, 0xf6, 0x19, 0xce, 0xc8, 0xa4, 0x99, 0xfb, 0x80, 0x7b, 0xa6, - 0x95, 0xc6, 0xad, 0x46, 0x87, 0x19, 0x1e, 0x6d, 0x26, 0xc5, 0x66, 0xae, 0xcd, 0x3c, 0xcd, 0xe0, 0x7c, 0xaf, 0x46, - 0xe1, 0xca, 0x2f, 0x36, 0x93, 0xc2, 0xf2, 0x0e, 0xb8, 0xcd, 0x11, 0x16, 0x4d, 0x8a, 0x73, 0x3c, 0x6b, 0xbe, 0xc2, - 0xb3, 0xe6, 0x87, 0x32, 0xa3, 0xb1, 0xc0, 0x02, 0x82, 0xf7, 0x41, 0x22, 0x9e, 0x55, 0xc9, 0x23, 0x2c, 0x1a, 0xa6, - 0x3c, 0x9e, 0x35, 0xaa, 0xd2, 0xcd, 0x05, 0x16, 0x0d, 0x53, 0xba, 0xf1, 0x01, 0xcf, 0x1a, 0xaf, 0xfe, 0xc5, 0xa4, - 0xa3, 0x14, 0xd0, 0x65, 0x8e, 0x96, 0x99, 0x1d, 0xe2, 0xd5, 0x6f, 0x6f, 0xdf, 0xb5, 0xaf, 0x3b, 0x87, 0x13, 0xec, - 0xd7, 0x2f, 0x33, 0x38, 0x96, 0xe9, 0x98, 0x35, 0x01, 0xa2, 0x19, 0xee, 0x1c, 0x4e, 0x71, 0xe7, 0x30, 0x73, 0x4d, - 0xad, 0x67, 0x0d, 0x72, 0xab, 0x43, 0x28, 0xea, 0x28, 0x0d, 0xe1, 0xe3, 0x27, 0x9b, 0x4e, 0x50, 0x0d, 0x94, 0xe8, - 0x70, 0x52, 0x03, 0x15, 0x7c, 0x2f, 0x6a, 0xdf, 0x55, 0xbd, 0x0a, 0x83, 0x2c, 0x94, 0x50, 0xb8, 0xe6, 0x06, 0x3c, - 0xb5, 0x14, 0x03, 0x99, 0x30, 0xc5, 0x02, 0xe5, 0x3b, 0xa0, 0x30, 0xca, 0x13, 0x33, 0xf4, 0x60, 0x3a, 0x26, 0xf1, - 0xff, 0xe7, 0xc9, 0x94, 0x43, 0x2f, 0xb7, 0xcc, 0xd6, 0xf4, 0xdc, 0x64, 0xc2, 0xe1, 0x03, 0x8f, 0xf5, 0x7f, 0xed, - 0x40, 0xb1, 0x01, 0x29, 0xfe, 0xbf, 0x74, 0x74, 0x21, 0x18, 0x21, 0x2b, 0x4a, 0x0b, 0x87, 0xf8, 0xdf, 0x1f, 0x56, - 0xd0, 0x7d, 0xb1, 0xd5, 0x7d, 0x61, 0xba, 0x0f, 0x9b, 0x36, 0xaa, 0x9c, 0xb4, 0xaa, 0x64, 0xc9, 0x7f, 0x9d, 0x6e, - 0x6d, 0x81, 0x46, 0xd4, 0xe8, 0xd9, 0x24, 0x6c, 0x70, 0xbf, 0x9d, 0xee, 0x40, 0xe6, 0x35, 0xb7, 0x2f, 0xa4, 0xc2, - 0xe1, 0x1b, 0xdc, 0xa9, 0x5e, 0xb6, 0xc0, 0x7b, 0x53, 0x19, 0x7d, 0x65, 0x1c, 0x5a, 0x0e, 0xd2, 0x4d, 0x53, 0x6e, - 0x63, 0x2c, 0x9d, 0x9c, 0x62, 0xe3, 0x8a, 0x08, 0x95, 0x6e, 0x2f, 0x41, 0x29, 0x3e, 0xd6, 0x4d, 0x66, 0xbe, 0x2e, - 0x74, 0x62, 0x2e, 0xa1, 0x1a, 0xe6, 0xf3, 0xee, 0x52, 0x27, 0x5a, 0xce, 0x6d, 0xde, 0xdd, 0x05, 0xf4, 0x09, 0x1a, - 0xd6, 0x46, 0x60, 0xb7, 0xcf, 0x0a, 0xa7, 0xdf, 0xa9, 0x0e, 0xc1, 0xf0, 0x00, 0x72, 0xa4, 0xc5, 0xf6, 0x81, 0x4d, - 0x6b, 0xd8, 0x75, 0xd1, 0x2c, 0x13, 0x6d, 0xab, 0x4d, 0x93, 0x6b, 0xf7, 0x30, 0x9f, 0x87, 0x3c, 0x05, 0x2f, 0xac, - 0x7e, 0x7c, 0x07, 0xbb, 0x71, 0x5b, 0x63, 0x24, 0xea, 0x4a, 0xa6, 0x12, 0xfa, 0xc9, 0x2d, 0x66, 0xc9, 0x9d, 0xf1, - 0x62, 0x54, 0xc6, 0xdf, 0xc7, 0xc4, 0xe5, 0x8f, 0x2a, 0x49, 0x0e, 0x2c, 0xfb, 0x1b, 0x2c, 0xb9, 0x05, 0xf3, 0xc4, - 0xb2, 0x9a, 0xc4, 0x3a, 0xb9, 0x0b, 0x16, 0x51, 0x9a, 0x46, 0xd6, 0x86, 0x01, 0x35, 0xcd, 0x58, 0xf5, 0xe0, 0x3e, - 0x04, 0x7a, 0xe8, 0x95, 0xa5, 0xb4, 0xeb, 0x2c, 0xad, 0x75, 0xaf, 0x4d, 0xf7, 0x9b, 0x03, 0x0a, 0xf8, 0xc2, 0x80, - 0x6b, 0xfa, 0x57, 0x93, 0x48, 0x86, 0xec, 0x1f, 0xce, 0x8a, 0xc7, 0x8b, 0xc2, 0x60, 0x9a, 0xe8, 0xe9, 0x24, 0x9b, - 0xb7, 0xc1, 0x54, 0x2f, 0x9b, 0x77, 0x6e, 0xb1, 0xfb, 0xbe, 0xb3, 0xdf, 0x77, 0x58, 0xf4, 0x98, 0xc9, 0x48, 0x99, - 0x29, 0xe6, 0xbf, 0xef, 0xec, 0xf7, 0x1d, 0xde, 0x1e, 0xcc, 0x8d, 0xbf, 0x50, 0x2c, 0xd9, 0x19, 0x2e, 0xc1, 0x84, - 0x3c, 0xe0, 0x6e, 0x6a, 0x59, 0x26, 0x08, 0x6c, 0x2d, 0x01, 0xe2, 0x7c, 0x3e, 0x8d, 0x2b, 0x5e, 0x0d, 0x01, 0xf7, - 0xe9, 0x5d, 0xdb, 0xab, 0x54, 0xe0, 0x31, 0x41, 0x23, 0x62, 0x62, 0xdb, 0x98, 0xd7, 0xcd, 0x80, 0xcb, 0x23, 0xba, - 0xd4, 0x93, 0x24, 0xc0, 0xab, 0x1a, 0x95, 0xb7, 0x29, 0x52, 0x7e, 0x91, 0x20, 0xc7, 0x17, 0x7b, 0x44, 0x15, 0x03, - 0x58, 0x95, 0x25, 0x7d, 0x02, 0xa9, 0xe7, 0x07, 0x13, 0xfd, 0xb2, 0x89, 0x3c, 0xf6, 0x9d, 0xdf, 0x2f, 0x4c, 0x4f, - 0x0b, 0xb9, 0x98, 0x4c, 0xc1, 0x87, 0x16, 0x58, 0x86, 0xc2, 0xd4, 0xab, 0x6c, 0xfd, 0x6b, 0x92, 0x9b, 0x00, 0x0a, - 0xa7, 0x9b, 0x32, 0xa1, 0x99, 0x5e, 0xd0, 0xdc, 0x58, 0x92, 0x72, 0x31, 0x79, 0x24, 0x6f, 0x5f, 0x02, 0x76, 0x53, - 0xa2, 0x1b, 0x3b, 0xf2, 0xde, 0xc2, 0x0e, 0xc0, 0x19, 0x61, 0xbb, 0x2a, 0x3e, 0x54, 0xa0, 0xf3, 0xc7, 0x39, 0x61, - 0xbb, 0xaa, 0x3e, 0x61, 0x36, 0x7b, 0x4a, 0x36, 0x86, 0xdb, 0x8b, 0xb3, 0x46, 0x8e, 0x8e, 0x3a, 0x69, 0xde, 0xf5, - 0xc4, 0xc0, 0x02, 0x34, 0x00, 0xee, 0xd6, 0xf6, 0x2c, 0xef, 0x6e, 0x08, 0xe8, 0x5d, 0x32, 0x69, 0xaf, 0xcb, 0x4d, - 0xca, 0x6a, 0xd5, 0xa9, 0xa8, 0x60, 0x81, 0xa7, 0xc1, 0x5e, 0xa0, 0xf6, 0x6b, 0x07, 0xc5, 0xb9, 0xca, 0x36, 0x4d, - 0xcf, 0xcb, 0xbe, 0xbb, 0x3b, 0x16, 0x19, 0xdb, 0xb4, 0xb7, 0x3b, 0x88, 0x84, 0xe5, 0x84, 0x75, 0xc0, 0x09, 0x57, - 0xb5, 0x03, 0x02, 0x74, 0x1d, 0x88, 0xdc, 0x58, 0x92, 0xe5, 0xba, 0x32, 0xba, 0x0f, 0xfc, 0x6e, 0x29, 0x91, 0x6e, - 0xb4, 0x25, 0xc1, 0xf4, 0x09, 0x46, 0x4d, 0x67, 0x9e, 0xa6, 0xae, 0xbd, 0xba, 0xbc, 0x29, 0xda, 0xfa, 0x37, 0xa0, - 0xb1, 0xd9, 0x1e, 0x26, 0x86, 0x32, 0x88, 0x81, 0xde, 0x47, 0xbc, 0xdb, 0x68, 0x64, 0x08, 0x14, 0x32, 0xd9, 0x00, - 0xcb, 0xc4, 0x6b, 0xd1, 0x0f, 0x0e, 0x0c, 0x3c, 0xaa, 0x04, 0x84, 0x29, 0x08, 0x21, 0x61, 0xd7, 0x06, 0x61, 0xc3, - 0xe5, 0xaa, 0xe5, 0xc2, 0x46, 0xaa, 0x0d, 0x1d, 0xfc, 0xbf, 0xc2, 0x65, 0xab, 0x67, 0x96, 0x8b, 0x62, 0x70, 0x33, - 0x37, 0x60, 0x91, 0x20, 0x3d, 0xda, 0x6c, 0x0f, 0xc5, 0xdd, 0xb9, 0xd8, 0x6c, 0x08, 0x48, 0xcc, 0x61, 0x82, 0xa2, - 0xe1, 0xdc, 0x18, 0x63, 0x95, 0x54, 0x5a, 0xd6, 0x9a, 0xc4, 0x1c, 0xf8, 0xd2, 0x85, 0xeb, 0xbe, 0xbc, 0x4d, 0x19, - 0xbe, 0x4b, 0x05, 0xbe, 0x01, 0x4f, 0x9a, 0x54, 0x62, 0xf7, 0x78, 0x41, 0xb1, 0x26, 0xba, 0xeb, 0xd9, 0xdb, 0x02, - 0xd6, 0xd9, 0xec, 0x11, 0x11, 0xfc, 0xae, 0x7e, 0xb5, 0xc1, 0x77, 0x0b, 0xbf, 0x02, 0xeb, 0xe7, 0xe0, 0x24, 0xc5, - 0xa2, 0x21, 0x9b, 0x85, 0x3b, 0x32, 0xa0, 0x5c, 0xc5, 0x2f, 0x87, 0xa9, 0x5b, 0xc5, 0x70, 0xed, 0xe3, 0x15, 0xfe, - 0xb0, 0xd1, 0x6e, 0x43, 0x95, 0xc5, 0xed, 0xde, 0x14, 0x0d, 0x59, 0x35, 0xbd, 0x23, 0x73, 0x23, 0xa5, 0xfe, 0xf5, - 0x01, 0xb7, 0xb6, 0xda, 0xf7, 0xd3, 0x7c, 0xeb, 0xd1, 0xb9, 0x6a, 0xda, 0xa7, 0xd6, 0x8a, 0xe0, 0xe0, 0x67, 0x0b, - 0x37, 0xb7, 0x06, 0x1c, 0xc0, 0xcf, 0xdf, 0xd1, 0x3c, 0xce, 0x20, 0x3a, 0xbd, 0xd5, 0x8c, 0xaf, 0xe2, 0xbf, 0x46, - 0x8d, 0xb8, 0x97, 0xfe, 0x95, 0xfc, 0x35, 0x6a, 0xa0, 0x1e, 0x8a, 0xe7, 0xb7, 0x2b, 0x36, 0x5b, 0x41, 0xb0, 0xb5, - 0x7b, 0x47, 0xf8, 0x75, 0x58, 0x92, 0x6b, 0x9a, 0xf3, 0x6c, 0xe5, 0x1e, 0x04, 0x5c, 0xb9, 0x57, 0x89, 0x56, 0xe6, - 0x8d, 0xab, 0x55, 0x2c, 0x87, 0x39, 0x04, 0x16, 0x8e, 0xf7, 0x9a, 0xbd, 0x7e, 0xab, 0xf9, 0x60, 0x60, 0xff, 0x35, - 0x11, 0xee, 0x51, 0x2d, 0x62, 0xdb, 0x9b, 0x8d, 0xad, 0x1f, 0x83, 0x61, 0x07, 0x84, 0x02, 0x07, 0xb9, 0xf4, 0x71, - 0x86, 0xac, 0xef, 0xc9, 0x6a, 0xc5, 0x5c, 0x34, 0x6b, 0xa7, 0xc1, 0x2f, 0x63, 0x33, 0x1d, 0xb6, 0x93, 0x4e, 0xd7, - 0x8b, 0xb1, 0xa4, 0x01, 0x91, 0xa6, 0x31, 0x83, 0x40, 0x52, 0x4b, 0xc3, 0x61, 0xcd, 0x6f, 0xa3, 0xb4, 0xba, 0x3f, - 0x82, 0x94, 0x1f, 0xa2, 0x94, 0x1f, 0x11, 0x08, 0xa0, 0x6d, 0x99, 0xa3, 0xb2, 0x21, 0xef, 0xbb, 0x74, 0xcf, 0x38, - 0x33, 0x34, 0xf8, 0x6a, 0xd5, 0xaa, 0x86, 0x29, 0x8a, 0xfa, 0x30, 0x97, 0x6b, 0x2c, 0xc8, 0x1b, 0xd0, 0x35, 0x2b, - 0x22, 0x7a, 0xa1, 0xab, 0x3c, 0xbc, 0x87, 0x8c, 0x25, 0x01, 0x27, 0xfd, 0x9e, 0xe8, 0x15, 0xe4, 0xf2, 0x61, 0x0c, - 0x3e, 0x66, 0x98, 0xf7, 0x75, 0xbf, 0x18, 0x0c, 0x50, 0xea, 0x9c, 0xce, 0x52, 0x13, 0x71, 0x25, 0xf0, 0x4b, 0x2e, - 0xc0, 0x2f, 0x59, 0x21, 0xd6, 0x2f, 0x06, 0xe4, 0x5e, 0x16, 0x4b, 0x70, 0xca, 0xdf, 0xe1, 0xf3, 0xf8, 0x30, 0x34, - 0x30, 0x35, 0xc3, 0x32, 0x17, 0xd9, 0x60, 0x31, 0x67, 0x2d, 0x81, 0xe0, 0x66, 0xc0, 0x5d, 0x6a, 0x43, 0xa2, 0xb1, - 0x06, 0x8a, 0x6e, 0xa3, 0xd0, 0xcc, 0xe8, 0xe9, 0x56, 0x1b, 0xfd, 0xc8, 0xe1, 0x85, 0xb9, 0x86, 0xb1, 0x08, 0x64, - 0x2e, 0x57, 0x3d, 0xf6, 0x97, 0x1f, 0x36, 0x2b, 0x0c, 0x5e, 0x91, 0xe9, 0xd0, 0x1d, 0xc7, 0x8c, 0xaf, 0xf2, 0xc4, - 0x31, 0x04, 0x99, 0x58, 0x2a, 0xdd, 0x70, 0x4c, 0x5c, 0x49, 0x9f, 0x89, 0x21, 0xdb, 0x0d, 0xcf, 0xcc, 0x85, 0x6e, - 0xb6, 0x7f, 0x38, 0xb7, 0x73, 0x4e, 0xb8, 0xd1, 0x4a, 0x1a, 0x6d, 0xd4, 0x33, 0x43, 0x55, 0x5d, 0x30, 0xbf, 0x87, - 0x4e, 0x4b, 0x8b, 0x9d, 0xab, 0x77, 0x37, 0x7c, 0x9d, 0xaf, 0x8c, 0xbf, 0xc5, 0xaa, 0xd0, 0x8a, 0x0c, 0xb7, 0x5b, - 0xc8, 0x9b, 0x33, 0x3d, 0xf4, 0x8a, 0x5c, 0xa8, 0x0e, 0x7f, 0x51, 0x57, 0x98, 0x07, 0x3b, 0xa3, 0x86, 0xf0, 0xe8, - 0xf7, 0x3a, 0x03, 0xe5, 0x1f, 0x4c, 0x4c, 0xe6, 0x2c, 0xb9, 0xa1, 0x85, 0x88, 0x7f, 0x7c, 0x21, 0x4c, 0xac, 0xaa, - 0x3d, 0x18, 0xc8, 0x9e, 0xa9, 0xb8, 0x07, 0xb7, 0x26, 0x7c, 0xcc, 0xd9, 0x28, 0xdd, 0x8b, 0x7e, 0x6c, 0x88, 0xc6, - 0x8f, 0xd1, 0x8f, 0xe0, 0xee, 0xec, 0x5e, 0x87, 0x2c, 0xe3, 0x42, 0xf8, 0x7b, 0xac, 0x87, 0xa5, 0x4a, 0x19, 0x6b, - 0xaf, 0x5b, 0x0e, 0x2f, 0xa4, 0xde, 0x64, 0xf1, 0x43, 0x47, 0xac, 0x6d, 0x0a, 0xd6, 0x21, 0x25, 0x85, 0x67, 0x57, - 0xcc, 0xad, 0x16, 0x73, 0x97, 0x5a, 0xc2, 0x5f, 0x5f, 0x3d, 0x2c, 0x55, 0xd0, 0x70, 0x10, 0xba, 0xd2, 0x16, 0x12, - 0x60, 0xe0, 0x52, 0xfa, 0x74, 0xba, 0x33, 0x89, 0xcc, 0xb2, 0x18, 0xde, 0x3d, 0xa8, 0x60, 0xfe, 0x3b, 0xdb, 0x08, - 0xab, 0x02, 0x97, 0x2b, 0x55, 0xd4, 0x4b, 0x49, 0x20, 0x00, 0x7d, 0xe9, 0x3d, 0x28, 0x2f, 0x8a, 0x6e, 0xa3, 0x21, - 0x41, 0x0b, 0x4b, 0xcd, 0xb5, 0x2a, 0xa6, 0xfb, 0xe1, 0xab, 0x86, 0xc1, 0x87, 0x77, 0x48, 0xdb, 0x78, 0x5a, 0x94, - 0x12, 0x6a, 0x77, 0xd0, 0x3e, 0x58, 0x65, 0x07, 0xe5, 0xdf, 0xc6, 0x14, 0xd9, 0xfc, 0x3e, 0xfb, 0x81, 0xba, 0x0e, - 0x07, 0xae, 0x60, 0xd5, 0x4b, 0x19, 0x05, 0x03, 0x56, 0x4e, 0x81, 0xda, 0x3b, 0xc9, 0x68, 0x36, 0x65, 0xa0, 0xee, - 0xb7, 0x45, 0xab, 0xb9, 0x3d, 0xa9, 0xfb, 0x0d, 0x19, 0x67, 0x1f, 0x61, 0x9c, 0x7d, 0x14, 0x78, 0xb1, 0x48, 0xf2, - 0x87, 0x8c, 0x35, 0x8e, 0x55, 0x53, 0xa0, 0xa3, 0x0e, 0x70, 0x67, 0xe0, 0xc0, 0x03, 0xb6, 0x28, 0x07, 0x07, 0xd4, - 0x59, 0xdc, 0xd3, 0x46, 0xe6, 0xbd, 0x3d, 0xa1, 0x76, 0x11, 0x0b, 0xdc, 0xac, 0x99, 0x69, 0x41, 0x6b, 0x85, 0x71, - 0x1e, 0x0f, 0x78, 0x9b, 0x67, 0xb5, 0xf8, 0x09, 0x1b, 0xd6, 0x54, 0xf5, 0x1b, 0x68, 0x8e, 0x6a, 0x41, 0x6e, 0x9e, - 0x18, 0x6f, 0x55, 0xd2, 0x8f, 0xa2, 0x81, 0xe5, 0x54, 0x88, 0x21, 0x19, 0xfd, 0xd6, 0x20, 0xb8, 0xd5, 0x5e, 0xad, - 0xb8, 0x47, 0x7c, 0x51, 0xf3, 0x56, 0x33, 0xb7, 0x00, 0xb4, 0x88, 0xa3, 0xf2, 0xde, 0x24, 0x02, 0xef, 0xdb, 0x32, - 0x42, 0xda, 0xb2, 0x6f, 0x9f, 0xae, 0x2c, 0x15, 0x9b, 0xef, 0xe8, 0x64, 0x90, 0x46, 0x76, 0x44, 0x11, 0xbe, 0x2e, - 0x21, 0x09, 0x57, 0x49, 0xd7, 0x2a, 0x93, 0x73, 0xa6, 0x52, 0x8e, 0xaf, 0x0b, 0x29, 0xf5, 0x95, 0xfd, 0x92, 0xb8, - 0xba, 0x93, 0x11, 0xf8, 0x7a, 0xc2, 0xf4, 0x3b, 0x5a, 0x4c, 0x18, 0xf8, 0x15, 0xf9, 0xdb, 0xb1, 0x94, 0x92, 0xcb, - 0x27, 0x22, 0xee, 0x53, 0x0c, 0xef, 0xae, 0x0e, 0xb0, 0x36, 0x21, 0x50, 0x4a, 0x5c, 0x84, 0x0b, 0xa2, 0x37, 0x85, - 0xbc, 0xbd, 0x8b, 0x0b, 0xec, 0x1c, 0x00, 0x4b, 0xa7, 0x49, 0x80, 0x7f, 0xf9, 0x98, 0x8f, 0xd5, 0x98, 0x53, 0xa3, - 0xeb, 0x77, 0xbf, 0x93, 0x6b, 0xa0, 0xb7, 0xa5, 0xa3, 0x60, 0xbf, 0x35, 0x80, 0x5c, 0xb8, 0x0b, 0x83, 0x8b, 0xaf, - 0xb0, 0xb6, 0x2c, 0x8c, 0x37, 0x16, 0x40, 0xef, 0x73, 0x06, 0x16, 0x6c, 0x98, 0x63, 0x0a, 0x8f, 0xd6, 0x4e, 0x98, - 0x0e, 0xa2, 0x82, 0x3c, 0x29, 0x9f, 0x25, 0xad, 0xd5, 0x7e, 0xcb, 0xc6, 0x70, 0x87, 0x91, 0x7c, 0xbb, 0x70, 0xe2, - 0xc0, 0x03, 0x32, 0x4d, 0x66, 0x9b, 0x7d, 0xe3, 0x23, 0x8f, 0xbc, 0x1e, 0xc7, 0xbb, 0x5a, 0x0a, 0xf3, 0xcd, 0x8a, - 0xae, 0x31, 0x84, 0xa2, 0x08, 0xfb, 0xfd, 0xaa, 0x62, 0x8a, 0x2a, 0x83, 0x36, 0x68, 0x58, 0xde, 0x88, 0x5f, 0xe0, - 0x8c, 0xa1, 0xf5, 0x42, 0xf6, 0x8e, 0xce, 0x3a, 0x9c, 0x39, 0xcc, 0x98, 0x12, 0x18, 0x95, 0x96, 0x05, 0x9d, 0x80, - 0xa3, 0x73, 0xf5, 0x41, 0x54, 0x5c, 0x1d, 0x2b, 0x00, 0x4f, 0x32, 0x85, 0x7f, 0xf2, 0x4d, 0xb0, 0xee, 0xb7, 0x6a, - 0x86, 0xa9, 0xbf, 0xe8, 0x6d, 0xd7, 0xf2, 0x65, 0x88, 0x23, 0x6d, 0x0c, 0xa1, 0x75, 0x6e, 0xef, 0x00, 0x45, 0x5c, - 0xd0, 0x8b, 0x54, 0xe3, 0x6b, 0xb5, 0x18, 0x9a, 0xf5, 0x35, 0xae, 0x63, 0xda, 0x20, 0x8a, 0x75, 0xd7, 0xc4, 0xd7, - 0xd5, 0x2b, 0xb0, 0x2a, 0x55, 0x70, 0x06, 0x09, 0x84, 0x55, 0x79, 0xd9, 0x90, 0x4a, 0x72, 0x69, 0x3a, 0x95, 0xa6, - 0xd3, 0x0a, 0xa1, 0x5c, 0x7a, 0x52, 0xde, 0xbf, 0x42, 0x08, 0x03, 0x53, 0x66, 0x07, 0x56, 0xa9, 0x2d, 0xac, 0x82, - 0x57, 0x2f, 0x36, 0xb0, 0x4a, 0xc2, 0xf1, 0x5c, 0xa2, 0x51, 0x51, 0xe1, 0x90, 0x21, 0x7d, 0x21, 0x16, 0x41, 0x02, - 0x60, 0xd1, 0xbb, 0xcc, 0xe5, 0x7d, 0x0f, 0x87, 0xc2, 0x9e, 0x64, 0x12, 0x4e, 0x37, 0xa1, 0x39, 0x3c, 0x0f, 0xac, - 0x7a, 0x1e, 0x21, 0x60, 0xe9, 0x39, 0x86, 0x67, 0xa1, 0xbf, 0xff, 0x1c, 0xad, 0xb3, 0x20, 0x4f, 0xff, 0x25, 0x4a, - 0x42, 0x63, 0xff, 0x39, 0x1e, 0x3a, 0x24, 0x0c, 0x07, 0xbe, 0x39, 0xc2, 0x0a, 0x07, 0xb7, 0x8a, 0xf8, 0x0c, 0xee, - 0xf0, 0xb1, 0x0e, 0x3d, 0x00, 0x2c, 0xa1, 0x38, 0x04, 0xf9, 0x06, 0x8a, 0x19, 0x1c, 0xd0, 0x64, 0x19, 0x5e, 0xe0, - 0x82, 0xd5, 0x42, 0x79, 0x7f, 0xdb, 0xf2, 0x52, 0x5a, 0xed, 0x92, 0xd7, 0x98, 0x03, 0x95, 0x9f, 0xe1, 0x85, 0xaf, - 0x30, 0xef, 0x55, 0xbb, 0x2f, 0x7c, 0xed, 0x80, 0x9e, 0x42, 0xc0, 0x48, 0xf7, 0x7b, 0x4d, 0xb8, 0xa7, 0xe8, 0x65, - 0x2e, 0x0e, 0xdb, 0x0e, 0xba, 0x17, 0x98, 0xab, 0xab, 0x2a, 0x6b, 0x0e, 0xa6, 0xd0, 0xe0, 0xa0, 0x0a, 0x67, 0x04, - 0xe6, 0xea, 0x45, 0x59, 0x70, 0x0e, 0xe2, 0x7d, 0x4f, 0x98, 0x9c, 0x32, 0x1a, 0xc0, 0x8b, 0xac, 0x7c, 0x74, 0xaa, - 0xc7, 0xc1, 0x65, 0xdc, 0xb0, 0x89, 0x2f, 0x84, 0x4f, 0x05, 0x56, 0xd2, 0x1a, 0x87, 0x46, 0x74, 0x44, 0xe7, 0x60, - 0xb6, 0x01, 0x14, 0xdc, 0x9d, 0x0f, 0x1b, 0x0b, 0x15, 0x3c, 0xc9, 0x5b, 0x7b, 0x41, 0x9b, 0x10, 0x67, 0xd2, 0x14, - 0xdc, 0x6d, 0x1b, 0x64, 0xf0, 0xe6, 0xb7, 0xff, 0x56, 0x58, 0x24, 0x18, 0x50, 0xa9, 0x49, 0x82, 0xf0, 0x04, 0xa5, - 0x91, 0x6e, 0xe5, 0x66, 0x02, 0xe9, 0x44, 0xd4, 0x8c, 0xba, 0x37, 0xce, 0x57, 0x47, 0x0d, 0x44, 0x45, 0x0d, 0x54, - 0x40, 0x0d, 0x64, 0x7d, 0xfb, 0x17, 0xb0, 0x10, 0x36, 0x42, 0x95, 0x08, 0x02, 0x22, 0xcc, 0xb5, 0xe1, 0x03, 0x8a, - 0x24, 0x84, 0xbc, 0x01, 0x54, 0x4c, 0xc9, 0x4b, 0x30, 0x1a, 0x87, 0xd7, 0x7b, 0xc0, 0xfd, 0xd2, 0x32, 0x0c, 0x9e, - 0x53, 0x30, 0xf9, 0x6f, 0x7d, 0x3e, 0x54, 0x2f, 0x57, 0x07, 0x21, 0xfc, 0x02, 0x62, 0x45, 0x38, 0xfe, 0xe2, 0x17, - 0x20, 0x9b, 0x0a, 0xcb, 0x83, 0x03, 0x09, 0x02, 0x3f, 0x44, 0x11, 0x0e, 0x78, 0x86, 0x97, 0xd9, 0x06, 0xd1, 0xf3, - 0xb3, 0x52, 0xd5, 0xac, 0x64, 0x30, 0xab, 0xc2, 0xd3, 0x38, 0xba, 0x26, 0x0c, 0x04, 0x17, 0x6a, 0xf7, 0x0d, 0x42, - 0xa0, 0x6c, 0xb9, 0x31, 0x74, 0xe9, 0x29, 0x98, 0x8f, 0xc6, 0xd1, 0x5b, 0x06, 0x0f, 0x0b, 0x1b, 0x93, 0x7f, 0xa6, - 0x59, 0xa6, 0x0d, 0xf3, 0xd8, 0x08, 0x9c, 0xd4, 0x29, 0x4a, 0x3e, 0x4b, 0x2e, 0xe2, 0xa8, 0x79, 0x19, 0xa1, 0x06, - 0xfc, 0xdb, 0xe0, 0xa8, 0x4b, 0x13, 0x3a, 0x1a, 0xf9, 0xe0, 0x37, 0x19, 0x31, 0x9b, 0x6c, 0xb5, 0x12, 0x15, 0x41, - 0x4f, 0xec, 0x06, 0x03, 0x56, 0xe2, 0x05, 0xb0, 0x0f, 0x96, 0x83, 0x25, 0xef, 0x44, 0xac, 0xfc, 0x29, 0x85, 0xc1, - 0xea, 0x39, 0x43, 0x08, 0x67, 0x41, 0xcc, 0xc6, 0xff, 0x7c, 0xa6, 0xe1, 0xfa, 0xf9, 0xf9, 0x3a, 0x46, 0x44, 0xfa, - 0x20, 0x72, 0x35, 0x76, 0x44, 0x04, 0x61, 0xcb, 0x74, 0xdf, 0x95, 0xf9, 0xc1, 0x5b, 0x57, 0x0f, 0x6c, 0xb8, 0x38, - 0x30, 0xa0, 0x46, 0x81, 0xd1, 0x0a, 0xce, 0x49, 0x39, 0x70, 0x50, 0x42, 0x68, 0x56, 0xc4, 0x53, 0x72, 0x09, 0x91, - 0xf0, 0x32, 0xd4, 0x05, 0xc3, 0x82, 0x40, 0x82, 0x9a, 0x82, 0x04, 0x95, 0xf9, 0xda, 0x23, 0x98, 0x75, 0x6e, 0x66, - 0x3b, 0x45, 0x5d, 0x17, 0xe4, 0xe7, 0x17, 0x1d, 0x8f, 0x80, 0xa5, 0x3d, 0x38, 0x28, 0x20, 0x82, 0x18, 0x50, 0xf0, - 0x52, 0x02, 0x0c, 0xc2, 0xf1, 0x15, 0x1b, 0x1a, 0xf0, 0xb9, 0x36, 0x5e, 0x07, 0xc6, 0xd6, 0xa7, 0x0c, 0x72, 0xf1, - 0xac, 0xda, 0xd3, 0x84, 0x90, 0xfd, 0x56, 0x4f, 0xa7, 0xdb, 0x11, 0x12, 0x7b, 0x1f, 0xb5, 0x09, 0x34, 0xe6, 0x48, - 0x77, 0xb5, 0x31, 0x5f, 0xd5, 0xf4, 0x88, 0xd5, 0x24, 0xa4, 0x0b, 0xd2, 0xe5, 0xf9, 0xb4, 0x67, 0x70, 0xc5, 0x2a, - 0x8d, 0x1c, 0x5c, 0x80, 0x3e, 0x1b, 0x10, 0xa0, 0x40, 0xa5, 0xa9, 0x44, 0x51, 0xc4, 0x45, 0x52, 0xb2, 0x61, 0x98, - 0x41, 0x98, 0xc2, 0x6a, 0x25, 0xe8, 0xc6, 0x1a, 0x00, 0xef, 0xcc, 0xec, 0x9f, 0xd2, 0x07, 0x9b, 0xae, 0xbd, 0x79, - 0x04, 0x10, 0x90, 0xfd, 0x76, 0xc9, 0xae, 0x8b, 0x8d, 0xca, 0x2c, 0xac, 0x65, 0x6c, 0xe5, 0xb6, 0x3d, 0xc6, 0xde, - 0x89, 0x6d, 0x3e, 0x01, 0x42, 0xd4, 0x96, 0x4c, 0x23, 0x44, 0x48, 0x2c, 0x62, 0x5d, 0x1b, 0xb2, 0xd1, 0x86, 0xc2, - 0x53, 0x89, 0x1c, 0xb8, 0x44, 0x13, 0x24, 0xdf, 0x71, 0x09, 0x0e, 0xe1, 0x85, 0x47, 0xf8, 0x5b, 0x60, 0x91, 0x0a, - 0xcc, 0xb0, 0x5c, 0xad, 0xa0, 0x9e, 0xc7, 0xfb, 0x6c, 0x33, 0x38, 0xa9, 0xdc, 0x18, 0xbb, 0xb4, 0x13, 0x8f, 0xcb, - 0x26, 0x24, 0xce, 0xa0, 0x5f, 0x5f, 0x11, 0xf5, 0xf6, 0xdb, 0xe9, 0x13, 0xff, 0x5e, 0x99, 0xdb, 0x81, 0xd8, 0xb0, - 0xde, 0x60, 0xf5, 0x01, 0xb4, 0xfc, 0x9f, 0xcc, 0x3f, 0x54, 0x16, 0xdc, 0x24, 0xa8, 0xcd, 0x45, 0xec, 0xb2, 0x2e, - 0x62, 0xa4, 0xb6, 0xb8, 0x3b, 0x84, 0xf8, 0x7f, 0xb6, 0xa2, 0x18, 0xf0, 0xa4, 0xe2, 0x9f, 0x63, 0xd4, 0x85, 0x50, - 0xd4, 0xd6, 0xc3, 0x06, 0x28, 0xed, 0x72, 0x5d, 0x89, 0x91, 0x21, 0x81, 0x7c, 0xeb, 0xc2, 0x0b, 0x9a, 0x93, 0x48, - 0x81, 0x9c, 0x1c, 0x44, 0x25, 0xcd, 0x36, 0x84, 0xb9, 0xee, 0x16, 0x8e, 0x99, 0xab, 0x0d, 0x5a, 0xc4, 0x2f, 0x80, - 0x9d, 0xe1, 0x46, 0xb2, 0x74, 0xe0, 0x53, 0x35, 0xf0, 0xf9, 0x35, 0x37, 0x14, 0x45, 0xa1, 0xde, 0x3b, 0xfb, 0xc8, - 0x1c, 0xfc, 0x4e, 0x03, 0xf1, 0x91, 0x3a, 0x1d, 0xc9, 0x46, 0xa8, 0x35, 0x67, 0xc7, 0xcb, 0x36, 0x23, 0x0c, 0x0a, - 0x1b, 0xbd, 0xaf, 0x42, 0x56, 0xb1, 0xb3, 0x53, 0x11, 0xcc, 0xe9, 0xab, 0xaa, 0x9c, 0x53, 0xb9, 0x65, 0x54, 0x4b, - 0x4d, 0x03, 0x44, 0xb8, 0xf2, 0x89, 0xe4, 0x51, 0x66, 0xc2, 0x3f, 0x18, 0x8c, 0xab, 0x47, 0x0a, 0x7f, 0xb4, 0x2b, - 0x76, 0xc8, 0x76, 0x74, 0xb8, 0x8d, 0xa0, 0x79, 0xa1, 0x82, 0x07, 0x1c, 0x95, 0x2c, 0x21, 0x52, 0xe4, 0x72, 0x5f, - 0xd5, 0x4c, 0xd9, 0xae, 0x23, 0x84, 0x90, 0xf6, 0x38, 0xeb, 0x86, 0x56, 0x0f, 0x3d, 0x52, 0x45, 0x39, 0xdc, 0xa2, - 0xb9, 0x2e, 0x40, 0x85, 0x11, 0x48, 0x97, 0x5f, 0xd8, 0x5d, 0x2a, 0x21, 0x7a, 0xf9, 0xda, 0x85, 0x30, 0x76, 0x56, - 0x96, 0xb8, 0x30, 0xa3, 0xb6, 0x61, 0x74, 0xdd, 0xc6, 0x70, 0x36, 0x30, 0x66, 0x1a, 0x94, 0xb4, 0x20, 0xd4, 0x75, - 0x97, 0x5e, 0x64, 0x26, 0xd0, 0x63, 0x4e, 0x68, 0x83, 0xe1, 0x29, 0xd1, 0x60, 0xd9, 0x54, 0x80, 0x05, 0xdf, 0xb2, - 0x48, 0xad, 0xcd, 0x26, 0x8b, 0x3f, 0xea, 0xd8, 0x3c, 0xed, 0x97, 0x57, 0xcc, 0x73, 0xe1, 0xa8, 0xdb, 0xf3, 0xcc, - 0xc7, 0xa3, 0x7b, 0xfa, 0xe6, 0xea, 0xc5, 0xcb, 0xd7, 0xaf, 0x56, 0xab, 0x36, 0x6b, 0xb6, 0x4f, 0xf0, 0x4f, 0xba, - 0x8c, 0x07, 0x5b, 0x46, 0x01, 0x3a, 0x38, 0xd8, 0xe7, 0xc6, 0x85, 0xe7, 0x0b, 0x9f, 0x43, 0xdc, 0x20, 0x3d, 0xc0, - 0x59, 0x51, 0xc6, 0x04, 0xb9, 0x8d, 0x7a, 0xd1, 0x5d, 0x04, 0x4a, 0xa8, 0x8a, 0xfc, 0x7d, 0xd8, 0x9c, 0xfd, 0x1e, - 0x04, 0x26, 0x82, 0xfa, 0x10, 0x01, 0x04, 0xe2, 0x95, 0xe2, 0x82, 0x30, 0x9f, 0x00, 0x51, 0xbc, 0x17, 0xc0, 0x99, - 0x9a, 0xa8, 0x55, 0x0b, 0x15, 0x17, 0x40, 0x12, 0x6d, 0x38, 0x4a, 0x7a, 0x64, 0x02, 0x78, 0x43, 0x50, 0x4a, 0xfb, - 0xab, 0x9b, 0x3b, 0x77, 0xa9, 0x1c, 0xf5, 0x5a, 0x69, 0x8e, 0xa7, 0xee, 0x73, 0x0a, 0x9f, 0xd3, 0xae, 0x3f, 0x1d, - 0xc4, 0x61, 0x8e, 0x17, 0x44, 0x1c, 0xfa, 0x67, 0x11, 0x97, 0xf3, 0x82, 0x7d, 0xe5, 0x72, 0xa1, 0xd2, 0xe5, 0x6d, - 0x2a, 0x93, 0xdb, 0xe6, 0xe8, 0x30, 0x2e, 0x92, 0xdb, 0xa6, 0x4a, 0x6e, 0x11, 0xbe, 0x4b, 0x65, 0x72, 0x67, 0x53, - 0xee, 0x9a, 0x0a, 0x6e, 0xbe, 0xb0, 0x80, 0x43, 0xd1, 0x16, 0x6d, 0x2c, 0x36, 0x8b, 0xda, 0x14, 0x57, 0x34, 0xc0, - 0xe0, 0xdf, 0x77, 0x6c, 0xfc, 0x30, 0x7c, 0x09, 0x2e, 0x4d, 0x9a, 0xc8, 0x4f, 0x20, 0xfd, 0xb4, 0x2a, 0x03, 0xf7, - 0x29, 0x69, 0x75, 0xa7, 0x17, 0xa2, 0xd9, 0xee, 0x36, 0x1a, 0x53, 0xd8, 0xbb, 0x19, 0xc9, 0x7d, 0xb1, 0x69, 0xc3, - 0xc4, 0xd7, 0xd9, 0xcf, 0x56, 0xab, 0xfd, 0x1c, 0x99, 0x0d, 0x37, 0x61, 0xb1, 0xee, 0x4f, 0x07, 0xb8, 0x85, 0x9f, - 0x67, 0x08, 0x2d, 0x59, 0x7f, 0x3a, 0x20, 0xac, 0x3f, 0x6d, 0xb4, 0x07, 0xd6, 0xd0, 0xce, 0x6c, 0xc5, 0x35, 0x84, - 0xd0, 0x9c, 0x0e, 0x8e, 0x4c, 0x49, 0xe9, 0xf2, 0xed, 0x17, 0xad, 0x02, 0xfa, 0xa9, 0x5a, 0xf0, 0x32, 0x89, 0x3b, - 0xd0, 0x17, 0xbd, 0xb0, 0x4f, 0xb7, 0x16, 0xe4, 0xf8, 0xa8, 0x72, 0xb5, 0xa7, 0x08, 0x9b, 0x9e, 0xd4, 0x61, 0x71, - 0x68, 0x9a, 0x71, 0x5d, 0x4a, 0xf7, 0x1d, 0x6a, 0x46, 0x3e, 0x3a, 0x58, 0x00, 0x82, 0x54, 0xf0, 0xc8, 0x0a, 0x17, - 0x4e, 0x29, 0x84, 0x8b, 0x83, 0xca, 0x16, 0x4c, 0x72, 0xd2, 0xea, 0xe6, 0xc6, 0xd2, 0x3f, 0x77, 0x11, 0x4d, 0x29, - 0xa6, 0x24, 0xf3, 0x25, 0x73, 0x03, 0x16, 0xba, 0x49, 0x79, 0xa6, 0xa0, 0x57, 0x1a, 0xe0, 0x11, 0x81, 0x78, 0x48, - 0xdd, 0xc2, 0x18, 0x78, 0xc5, 0xd3, 0x66, 0xd1, 0x67, 0x03, 0x74, 0x74, 0x8c, 0x69, 0xff, 0x53, 0x36, 0x6f, 0xc3, - 0x63, 0x81, 0x9f, 0x06, 0x64, 0xda, 0x94, 0x65, 0x82, 0x80, 0x84, 0x51, 0x53, 0x1e, 0xc2, 0x5e, 0x42, 0x38, 0xb3, - 0x15, 0xb3, 0x3e, 0x1b, 0x34, 0xa7, 0x65, 0xc5, 0x8e, 0xaf, 0xd8, 0x90, 0x65, 0x82, 0xad, 0xd8, 0x70, 0x15, 0xc3, - 0xd7, 0x19, 0x0c, 0x08, 0x42, 0x00, 0x30, 0x00, 0x80, 0x46, 0x41, 0x34, 0x5f, 0xac, 0x88, 0xdf, 0xec, 0xf6, 0x1e, - 0xbf, 0x05, 0x16, 0x68, 0xb5, 0xfd, 0xbf, 0x0b, 0x65, 0xc0, 0x9e, 0xb2, 0x30, 0x31, 0x73, 0x0b, 0xab, 0xa2, 0x03, - 0xa8, 0x94, 0x08, 0x53, 0x18, 0xc8, 0xec, 0x67, 0x06, 0x6a, 0x81, 0xd6, 0x20, 0xef, 0xeb, 0x41, 0x33, 0x83, 0x23, - 0x06, 0xde, 0xa1, 0x21, 0x53, 0x63, 0x4c, 0x18, 0xe7, 0x30, 0xc5, 0xcc, 0x80, 0x67, 0x9a, 0xb6, 0xd6, 0xd2, 0xc8, - 0x72, 0xbd, 0xbc, 0xf7, 0xb7, 0x8e, 0x55, 0xbf, 0x68, 0xb6, 0x07, 0x68, 0x9f, 0x10, 0xfb, 0x31, 0x80, 0x4d, 0xe6, - 0x52, 0x1b, 0xe6, 0xfb, 0xa8, 0x93, 0xda, 0x4f, 0xf8, 0x33, 0x58, 0x9b, 0x1d, 0x00, 0x3a, 0x32, 0x6c, 0xd6, 0x5f, - 0xd6, 0x54, 0x5e, 0x1f, 0x77, 0x46, 0xa9, 0xdc, 0xf5, 0xee, 0x74, 0xa0, 0x29, 0x0e, 0xbd, 0xf5, 0x70, 0xf9, 0x50, - 0x0f, 0x01, 0x33, 0x06, 0x73, 0xcb, 0x8c, 0xbe, 0x17, 0x22, 0xb9, 0x20, 0x12, 0x4b, 0x83, 0x35, 0x0c, 0xf6, 0xd6, - 0xc1, 0x81, 0xa9, 0xc6, 0x1a, 0xf0, 0x3c, 0x29, 0x02, 0xc1, 0xc0, 0x47, 0x50, 0x06, 0x34, 0x51, 0xe6, 0x36, 0x9c, - 0x7c, 0x64, 0xee, 0x17, 0x2e, 0x6f, 0x1f, 0x0b, 0xa7, 0x6d, 0x35, 0xd7, 0xe3, 0x65, 0x81, 0xbb, 0xf2, 0x5e, 0xd2, - 0x2a, 0xb8, 0x91, 0xbd, 0xc9, 0x53, 0xe6, 0x6e, 0xdd, 0x97, 0xea, 0xec, 0x6e, 0xa6, 0x53, 0x36, 0xd3, 0xd9, 0x6e, - 0x26, 0xd4, 0xcc, 0x7c, 0xcb, 0x2a, 0xd2, 0x9c, 0xac, 0x89, 0x9a, 0x53, 0xf1, 0x13, 0x9d, 0x83, 0x76, 0x94, 0xdb, - 0x7b, 0x55, 0x38, 0xb9, 0x72, 0x72, 0xb9, 0x9f, 0x1b, 0xe2, 0x8a, 0xcc, 0x85, 0x3a, 0x04, 0x78, 0x79, 0x51, 0x3e, - 0x3e, 0xc0, 0xa5, 0xf8, 0x55, 0x8e, 0x5c, 0x94, 0x53, 0x21, 0xb5, 0x14, 0x2c, 0x42, 0x06, 0x55, 0x5d, 0x0c, 0xec, - 0xa5, 0xdd, 0x7b, 0xa2, 0xc7, 0xfb, 0x55, 0xc4, 0xbc, 0x81, 0x79, 0xee, 0xe3, 0x7b, 0x9a, 0x62, 0xa7, 0x26, 0xce, - 0xc8, 0x87, 0x2c, 0xce, 0x41, 0x36, 0xeb, 0x57, 0xaf, 0xfd, 0x36, 0xda, 0xb8, 0x68, 0xc6, 0xa2, 0x67, 0x9e, 0x38, - 0xf9, 0xa1, 0x30, 0xc6, 0x01, 0xd6, 0xd1, 0x1f, 0x61, 0x6a, 0xc1, 0x9e, 0x25, 0x9e, 0x42, 0x27, 0xb7, 0x36, 0xed, - 0x2e, 0x4c, 0xbb, 0x33, 0x69, 0x1d, 0x28, 0x07, 0xa4, 0xd9, 0x95, 0xe9, 0xdc, 0xf9, 0xef, 0x3b, 0x78, 0xe9, 0x76, - 0x0d, 0x91, 0xb8, 0xe7, 0x8f, 0x8c, 0x31, 0xc4, 0x1b, 0xb0, 0x11, 0x55, 0x07, 0x07, 0x7f, 0x38, 0xef, 0xdb, 0x4a, - 0xee, 0xfb, 0x56, 0x38, 0xb0, 0x0d, 0xa6, 0xd2, 0xe5, 0x8d, 0x64, 0xb6, 0x00, 0xbb, 0xce, 0xfd, 0x6f, 0xc4, 0xc3, - 0x17, 0x21, 0xd3, 0x62, 0x5d, 0xc5, 0x5f, 0xc9, 0x51, 0xe9, 0x21, 0xaa, 0x21, 0x02, 0x69, 0x65, 0x5d, 0x1a, 0x9a, - 0x8e, 0x5e, 0x4d, 0xe9, 0x48, 0xde, 0xbc, 0x95, 0x52, 0x0f, 0xec, 0x8b, 0xdc, 0x3a, 0x81, 0x47, 0x0b, 0x6b, 0x0c, - 0xcd, 0x5d, 0xe9, 0x9d, 0x64, 0x03, 0xa2, 0xd6, 0xc7, 0x1d, 0x4a, 0x22, 0xb1, 0xa8, 0xee, 0x42, 0x38, 0xdc, 0x85, - 0x60, 0x5e, 0x06, 0x6d, 0x83, 0xd8, 0xed, 0x2e, 0x68, 0x1b, 0x38, 0x75, 0xdb, 0xc0, 0xed, 0xc1, 0x60, 0x61, 0xef, - 0xc3, 0xcb, 0xb1, 0x1c, 0x0b, 0x7f, 0x4d, 0x66, 0x1f, 0x00, 0x02, 0xb5, 0x0f, 0x2b, 0x9e, 0x38, 0x10, 0x24, 0xce, - 0x70, 0xf4, 0x3d, 0x67, 0x37, 0xd6, 0x72, 0x78, 0x36, 0x5f, 0x68, 0x36, 0x32, 0x77, 0xd4, 0xa0, 0xe2, 0xab, 0xfb, - 0x79, 0xfd, 0x94, 0xd5, 0x74, 0xe3, 0xf7, 0x20, 0x8c, 0x84, 0x53, 0x76, 0x18, 0x85, 0x84, 0x0d, 0x66, 0x55, 0xc6, - 0x6b, 0xfb, 0x0d, 0xe2, 0x3d, 0x68, 0x13, 0x4e, 0xb0, 0xa8, 0x5d, 0x50, 0x45, 0xd8, 0xc6, 0x1b, 0x0b, 0xa2, 0x3c, - 0xbc, 0xd9, 0x32, 0x9a, 0x5e, 0xae, 0x21, 0xd0, 0x71, 0x2f, 0x6a, 0x46, 0x0d, 0x96, 0xba, 0xa0, 0xcc, 0x3e, 0xc2, - 0xb8, 0xba, 0x38, 0x31, 0x71, 0xda, 0x4b, 0xbd, 0xfa, 0x6f, 0x19, 0x18, 0xe0, 0x0b, 0xf0, 0x12, 0x0b, 0xa3, 0xbb, - 0xf6, 0x75, 0x03, 0xea, 0xcb, 0x06, 0x1b, 0xa0, 0xd5, 0xaa, 0x55, 0x3e, 0x03, 0xe5, 0xae, 0xb9, 0x84, 0xbd, 0xe6, - 0x12, 0xee, 0x9a, 0x4b, 0xf8, 0x6b, 0x2e, 0x61, 0xae, 0xb9, 0x84, 0xbf, 0xe6, 0xf2, 0x20, 0xfc, 0x33, 0x88, 0xe3, - 0x18, 0x73, 0x88, 0xab, 0xa8, 0x6d, 0x64, 0x3c, 0xb8, 0xf0, 0xdc, 0x67, 0x89, 0x2a, 0x97, 0x3f, 0x8c, 0x21, 0xb7, - 0x65, 0x2b, 0x61, 0xdc, 0xa6, 0x98, 0x82, 0xc8, 0xe9, 0x07, 0x07, 0xa5, 0xbb, 0x33, 0xf8, 0xa8, 0xa7, 0x1c, 0x2f, - 0xad, 0x13, 0xed, 0x1f, 0xa0, 0x93, 0x37, 0xbf, 0x3e, 0xa6, 0x72, 0x4d, 0x84, 0x33, 0xb9, 0xdf, 0x6f, 0x7b, 0x4a, - 0xf1, 0x67, 0x66, 0xc2, 0x93, 0xf3, 0x44, 0x1b, 0x11, 0x04, 0x21, 0x4a, 0x14, 0xce, 0x88, 0xfc, 0x7f, 0xd9, 0x7b, - 0xd7, 0xe5, 0xb6, 0x91, 0x2c, 0x5d, 0xf4, 0x55, 0x24, 0x86, 0xcd, 0x02, 0xcc, 0x24, 0x45, 0x79, 0xef, 0x99, 0x88, - 0x03, 0x2a, 0xc5, 0xf0, 0xa5, 0xdc, 0xe5, 0xee, 0xf2, 0xa5, 0x2d, 0x77, 0x75, 0x55, 0x33, 0x78, 0x58, 0x10, 0x90, - 0x14, 0xe0, 0x02, 0x01, 0x16, 0x00, 0x4a, 0xa4, 0x49, 0xbc, 0xfb, 0x8e, 0xb5, 0x56, 0x5e, 0x41, 0x50, 0x76, 0xcf, - 0xec, 0xf9, 0x75, 0xce, 0x1f, 0x5b, 0x4c, 0x24, 0x12, 0x79, 0xcf, 0x95, 0xeb, 0xf2, 0x7d, 0xb4, 0xde, 0x55, 0x28, - 0x3c, 0xaa, 0xa2, 0x94, 0x5b, 0xc9, 0xab, 0x0c, 0x82, 0xd8, 0xd1, 0x0b, 0xc3, 0x9f, 0x40, 0x08, 0x41, 0x84, 0x09, - 0xbf, 0x0e, 0x33, 0xda, 0xce, 0x22, 0x9d, 0xf4, 0xdb, 0x30, 0xc3, 0x0d, 0xac, 0xe4, 0xe7, 0xaa, 0xcf, 0xf6, 0xdb, - 0x20, 0x64, 0xbb, 0x20, 0x62, 0xb7, 0xc5, 0x36, 0x28, 0x6d, 0x5f, 0x10, 0x65, 0xf8, 0x5b, 0x7a, 0xbd, 0x3c, 0x84, - 0x78, 0x9f, 0x5e, 0x9a, 0x9f, 0xa5, 0xad, 0x28, 0xc0, 0x7d, 0x84, 0x1e, 0xd5, 0x81, 0x60, 0x27, 0x3c, 0xe1, 0x01, - 0x9c, 0xac, 0x66, 0x15, 0x7f, 0x92, 0x82, 0x38, 0x51, 0x70, 0x08, 0xb8, 0xda, 0xde, 0xa4, 0x5f, 0xc1, 0xf0, 0xa5, - 0x83, 0x2d, 0x87, 0xb7, 0xc5, 0xb6, 0xc7, 0x4a, 0xfe, 0x11, 0xd8, 0xb7, 0x7a, 0x32, 0x56, 0xb7, 0x07, 0xce, 0xba, - 0x94, 0xa2, 0xe3, 0x4d, 0x71, 0x78, 0x7b, 0x3e, 0xdb, 0x6f, 0x83, 0x88, 0xed, 0x82, 0x0c, 0x6b, 0x9d, 0x34, 0xfc, - 0xaf, 0xb4, 0x75, 0xb0, 0x18, 0x61, 0xff, 0x97, 0xf5, 0xc0, 0x4b, 0x48, 0x0d, 0x05, 0x2e, 0x06, 0x1b, 0x8e, 0xd6, - 0x76, 0x99, 0x06, 0x6e, 0x6a, 0xd0, 0xeb, 0x7b, 0x0a, 0x51, 0x5e, 0x32, 0x9a, 0x1b, 0xc1, 0xba, 0x31, 0xe4, 0xe2, - 0x70, 0xdc, 0x2c, 0x87, 0xbc, 0xa4, 0xe9, 0x34, 0x08, 0xa5, 0x3b, 0xcb, 0x1a, 0x92, 0x28, 0xfb, 0x20, 0xd4, 0xae, - 0x2d, 0xfb, 0x6d, 0x60, 0xfb, 0xf2, 0x47, 0xc3, 0xd8, 0xbf, 0x58, 0x3e, 0x13, 0xd2, 0x45, 0x3c, 0x07, 0x41, 0xd4, - 0x7e, 0x9e, 0x0d, 0x37, 0xfe, 0xc5, 0xfa, 0x99, 0x50, 0x7e, 0xe3, 0xb9, 0x2d, 0x87, 0xd4, 0x59, 0x0b, 0x5f, 0x18, - 0x0f, 0x0f, 0xae, 0x0c, 0x6d, 0x87, 0x83, 0xd0, 0x7f, 0x9b, 0x35, 0x82, 0x1b, 0x1b, 0xda, 0xe7, 0x0b, 0x1f, 0xb6, - 0x36, 0x1a, 0x6b, 0x8a, 0xe9, 0x16, 0xfa, 0x37, 0x99, 0x2d, 0xed, 0x69, 0x54, 0xf2, 0xe2, 0xd4, 0x34, 0x62, 0x21, - 0x0c, 0x18, 0xfa, 0xc9, 0x7c, 0x04, 0xd5, 0xdc, 0xf1, 0x08, 0x64, 0xf2, 0x81, 0x1e, 0xac, 0x49, 0xad, 0xfa, 0x6b, - 0x98, 0xc9, 0xff, 0x23, 0x15, 0x16, 0xa3, 0xbb, 0x6d, 0x98, 0xa9, 0x3f, 0x22, 0xf9, 0x07, 0xcb, 0xf9, 0x2e, 0xf5, - 0x42, 0xed, 0xc7, 0xc2, 0x0a, 0x0c, 0x4a, 0x54, 0x0d, 0xe8, 0x81, 0x08, 0xaa, 0x32, 0x48, 0x33, 0xac, 0xce, 0x41, - 0xbf, 0x7b, 0x5a, 0x75, 0x24, 0x87, 0xb4, 0x56, 0x43, 0x2a, 0x98, 0x2a, 0x35, 0xc8, 0x0f, 0x87, 0xbb, 0x94, 0xe9, - 0x32, 0xe0, 0x92, 0x7e, 0x97, 0x2a, 0xa5, 0xf0, 0x9f, 0x08, 0x40, 0xe7, 0xe0, 0x1e, 0x5f, 0x8e, 0x81, 0x34, 0xc3, - 0xc2, 0x6f, 0xcd, 0x8e, 0xaf, 0x49, 0xb8, 0x4d, 0x82, 0x8b, 0x01, 0xce, 0xd1, 0x55, 0x58, 0xde, 0xa5, 0x10, 0x41, - 0x55, 0x42, 0x7d, 0x2b, 0xd3, 0xa0, 0xb4, 0xd5, 0x20, 0xac, 0x49, 0xa8, 0x33, 0xc9, 0x46, 0xa5, 0xed, 0x46, 0x61, - 0xb6, 0x88, 0xeb, 0x19, 0x61, 0xcd, 0xd9, 0x4c, 0x35, 0x30, 0x69, 0x38, 0x6e, 0x1a, 0xad, 0x45, 0x85, 0x9a, 0xc2, - 0xbc, 0xc6, 0x55, 0xa5, 0xaa, 0xbb, 0x39, 0xb5, 0x94, 0x96, 0xed, 0x55, 0x37, 0xc9, 0x86, 0x5c, 0x86, 0x32, 0x0c, - 0x36, 0x72, 0x04, 0x13, 0x48, 0x92, 0x33, 0x7f, 0x23, 0xff, 0x50, 0x9b, 0xae, 0x05, 0xcc, 0x31, 0x66, 0xd9, 0xb0, - 0xa0, 0x57, 0xe0, 0x1e, 0x68, 0xa5, 0xe7, 0xd3, 0xec, 0x22, 0x0f, 0x92, 0x61, 0xa1, 0x97, 0x4d, 0xc6, 0xff, 0x14, - 0x46, 0x9a, 0xcc, 0x58, 0xc9, 0x22, 0xdb, 0xd5, 0x29, 0x71, 0x1e, 0x27, 0xb0, 0x3d, 0x9a, 0xde, 0xf2, 0x7d, 0x06, - 0x51, 0x41, 0xa0, 0x60, 0xc6, 0x7c, 0xd9, 0xc5, 0x73, 0xdf, 0x67, 0x96, 0xa9, 0xfb, 0x70, 0x30, 0x66, 0x6c, 0xbf, - 0xdf, 0xcf, 0xfb, 0x7d, 0x35, 0xdf, 0xfa, 0xfd, 0xe4, 0xda, 0xfc, 0xed, 0x01, 0x83, 0x82, 0x9c, 0x88, 0xa6, 0x42, - 0x04, 0xff, 0x90, 0x3c, 0x43, 0x32, 0xba, 0xe3, 0x3e, 0xb7, 0x9c, 0x2d, 0xab, 0x23, 0x10, 0xcc, 0xc3, 0xe1, 0x52, - 0x81, 0x5d, 0x4b, 0x14, 0x09, 0x59, 0xfe, 0x33, 0x30, 0x9e, 0xb9, 0x0f, 0xb0, 0x64, 0x00, 0xc2, 0x56, 0x79, 0xba, - 0xde, 0xf3, 0x55, 0xf0, 0x4e, 0xc7, 0xbb, 0xc6, 0x8a, 0x0c, 0xc4, 0x2d, 0xb0, 0x11, 0x6b, 0xed, 0x01, 0x39, 0x53, - 0x80, 0xe3, 0xc5, 0xe1, 0x70, 0x2e, 0x7f, 0xe9, 0x66, 0xeb, 0x04, 0x2a, 0x05, 0x6e, 0x8f, 0x4e, 0x0e, 0xfe, 0x3b, - 0xd0, 0x0c, 0xca, 0x61, 0x5e, 0x6f, 0x7f, 0x67, 0x4e, 0x7e, 0x7a, 0x8a, 0x7f, 0xc2, 0x43, 0x74, 0xfa, 0xed, 0xde, - 0xfc, 0x41, 0x51, 0x79, 0x38, 0xa8, 0xc5, 0x7f, 0xce, 0x79, 0x05, 0xbf, 0xf0, 0x4d, 0x60, 0x36, 0x99, 0x7a, 0x27, - 0xdf, 0xe4, 0x39, 0x53, 0xaf, 0xf1, 0x8a, 0xc9, 0x77, 0x38, 0x9c, 0x8b, 0x51, 0xbd, 0x1d, 0x39, 0xd1, 0x4e, 0x39, - 0xc6, 0xc1, 0xe0, 0xbf, 0x88, 0xb6, 0x09, 0x01, 0x86, 0xd4, 0x2d, 0x69, 0x66, 0xe3, 0xca, 0x12, 0xcf, 0xd2, 0xf9, - 0xe5, 0xa4, 0x2e, 0x77, 0x5a, 0xf1, 0xb4, 0x07, 0x16, 0xb7, 0x35, 0x78, 0x01, 0xdc, 0x5b, 0x6c, 0x5d, 0x29, 0x38, - 0x5c, 0x40, 0x9c, 0xe2, 0x04, 0x44, 0xd0, 0x7e, 0x5f, 0xe2, 0xbd, 0x82, 0x3e, 0xe9, 0x47, 0x08, 0x86, 0xfc, 0x59, - 0x02, 0xee, 0x7a, 0xbd, 0x1a, 0xe3, 0x7b, 0x29, 0x04, 0xd7, 0x67, 0x1a, 0x80, 0x16, 0xfc, 0x2e, 0x1f, 0xcb, 0xe9, - 0x37, 0x11, 0x78, 0xb6, 0xec, 0x4d, 0x94, 0xbb, 0x0d, 0x4f, 0xfb, 0x47, 0x0b, 0x01, 0x58, 0x8a, 0x67, 0x4a, 0xb0, - 0x20, 0xa7, 0x98, 0x8b, 0xff, 0x17, 0x7c, 0xc4, 0x7c, 0x4f, 0xba, 0x88, 0xad, 0xb7, 0x4f, 0x2e, 0x0c, 0x24, 0xd0, - 0x74, 0x00, 0x7e, 0xbc, 0x0a, 0xe8, 0xca, 0xf8, 0xf9, 0x59, 0xd6, 0x63, 0x7d, 0xfc, 0xa7, 0xe0, 0x3e, 0xfd, 0x4c, - 0xe1, 0xa3, 0xc3, 0x71, 0x95, 0x8e, 0x76, 0x94, 0x82, 0xe8, 0xe8, 0xf6, 0xf9, 0x94, 0x67, 0xdf, 0x55, 0x40, 0x6e, - 0x39, 0x6a, 0x4f, 0x05, 0x60, 0xb1, 0xa5, 0x23, 0xf0, 0x69, 0x96, 0x4f, 0xc8, 0xf7, 0x7a, 0x2a, 0xae, 0x2e, 0x75, - 0xba, 0xb8, 0x1e, 0x4f, 0xe1, 0x7f, 0x20, 0xf6, 0xb0, 0x4c, 0x91, 0x1d, 0xbb, 0x2e, 0x7e, 0x10, 0x6f, 0x6b, 0x3b, - 0xfa, 0x63, 0x07, 0x91, 0x8e, 0x7b, 0x72, 0xa1, 0xbe, 0x84, 0x54, 0x72, 0xa1, 0x6e, 0x20, 0x76, 0xa1, 0xc6, 0x3b, - 0x2e, 0x62, 0xad, 0xbf, 0xab, 0x51, 0xb0, 0x12, 0x70, 0xa6, 0xbd, 0x03, 0x83, 0x0d, 0xac, 0x5b, 0x96, 0xc1, 0xdf, - 0x70, 0x4d, 0x13, 0xb8, 0x61, 0x91, 0xf5, 0xde, 0x60, 0x2b, 0xbd, 0x03, 0x47, 0xcb, 0xc4, 0xb9, 0x94, 0x64, 0x65, - 0x8b, 0x8c, 0xab, 0x47, 0x21, 0x55, 0xd3, 0xfd, 0xad, 0xa8, 0x1f, 0x84, 0xc8, 0x83, 0x55, 0xca, 0xa2, 0x62, 0x05, - 0x32, 0x7b, 0xf0, 0xf7, 0x90, 0x91, 0xa3, 0x1c, 0x38, 0x0a, 0xfd, 0xbd, 0x09, 0x74, 0x9e, 0xbf, 0x86, 0x3a, 0x8f, - 0x04, 0x5b, 0xa9, 0x87, 0xc2, 0xca, 0x0b, 0x88, 0x0e, 0xb6, 0x30, 0x56, 0x79, 0x12, 0x2a, 0x36, 0x65, 0x22, 0x8f, - 0x83, 0x5a, 0x02, 0xc6, 0x0a, 0x82, 0x39, 0xcb, 0xa5, 0x0b, 0x52, 0xd5, 0xe8, 0x61, 0x91, 0xb9, 0x9f, 0x0a, 0xca, - 0xff, 0x54, 0xe5, 0x84, 0xeb, 0xcb, 0x10, 0xe0, 0x68, 0x9f, 0x82, 0x28, 0x31, 0xd6, 0x2f, 0x5a, 0xbc, 0x93, 0x99, - 0xb3, 0xa9, 0xed, 0x25, 0xc8, 0xd8, 0x0e, 0xbf, 0x42, 0x68, 0xb5, 0x50, 0x64, 0xd1, 0x70, 0xc1, 0x74, 0x7b, 0x4a, - 0xab, 0xee, 0x61, 0xc3, 0xb3, 0xd2, 0x43, 0xa5, 0xbe, 0x8d, 0x09, 0x2c, 0xab, 0x94, 0xe1, 0xdb, 0x09, 0x55, 0x27, - 0x06, 0x15, 0xeb, 0x86, 0x2d, 0xe1, 0x10, 0x8b, 0x49, 0x63, 0x9d, 0x0d, 0x78, 0xc4, 0x12, 0xf8, 0x67, 0xc3, 0xc7, - 0x6c, 0xc9, 0xa3, 0xc9, 0xe6, 0x6a, 0xd9, 0xef, 0x97, 0x5e, 0xe8, 0xd5, 0xb3, 0xec, 0x69, 0x34, 0x9f, 0xe5, 0x73, - 0x1f, 0x15, 0x17, 0x93, 0xc1, 0x60, 0xe3, 0x67, 0xc3, 0x21, 0x4b, 0x86, 0xc3, 0x49, 0xf6, 0x14, 0x5e, 0x7b, 0xca, - 0x23, 0xb5, 0xa4, 0x92, 0xab, 0x0c, 0xf6, 0xf7, 0x01, 0x8f, 0x7c, 0xd6, 0xf9, 0x69, 0xd9, 0x74, 0xe9, 0x7e, 0x66, - 0x75, 0x40, 0xa9, 0x3b, 0xc0, 0xc6, 0xdb, 0x06, 0x1d, 0xf9, 0xb7, 0x3b, 0xa4, 0xd4, 0x4d, 0x06, 0x60, 0x37, 0x1a, - 0xe0, 0x90, 0xa9, 0x5e, 0x8a, 0xac, 0x5e, 0xca, 0x54, 0x2f, 0xc9, 0xca, 0x25, 0x58, 0x48, 0x4c, 0x95, 0xdb, 0xc8, - 0xca, 0x2d, 0x1b, 0xae, 0x87, 0x83, 0xad, 0x15, 0x97, 0xcd, 0x1d, 0xdc, 0x17, 0x56, 0x14, 0xf8, 0x7f, 0xcb, 0x16, - 0xec, 0x5e, 0x1e, 0x03, 0xef, 0xd0, 0x31, 0x09, 0x2e, 0x10, 0xf7, 0xec, 0x16, 0xec, 0xb0, 0xf0, 0x17, 0x5c, 0x27, - 0xc7, 0x6c, 0x87, 0x8f, 0x42, 0xaf, 0x60, 0xb7, 0x3e, 0x01, 0xed, 0x82, 0xad, 0x01, 0xb2, 0xb1, 0x2d, 0x3e, 0xba, - 0x3b, 0x1c, 0xde, 0x79, 0x3e, 0x7b, 0xc0, 0x1f, 0xe7, 0x77, 0x87, 0xc3, 0xce, 0x33, 0xea, 0xbd, 0x1b, 0x9e, 0xb0, - 0x0f, 0x3c, 0x99, 0xdc, 0x5c, 0xf1, 0x78, 0x32, 0x18, 0xdc, 0xf8, 0x0b, 0x5e, 0xcf, 0x6e, 0x40, 0x3b, 0x70, 0xbe, - 0x90, 0xba, 0x66, 0xef, 0x96, 0x67, 0xde, 0x02, 0xc7, 0xe6, 0x16, 0x8e, 0xde, 0x7e, 0xdf, 0xbb, 0xe3, 0x91, 0x77, - 0x4b, 0x2a, 0xa6, 0x15, 0x57, 0x1c, 0x6f, 0x5b, 0xdc, 0x4f, 0x57, 0x3c, 0x84, 0x47, 0x58, 0x95, 0xe9, 0x4d, 0xf0, - 0xc1, 0x67, 0x2b, 0xcd, 0x02, 0xf7, 0x80, 0x39, 0xd6, 0x64, 0x27, 0x34, 0x13, 0x7f, 0x85, 0xfd, 0x73, 0xa3, 0xfa, - 0x87, 0xe6, 0x7f, 0xa9, 0xfb, 0x09, 0xdc, 0xbe, 0xc8, 0x82, 0xc4, 0x3e, 0xf0, 0x1b, 0x76, 0xcf, 0x0d, 0xdb, 0xec, - 0x99, 0x29, 0xfb, 0x44, 0xa9, 0xf1, 0x23, 0xa5, 0xae, 0x2d, 0xc3, 0x4a, 0xe6, 0xee, 0xcb, 0x08, 0x1c, 0x0e, 0xc8, - 0x4f, 0x77, 0x88, 0x83, 0xd0, 0xba, 0xc9, 0x6a, 0xae, 0x28, 0xe7, 0x42, 0x5b, 0x66, 0x5e, 0x0e, 0x2c, 0x66, 0x29, - 0x85, 0xc6, 0x02, 0x00, 0xc1, 0xa4, 0xd0, 0xda, 0x7b, 0x19, 0x40, 0x4e, 0xd0, 0xf0, 0xc7, 0xe6, 0xaa, 0x28, 0x6b, - 0xd9, 0x92, 0x10, 0x65, 0xbb, 0x1e, 0x5e, 0x22, 0x64, 0x5a, 0xbf, 0x7f, 0x4e, 0x24, 0x6b, 0x93, 0xea, 0xaa, 0x46, - 0x4b, 0x40, 0x45, 0x96, 0x80, 0x89, 0x5f, 0x69, 0x3e, 0x01, 0x78, 0xd2, 0xf1, 0xa0, 0x7a, 0xca, 0x6b, 0x26, 0x88, - 0x6c, 0xa3, 0xf2, 0x27, 0xc5, 0x35, 0x92, 0x11, 0x14, 0x4f, 0x6b, 0x95, 0xb1, 0x30, 0xcc, 0x03, 0x05, 0xe4, 0xdd, - 0xbb, 0x53, 0xdf, 0xda, 0x1f, 0x3b, 0xf6, 0x6c, 0xad, 0x42, 0x2d, 0xd4, 0x14, 0x2e, 0x39, 0x44, 0x57, 0x90, 0x81, - 0x42, 0xc6, 0x93, 0xd7, 0x83, 0xcb, 0x49, 0x74, 0xc5, 0x05, 0x3a, 0xe3, 0xeb, 0x9b, 0x6e, 0x3a, 0x8b, 0x9e, 0x56, - 0xf3, 0x09, 0x29, 0xc9, 0x0e, 0x87, 0x6c, 0x54, 0xd5, 0xc5, 0x7a, 0x1a, 0xca, 0x9f, 0x1e, 0x82, 0xaf, 0x17, 0xd4, - 0x6b, 0xb2, 0x4a, 0xf5, 0x53, 0xaa, 0x94, 0x17, 0x0d, 0x2f, 0xfd, 0xa7, 0x95, 0xdc, 0xf7, 0x80, 0xb4, 0x96, 0x97, - 0x5c, 0xbe, 0x1f, 0x21, 0xc6, 0x88, 0x1f, 0x78, 0x25, 0x8f, 0x58, 0xa8, 0xa6, 0x70, 0xcd, 0x23, 0x04, 0x79, 0xcb, - 0x74, 0xf0, 0xb7, 0x9e, 0x38, 0xdd, 0x9f, 0x28, 0xed, 0xe2, 0x0b, 0x8b, 0xba, 0x27, 0x6b, 0xeb, 0x06, 0xe4, 0x60, - 0xc3, 0x74, 0x51, 0x90, 0x6d, 0x4a, 0x23, 0x68, 0xa3, 0xe5, 0xc0, 0x86, 0x53, 0xa9, 0x0d, 0x67, 0xae, 0x21, 0xb8, - 0xcf, 0xcf, 0xd3, 0xd1, 0x02, 0x3e, 0xa4, 0xba, 0xbd, 0xc4, 0xcf, 0x87, 0x0d, 0x8f, 0x80, 0xcc, 0x8e, 0xf8, 0xcc, - 0x26, 0x92, 0x4e, 0xea, 0x5c, 0x01, 0xbb, 0x9d, 0xbd, 0x03, 0x39, 0x62, 0xe6, 0xbe, 0x42, 0xf5, 0x2d, 0x1a, 0x70, - 0x65, 0xac, 0x7d, 0x4d, 0x32, 0x16, 0x5e, 0x95, 0xd3, 0x70, 0x00, 0x30, 0x74, 0x19, 0x7d, 0x6d, 0xb9, 0xc9, 0xb2, - 0x9f, 0x0b, 0x08, 0x82, 0x28, 0x89, 0xc7, 0x07, 0xbc, 0x2f, 0xab, 0xa1, 0x46, 0xc9, 0xc7, 0xb2, 0x91, 0x4a, 0xaf, - 0x44, 0x7f, 0x37, 0xe6, 0x12, 0x03, 0xbe, 0xab, 0xda, 0x82, 0xc2, 0x79, 0x7e, 0x38, 0x9c, 0xe7, 0x23, 0xe3, 0x59, - 0x06, 0xaa, 0x95, 0x69, 0x1d, 0xc4, 0x66, 0xbe, 0x58, 0xf8, 0x8b, 0x9d, 0x93, 0x88, 0x28, 0x08, 0xec, 0x48, 0x78, - 0x10, 0xa9, 0x5f, 0x55, 0x9e, 0xee, 0x54, 0x9f, 0xed, 0x17, 0x36, 0x91, 0x5e, 0x50, 0x32, 0xf9, 0x24, 0xd8, 0xab, - 0xfe, 0x0e, 0xc2, 0x86, 0xf0, 0xe6, 0x55, 0xaf, 0xb3, 0x4c, 0xcd, 0x4a, 0x90, 0x30, 0x63, 0x8e, 0xe0, 0x71, 0xd8, - 0x69, 0x6c, 0xc3, 0x63, 0x0b, 0x8e, 0xce, 0x5b, 0xb3, 0x3b, 0xb6, 0x62, 0xb7, 0xaa, 0x4e, 0x0b, 0x1e, 0x4e, 0x87, - 0x97, 0x01, 0xae, 0xbe, 0xf5, 0x39, 0xe7, 0x77, 0x74, 0x82, 0xad, 0x07, 0x3c, 0x9a, 0x88, 0xd9, 0xfa, 0x69, 0xa4, - 0x16, 0xcf, 0x7a, 0xc8, 0x17, 0xb4, 0xfe, 0xc4, 0xec, 0xce, 0x24, 0xdf, 0x0d, 0xf8, 0x62, 0xb2, 0x7e, 0x1a, 0xc1, - 0xab, 0x4f, 0xc1, 0x8a, 0x91, 0x39, 0xb3, 0x6c, 0xfd, 0x34, 0xc2, 0x31, 0xbb, 0x7b, 0x1a, 0xd1, 0xa8, 0xad, 0xe4, - 0xbe, 0x74, 0xdb, 0x80, 0xb0, 0x72, 0xcb, 0x62, 0x78, 0x0d, 0xc4, 0x33, 0x6d, 0x24, 0x5d, 0x4b, 0x43, 0x6f, 0xcc, - 0xc3, 0x69, 0x1c, 0xac, 0xa9, 0x15, 0xf2, 0xcc, 0x10, 0xb3, 0xf8, 0x69, 0x34, 0x67, 0x2b, 0xac, 0xc8, 0x86, 0xc7, - 0x83, 0xcb, 0xc9, 0xe6, 0x8a, 0xaf, 0x81, 0xfc, 0x6c, 0xb2, 0x31, 0x5b, 0xd4, 0x2d, 0x17, 0xb3, 0xcd, 0xd3, 0x68, - 0x3e, 0x59, 0x41, 0xcf, 0xda, 0x03, 0xe6, 0xbd, 0x01, 0x11, 0x4a, 0x42, 0x6a, 0xca, 0x4d, 0xaf, 0xc7, 0xd6, 0xe3, - 0xe0, 0x8e, 0xad, 0x2f, 0x83, 0x5b, 0xb6, 0x1e, 0x03, 0x11, 0x07, 0xf5, 0xbb, 0xb7, 0x81, 0xc5, 0x17, 0xb1, 0xf5, - 0xa5, 0x49, 0xdb, 0x3c, 0x8d, 0x98, 0x3b, 0x38, 0x0d, 0x5c, 0xb0, 0x36, 0x99, 0xb7, 0x62, 0x70, 0x09, 0x59, 0x7a, - 0x31, 0xdb, 0x0c, 0x2f, 0xd9, 0x7a, 0x84, 0x53, 0x3d, 0xf1, 0xd9, 0x1d, 0xbf, 0x65, 0x09, 0x5f, 0x35, 0xf1, 0xd5, - 0x06, 0x34, 0xa2, 0x47, 0x19, 0xf4, 0x15, 0xd4, 0xcc, 0x9c, 0x57, 0x16, 0x46, 0xe5, 0xbe, 0x05, 0x07, 0x14, 0xa4, - 0x6d, 0x80, 0x20, 0x89, 0x67, 0xf7, 0x2a, 0x5c, 0xdf, 0x48, 0x61, 0xc0, 0x4d, 0x60, 0x06, 0x0c, 0x4c, 0x3f, 0x83, - 0x1f, 0x56, 0xba, 0x44, 0x88, 0xb3, 0x9f, 0x52, 0x92, 0xcc, 0xf3, 0xd7, 0x22, 0xcd, 0xdd, 0xc2, 0x75, 0x0a, 0xb3, - 0xa2, 0x40, 0xf5, 0x53, 0x52, 0x1a, 0x58, 0xa8, 0x44, 0xa6, 0x52, 0xf0, 0xcb, 0xe6, 0x3c, 0xca, 0x8e, 0xd1, 0xb9, - 0xce, 0x2f, 0x27, 0xce, 0xe9, 0xa4, 0xef, 0x3f, 0x70, 0x0c, 0x5b, 0xc8, 0xc0, 0x85, 0x3f, 0xf5, 0x84, 0x71, 0x6a, - 0x05, 0x62, 0x2a, 0x79, 0xf6, 0x14, 0x3e, 0x13, 0x5a, 0x1d, 0x5d, 0xf8, 0x7e, 0x50, 0x68, 0x93, 0x74, 0x0b, 0x92, - 0x14, 0x3c, 0x45, 0xcf, 0x39, 0x6f, 0x03, 0x95, 0x62, 0x44, 0x0b, 0x22, 0x6d, 0x2d, 0x33, 0x07, 0x69, 0x4b, 0xf3, - 0x5d, 0x13, 0x3f, 0x87, 0x05, 0x5c, 0x44, 0x0b, 0x5b, 0xc3, 0xa3, 0x2a, 0x56, 0xee, 0x4d, 0x9e, 0x23, 0x9c, 0xd1, - 0xa5, 0x4c, 0x00, 0x5c, 0xef, 0xd7, 0x61, 0xad, 0xf0, 0x8a, 0x9a, 0x45, 0x5e, 0xd4, 0xf4, 0xc9, 0x16, 0xb8, 0x8f, - 0x45, 0x89, 0x02, 0x67, 0x2d, 0x18, 0xb0, 0x15, 0x96, 0xec, 0xa4, 0xb0, 0x29, 0x5a, 0x42, 0x6f, 0x8f, 0x9f, 0x0e, - 0x6a, 0x26, 0x03, 0x68, 0x02, 0x68, 0x3c, 0xfe, 0x05, 0xa0, 0xa6, 0x37, 0xb5, 0x58, 0x57, 0x41, 0xa9, 0x94, 0x9b, - 0xf0, 0x33, 0x30, 0xcc, 0xf0, 0x43, 0x21, 0xb7, 0x89, 0x12, 0x39, 0x3f, 0x16, 0xa5, 0x58, 0x96, 0xa2, 0x4a, 0xda, - 0x0d, 0x05, 0x8f, 0x08, 0xb7, 0x41, 0x63, 0xe6, 0xf6, 0x44, 0x17, 0xad, 0x08, 0xe5, 0xd8, 0xac, 0x63, 0xa4, 0x51, - 0x66, 0x27, 0xbb, 0x4e, 0x16, 0xda, 0xef, 0xab, 0x1c, 0xb2, 0x0e, 0x58, 0x23, 0xf9, 0x7a, 0xcd, 0xa1, 0xdb, 0x46, - 0x79, 0xf1, 0xe0, 0xf9, 0x0a, 0x4e, 0x73, 0x3c, 0xb1, 0xbb, 0x5e, 0x77, 0x8a, 0x44, 0xbc, 0xc2, 0x49, 0x95, 0x8f, - 0x64, 0xe1, 0xb8, 0x73, 0xa7, 0xb5, 0x58, 0x55, 0x2e, 0xeb, 0xa9, 0xc5, 0x11, 0x81, 0x4f, 0xe5, 0xd1, 0x5e, 0x68, - 0x5b, 0x14, 0x0b, 0x61, 0xf4, 0xe8, 0x84, 0x9f, 0x94, 0xc0, 0xfa, 0x3a, 0x1c, 0x96, 0x7e, 0xc4, 0xd1, 0xef, 0x34, - 0x1a, 0x2d, 0x08, 0x69, 0x78, 0xea, 0x45, 0xa3, 0x45, 0x5d, 0xd4, 0x61, 0x76, 0x9d, 0xeb, 0x81, 0xc2, 0x30, 0x02, - 0xf5, 0x83, 0xab, 0x0c, 0x3e, 0x8b, 0x10, 0x35, 0x0f, 0x4c, 0xb3, 0x21, 0x1c, 0x75, 0x81, 0x87, 0x56, 0xd0, 0x62, - 0x66, 0x3e, 0x0a, 0x31, 0x7c, 0x48, 0x17, 0xe7, 0x4f, 0xc8, 0xca, 0x07, 0xd8, 0x1d, 0xba, 0x0b, 0xe5, 0x9c, 0xa9, - 0x18, 0xe0, 0x47, 0x01, 0xf9, 0x28, 0x01, 0x37, 0x03, 0x64, 0x8f, 0x2c, 0x01, 0xc4, 0x8a, 0xd1, 0xd1, 0xe4, 0x73, - 0xdf, 0x8b, 0x14, 0xbc, 0xb3, 0xcf, 0x72, 0x35, 0x61, 0x28, 0x7c, 0x62, 0xa0, 0x9b, 0xdf, 0xf8, 0xed, 0x79, 0x0b, - 0x46, 0x76, 0x49, 0x8a, 0xd7, 0x9a, 0xe1, 0x7e, 0x03, 0x6e, 0x47, 0x40, 0x59, 0x53, 0x1d, 0x93, 0x6c, 0xd3, 0x10, - 0xc9, 0x80, 0x19, 0x31, 0x22, 0xa8, 0x2c, 0x17, 0xfe, 0x77, 0x2f, 0x8b, 0x02, 0x07, 0x70, 0x35, 0x93, 0xc1, 0x6b, - 0x17, 0x46, 0x05, 0xc0, 0x39, 0x0d, 0x9d, 0xd2, 0x5e, 0x55, 0x1d, 0x92, 0x55, 0xf3, 0x83, 0xd9, 0xbc, 0x69, 0x98, - 0x18, 0x11, 0x44, 0x17, 0xe1, 0x04, 0xd3, 0x2b, 0xd2, 0xd7, 0x4a, 0x4e, 0x47, 0xab, 0x8e, 0xd6, 0x12, 0x13, 0x73, - 0x45, 0xf1, 0xd7, 0x80, 0xc7, 0x0d, 0x5e, 0x9d, 0xa4, 0xe9, 0x44, 0xf5, 0xe8, 0xf1, 0xeb, 0x34, 0x9d, 0x94, 0xb8, - 0x2b, 0xfc, 0x06, 0x5c, 0x34, 0xdb, 0x7c, 0xe8, 0xc7, 0x2f, 0x28, 0xe2, 0xa2, 0x06, 0x57, 0xde, 0xa9, 0xbe, 0x52, - 0x7d, 0x04, 0xb5, 0xf0, 0xc4, 0xc8, 0x5a, 0x78, 0x72, 0xc9, 0x5a, 0x0b, 0x82, 0x99, 0xcd, 0x81, 0x0b, 0xf9, 0x95, - 0x52, 0xc4, 0x9b, 0x48, 0xa8, 0xc5, 0xa0, 0xf5, 0x98, 0x39, 0xab, 0x46, 0x0b, 0x95, 0x19, 0xa1, 0x7d, 0x5b, 0x8b, - 0xce, 0x6f, 0xe4, 0xa7, 0x3c, 0xb5, 0x2f, 0xdb, 0xe3, 0x7c, 0xbc, 0x47, 0x77, 0xd5, 0x59, 0x66, 0x52, 0xc6, 0x27, - 0xb3, 0x04, 0x85, 0xbb, 0x04, 0x1b, 0x90, 0x64, 0xbf, 0xd5, 0x01, 0x32, 0x6a, 0xaf, 0xfd, 0xae, 0xb3, 0x7c, 0x75, - 0xb3, 0x35, 0x14, 0x95, 0x5a, 0x49, 0x8a, 0x83, 0x0c, 0xd7, 0x6d, 0xe5, 0xc3, 0xc5, 0x05, 0xf4, 0x8c, 0x91, 0xc8, - 0x3c, 0x7f, 0x22, 0x5f, 0x82, 0x73, 0xc6, 0x59, 0x21, 0x30, 0x61, 0xac, 0xde, 0xb5, 0x96, 0x4a, 0x43, 0x8a, 0xb1, - 0xa3, 0x51, 0x96, 0x55, 0x96, 0x2e, 0xb3, 0xb5, 0x84, 0x2d, 0xab, 0xc8, 0x2d, 0x6c, 0x99, 0xc9, 0x6a, 0x7e, 0xa8, - 0xb8, 0x83, 0xf2, 0xcd, 0xd6, 0x19, 0xdf, 0x4b, 0x64, 0xef, 0x36, 0x50, 0xc2, 0xf5, 0xe8, 0x3f, 0x90, 0x7e, 0x9b, - 0x61, 0x9c, 0x72, 0x5b, 0x49, 0x0b, 0x70, 0xfa, 0x87, 0xc3, 0x87, 0x0a, 0x83, 0x06, 0x47, 0x18, 0x47, 0xd6, 0xef, - 0xdf, 0x56, 0x5e, 0x8d, 0x89, 0x3a, 0x3e, 0xab, 0xdf, 0xaf, 0xe8, 0xe1, 0xb4, 0x1a, 0xad, 0xd2, 0x2d, 0xb2, 0x13, - 0xda, 0x58, 0xf9, 0x41, 0xad, 0x80, 0xd9, 0x5b, 0x9f, 0x4f, 0x07, 0xa0, 0x63, 0x01, 0x12, 0xcd, 0x66, 0x22, 0x31, - 0x27, 0xdd, 0x93, 0xf0, 0xf8, 0xc0, 0x02, 0x07, 0x98, 0x8a, 0xff, 0x43, 0x78, 0x33, 0xb0, 0x41, 0xa3, 0x44, 0x5f, - 0xa3, 0xab, 0xda, 0xdc, 0xe8, 0x78, 0xe9, 0x29, 0x24, 0xb2, 0x82, 0x55, 0x73, 0x5f, 0x6e, 0xe0, 0xb4, 0x87, 0x9a, - 0x43, 0x65, 0x09, 0xfe, 0xf6, 0xcb, 0xfc, 0x70, 0x58, 0x67, 0x50, 0xd8, 0x6e, 0x2d, 0xb4, 0x37, 0x66, 0xa9, 0x86, - 0x8a, 0x70, 0xd0, 0xf9, 0x4a, 0xcc, 0xea, 0x11, 0xfd, 0x3d, 0x3f, 0x1c, 0x56, 0x04, 0x06, 0x1c, 0x96, 0x32, 0x13, - 0x2d, 0x14, 0x4b, 0xeb, 0x6c, 0x46, 0x75, 0xe0, 0x81, 0x89, 0x39, 0x0b, 0x77, 0x00, 0xda, 0xa4, 0x56, 0x81, 0x5e, - 0x45, 0xf4, 0x13, 0xf7, 0x6b, 0xfb, 0xf5, 0x7a, 0x64, 0x96, 0x8e, 0xdc, 0x18, 0x0b, 0x00, 0x0e, 0x3c, 0xaf, 0x49, - 0x9e, 0x93, 0xaf, 0xa1, 0xdd, 0x93, 0x0b, 0xf9, 0x13, 0x94, 0x2d, 0x3c, 0x57, 0x4d, 0x2b, 0x8b, 0x15, 0x57, 0xd5, - 0xab, 0x0b, 0x5e, 0x99, 0x4c, 0xab, 0xb4, 0x12, 0x95, 0x12, 0x0c, 0xa8, 0x4b, 0xbc, 0xd6, 0x34, 0xa3, 0xd4, 0x46, - 0x9d, 0x89, 0x1a, 0xb0, 0xc1, 0x7e, 0xaa, 0x36, 0x3a, 0x39, 0x97, 0xcf, 0x2f, 0x8d, 0xc3, 0xa7, 0x5d, 0xbd, 0x99, - 0xa9, 0x1c, 0xf8, 0x6b, 0xe5, 0x43, 0xab, 0xc7, 0x40, 0x07, 0xe4, 0xf4, 0xc7, 0xb0, 0x98, 0xd8, 0x1d, 0x9a, 0xb7, - 0xbb, 0xcb, 0xea, 0x22, 0xbd, 0xd3, 0x94, 0xcc, 0xea, 0x2d, 0x9f, 0x59, 0x3d, 0x3a, 0xe0, 0xc5, 0x63, 0xbd, 0x57, - 0x98, 0x49, 0x04, 0x17, 0x43, 0x35, 0x89, 0xec, 0x0e, 0xb4, 0xe6, 0x51, 0xc5, 0x04, 0xf8, 0x41, 0xa9, 0x35, 0xbd, - 0xb7, 0xbb, 0x42, 0x9d, 0x52, 0x78, 0xdc, 0x5a, 0xf2, 0x03, 0x73, 0xa7, 0x5d, 0xeb, 0x7c, 0x3c, 0xbf, 0xf4, 0xfd, - 0x46, 0x9e, 0xd0, 0x66, 0x67, 0x72, 0xfa, 0x27, 0x6f, 0xf5, 0x0f, 0x53, 0x7d, 0x0b, 0xdd, 0x09, 0xfa, 0x0c, 0x5d, - 0x55, 0xdd, 0x95, 0xd8, 0xc2, 0x50, 0x4f, 0x2c, 0xf2, 0x42, 0x9e, 0xb4, 0xc6, 0x8e, 0x83, 0xbd, 0x01, 0x4e, 0xfc, - 0xf2, 0x70, 0x10, 0x57, 0xb9, 0xcf, 0xce, 0xbb, 0x46, 0x56, 0x0e, 0x60, 0x05, 0x51, 0x30, 0x6e, 0xcd, 0xc7, 0x36, - 0x48, 0x97, 0xb8, 0x1a, 0x1f, 0xbf, 0xa1, 0x58, 0x26, 0x9b, 0x88, 0x8b, 0x8b, 0xfc, 0xe9, 0x73, 0x20, 0x2d, 0xeb, - 0xf7, 0xa3, 0xeb, 0xcb, 0xe9, 0xf3, 0x61, 0x14, 0x80, 0x63, 0x97, 0xbd, 0xbc, 0x8c, 0xf9, 0xea, 0x92, 0x59, 0xa6, - 0xb0, 0xc8, 0x37, 0x03, 0xaa, 0x4b, 0x56, 0x4b, 0xd7, 0x2b, 0xc0, 0xd2, 0xe5, 0x37, 0x0f, 0x61, 0x6a, 0x40, 0x23, - 0x6b, 0xee, 0x4e, 0x73, 0x2d, 0x50, 0xea, 0x79, 0x3f, 0x33, 0xe4, 0xeb, 0x32, 0xe8, 0x0a, 0xd2, 0x3d, 0x8f, 0x48, - 0x2f, 0xf7, 0xd2, 0xe9, 0x7e, 0x5f, 0x0a, 0xb0, 0xd4, 0x97, 0xe2, 0x0b, 0x28, 0x2c, 0x1a, 0xdf, 0x08, 0xd0, 0xd6, - 0x50, 0x4d, 0x7b, 0xa5, 0xa8, 0x7a, 0x41, 0xaf, 0x14, 0x5f, 0x7a, 0x7a, 0xa8, 0xcc, 0x97, 0xa5, 0xa3, 0xff, 0x09, - 0x35, 0x17, 0x9c, 0x10, 0x33, 0x31, 0x07, 0x50, 0x09, 0xda, 0xf8, 0x56, 0x47, 0x1b, 0x9f, 0xea, 0x55, 0xdc, 0xf4, - 0x79, 0x6d, 0x2d, 0x73, 0x42, 0xd8, 0x74, 0x2f, 0x01, 0x2a, 0xf2, 0x4a, 0x78, 0x04, 0xcb, 0x2f, 0x7f, 0xc8, 0xd3, - 0x15, 0xa2, 0x75, 0xdc, 0xb3, 0xcc, 0xa5, 0xb1, 0x7f, 0x63, 0x30, 0x7d, 0x7d, 0xbb, 0x2d, 0xf2, 0x53, 0x13, 0x13, - 0xd6, 0x63, 0x45, 0xdf, 0xbc, 0x0f, 0x57, 0x02, 0x05, 0x0e, 0x25, 0x12, 0xdb, 0x54, 0xa1, 0x88, 0x07, 0x49, 0x9f, - 0x2e, 0x5a, 0x9f, 0x06, 0x98, 0x5a, 0xcb, 0x81, 0x39, 0x84, 0xab, 0xb8, 0xf0, 0xd1, 0xd3, 0xb7, 0x98, 0x85, 0xf3, - 0x89, 0xf7, 0xc9, 0x2b, 0x46, 0xe6, 0xe3, 0x3e, 0x2a, 0x95, 0xf4, 0xcf, 0xc3, 0x61, 0x56, 0xcd, 0x7d, 0x87, 0x3e, - 0xd2, 0x43, 0x95, 0x0b, 0xca, 0xde, 0x18, 0x93, 0x08, 0x94, 0xc6, 0x78, 0x1f, 0x07, 0xc7, 0x79, 0x9f, 0x06, 0x90, - 0xda, 0x27, 0x3e, 0x90, 0x92, 0xc3, 0x73, 0x8e, 0x39, 0xa1, 0xb4, 0x22, 0xac, 0xe2, 0x8b, 0x0c, 0xe5, 0xba, 0x53, - 0x0a, 0x26, 0x39, 0x24, 0x18, 0xfe, 0xaa, 0x79, 0x13, 0x2b, 0x10, 0x76, 0xcd, 0xbc, 0x1a, 0x3d, 0xa9, 0x92, 0xb0, - 0x14, 0x70, 0x54, 0x66, 0x9e, 0x61, 0x6f, 0x78, 0x62, 0x18, 0x39, 0x58, 0xee, 0x8f, 0xea, 0x44, 0xe4, 0x1e, 0x5d, - 0x60, 0x54, 0x16, 0x9e, 0x37, 0x74, 0xa5, 0x41, 0x25, 0xd9, 0xf1, 0x57, 0x5c, 0x03, 0x6a, 0x6b, 0x8c, 0x18, 0x0a, - 0x18, 0x05, 0xaf, 0xed, 0x0f, 0x21, 0x8b, 0xb2, 0xf5, 0x1b, 0x1c, 0xf3, 0x59, 0xc9, 0x5d, 0xef, 0x70, 0x16, 0x5a, - 0x42, 0x9e, 0xdc, 0x31, 0x48, 0xd3, 0x58, 0x1a, 0x01, 0x27, 0x22, 0xd9, 0xc6, 0x52, 0x38, 0x02, 0x08, 0x08, 0x74, - 0x53, 0x66, 0x18, 0xd3, 0xc1, 0xc8, 0xf3, 0xa4, 0x67, 0xbc, 0x57, 0xe1, 0x29, 0xa4, 0xc9, 0xf6, 0xf5, 0xfc, 0xbd, - 0x11, 0x64, 0xe5, 0x96, 0x73, 0x3c, 0x2c, 0xbe, 0x71, 0xf6, 0x55, 0x4e, 0x9e, 0x62, 0x96, 0x91, 0xde, 0x29, 0xe6, - 0x05, 0xfc, 0xa9, 0x2c, 0xf5, 0x39, 0x4a, 0x6f, 0x99, 0x4f, 0x56, 0x91, 0x74, 0xe9, 0x6d, 0xfa, 0xfd, 0x78, 0xa4, - 0x0e, 0x35, 0x7f, 0x1f, 0x8f, 0xe4, 0x19, 0xb6, 0x61, 0x09, 0x0b, 0xad, 0x82, 0x31, 0x80, 0x24, 0x36, 0x22, 0x1a, - 0x8c, 0xf6, 0xe6, 0x70, 0x38, 0xdf, 0x98, 0xb3, 0x64, 0x0f, 0xae, 0xaf, 0x3c, 0x31, 0xef, 0xc0, 0x97, 0x79, 0x4c, - 0x10, 0xb1, 0x99, 0xb7, 0x61, 0x35, 0x78, 0xb0, 0x83, 0xeb, 0x23, 0xb6, 0x28, 0xd6, 0x3a, 0x96, 0xca, 0x3a, 0x38, - 0xad, 0x63, 0xd3, 0x8c, 0x94, 0x22, 0xfb, 0x1c, 0xfb, 0x7b, 0x37, 0xb8, 0xba, 0x36, 0x06, 0xb5, 0xc6, 0x1d, 0xe6, - 0xce, 0xa9, 0x80, 0x7a, 0x4c, 0x57, 0x50, 0x3d, 0xab, 0xc8, 0x97, 0xdf, 0xda, 0x39, 0x20, 0x68, 0x04, 0x02, 0x17, - 0x0d, 0xb4, 0x6a, 0x97, 0x72, 0xde, 0x05, 0x84, 0xf8, 0x2e, 0x05, 0x7d, 0x3a, 0x83, 0x4d, 0x6c, 0x3e, 0x81, 0x58, - 0x34, 0xdd, 0xe7, 0x5a, 0x33, 0x5f, 0x8c, 0x68, 0x67, 0xd6, 0xdd, 0x22, 0xb7, 0x5a, 0x88, 0x64, 0xf4, 0x6c, 0x33, - 0xe1, 0xa2, 0x43, 0x39, 0x23, 0x01, 0x13, 0xb4, 0xb6, 0x52, 0xf2, 0xb9, 0xee, 0x75, 0x82, 0xf6, 0x40, 0xd2, 0xba, - 0x7f, 0xb3, 0xe8, 0x8c, 0x92, 0x93, 0xeb, 0x4d, 0xce, 0x20, 0x05, 0x0b, 0xb6, 0x97, 0x39, 0xe1, 0x06, 0xf8, 0xc4, - 0x66, 0xc9, 0x69, 0x1a, 0xe4, 0xb1, 0x30, 0x1e, 0x79, 0x6d, 0x7e, 0x59, 0x40, 0x87, 0x92, 0x45, 0x23, 0xc4, 0x03, - 0xec, 0x1c, 0x92, 0xab, 0x02, 0x75, 0xd3, 0x40, 0x57, 0xae, 0x9c, 0x29, 0xa6, 0xc0, 0x85, 0x50, 0x10, 0xb5, 0xa3, - 0x93, 0xa8, 0x9c, 0xf7, 0x49, 0x75, 0x99, 0x4f, 0x0b, 0x69, 0x1a, 0xc8, 0xa7, 0x95, 0x63, 0x1e, 0xd8, 0xd9, 0xc6, - 0x35, 0x81, 0x81, 0x4e, 0xed, 0x6b, 0x51, 0xce, 0xb1, 0x8a, 0xe8, 0x7d, 0xfe, 0xb1, 0xb2, 0xa7, 0x0f, 0x22, 0x6c, - 0x54, 0xa0, 0xb1, 0x94, 0x18, 0x1b, 0x39, 0xfe, 0x2d, 0x51, 0x36, 0x64, 0x08, 0x08, 0x21, 0x6d, 0xe4, 0xf4, 0xc3, - 0xfa, 0xf2, 0x36, 0xd3, 0xfe, 0x9f, 0x24, 0x7e, 0x1b, 0xec, 0xe5, 0xd4, 0x9f, 0x7a, 0xc4, 0xe3, 0xb5, 0x46, 0x8f, - 0x29, 0xe9, 0x36, 0xc8, 0x53, 0xe5, 0x29, 0x48, 0x26, 0x8c, 0x25, 0x04, 0x8b, 0x72, 0xc1, 0x73, 0x5e, 0x71, 0x09, - 0xf7, 0x51, 0xcb, 0x8a, 0x08, 0x55, 0x89, 0x9c, 0x3e, 0x5f, 0x01, 0xcf, 0x04, 0x04, 0x3a, 0xc6, 0x48, 0xa3, 0x0a, - 0xbe, 0x04, 0xc6, 0x3a, 0x50, 0x76, 0x9a, 0x91, 0xe0, 0xb2, 0x7b, 0x83, 0x44, 0xa9, 0xaf, 0x49, 0x49, 0xfa, 0x4e, - 0xd4, 0x78, 0x25, 0x56, 0x11, 0x09, 0x64, 0xa8, 0x21, 0x62, 0x55, 0x3d, 0x75, 0xaf, 0x8a, 0xc9, 0x60, 0x50, 0xf9, - 0x72, 0x7a, 0xe2, 0x0d, 0x0d, 0x95, 0x77, 0x5d, 0xd1, 0x4e, 0x4f, 0xb4, 0x52, 0xde, 0x42, 0x5a, 0x82, 0xa6, 0x61, - 0xa4, 0x39, 0x94, 0xba, 0x92, 0xee, 0xc6, 0x20, 0xbe, 0x64, 0xa2, 0x67, 0x3b, 0xb5, 0xa3, 0xb4, 0x25, 0xed, 0x21, - 0xa4, 0xe7, 0x2e, 0xf9, 0x98, 0x85, 0x5c, 0xdd, 0x29, 0x27, 0xe5, 0x55, 0x88, 0x4e, 0xee, 0x7b, 0x0c, 0x89, 0x40, - 0x9f, 0x73, 0x0c, 0xeb, 0xa2, 0xa1, 0xce, 0x61, 0x85, 0x98, 0x2d, 0x94, 0x30, 0x5f, 0x32, 0x9e, 0x4a, 0x06, 0x0d, - 0x80, 0x0c, 0xf8, 0xe2, 0x65, 0x60, 0xf9, 0x2b, 0x88, 0x1f, 0x6d, 0x7c, 0x38, 0xfc, 0x59, 0x53, 0x88, 0xed, 0x9f, - 0xb0, 0x19, 0xc2, 0xa3, 0x7a, 0xc0, 0x33, 0xdf, 0xc4, 0x09, 0x5a, 0x01, 0x49, 0x99, 0x1d, 0x4d, 0x64, 0xaf, 0x7a, - 0x08, 0xa7, 0xb2, 0x02, 0x75, 0x94, 0x75, 0x56, 0xc2, 0x8f, 0x30, 0xd5, 0xad, 0xc4, 0x5a, 0xa0, 0xcd, 0xd5, 0x8a, - 0xb5, 0x00, 0x0e, 0xfc, 0x1c, 0x82, 0x27, 0xf2, 0x39, 0xb8, 0x18, 0x14, 0xe0, 0x73, 0x00, 0xbc, 0xc8, 0x5d, 0x78, - 0x30, 0x8f, 0x2c, 0xab, 0x11, 0x86, 0xa3, 0x8a, 0x58, 0xbf, 0x66, 0x3b, 0xf2, 0x81, 0xdb, 0x31, 0x3e, 0xd7, 0x1e, - 0x4b, 0x96, 0x83, 0x51, 0xe6, 0x5e, 0x2d, 0xd1, 0xf3, 0x26, 0x8d, 0x9b, 0xd1, 0x93, 0x7d, 0x2d, 0xff, 0x17, 0xf4, - 0x32, 0xe8, 0x6f, 0xe1, 0x96, 0xd7, 0xfc, 0x6e, 0x41, 0xa4, 0x99, 0x5e, 0x41, 0xa4, 0x8c, 0x1a, 0x91, 0x31, 0x84, - 0x4d, 0xaa, 0x9b, 0xdb, 0xa4, 0xba, 0x10, 0xf0, 0x74, 0x44, 0xaa, 0x6b, 0x21, 0x6d, 0xe4, 0xd3, 0x3a, 0x90, 0xb1, - 0x48, 0xef, 0x7f, 0xfc, 0xcb, 0x8b, 0xcf, 0x6f, 0x7f, 0xf9, 0x71, 0xf1, 0xf6, 0xfd, 0x9b, 0xb7, 0xef, 0xdf, 0x7e, - 0xfe, 0x8d, 0x20, 0x3c, 0xa6, 0x42, 0x65, 0xf8, 0xf8, 0xe1, 0xe6, 0xad, 0x93, 0xc1, 0xf6, 0x66, 0xc8, 0xda, 0x37, - 0x72, 0x30, 0x04, 0x22, 0x1b, 0x84, 0x0c, 0xb2, 0x53, 0x32, 0xc7, 0x4c, 0xcc, 0x31, 0xf6, 0x4e, 0x60, 0xb2, 0x05, - 0x9c, 0x63, 0x99, 0x97, 0x8c, 0xc8, 0x55, 0xa1, 0xf5, 0x03, 0x5a, 0xf0, 0x0e, 0x5c, 0x64, 0xd2, 0xfc, 0xee, 0x17, - 0x82, 0xd8, 0xa7, 0x95, 0x94, 0xfb, 0x6a, 0x5b, 0xf3, 0x7c, 0x7b, 0xbf, 0x97, 0x70, 0xfe, 0x73, 0x69, 0x44, 0x2d, - 0xc0, 0x01, 0xf8, 0x1c, 0xfe, 0xb8, 0xd2, 0x96, 0x34, 0x99, 0x45, 0xfb, 0x19, 0x43, 0xd0, 0xa5, 0xc1, 0x07, 0xb1, - 0x47, 0x5e, 0xea, 0x93, 0x85, 0x04, 0xee, 0x88, 0xe1, 0xd3, 0x8a, 0xa0, 0x57, 0x8c, 0x28, 0x2e, 0xb9, 0x42, 0xa5, - 0x94, 0xfc, 0x1b, 0x65, 0x17, 0x15, 0x72, 0x56, 0xb0, 0x7b, 0x45, 0x8e, 0x8c, 0x1f, 0x04, 0x13, 0x5f, 0x0e, 0xee, - 0xbf, 0xc4, 0x3b, 0x9c, 0x29, 0x8e, 0xe4, 0x84, 0x3f, 0x64, 0x18, 0xd8, 0x9f, 0x83, 0xcf, 0xab, 0xc3, 0xbc, 0xbc, - 0xd1, 0xa7, 0xdc, 0x92, 0x8f, 0x27, 0xcb, 0x2b, 0x30, 0xd8, 0x2f, 0x55, 0x73, 0xd7, 0xbc, 0x9e, 0x2d, 0xe7, 0x6c, - 0x3f, 0x8b, 0xe6, 0xc1, 0x1d, 0x9b, 0x65, 0xf3, 0x60, 0xd5, 0xf0, 0x35, 0xbb, 0xe5, 0x6b, 0xab, 0x6a, 0x6b, 0xbb, - 0x6a, 0x93, 0x0d, 0xbf, 0x05, 0x09, 0xe1, 0x26, 0xf3, 0x80, 0xf7, 0xf8, 0xce, 0x67, 0x1b, 0x90, 0x68, 0x57, 0x6c, - 0x03, 0x17, 0xb1, 0x35, 0xff, 0xb1, 0xf2, 0x36, 0xac, 0x64, 0xe7, 0x63, 0x96, 0xe3, 0xfc, 0xf3, 0xe1, 0x01, 0xed, - 0x85, 0xfa, 0xd9, 0xa5, 0x7a, 0x36, 0x51, 0x76, 0xb3, 0xcd, 0x68, 0x71, 0x9f, 0x56, 0x9b, 0x30, 0x43, 0xcf, 0x72, - 0xf8, 0x68, 0x2b, 0x05, 0x3f, 0xbd, 0xc0, 0x2f, 0xd9, 0x51, 0x5b, 0x69, 0xdb, 0xae, 0x4a, 0x6c, 0x05, 0x2d, 0x8a, - 0xac, 0x56, 0x78, 0x60, 0xce, 0xaf, 0x61, 0x01, 0x63, 0xcf, 0x71, 0xce, 0x6b, 0x7f, 0x84, 0x8c, 0xf7, 0x0e, 0x00, - 0x5a, 0xe6, 0x38, 0xc0, 0x23, 0x56, 0x8c, 0xa2, 0xc1, 0x3b, 0xbf, 0x54, 0x56, 0x2b, 0xcd, 0x49, 0x68, 0x1b, 0xb1, - 0x6a, 0x39, 0x52, 0x35, 0x23, 0xd2, 0x07, 0xe9, 0x79, 0xdf, 0x23, 0xaa, 0xc1, 0x9e, 0xcc, 0xeb, 0xc0, 0x3e, 0xbd, - 0x6a, 0xad, 0xea, 0xce, 0xef, 0xa9, 0xd2, 0x25, 0x47, 0xb6, 0xfc, 0x74, 0x19, 0x3e, 0xa8, 0x3f, 0x25, 0xd7, 0x87, - 0x02, 0x47, 0x78, 0xac, 0x02, 0xce, 0xd7, 0x2b, 0xd1, 0xee, 0x44, 0xd8, 0x95, 0x4b, 0x40, 0x88, 0x2f, 0x69, 0x9a, - 0xe3, 0x71, 0x44, 0x13, 0x11, 0x36, 0x31, 0xfa, 0x0b, 0xbb, 0x0f, 0x25, 0x96, 0xf3, 0x5c, 0x83, 0x92, 0x4b, 0x06, - 0xef, 0x49, 0x7b, 0x0d, 0x9a, 0xe5, 0x55, 0xa9, 0xc9, 0x44, 0x0e, 0xca, 0x87, 0x43, 0x01, 0x7b, 0xa9, 0xf1, 0xd3, - 0x84, 0x9f, 0xb0, 0xbc, 0xb5, 0xb7, 0xa6, 0x14, 0x95, 0x34, 0x40, 0x05, 0x3e, 0x66, 0xf0, 0xbf, 0x3b, 0x43, 0x2c, - 0x98, 0xa2, 0xe3, 0x87, 0x33, 0x31, 0xb7, 0x9e, 0x5b, 0x65, 0x1d, 0x65, 0x6b, 0x94, 0x13, 0xf0, 0x6f, 0xa9, 0x8e, - 0x93, 0x44, 0x38, 0xf5, 0x1e, 0x71, 0x51, 0xf7, 0x72, 0x88, 0xba, 0x61, 0x6f, 0x2b, 0x1d, 0x6c, 0x39, 0x4d, 0x83, - 0x23, 0xf1, 0x2b, 0xf5, 0xd9, 0x87, 0xcc, 0xe2, 0x51, 0x47, 0x36, 0xa2, 0x24, 0x8d, 0x63, 0x91, 0xc3, 0xf6, 0xbe, - 0x90, 0xfb, 0x7f, 0xbf, 0x0f, 0xe1, 0xa4, 0x55, 0x90, 0x94, 0x9e, 0x40, 0x44, 0x38, 0x3a, 0xfc, 0x88, 0xf0, 0x44, - 0xaa, 0x0a, 0x9f, 0xd4, 0x27, 0x6e, 0xcc, 0xee, 0x85, 0x39, 0xaa, 0xb7, 0x00, 0xc3, 0x58, 0x6f, 0x2d, 0x42, 0x12, - 0xad, 0x34, 0xa3, 0xad, 0x07, 0xc4, 0x88, 0x0f, 0x6b, 0x8b, 0x0c, 0xc6, 0xda, 0x92, 0x48, 0x00, 0xbf, 0x23, 0x21, - 0x43, 0xdb, 0x46, 0x60, 0xc6, 0xf0, 0x76, 0x56, 0x5c, 0xba, 0x0e, 0xdb, 0x9c, 0xc3, 0x17, 0xb2, 0xd0, 0xac, 0x23, - 0x4a, 0x13, 0x84, 0xfc, 0x03, 0x4e, 0x16, 0x0a, 0xa3, 0x79, 0x7d, 0x94, 0x4e, 0x12, 0xeb, 0x87, 0xae, 0x52, 0xc1, - 0x66, 0x73, 0x83, 0xfa, 0xb2, 0xa3, 0xe4, 0x57, 0xe0, 0xa4, 0xe3, 0x24, 0x8b, 0x1c, 0x44, 0x2d, 0x2a, 0xe7, 0x26, - 0x09, 0x4b, 0xbb, 0x3a, 0xd5, 0x66, 0xbd, 0x2e, 0xca, 0xba, 0x7a, 0x2d, 0x22, 0x45, 0xef, 0xa3, 0x1e, 0x3d, 0x91, - 0x90, 0x0a, 0xad, 0x4a, 0xed, 0xf2, 0x08, 0xdc, 0x36, 0xb5, 0x62, 0x5b, 0x2e, 0x61, 0x89, 0x1a, 0xff, 0x19, 0xfa, - 0x28, 0x17, 0x0f, 0x32, 0x40, 0xa3, 0xe3, 0xa9, 0x79, 0xeb, 0x91, 0x57, 0x8e, 0xf2, 0x4b, 0xab, 0x4d, 0xfa, 0x15, - 0x90, 0x19, 0xed, 0x1f, 0x2d, 0x25, 0x90, 0x19, 0x98, 0x49, 0x4b, 0x43, 0x22, 0x47, 0x31, 0x4b, 0xf3, 0x3f, 0x70, - 0xc5, 0x56, 0x88, 0x34, 0xac, 0xe6, 0x1e, 0x7f, 0x51, 0x79, 0xb5, 0x5c, 0xcb, 0x4c, 0x73, 0xb3, 0xc4, 0xb1, 0x62, - 0x71, 0x51, 0xaf, 0x2b, 0x91, 0x05, 0x42, 0x1c, 0x61, 0x1a, 0xeb, 0xa9, 0x37, 0x4a, 0xab, 0x8f, 0x48, 0x28, 0xf3, - 0x23, 0xf6, 0x76, 0xec, 0xf5, 0x20, 0x0b, 0x71, 0x6c, 0x39, 0xd8, 0x6c, 0xbd, 0xcf, 0x65, 0x2a, 0xe2, 0xb3, 0xba, - 0x38, 0xdb, 0x54, 0xe2, 0xac, 0x4e, 0xc4, 0xd9, 0x0f, 0x90, 0xf3, 0x87, 0x33, 0x2a, 0xfa, 0xec, 0x21, 0xad, 0x93, - 0x62, 0x53, 0xd3, 0x93, 0x37, 0x58, 0xc6, 0x0f, 0x67, 0xc4, 0x55, 0x73, 0x46, 0x23, 0x19, 0x8f, 0xce, 0x3e, 0x66, - 0x40, 0xf2, 0x7a, 0x96, 0xae, 0x60, 0xf0, 0xce, 0xc2, 0x3c, 0x3e, 0x2b, 0xc5, 0x1d, 0x58, 0x9c, 0xca, 0xce, 0xf7, - 0x20, 0xc3, 0x2a, 0xfc, 0x43, 0x9c, 0x01, 0xb4, 0xeb, 0x59, 0x5a, 0x9f, 0xa5, 0xd5, 0x59, 0x5e, 0xd4, 0x67, 0x4a, - 0x0a, 0x87, 0x30, 0x7e, 0x78, 0x4f, 0x5f, 0xd9, 0xe5, 0x6d, 0x16, 0x77, 0x59, 0xe4, 0x4f, 0xd1, 0xab, 0x88, 0x98, - 0x34, 0x2a, 0xe1, 0xb5, 0xfb, 0xdb, 0xe6, 0xfe, 0xe1, 0x75, 0x63, 0xf7, 0xb3, 0x3b, 0x46, 0x74, 0x41, 0x3d, 0x5e, - 0x49, 0x4a, 0x05, 0x05, 0x04, 0x4e, 0x34, 0x6b, 0x3c, 0xb8, 0xe3, 0x80, 0x57, 0x03, 0x5b, 0xb2, 0xb5, 0xcf, 0xaf, - 0x63, 0x19, 0xa6, 0xbd, 0x09, 0xf0, 0xaf, 0xb2, 0x37, 0x5d, 0x07, 0x4b, 0xbc, 0x6f, 0x21, 0xdb, 0xd0, 0xdb, 0xd7, - 0xfc, 0x85, 0x97, 0xab, 0xbf, 0xd9, 0x3f, 0x00, 0x08, 0x03, 0x62, 0x56, 0x7d, 0x34, 0x71, 0xef, 0xac, 0x2c, 0x3b, - 0x27, 0xcb, 0xae, 0x87, 0x7e, 0x4d, 0x62, 0x54, 0x5a, 0x59, 0x4a, 0x27, 0x4b, 0x09, 0x59, 0xc0, 0x27, 0x46, 0x53, - 0x1b, 0x01, 0x84, 0xed, 0x28, 0x95, 0x2f, 0x54, 0x5e, 0x44, 0xe1, 0x9c, 0xe0, 0x79, 0x22, 0x46, 0xf7, 0x56, 0x32, - 0x60, 0x38, 0x84, 0x60, 0x0e, 0xda, 0x62, 0x6f, 0xe8, 0x26, 0xe2, 0xaf, 0x37, 0x45, 0xf9, 0x36, 0x26, 0x9f, 0x82, - 0xdd, 0xc9, 0xc7, 0x25, 0x3c, 0x2e, 0x4f, 0x3e, 0x0e, 0xd1, 0x23, 0xe1, 0xe4, 0x63, 0xf0, 0x3d, 0x92, 0xf3, 0xba, - 0xeb, 0x71, 0x82, 0xdc, 0x42, 0xba, 0xbf, 0x1d, 0x93, 0x00, 0xcd, 0x6b, 0x58, 0x8e, 0x9a, 0x8a, 0x6b, 0x66, 0xc6, - 0x78, 0xde, 0xe8, 0xfd, 0xb1, 0xe3, 0x2d, 0x53, 0x28, 0x66, 0x31, 0xaf, 0xe1, 0xf7, 0xac, 0x0a, 0xd4, 0x5d, 0x6f, - 0x93, 0xdc, 0x32, 0xab, 0xe7, 0x68, 0xf7, 0xfd, 0x50, 0x27, 0x82, 0xda, 0xdf, 0x61, 0xcf, 0x33, 0xeb, 0x5d, 0x15, - 0x03, 0x97, 0x2a, 0xd9, 0x21, 0x53, 0xd5, 0xf4, 0x40, 0xa5, 0x34, 0x78, 0x7a, 0x69, 0x5d, 0xbe, 0x54, 0xda, 0xc8, - 0x33, 0xcd, 0x6f, 0x00, 0x2f, 0xa6, 0x2e, 0x8b, 0xdd, 0x37, 0xf7, 0x15, 0xdc, 0xc6, 0xfb, 0xfd, 0x65, 0xe5, 0x99, - 0x9f, 0xb8, 0x00, 0xec, 0x4d, 0x85, 0xd6, 0x09, 0x94, 0x1a, 0xd6, 0xe1, 0xab, 0x44, 0x44, 0x7f, 0xb4, 0xcb, 0x75, - 0xe6, 0x3a, 0x60, 0x44, 0x11, 0xbf, 0x8d, 0x47, 0x7f, 0x80, 0xe2, 0xda, 0xd8, 0x03, 0xc2, 0x3a, 0x24, 0xf4, 0x19, - 0x01, 0x48, 0x3d, 0xe6, 0x28, 0x01, 0xcd, 0x8a, 0xe6, 0x8e, 0xc9, 0xcf, 0xf5, 0x95, 0xd2, 0xdf, 0x2f, 0x2b, 0x8f, - 0xcc, 0x29, 0x6d, 0x33, 0x8d, 0xd5, 0x9a, 0x4a, 0x20, 0xbc, 0xa2, 0x92, 0x55, 0xf8, 0x6c, 0xde, 0x88, 0x7e, 0x5f, - 0x1e, 0xe1, 0x69, 0xf5, 0xe3, 0x16, 0xe3, 0x5b, 0x01, 0xd1, 0x48, 0x00, 0xf4, 0x13, 0xc0, 0xbc, 0xc8, 0x66, 0x76, - 0x1f, 0x07, 0x54, 0x29, 0xd1, 0x34, 0xce, 0xe6, 0xf9, 0x3d, 0xbd, 0x29, 0x3b, 0xe8, 0xd4, 0xa9, 0x02, 0x17, 0x5c, - 0x95, 0x8c, 0x57, 0xd6, 0x13, 0xf9, 0xfc, 0xe6, 0x76, 0x93, 0x66, 0xf1, 0x87, 0xf2, 0x1f, 0x38, 0xb6, 0xba, 0x0e, - 0x8f, 0x4c, 0x9d, 0xae, 0x9d, 0x47, 0x5a, 0x7b, 0x21, 0x20, 0xa2, 0x5d, 0x43, 0xad, 0x17, 0x16, 0x7a, 0xa4, 0x27, - 0xc2, 0x39, 0x49, 0xd4, 0xb4, 0x03, 0x2d, 0x8d, 0xd0, 0xd7, 0xd7, 0x9c, 0xfe, 0xc2, 0x60, 0xed, 0xf3, 0x31, 0x03, - 0xb2, 0x12, 0xfd, 0x58, 0x3d, 0x34, 0x36, 0x73, 0xe8, 0x59, 0xab, 0xf2, 0xcc, 0xab, 0x0e, 0x07, 0xc4, 0x87, 0xd1, - 0x5f, 0xf2, 0xfb, 0xfd, 0xd7, 0x34, 0xff, 0x98, 0x50, 0xe3, 0x67, 0x9b, 0x01, 0xba, 0xf6, 0x5d, 0x79, 0x20, 0xea, - 0xb9, 0x56, 0x09, 0x42, 0xbc, 0x41, 0x4c, 0x34, 0x23, 0xe6, 0xe0, 0xb4, 0x43, 0xcd, 0x3f, 0x49, 0x0d, 0x08, 0x51, - 0xe2, 0x75, 0x4c, 0x59, 0x90, 0xd3, 0x26, 0x8e, 0xf4, 0xa3, 0x70, 0x22, 0x3f, 0x89, 0xaa, 0xc8, 0xee, 0xe1, 0x82, - 0xc1, 0xd4, 0x7b, 0xda, 0x2f, 0xd1, 0x6f, 0x09, 0x47, 0xce, 0xd1, 0xaa, 0x10, 0x44, 0x4e, 0x08, 0x6b, 0x0d, 0x61, - 0x82, 0xd8, 0x20, 0x5e, 0xf6, 0x5d, 0x92, 0xe1, 0x48, 0xc1, 0x65, 0x1d, 0x3b, 0xc6, 0x5c, 0x1d, 0x55, 0xaf, 0x01, - 0x8c, 0x57, 0x8e, 0xa0, 0xd9, 0x28, 0xb2, 0x4b, 0x88, 0x2a, 0x72, 0x3c, 0x01, 0xb5, 0x83, 0xd2, 0xd8, 0x4c, 0xcf, - 0xc7, 0x41, 0x3e, 0x5a, 0x54, 0xa8, 0x73, 0x62, 0x19, 0xaf, 0x01, 0x58, 0x3b, 0x57, 0xfd, 0x3c, 0xab, 0xc1, 0x93, - 0x86, 0xf8, 0x7c, 0x8c, 0xb6, 0x57, 0x36, 0x07, 0xd5, 0x76, 0x3a, 0x2b, 0xaf, 0x98, 0x2e, 0x07, 0xc6, 0x7d, 0xc3, - 0x2b, 0x8a, 0x33, 0xfc, 0xe4, 0xc1, 0x16, 0xe7, 0x4f, 0x37, 0xd4, 0x7e, 0xcc, 0x8d, 0x7a, 0x18, 0x68, 0x2d, 0x78, - 0x53, 0x10, 0xeb, 0xef, 0xc7, 0x8e, 0x6c, 0x1f, 0xb4, 0xc8, 0x68, 0xf2, 0xd9, 0xcf, 0x3f, 0x96, 0xe9, 0x2a, 0x85, - 0xfb, 0x92, 0x93, 0x45, 0x33, 0x0f, 0x81, 0xbd, 0x21, 0x86, 0xeb, 0xa3, 0xc2, 0x23, 0xca, 0xfa, 0x7d, 0xf8, 0x7d, - 0x95, 0x81, 0x29, 0x06, 0xae, 0x2b, 0x04, 0xe3, 0x21, 0x10, 0xc4, 0xc3, 0x34, 0x3a, 0x19, 0xd4, 0xa0, 0x0d, 0xdf, - 0x00, 0x64, 0x06, 0x78, 0x64, 0x2e, 0x3d, 0x02, 0xee, 0x02, 0xd7, 0x9e, 0x8c, 0xc7, 0xfe, 0xc4, 0x34, 0x34, 0x6a, - 0x4a, 0x33, 0x3d, 0x37, 0x7e, 0xd3, 0x51, 0x2d, 0xd7, 0xce, 0x7f, 0x7c, 0xc9, 0x6f, 0xd0, 0x0b, 0x5a, 0x5e, 0xee, - 0x23, 0x75, 0xb9, 0xcf, 0x28, 0x2e, 0x13, 0xc9, 0x61, 0x41, 0x2c, 0x4b, 0x38, 0xf0, 0x18, 0x95, 0x2c, 0xb6, 0xf4, - 0x58, 0x15, 0x2d, 0x5f, 0x94, 0x1b, 0xa4, 0x43, 0x27, 0x04, 0x4b, 0x54, 0x10, 0x2c, 0x81, 0x71, 0x11, 0x6b, 0xbe, - 0x19, 0xe4, 0x2c, 0x9e, 0x6d, 0xe6, 0x1c, 0x09, 0xeb, 0x92, 0xc3, 0xa1, 0x90, 0x60, 0x33, 0xd9, 0x6c, 0x3d, 0x67, - 0x6b, 0x9f, 0x81, 0x12, 0xa0, 0x94, 0x69, 0x82, 0xd2, 0xb4, 0x62, 0x2b, 0x6e, 0x5a, 0x83, 0xd5, 0x6a, 0xca, 0x56, - 0x35, 0x65, 0xe7, 0x34, 0xe5, 0xa8, 0x82, 0x92, 0x13, 0x4a, 0x51, 0x86, 0x01, 0x8c, 0xd8, 0x24, 0xba, 0xca, 0xd0, - 0xc7, 0x3b, 0xe1, 0x11, 0x54, 0x11, 0x91, 0x4f, 0x18, 0x42, 0x60, 0x22, 0x8a, 0x0b, 0x55, 0x28, 0x06, 0xc8, 0x88, - 0x04, 0x82, 0x89, 0x4a, 0x9d, 0x02, 0xf3, 0xd1, 0x54, 0x31, 0x6c, 0xda, 0x13, 0xe5, 0x7b, 0xea, 0xb8, 0x47, 0xd9, - 0xe6, 0x6f, 0x62, 0x17, 0x84, 0xc8, 0xdd, 0xb8, 0x53, 0x3f, 0x23, 0xde, 0xdb, 0x1d, 0x61, 0xfc, 0x64, 0xc7, 0x2d, - 0xc2, 0x15, 0xc1, 0x96, 0x6a, 0x0e, 0xb1, 0x98, 0x57, 0x93, 0x04, 0xb5, 0x2c, 0x89, 0xbf, 0xe1, 0xc9, 0x20, 0x67, - 0x4b, 0xf0, 0xa0, 0x9d, 0xb3, 0x0c, 0xf0, 0x57, 0xac, 0x16, 0xfd, 0x56, 0x7b, 0x4b, 0x90, 0x9f, 0x36, 0x76, 0xa3, - 0x30, 0x31, 0x82, 0x44, 0xdd, 0xae, 0x0c, 0xe4, 0x87, 0x8f, 0x38, 0x1d, 0x8f, 0x3d, 0x65, 0xcc, 0xad, 0x4c, 0x2f, - 0xd3, 0xb9, 0x92, 0x6f, 0xe4, 0x5e, 0xfa, 0xd8, 0x4b, 0xb0, 0x73, 0xc0, 0x1b, 0x48, 0x1b, 0x78, 0x03, 0xdb, 0x85, - 0xd7, 0x06, 0x09, 0x33, 0x02, 0x6c, 0x71, 0x7c, 0x8c, 0x94, 0xc0, 0x10, 0x8e, 0xb3, 0x14, 0x80, 0x69, 0xf4, 0x65, - 0xb6, 0xb2, 0x2f, 0xb3, 0x5a, 0xb3, 0xa5, 0x72, 0xba, 0x77, 0x6e, 0xdd, 0xce, 0x27, 0x12, 0x00, 0x4c, 0xea, 0x1c, - 0x88, 0x33, 0x13, 0xec, 0xd2, 0x24, 0xb2, 0x7c, 0x0a, 0xf3, 0x3b, 0xf1, 0xa6, 0x2c, 0x56, 0xaa, 0x2b, 0xda, 0x3e, - 0x33, 0xf9, 0x8c, 0x74, 0x12, 0x2a, 0xa0, 0xa0, 0x90, 0x6b, 0x7d, 0xfa, 0x3e, 0x7c, 0x1f, 0x14, 0x1a, 0x98, 0xad, - 0xc2, 0x3d, 0x4d, 0xd6, 0x48, 0xbd, 0x51, 0xf5, 0xfb, 0xe4, 0x1a, 0x48, 0x75, 0xe6, 0xd0, 0xb2, 0x27, 0x15, 0x06, - 0x88, 0x1d, 0xf5, 0x19, 0x09, 0x75, 0x20, 0xf5, 0x80, 0x21, 0x44, 0xdb, 0xf4, 0xf1, 0x27, 0x43, 0xa2, 0x0b, 0xb0, - 0x85, 0x68, 0x03, 0x3f, 0xfe, 0x04, 0xfb, 0x2c, 0x08, 0x8f, 0x69, 0xfe, 0x0e, 0x92, 0x8e, 0x0d, 0x9c, 0x56, 0x9f, - 0x82, 0x0f, 0x92, 0x1c, 0x4c, 0xd4, 0xc1, 0xcb, 0xfd, 0xa5, 0xdf, 0x87, 0x2d, 0x3b, 0x97, 0x52, 0x1d, 0x2b, 0xf5, - 0xb6, 0xad, 0xfd, 0x20, 0xda, 0x82, 0x23, 0x8b, 0xf8, 0x87, 0x0c, 0x11, 0xc1, 0xcc, 0x20, 0xc2, 0xae, 0x85, 0xba, - 0xdb, 0x53, 0x6a, 0x59, 0xd4, 0xdb, 0x9e, 0x52, 0xea, 0x36, 0x0c, 0xdf, 0x4d, 0x30, 0x53, 0xdc, 0xf0, 0x3f, 0x32, - 0x2f, 0xd4, 0x1b, 0x8f, 0x45, 0x81, 0xee, 0xf9, 0xfb, 0x25, 0xaf, 0x66, 0x1b, 0x65, 0xc2, 0xbc, 0xe3, 0xcb, 0x59, - 0x28, 0xbb, 0x5a, 0x1a, 0x77, 0xbe, 0x78, 0x4b, 0x35, 0x1f, 0xfc, 0xc3, 0x21, 0x81, 0x78, 0xa3, 0xf8, 0xea, 0xae, - 0x91, 0x5b, 0xd7, 0x64, 0x73, 0x55, 0x02, 0xea, 0xf7, 0xf9, 0x1a, 0xf7, 0x5b, 0xac, 0x7f, 0xf7, 0x34, 0xc8, 0x58, - 0xcd, 0x70, 0xc5, 0x14, 0x3e, 0x05, 0x80, 0xc1, 0xe1, 0x54, 0x90, 0x16, 0x78, 0xc3, 0xcb, 0xe1, 0xe5, 0x64, 0x43, - 0x26, 0xdd, 0x8d, 0x8f, 0xdc, 0x59, 0xa0, 0xea, 0xfd, 0x8e, 0xe2, 0xa4, 0x41, 0xa2, 0xb1, 0xd7, 0xe0, 0x8b, 0x2c, - 0xa3, 0x5c, 0x34, 0x71, 0x1f, 0x93, 0xaf, 0xf4, 0x00, 0xe6, 0x2a, 0x94, 0x00, 0xd1, 0x6f, 0x2c, 0x8b, 0x8d, 0x68, - 0x5b, 0x6c, 0x60, 0x29, 0x55, 0x73, 0xbd, 0x9a, 0xbe, 0x78, 0x25, 0x9a, 0xf7, 0xd1, 0x8c, 0x53, 0x1a, 0x0d, 0x38, - 0x4e, 0xa3, 0x70, 0xfb, 0xe1, 0x5e, 0x94, 0xcb, 0x0c, 0x2c, 0xd9, 0x2a, 0x9c, 0xe2, 0xb2, 0x51, 0x67, 0xc4, 0x8b, - 0x3c, 0x56, 0x00, 0x1d, 0x8f, 0x09, 0x80, 0xea, 0x82, 0x80, 0x8a, 0x68, 0x29, 0xbd, 0x15, 0x5a, 0x2c, 0xd4, 0x1b, - 0x8e, 0x52, 0xf8, 0x23, 0xfd, 0x79, 0x90, 0x4f, 0x01, 0x88, 0x5d, 0x1f, 0x47, 0x6f, 0x8a, 0x92, 0x3e, 0x55, 0xcc, - 0x72, 0x39, 0x98, 0xc0, 0xae, 0x4e, 0x64, 0xa8, 0x15, 0xe4, 0xad, 0xba, 0xf2, 0x56, 0x26, 0x6f, 0x63, 0x9c, 0x92, - 0x1f, 0xb9, 0xe9, 0x58, 0x23, 0x06, 0x5e, 0x79, 0x5a, 0xa7, 0x09, 0xd2, 0xe4, 0x02, 0x18, 0x86, 0xf8, 0x36, 0xf3, - 0x5e, 0x78, 0x8e, 0x54, 0x05, 0xc9, 0x6c, 0x97, 0x79, 0xea, 0x22, 0xaa, 0xaf, 0x9c, 0x5a, 0x3a, 0x73, 0xfa, 0x11, - 0xc0, 0x7b, 0x4c, 0x4d, 0x1a, 0xf2, 0x11, 0x6e, 0x4b, 0xf1, 0xf5, 0x56, 0x5d, 0xe3, 0xa5, 0xd1, 0xb9, 0x7b, 0xf9, - 0xd2, 0x9d, 0x06, 0xfd, 0x14, 0x04, 0xe5, 0x7c, 0x51, 0x0a, 0xd8, 0x53, 0x66, 0x73, 0xbd, 0x5a, 0xb5, 0x42, 0xeb, - 0x70, 0x18, 0x6b, 0x47, 0x21, 0xad, 0xce, 0x02, 0xb6, 0x1a, 0xe9, 0x94, 0x00, 0x21, 0x38, 0x4e, 0xc3, 0x4e, 0x30, - 0xee, 0xd2, 0x69, 0x44, 0xd6, 0x2b, 0x25, 0xe9, 0xc2, 0x0c, 0x92, 0x7f, 0x92, 0xd7, 0x33, 0xa0, 0x25, 0x80, 0x43, - 0x11, 0x4b, 0x78, 0x38, 0x49, 0xae, 0x00, 0x3a, 0x1d, 0x0e, 0x2a, 0x0d, 0xcd, 0x59, 0xcd, 0x92, 0xf9, 0x24, 0x96, - 0xaa, 0xca, 0xc3, 0xc1, 0x53, 0x6e, 0x06, 0xfd, 0x7e, 0x36, 0x2d, 0x95, 0x0b, 0x40, 0x10, 0xeb, 0xc2, 0x00, 0xf1, - 0x48, 0x0b, 0x4f, 0x16, 0x7d, 0x4a, 0xe2, 0x97, 0xb3, 0x64, 0x6e, 0xb2, 0xe1, 0x1d, 0x18, 0xc1, 0x66, 0x5c, 0x97, - 0x94, 0x69, 0x8f, 0xca, 0xef, 0x19, 0x3d, 0xb5, 0x7d, 0xad, 0xd5, 0x16, 0xb1, 0xae, 0x83, 0xab, 0x12, 0xf5, 0x14, - 0x1f, 0x94, 0x24, 0x78, 0xbf, 0x76, 0x6e, 0x46, 0xca, 0xd7, 0x22, 0xf7, 0x83, 0x76, 0xa6, 0x56, 0x0e, 0x1c, 0x81, - 0x1c, 0xab, 0xa8, 0xe4, 0xf5, 0xae, 0x43, 0xf0, 0xe8, 0xae, 0x54, 0xa0, 0x1c, 0x7c, 0x0d, 0x62, 0x74, 0x7d, 0xd5, - 0x59, 0x43, 0xcd, 0x34, 0xaa, 0x3c, 0x82, 0x4e, 0x1d, 0xc0, 0x93, 0x82, 0x97, 0x5a, 0xfd, 0x78, 0x38, 0x78, 0xe6, - 0x07, 0x7f, 0x95, 0xe9, 0x5b, 0x88, 0x89, 0x72, 0xaa, 0x11, 0x12, 0x57, 0x4a, 0x12, 0xf1, 0xf1, 0xa2, 0x65, 0xc5, - 0xa8, 0x0c, 0x1f, 0x78, 0xa5, 0xca, 0x57, 0xa7, 0x2a, 0x2f, 0x46, 0xda, 0x96, 0xc0, 0x6b, 0xf2, 0x0f, 0x91, 0x6b, - 0xde, 0xfa, 0xba, 0xab, 0x0c, 0x7d, 0x27, 0x2b, 0xd0, 0x11, 0x6c, 0x65, 0x29, 0x39, 0xe0, 0x93, 0xea, 0xae, 0x5a, - 0xb5, 0x3e, 0xa7, 0x6c, 0x23, 0xdc, 0xe4, 0xd7, 0xb1, 0x83, 0x23, 0xe5, 0x37, 0x78, 0x2e, 0x80, 0xbd, 0x06, 0xec, - 0xcd, 0x39, 0x2b, 0x9a, 0x47, 0x87, 0xb4, 0x2d, 0xd0, 0xc8, 0xcc, 0xed, 0x5c, 0xdd, 0xb7, 0xe5, 0x51, 0x1a, 0x43, - 0x64, 0xda, 0x23, 0xd3, 0xc1, 0x66, 0x94, 0xff, 0x9e, 0xf2, 0x5b, 0x85, 0x63, 0xe0, 0xdb, 0xa9, 0x77, 0x00, 0x55, - 0x4f, 0x1b, 0x64, 0xac, 0x19, 0x86, 0x56, 0x76, 0xb9, 0x14, 0x5a, 0x82, 0x96, 0xba, 0x09, 0x82, 0xf3, 0x23, 0xa2, - 0x1c, 0x01, 0xe8, 0x22, 0x05, 0x4c, 0xf0, 0x53, 0xda, 0xee, 0x7e, 0x7f, 0x9d, 0x7a, 0xe4, 0xde, 0x15, 0x6a, 0x94, - 0x50, 0x82, 0xb1, 0x9f, 0x68, 0xcc, 0xa0, 0xa3, 0x2b, 0x72, 0xc2, 0xb3, 0x56, 0x87, 0x75, 0xdd, 0x94, 0x41, 0x59, - 0x1c, 0xf3, 0x6a, 0x3a, 0xfb, 0xfd, 0xc9, 0xbe, 0x6e, 0x90, 0x85, 0xfc, 0x77, 0xd6, 0x43, 0x32, 0xe8, 0x1e, 0x84, - 0x42, 0xf4, 0xe6, 0xc1, 0x0c, 0xff, 0x63, 0x1b, 0x9e, 0x7d, 0xc7, 0x8d, 0x3a, 0x01, 0xcc, 0x11, 0xd7, 0x4b, 0x4f, - 0xd1, 0xd6, 0xc3, 0x2d, 0x90, 0xad, 0xf1, 0xf2, 0xd6, 0x5e, 0x03, 0x39, 0xc5, 0xf1, 0xdf, 0xf1, 0x4c, 0xad, 0x6c, - 0xf0, 0xd3, 0x53, 0xb6, 0x03, 0x0f, 0x2f, 0x42, 0x40, 0x31, 0x2c, 0x1b, 0x7f, 0x67, 0x39, 0xce, 0xe8, 0xbf, 0x79, - 0xc4, 0x30, 0x58, 0x44, 0x7e, 0x7c, 0x59, 0x0a, 0xf1, 0x55, 0x78, 0x6f, 0x2b, 0xef, 0x8e, 0x9c, 0x32, 0xef, 0xf4, - 0x30, 0xba, 0x2e, 0x49, 0xdf, 0x25, 0x1f, 0x5b, 0xc3, 0xf6, 0xbb, 0x76, 0xbf, 0x19, 0x22, 0x08, 0xa1, 0x1c, 0x3f, - 0x67, 0x74, 0x42, 0xe3, 0xc3, 0x6a, 0x76, 0x7a, 0xfd, 0xde, 0x39, 0x5e, 0xb0, 0x35, 0x1a, 0xe0, 0xf1, 0xd0, 0xc5, - 0x3c, 0x51, 0x43, 0xa7, 0xeb, 0xda, 0x39, 0x78, 0x60, 0x90, 0xe5, 0xc9, 0x77, 0x0c, 0x4b, 0xec, 0x4f, 0x22, 0x9e, - 0xb4, 0x55, 0x1b, 0x9b, 0x23, 0xd5, 0x46, 0xcd, 0xc0, 0x0f, 0x5e, 0x41, 0x81, 0xd1, 0x05, 0x69, 0x05, 0xc6, 0xe1, - 0x08, 0x40, 0x56, 0x8c, 0xe3, 0x91, 0xc1, 0x04, 0x86, 0x74, 0x43, 0x51, 0x00, 0x1e, 0x1e, 0xc7, 0x83, 0x90, 0x01, - 0xa4, 0x0b, 0x1e, 0x1a, 0xb6, 0x49, 0x48, 0xf9, 0x79, 0x9e, 0xd7, 0x6a, 0x08, 0x7d, 0x67, 0xa1, 0x3a, 0xf6, 0x23, - 0xed, 0x15, 0xeb, 0x5a, 0x95, 0x8e, 0x6c, 0x75, 0x80, 0xbe, 0x21, 0x03, 0xdf, 0x3a, 0xb6, 0x00, 0x88, 0x96, 0xf8, - 0x2d, 0xf5, 0x6a, 0x5f, 0xc6, 0xac, 0x50, 0xaf, 0x2f, 0x4c, 0xbb, 0x5e, 0x4b, 0x8b, 0x02, 0x2a, 0x6e, 0x5b, 0xb5, - 0x3d, 0x92, 0xf3, 0x1f, 0xdf, 0x75, 0xb4, 0xe3, 0xb3, 0x53, 0x63, 0x4b, 0x28, 0x73, 0x8b, 0x27, 0xb2, 0x3a, 0xda, - 0x52, 0x9d, 0xea, 0x03, 0x2e, 0x35, 0xa9, 0xce, 0x0c, 0x0c, 0xaf, 0x11, 0xa0, 0xdc, 0x42, 0x24, 0x8d, 0xc3, 0xde, - 0xf9, 0x64, 0x50, 0x30, 0xb7, 0x48, 0x40, 0x02, 0xdb, 0xd8, 0xda, 0x45, 0x73, 0xfd, 0xfa, 0x2d, 0xf5, 0xaa, 0x36, - 0x55, 0x3d, 0x78, 0xe3, 0x05, 0xce, 0xde, 0x69, 0x2d, 0x20, 0x80, 0xc2, 0xd6, 0xb2, 0x1c, 0x9c, 0xbb, 0x5d, 0xd5, - 0x52, 0x51, 0x46, 0xfd, 0xfe, 0xf9, 0x6f, 0x29, 0x2a, 0x62, 0x4f, 0x15, 0xa7, 0xac, 0xdf, 0x6e, 0x99, 0x8b, 0xca, - 0x92, 0x37, 0xa8, 0xa2, 0xb5, 0x3a, 0x6a, 0x2a, 0xd7, 0xcd, 0x55, 0x4b, 0x26, 0x88, 0xd1, 0x7d, 0xba, 0xd6, 0xb9, - 0x53, 0xef, 0xbd, 0x8a, 0x23, 0x06, 0x82, 0x9b, 0xee, 0xf1, 0xc1, 0x41, 0x68, 0x54, 0x94, 0x0b, 0x6e, 0x94, 0x56, - 0x95, 0x94, 0x42, 0xde, 0xaa, 0x68, 0xce, 0xf4, 0x11, 0x00, 0x11, 0x60, 0x95, 0xa8, 0xff, 0xcd, 0x97, 0xc6, 0x78, - 0xf0, 0xc0, 0xd7, 0xe4, 0x3a, 0xb6, 0xde, 0x3f, 0xad, 0x91, 0x56, 0x1b, 0xc7, 0xa4, 0x56, 0xbd, 0x6c, 0x15, 0x2f, - 0xbb, 0xd7, 0xa9, 0x18, 0x3c, 0xff, 0x9f, 0xfb, 0x00, 0x35, 0xa2, 0xa5, 0x0c, 0x6e, 0x5d, 0x0d, 0xd0, 0xf8, 0x70, - 0x2c, 0x7c, 0xe3, 0x87, 0x8c, 0xf3, 0xc1, 0x0c, 0x1d, 0xd5, 0xe6, 0xe0, 0x80, 0xe0, 0xa8, 0xee, 0xd1, 0x98, 0x30, - 0x0b, 0xe7, 0x1e, 0x04, 0xaa, 0x4f, 0xdc, 0x67, 0x5c, 0x7b, 0x41, 0x9b, 0xc0, 0x27, 0xeb, 0xba, 0xa6, 0x08, 0x70, - 0x11, 0x1b, 0x13, 0x31, 0xc4, 0x65, 0x93, 0x48, 0x7d, 0x33, 0x06, 0x05, 0x40, 0x71, 0x5d, 0x91, 0x5c, 0xba, 0x48, - 0xf3, 0x4a, 0x94, 0xb5, 0x6e, 0x46, 0xc5, 0x8a, 0x21, 0x00, 0x3c, 0x04, 0xc5, 0x55, 0x65, 0x26, 0x34, 0x62, 0x03, - 0xa9, 0x2c, 0x05, 0xab, 0x86, 0x85, 0xdf, 0xb4, 0xdf, 0x24, 0x27, 0xbd, 0xf3, 0x71, 0xeb, 0xdc, 0xb1, 0xef, 0x1d, - 0x85, 0x94, 0xf6, 0x50, 0x4c, 0x10, 0x04, 0x3f, 0xad, 0xc3, 0xf9, 0x33, 0x7e, 0x4d, 0x60, 0x2a, 0xb2, 0x19, 0x03, - 0x0e, 0x42, 0x44, 0x66, 0xfc, 0x9e, 0xc3, 0x6b, 0x5e, 0x4e, 0xc2, 0xe1, 0xd0, 0x07, 0x7d, 0x28, 0xcf, 0x66, 0xe1, - 0x50, 0xcc, 0xa5, 0xf7, 0x3a, 0x58, 0xeb, 0x42, 0x5e, 0x4f, 0x42, 0x44, 0x0b, 0x0d, 0x7d, 0x70, 0x5e, 0x77, 0xcd, - 0x11, 0x96, 0x00, 0x34, 0x71, 0xf4, 0x65, 0xfd, 0x7e, 0xe4, 0x69, 0x43, 0x8b, 0x14, 0x17, 0x8d, 0x32, 0x9b, 0xe5, - 0xb2, 0x13, 0x36, 0xae, 0xdd, 0x02, 0xa1, 0x78, 0x98, 0xb6, 0x50, 0xb5, 0x9e, 0xea, 0xf5, 0xdc, 0xb4, 0xfb, 0xee, - 0x51, 0xb5, 0xca, 0x91, 0xce, 0xda, 0x74, 0xa5, 0x56, 0xb7, 0x8c, 0xaa, 0x75, 0x96, 0x46, 0x54, 0xb9, 0x49, 0xee, - 0x1a, 0xb5, 0xe0, 0x93, 0x0d, 0x5d, 0xa6, 0xec, 0x6c, 0x0d, 0x4e, 0x1c, 0x79, 0x2e, 0xb9, 0xe5, 0xbb, 0xf3, 0x8a, - 0xee, 0x4e, 0xb5, 0x6f, 0x01, 0xee, 0xcd, 0xb0, 0x21, 0x73, 0x5e, 0x63, 0xa7, 0x41, 0x98, 0x04, 0x7e, 0xc4, 0x3e, - 0x66, 0xc8, 0x06, 0x03, 0x3a, 0x0a, 0xe9, 0x7f, 0x6d, 0x99, 0x23, 0x01, 0x93, 0xbf, 0x9e, 0xfb, 0xcd, 0xa2, 0xc8, - 0x61, 0x31, 0x7e, 0xdc, 0x60, 0xa4, 0xb1, 0x5a, 0x83, 0x61, 0x79, 0x87, 0xc8, 0x9f, 0xda, 0x1d, 0xd3, 0x54, 0xc7, - 0x9b, 0xf5, 0x5a, 0xf3, 0xab, 0xa7, 0x4f, 0x75, 0x7d, 0xfe, 0xdb, 0xf7, 0x97, 0x61, 0xcd, 0xec, 0x0f, 0x41, 0x28, - 0xed, 0xde, 0x2d, 0xce, 0x1d, 0x89, 0xde, 0xb1, 0xd2, 0xcc, 0x2e, 0xed, 0x92, 0x5d, 0x9a, 0xd2, 0x6e, 0xc8, 0xf5, - 0xea, 0x1b, 0xe5, 0x8d, 0x9d, 0x57, 0x4c, 0xf7, 0xef, 0x85, 0xde, 0x51, 0x4e, 0xd5, 0x04, 0x22, 0x9a, 0xb4, 0x23, - 0x71, 0xbb, 0x57, 0x86, 0xcf, 0x27, 0x79, 0xbb, 0x84, 0xa3, 0xae, 0x61, 0xb9, 0xf9, 0xf6, 0x3f, 0xf2, 0xaa, 0xb3, - 0xc2, 0xed, 0x97, 0xc6, 0xac, 0xfd, 0x29, 0x88, 0xab, 0xfa, 0xc3, 0x7b, 0x52, 0x33, 0x25, 0xff, 0x57, 0x3d, 0x06, - 0xae, 0x7e, 0x32, 0xed, 0xe8, 0x9e, 0x42, 0xd8, 0x60, 0xf6, 0xf3, 0xe3, 0x87, 0x16, 0xac, 0xaa, 0x0b, 0x14, 0xc9, - 0x01, 0x74, 0xee, 0x92, 0x11, 0xde, 0xef, 0x18, 0xe7, 0xfe, 0xd5, 0x2f, 0x6a, 0x72, 0x84, 0x88, 0x76, 0x11, 0x0e, - 0x00, 0xe2, 0x4e, 0x53, 0x59, 0x87, 0x1a, 0xa0, 0x0f, 0x08, 0xac, 0x43, 0xdf, 0x66, 0x00, 0x07, 0x7d, 0xb4, 0x79, - 0x16, 0x81, 0xbc, 0xee, 0xdd, 0xb3, 0x77, 0x6c, 0xe7, 0xf3, 0xeb, 0x55, 0xea, 0xdd, 0xa3, 0x43, 0xf0, 0xf9, 0xd8, - 0x9f, 0x5e, 0x06, 0x06, 0x17, 0x9a, 0xbd, 0x7b, 0x26, 0xd8, 0x8e, 0xed, 0x9e, 0x21, 0x52, 0x51, 0x77, 0xfe, 0xe1, - 0xa5, 0x89, 0x9e, 0x77, 0x5e, 0xb8, 0xe3, 0x4b, 0x00, 0x0f, 0x64, 0x31, 0xa0, 0xf8, 0x2c, 0xbd, 0x7f, 0xb2, 0x04, - 0xd4, 0xe4, 0xb7, 0x7c, 0xed, 0xbd, 0xa7, 0xd4, 0x05, 0xfc, 0x39, 0xa0, 0xf4, 0x49, 0xce, 0xbd, 0xbb, 0xe1, 0xad, - 0x7f, 0xf1, 0x1c, 0x9c, 0x27, 0x56, 0xc3, 0x05, 0xfc, 0x55, 0xf0, 0xa1, 0x77, 0x37, 0xc0, 0xc4, 0x92, 0x0f, 0xbd, - 0xd5, 0x00, 0x52, 0x15, 0x2e, 0x24, 0xc6, 0x3e, 0xfc, 0x1a, 0xe4, 0x0c, 0xff, 0xf8, 0x4d, 0x63, 0xb0, 0xfe, 0x1a, - 0x14, 0x1a, 0x8d, 0xb5, 0x54, 0x21, 0x4b, 0xb1, 0x38, 0x13, 0x60, 0x13, 0x8e, 0xbb, 0x7d, 0xb1, 0xaa, 0xcd, 0x5a, - 0xd0, 0x9f, 0x8f, 0xf8, 0x1e, 0x8d, 0xd5, 0x55, 0x39, 0x17, 0xe5, 0x27, 0xa4, 0x4f, 0x75, 0x7c, 0x8c, 0x8a, 0x4d, - 0xdd, 0x9d, 0x4e, 0xb5, 0xea, 0x48, 0xfb, 0x4d, 0xb9, 0x06, 0x3b, 0x5e, 0x27, 0x47, 0x96, 0xc2, 0xb3, 0x0e, 0x3b, - 0x2f, 0x9d, 0x12, 0x1d, 0x86, 0xf1, 0x6e, 0xab, 0x9e, 0x31, 0x94, 0xe7, 0x06, 0x63, 0xba, 0xe0, 0x11, 0xbf, 0x1e, - 0xe4, 0x32, 0x34, 0xe6, 0x23, 0xb2, 0x61, 0x28, 0x1f, 0x5a, 0x64, 0x48, 0x88, 0x78, 0x0f, 0x95, 0x80, 0x6d, 0x0b, - 0xca, 0xa4, 0x80, 0xb3, 0x68, 0xf0, 0x5b, 0xed, 0xe5, 0xc0, 0x7b, 0x10, 0xf9, 0x8d, 0x74, 0x29, 0x97, 0xd8, 0xe8, - 0xc4, 0xb1, 0x2c, 0xb4, 0xf3, 0xb8, 0xfe, 0x3a, 0x06, 0xf5, 0x7b, 0xa5, 0xdf, 0xa0, 0x9c, 0xfd, 0x49, 0xb2, 0x4e, - 0x1b, 0x4f, 0x8c, 0x7f, 0xb9, 0xca, 0x3f, 0x45, 0x4b, 0x3d, 0xfc, 0x7f, 0xc6, 0x14, 0x4a, 0xff, 0x2a, 0x2d, 0xa3, - 0xcd, 0x6a, 0x29, 0x4a, 0x91, 0x47, 0xe2, 0xe4, 0x6b, 0x91, 0x9d, 0xcb, 0x77, 0x3e, 0x85, 0x7e, 0x01, 0x68, 0xd9, - 0x27, 0xc8, 0xe8, 0x5f, 0x98, 0xe0, 0xc3, 0x5f, 0xb4, 0x73, 0x6d, 0xce, 0xc7, 0x93, 0xfc, 0xca, 0xda, 0xbb, 0x1d, - 0x2f, 0x12, 0xa3, 0x18, 0xcb, 0x7d, 0xd5, 0xcd, 0xca, 0x89, 0x4a, 0x0e, 0x8c, 0x74, 0x4d, 0xf6, 0x72, 0x25, 0xeb, - 0x76, 0xba, 0x95, 0x40, 0x44, 0x15, 0x78, 0x8f, 0x71, 0x15, 0xfb, 0x08, 0xa6, 0xeb, 0x8e, 0xcb, 0x68, 0xc7, 0x7b, - 0xc6, 0xab, 0x13, 0x65, 0x05, 0xb7, 0x1b, 0xd1, 0x9e, 0xd0, 0xd1, 0x4f, 0x93, 0xda, 0xb2, 0x70, 0x00, 0x72, 0x97, - 0x30, 0x96, 0x0d, 0xc1, 0x8a, 0x41, 0xe9, 0xeb, 0x35, 0x25, 0xcb, 0x02, 0x2c, 0x3a, 0xbb, 0x8c, 0x40, 0x0c, 0xeb, - 0xa6, 0x39, 0xa1, 0xe3, 0xa5, 0x8b, 0xf3, 0x5e, 0xab, 0x48, 0xc1, 0x33, 0x5a, 0x74, 0xcc, 0x4d, 0x47, 0xba, 0x31, - 0xda, 0xdb, 0x97, 0x06, 0x21, 0xc5, 0xf3, 0x07, 0xb6, 0x5a, 0x17, 0x17, 0x89, 0x57, 0xc8, 0x44, 0x0b, 0x62, 0x29, - 0x02, 0x33, 0x5e, 0x68, 0x1a, 0x61, 0x82, 0x32, 0x25, 0x58, 0xb4, 0x46, 0x87, 0xf6, 0x87, 0x25, 0xec, 0x1e, 0x63, - 0x04, 0x08, 0x54, 0x99, 0xbe, 0x84, 0xad, 0x09, 0xb3, 0xa9, 0x8b, 0x0d, 0xd0, 0x56, 0x31, 0x34, 0x08, 0x6b, 0x43, - 0xcc, 0xa7, 0x34, 0xbf, 0xfb, 0x27, 0x16, 0x63, 0x7b, 0x02, 0xb1, 0xbd, 0xdb, 0x35, 0x09, 0xd3, 0xbd, 0x16, 0x37, - 0xd6, 0xcb, 0xed, 0x29, 0xc7, 0xd4, 0x8e, 0xb5, 0x51, 0x3b, 0xd6, 0x52, 0xef, 0x58, 0x6b, 0xbd, 0x63, 0xdd, 0x35, - 0xfc, 0x63, 0xe6, 0xc5, 0x2c, 0x01, 0xfd, 0xee, 0x8a, 0xab, 0x06, 0x41, 0x33, 0x36, 0xec, 0x16, 0x7e, 0x4b, 0xac, - 0xdd, 0xd2, 0xbf, 0x58, 0xb2, 0x85, 0xe9, 0x03, 0xdd, 0x3a, 0xc0, 0x32, 0xa2, 0x26, 0xdf, 0x23, 0xef, 0xa6, 0xb3, - 0xa2, 0x70, 0x7b, 0x62, 0x0b, 0x9f, 0xbd, 0x33, 0x6f, 0xde, 0x3f, 0x8b, 0x20, 0xf7, 0x8e, 0x7b, 0xf7, 0xc3, 0x77, - 0xfe, 0x85, 0x6e, 0x81, 0x9c, 0xcc, 0x72, 0x06, 0x52, 0x47, 0x7c, 0x86, 0x68, 0x65, 0x4f, 0xf9, 0x4e, 0xc8, 0x9d, - 0x6d, 0xfd, 0xec, 0xde, 0xdd, 0xd6, 0xee, 0x9e, 0xdd, 0xb3, 0x6a, 0x44, 0xb1, 0xe2, 0x34, 0x45, 0xc2, 0x2c, 0xda, - 0x00, 0x4f, 0xbd, 0x7c, 0xbf, 0x63, 0xc7, 0x1c, 0xee, 0x9e, 0x75, 0x74, 0xbc, 0x9c, 0x03, 0x76, 0xf7, 0x1f, 0x6d, - 0xc2, 0xc6, 0x4a, 0xd7, 0x2a, 0x74, 0xb8, 0x7b, 0x96, 0x69, 0x3c, 0x87, 0x23, 0xf9, 0x74, 0xac, 0xb1, 0x41, 0x50, - 0xd7, 0xe7, 0x0c, 0x6a, 0xc7, 0xee, 0x6b, 0xc2, 0x2e, 0x3b, 0xe6, 0xb5, 0xae, 0x79, 0x7b, 0xe5, 0xa9, 0xd8, 0x10, - 0xd0, 0xe1, 0x6b, 0x75, 0x83, 0xfc, 0x4b, 0xe0, 0x14, 0x01, 0x20, 0x87, 0xe3, 0x25, 0x8f, 0x7d, 0x9f, 0x66, 0x69, - 0xbd, 0x43, 0xad, 0x45, 0x65, 0x59, 0x86, 0xb5, 0xf7, 0x83, 0x56, 0x0c, 0x4b, 0x4d, 0xff, 0x74, 0x1c, 0xb8, 0x9d, - 0xed, 0x56, 0xc6, 0x2e, 0xe3, 0x59, 0x71, 0xf1, 0xcb, 0x69, 0xa1, 0x5c, 0xbb, 0x79, 0x1b, 0xbf, 0x69, 0xb5, 0x64, - 0x69, 0xad, 0x87, 0xbc, 0xb4, 0x2c, 0x22, 0x10, 0xc0, 0x70, 0xa4, 0xec, 0x62, 0x09, 0xf7, 0x08, 0xab, 0x7b, 0x10, - 0x4a, 0xe6, 0x85, 0x8b, 0xe7, 0x2c, 0x86, 0x44, 0x80, 0xed, 0x0e, 0x15, 0xdb, 0xc2, 0xc5, 0x73, 0xb6, 0xe1, 0x45, - 0xbf, 0x9f, 0xa9, 0x4e, 0x21, 0xeb, 0xce, 0x92, 0x6f, 0x54, 0x73, 0xac, 0xa1, 0x66, 0x6b, 0x93, 0x6c, 0x8d, 0x73, - 0x5b, 0xf1, 0x71, 0xd7, 0x56, 0x7c, 0xac, 0xac, 0x75, 0xe9, 0x5e, 0xef, 0x51, 0x5d, 0x00, 0x5b, 0xff, 0xed, 0xf1, - 0xca, 0xf5, 0x7c, 0x46, 0x00, 0x5f, 0x0b, 0x3e, 0x9e, 0x2c, 0xd0, 0xab, 0x64, 0xe1, 0xdf, 0x0e, 0xd4, 0xf8, 0x3b, - 0x9d, 0xbb, 0x00, 0xe8, 0x4a, 0xca, 0x2b, 0x20, 0xef, 0x20, 0xc7, 0xdc, 0xb2, 0x2b, 0xef, 0x4f, 0xbe, 0xc3, 0xde, - 0xf1, 0x7a, 0xb6, 0x98, 0xb3, 0x1d, 0x38, 0x15, 0x24, 0x03, 0x7b, 0x59, 0xb1, 0x5d, 0x10, 0xdb, 0x09, 0xbf, 0x11, - 0x30, 0xe5, 0x0b, 0x08, 0xe2, 0x0a, 0x6e, 0x21, 0x0e, 0x4f, 0xfe, 0x39, 0xb8, 0x6f, 0x6d, 0xd6, 0xf7, 0xcc, 0xea, - 0x9c, 0x60, 0xcd, 0xac, 0x1e, 0x0c, 0x96, 0xcd, 0x64, 0xd5, 0xef, 0x7b, 0x3b, 0xed, 0xf8, 0x74, 0x27, 0x75, 0x62, - 0xa7, 0xb5, 0x5a, 0x0b, 0xf6, 0x4e, 0x6a, 0x5d, 0x8c, 0xa1, 0x07, 0x88, 0x9f, 0x6e, 0x07, 0xfc, 0xbe, 0x63, 0x6d, - 0x79, 0xef, 0xd8, 0x82, 0xed, 0xe0, 0x12, 0xd4, 0xb4, 0x97, 0xfd, 0x49, 0xe5, 0x82, 0x76, 0xec, 0x92, 0x78, 0x38, - 0x63, 0x56, 0x29, 0x33, 0xeb, 0xa4, 0xba, 0x12, 0x9d, 0x31, 0x9d, 0xb5, 0x9e, 0xcf, 0xd5, 0x7c, 0x52, 0x68, 0x50, - 0xbf, 0x73, 0xe2, 0x23, 0x2a, 0x3a, 0x4f, 0x60, 0x6b, 0x59, 0x41, 0xac, 0xf6, 0x39, 0x58, 0x6b, 0xb5, 0x4b, 0xbf, - 0x97, 0x0f, 0xb8, 0x4d, 0x39, 0xac, 0x03, 0x83, 0x9a, 0x13, 0x2b, 0xea, 0x31, 0xdb, 0x31, 0x6e, 0x7e, 0x7a, 0xf9, - 0x83, 0x13, 0x96, 0xac, 0x58, 0xed, 0x4f, 0x7f, 0x79, 0xe6, 0xe9, 0xef, 0xd4, 0xfe, 0x85, 0xf0, 0x83, 0xf1, 0xbf, - 0x6b, 0xf7, 0xb5, 0x16, 0xa3, 0xb2, 0x55, 0x8e, 0xd0, 0xb8, 0x5b, 0x49, 0x93, 0xe5, 0x67, 0xe1, 0x09, 0x6b, 0xc1, - 0xb3, 0x5c, 0x2f, 0xd1, 0xac, 0x80, 0x15, 0xd6, 0x32, 0x09, 0x57, 0x18, 0xab, 0xa5, 0xad, 0xbe, 0x45, 0xd3, 0x1c, - 0x1f, 0xce, 0xb5, 0x41, 0x99, 0x72, 0x76, 0x46, 0xac, 0x86, 0xcb, 0xb0, 0x34, 0xa1, 0x08, 0xd9, 0xbd, 0x1d, 0xdc, - 0xd8, 0x29, 0x4b, 0x29, 0xc3, 0x39, 0x06, 0x13, 0x1e, 0x89, 0x51, 0x95, 0xef, 0xef, 0x4b, 0x8a, 0x9c, 0xb6, 0xe5, - 0xa0, 0x0a, 0x61, 0x1f, 0x49, 0x94, 0xc0, 0xad, 0x48, 0x0b, 0x45, 0xca, 0xe2, 0x6f, 0x07, 0xe8, 0x02, 0x2f, 0xa0, - 0xae, 0x46, 0xdd, 0xfe, 0x70, 0xc4, 0xc3, 0x47, 0xa6, 0x3e, 0x30, 0x62, 0x49, 0xa0, 0xb6, 0x17, 0x59, 0x7a, 0x07, - 0x2a, 0xfc, 0x1e, 0xae, 0x26, 0x62, 0x3f, 0xb7, 0xa4, 0xa8, 0xc8, 0x46, 0x7a, 0x43, 0x6b, 0xf0, 0x08, 0xad, 0x29, - 0x2f, 0x9d, 0x54, 0x9b, 0x74, 0xde, 0x11, 0x72, 0xac, 0xbe, 0xb5, 0x84, 0xd1, 0xae, 0xe8, 0xc5, 0xbd, 0xa3, 0xf7, - 0x3c, 0x5d, 0xf5, 0xdc, 0x9f, 0xb8, 0x62, 0x9e, 0xdc, 0x46, 0xa0, 0x6e, 0x05, 0xd5, 0xed, 0x83, 0x4a, 0xb0, 0x60, - 0x49, 0xbb, 0x8f, 0xdf, 0xce, 0xda, 0x81, 0xa8, 0x8c, 0x55, 0xfa, 0x96, 0x24, 0xec, 0x89, 0x41, 0xa7, 0x50, 0x95, - 0xdb, 0xdd, 0xd1, 0x16, 0xb8, 0x8e, 0x59, 0x8a, 0x5e, 0xd8, 0x22, 0x77, 0xcb, 0xbf, 0x7b, 0xae, 0xc8, 0xd9, 0x2f, - 0x01, 0xc1, 0xa9, 0xf9, 0x86, 0xf8, 0x72, 0x84, 0x47, 0xd5, 0x2d, 0x70, 0x9c, 0xbe, 0x03, 0xf8, 0x87, 0xc3, 0x25, - 0x68, 0x02, 0x62, 0xc1, 0x7a, 0x69, 0xdc, 0x63, 0xbd, 0xb8, 0xd8, 0xdc, 0x25, 0xf9, 0x06, 0x9c, 0x19, 0x28, 0xd5, - 0xd2, 0x0f, 0x1c, 0xab, 0x05, 0x54, 0x38, 0x98, 0x9d, 0xd4, 0x0b, 0xcb, 0xa8, 0xc7, 0xf4, 0xf9, 0x19, 0xec, 0x1d, - 0x21, 0x01, 0x70, 0xbf, 0xec, 0x03, 0x12, 0xf0, 0xd0, 0x99, 0x1d, 0x10, 0x4e, 0x98, 0x45, 0x55, 0x20, 0x91, 0x1c, - 0xe9, 0x67, 0x8f, 0x99, 0x48, 0xfe, 0x60, 0xd6, 0x73, 0x4e, 0x89, 0x1e, 0xeb, 0xa9, 0x23, 0xa4, 0xc7, 0x7a, 0xd6, - 0x11, 0xd1, 0x63, 0x3d, 0xeb, 0xf8, 0xe8, 0xb1, 0x9e, 0x39, 0x76, 0x7a, 0x10, 0x98, 0x00, 0x91, 0x07, 0xac, 0x47, - 0x93, 0xa9, 0xa7, 0xb8, 0x07, 0x88, 0x06, 0x81, 0xf5, 0xa4, 0x70, 0xde, 0x03, 0xe4, 0x31, 0x12, 0xab, 0x83, 0xde, - 0x7f, 0x8c, 0x9f, 0xf6, 0x8c, 0x8c, 0x3c, 0x6e, 0x1d, 0x56, 0xff, 0xeb, 0x3f, 0x21, 0x00, 0x0e, 0xcf, 0xa6, 0xde, - 0xe5, 0x18, 0xb2, 0xca, 0x32, 0x02, 0xc9, 0x4f, 0x0c, 0xbe, 0x7c, 0x01, 0x50, 0xf5, 0x99, 0xae, 0xd5, 0xe4, 0xa8, - 0x3d, 0xe6, 0xd0, 0x15, 0x03, 0xc0, 0x36, 0x2c, 0x51, 0x55, 0x0b, 0x9b, 0xb0, 0xb8, 0xfd, 0x0c, 0xa3, 0xb9, 0x6c, - 0x7a, 0x41, 0x03, 0xf5, 0x08, 0xc1, 0x2f, 0xad, 0x87, 0xd6, 0x5a, 0xa6, 0x1c, 0xba, 0x36, 0x8a, 0x2a, 0x1b, 0xea, - 0x12, 0x56, 0x6b, 0x11, 0xd5, 0x44, 0x91, 0x72, 0xc9, 0x28, 0x8a, 0xa5, 0x0a, 0xf6, 0x99, 0xb8, 0x83, 0xa8, 0x79, - 0xda, 0x6a, 0xab, 0x60, 0x7f, 0x07, 0x08, 0x6b, 0x61, 0x2d, 0xa4, 0x33, 0xa8, 0xbd, 0xd3, 0x8f, 0x94, 0xbf, 0xbc, - 0x90, 0xdb, 0xb9, 0x85, 0x22, 0xdc, 0x9e, 0x83, 0xf2, 0xa6, 0xae, 0x4a, 0x45, 0x34, 0x5a, 0x02, 0xa5, 0xcc, 0x09, - 0x22, 0x0b, 0x10, 0xc0, 0x71, 0x03, 0x81, 0xcf, 0x6b, 0x7c, 0x02, 0x8d, 0x42, 0x20, 0x3f, 0xb0, 0x0a, 0xd7, 0x1e, - 0xd2, 0x52, 0x6b, 0x44, 0x94, 0x88, 0x1f, 0x5d, 0x3d, 0xc7, 0xf6, 0xd5, 0xd3, 0x58, 0x5b, 0x4a, 0x13, 0xc4, 0x4f, - 0x2c, 0xb6, 0x10, 0x13, 0x44, 0x75, 0x88, 0x8e, 0x60, 0x39, 0x21, 0x44, 0xe1, 0x0f, 0xa1, 0x9f, 0x1a, 0xf8, 0x4b, - 0xb6, 0x2c, 0xf2, 0x9a, 0x60, 0x31, 0x2b, 0x06, 0x68, 0x55, 0x04, 0x9e, 0xe9, 0x6c, 0xa9, 0xcc, 0x69, 0x1e, 0x1d, - 0xd9, 0xc1, 0x79, 0xd7, 0xc1, 0x5e, 0xfa, 0x32, 0x76, 0xb2, 0x6c, 0x1a, 0xb5, 0xb1, 0x21, 0x12, 0x5e, 0x91, 0xbf, - 0xca, 0x52, 0xe3, 0x1c, 0x99, 0xcb, 0xf5, 0x5d, 0x17, 0x77, 0x77, 0xb4, 0x4d, 0x58, 0x85, 0x08, 0x75, 0xdb, 0x50, - 0xb9, 0x14, 0x66, 0x63, 0xd3, 0x34, 0xc0, 0x17, 0x8a, 0x4a, 0xa5, 0x2a, 0xb5, 0x95, 0x4a, 0x4e, 0x78, 0xd7, 0x37, - 0xb5, 0x48, 0x5d, 0x11, 0x6c, 0x63, 0x86, 0x7a, 0x28, 0x37, 0x6a, 0xec, 0xdb, 0x8e, 0x55, 0x7a, 0x87, 0x09, 0x72, - 0x46, 0x5e, 0xe4, 0xe0, 0xa2, 0xa4, 0x20, 0x73, 0x35, 0x84, 0xf9, 0xa3, 0x86, 0x4f, 0x0b, 0xcb, 0x3d, 0x94, 0x80, - 0xd9, 0x51, 0xc3, 0xcb, 0x08, 0x81, 0x88, 0x4b, 0x65, 0x5f, 0x31, 0xf1, 0x7b, 0x0a, 0x66, 0xc9, 0x84, 0xee, 0x45, - 0x2c, 0x8c, 0xd0, 0xc6, 0x27, 0x49, 0x32, 0xf5, 0x34, 0x05, 0x37, 0x72, 0x19, 0xe6, 0x68, 0x84, 0x96, 0x7c, 0xe4, - 0x40, 0xfa, 0x5a, 0x4e, 0x25, 0xf8, 0x88, 0x3a, 0x05, 0x1c, 0xcf, 0xcf, 0x0b, 0xeb, 0x27, 0xcb, 0x25, 0xe6, 0xb2, - 0x36, 0xff, 0x65, 0x47, 0xc7, 0x60, 0x97, 0xa7, 0x89, 0xe3, 0xea, 0x3f, 0xaa, 0x92, 0xe2, 0xe1, 0xe7, 0x34, 0x07, - 0x14, 0xc1, 0xcc, 0x9e, 0x62, 0x7c, 0xec, 0xb3, 0x4c, 0x01, 0x7f, 0xbb, 0xde, 0x5a, 0x32, 0xb1, 0x4b, 0xda, 0xcd, - 0x95, 0xf1, 0x4b, 0x6d, 0xd8, 0x71, 0x70, 0x6e, 0x00, 0x8a, 0xb3, 0x46, 0x87, 0xe5, 0xb5, 0x6e, 0x5b, 0x15, 0x2a, - 0x50, 0xeb, 0x7f, 0xef, 0x16, 0xa6, 0xbc, 0xcd, 0x4b, 0xe5, 0x6d, 0x1e, 0x9a, 0x00, 0x81, 0xc8, 0x0c, 0x79, 0xd6, - 0x74, 0x4c, 0x12, 0xf7, 0x8e, 0x94, 0xb4, 0xef, 0x48, 0xf1, 0xa3, 0x77, 0x24, 0xe4, 0x5b, 0x42, 0x47, 0xf6, 0x25, - 0x27, 0x27, 0x50, 0x66, 0xb0, 0x97, 0xd7, 0x4c, 0xf6, 0x0f, 0x68, 0x2f, 0x9c, 0xcb, 0xf2, 0x8a, 0xbf, 0x13, 0xde, - 0xda, 0x9f, 0xae, 0x4f, 0xbb, 0xaa, 0xde, 0x7e, 0x63, 0x66, 0x1e, 0x0e, 0xc5, 0xe1, 0x50, 0x99, 0xa0, 0xdd, 0x05, - 0x17, 0x83, 0x9c, 0xdd, 0xbb, 0xf1, 0xf1, 0xef, 0x38, 0x8a, 0xd8, 0x4a, 0x79, 0x24, 0x5d, 0xa8, 0xc4, 0xf0, 0xd2, - 0xc0, 0xc3, 0xec, 0xf8, 0x78, 0xb2, 0xbb, 0xba, 0x9f, 0x0c, 0x06, 0x3b, 0xd5, 0xb7, 0x5b, 0x5e, 0xcf, 0x76, 0x73, - 0xf6, 0xc0, 0x6f, 0xa7, 0xdb, 0x60, 0xdf, 0xc0, 0xb6, 0xbb, 0xbb, 0x12, 0x87, 0xc3, 0xee, 0x9a, 0x2f, 0xfc, 0xfd, - 0x03, 0x02, 0x3a, 0xf3, 0xf3, 0x71, 0x1b, 0xe3, 0xe7, 0xa6, 0xed, 0xaa, 0xb5, 0x03, 0x78, 0xfa, 0x1f, 0xbd, 0x9b, - 0xd9, 0x72, 0xee, 0xb3, 0x27, 0xfc, 0x01, 0xfc, 0xf3, 0x71, 0x93, 0x44, 0xea, 0x13, 0xed, 0x32, 0x79, 0x03, 0x0e, - 0xe4, 0x3b, 0x9f, 0xbd, 0xe5, 0x0f, 0xb3, 0xe5, 0x9c, 0x17, 0x87, 0xc3, 0x87, 0x69, 0x88, 0x64, 0x4d, 0x61, 0x45, - 0x2c, 0x29, 0x9e, 0x1f, 0x84, 0xc7, 0xef, 0x45, 0x64, 0x88, 0xb4, 0xdc, 0xbb, 0x43, 0x76, 0xc3, 0x22, 0x3f, 0x80, - 0x0f, 0xb2, 0x9d, 0x3f, 0x91, 0x35, 0xa5, 0xfb, 0xc5, 0x13, 0xff, 0x70, 0xa0, 0xbf, 0xde, 0xfa, 0x87, 0xc3, 0x07, - 0xf6, 0x80, 0xe0, 0xe8, 0x7c, 0x07, 0xfd, 0xa3, 0x6f, 0x1d, 0x50, 0x95, 0xe1, 0xbb, 0xd9, 0x66, 0xee, 0x5f, 0xaf, - 0xd8, 0x1d, 0x70, 0xa1, 0x28, 0x2f, 0xb4, 0x1b, 0xf6, 0x80, 0x5e, 0x67, 0xe4, 0x44, 0x34, 0xdb, 0xcd, 0x7d, 0x16, - 0xe3, 0x73, 0x75, 0x5f, 0x4c, 0xbe, 0x79, 0x5f, 0xdc, 0xb1, 0x6d, 0xf7, 0x7d, 0x51, 0xbe, 0xe9, 0xae, 0x9f, 0x2d, - 0xdb, 0xb1, 0x07, 0x98, 0x61, 0xef, 0xf8, 0x4d, 0x73, 0xec, 0x18, 0xfb, 0xcd, 0x1b, 0x23, 0x80, 0x32, 0x5b, 0xb0, - 0x58, 0x70, 0x50, 0xaa, 0x55, 0xdb, 0x92, 0xc8, 0x2b, 0x1d, 0xa8, 0x36, 0x23, 0xb8, 0xaf, 0x16, 0x72, 0xe6, 0x99, - 0x81, 0xbe, 0xad, 0x10, 0x2d, 0x1c, 0x36, 0xe0, 0x6f, 0xb4, 0x75, 0x8c, 0x61, 0x9a, 0xd5, 0x4c, 0xdb, 0xa2, 0x2e, - 0xbf, 0xef, 0x3d, 0x93, 0xdf, 0xc8, 0xc0, 0x16, 0x22, 0x29, 0x1c, 0xc7, 0x17, 0xcf, 0x4f, 0xf8, 0xaf, 0x5a, 0x1e, - 0xb5, 0xda, 0x2f, 0x94, 0xfa, 0xf4, 0x15, 0x1d, 0xd1, 0xc4, 0xbd, 0x68, 0xcb, 0xb0, 0x46, 0x59, 0x53, 0x4b, 0x87, - 0x61, 0x5c, 0xc3, 0xbe, 0x3c, 0x70, 0xe8, 0x3b, 0x20, 0xd0, 0x56, 0xa9, 0x14, 0x68, 0xe1, 0x18, 0x46, 0x61, 0x16, - 0x52, 0x1e, 0x17, 0x66, 0x29, 0xef, 0xb1, 0x40, 0x8b, 0x5b, 0x75, 0x8f, 0xa9, 0xed, 0x16, 0x44, 0x58, 0xbd, 0x65, - 0x9c, 0x5f, 0x36, 0xaa, 0x70, 0x5b, 0x80, 0xa2, 0x08, 0xca, 0x60, 0x4f, 0x72, 0xdb, 0x42, 0x49, 0xb3, 0x51, 0x58, - 0x8b, 0xbb, 0xa2, 0xdc, 0xf5, 0x1a, 0xb6, 0xc0, 0x0b, 0xaa, 0x7e, 0x42, 0xd8, 0x96, 0x3d, 0xeb, 0x50, 0x2e, 0xd2, - 0x7f, 0xcb, 0xd2, 0xf3, 0xfd, 0xd6, 0x9c, 0xff, 0xe9, 0x2b, 0xfa, 0xa8, 0xfc, 0xf7, 0x2f, 0xe9, 0x27, 0x83, 0x65, - 0xe4, 0x94, 0xfa, 0x25, 0x1a, 0xdd, 0xa6, 0x39, 0x61, 0x6c, 0xf9, 0xfa, 0xe9, 0x77, 0xc8, 0x14, 0x24, 0x87, 0x52, - 0xaa, 0x72, 0xb2, 0x87, 0xbe, 0xf0, 0xba, 0x0f, 0x33, 0xc1, 0x00, 0x84, 0xd7, 0x68, 0x53, 0x4d, 0x98, 0xc4, 0xa3, - 0x2b, 0xf8, 0xbf, 0x11, 0xc4, 0xa0, 0x7d, 0xa2, 0xa8, 0x63, 0xdb, 0x48, 0xd7, 0x6d, 0xe7, 0x20, 0xb9, 0x53, 0x57, - 0xfe, 0xa8, 0x9c, 0xfc, 0x3b, 0x1a, 0x22, 0xaf, 0xb8, 0x42, 0xac, 0x2c, 0xb8, 0xc4, 0x62, 0xa8, 0x48, 0x01, 0xae, - 0x21, 0x88, 0x94, 0x45, 0x49, 0xe1, 0x96, 0x83, 0xaa, 0x08, 0xc0, 0xb8, 0x5a, 0x1d, 0x75, 0x22, 0x7c, 0xdc, 0x5a, - 0x8b, 0x10, 0xac, 0x68, 0xd4, 0xca, 0x5a, 0x81, 0x2f, 0x48, 0x5f, 0x3a, 0x14, 0xc4, 0xf4, 0x28, 0xa4, 0xaa, 0x74, - 0x28, 0x90, 0xe6, 0x50, 0xf1, 0x8d, 0xc1, 0x46, 0x51, 0x91, 0x9e, 0xbf, 0x34, 0x29, 0xb9, 0x34, 0x66, 0x7c, 0x14, - 0x65, 0x24, 0xf2, 0x3a, 0xbc, 0x13, 0xd3, 0x02, 0xf9, 0x46, 0x8f, 0x1f, 0x04, 0x97, 0xf0, 0x6e, 0xc8, 0xbd, 0x02, - 0x6c, 0x09, 0xd8, 0x01, 0xee, 0x95, 0x19, 0xe5, 0x3a, 0xad, 0xeb, 0xb7, 0xd6, 0x43, 0x31, 0x0c, 0x9f, 0x59, 0x02, - 0xdb, 0xd1, 0x3a, 0x3a, 0xd2, 0xc3, 0x87, 0xff, 0x75, 0x55, 0x73, 0xd4, 0xa9, 0x5c, 0xce, 0x8e, 0x27, 0x2c, 0x45, - 0xcc, 0xa0, 0xfb, 0xeb, 0xf6, 0x95, 0x00, 0xba, 0x5d, 0x16, 0xf3, 0x6c, 0xb4, 0x93, 0x7f, 0x4b, 0x37, 0x56, 0x94, - 0x36, 0xf1, 0x2e, 0xeb, 0x8d, 0xfd, 0xe1, 0xe8, 0x3f, 0x9e, 0xbd, 0x9f, 0x10, 0xaa, 0xce, 0x86, 0xad, 0x75, 0x9c, - 0xcb, 0xff, 0xfa, 0xcf, 0x31, 0x59, 0x41, 0x50, 0x10, 0x96, 0x9d, 0x62, 0xa2, 0x82, 0x51, 0xa4, 0x58, 0xf3, 0xf1, - 0x64, 0x8d, 0x3a, 0xe1, 0xb5, 0xbf, 0xd4, 0x3a, 0x61, 0x62, 0x64, 0xa5, 0xf2, 0xd7, 0xac, 0x62, 0x77, 0x2a, 0xb3, - 0x80, 0xcc, 0x83, 0x7c, 0xb2, 0x36, 0x1a, 0xcc, 0x15, 0xaf, 0x67, 0xeb, 0xb9, 0x54, 0x3e, 0x83, 0x29, 0x67, 0x39, - 0x38, 0x59, 0x0a, 0xbb, 0x27, 0x81, 0xa2, 0x35, 0x43, 0xd7, 0xfe, 0x14, 0x5b, 0xf5, 0x3a, 0xad, 0x6a, 0x80, 0x07, - 0x84, 0x18, 0x18, 0x6a, 0xaf, 0x16, 0x1e, 0x5a, 0x0b, 0x60, 0xed, 0x8f, 0x4a, 0x3f, 0x18, 0x4f, 0x96, 0x7c, 0x81, - 0xfc, 0xcb, 0x91, 0xa3, 0x76, 0xef, 0xf7, 0xbd, 0x7b, 0x90, 0x82, 0x23, 0xd7, 0x42, 0x81, 0x44, 0x40, 0x0b, 0xbe, - 0xf1, 0x95, 0x0f, 0xc6, 0x3b, 0xd4, 0x56, 0x83, 0x82, 0xda, 0xd1, 0x2d, 0x8f, 0x1d, 0xbd, 0xf3, 0xfd, 0x09, 0x7d, - 0xf5, 0x42, 0x0b, 0xc7, 0xdf, 0x38, 0x23, 0xd7, 0x6c, 0xd5, 0x21, 0x47, 0x34, 0x93, 0x0e, 0x21, 0x62, 0xc5, 0xd6, - 0xec, 0x1d, 0xa9, 0x9c, 0x3b, 0x87, 0xec, 0xf4, 0x11, 0xaa, 0xf4, 0x5a, 0x8f, 0x6f, 0x27, 0x4a, 0x77, 0x7b, 0xbc, - 0x9b, 0x7c, 0xcf, 0x26, 0x22, 0x06, 0x03, 0xda, 0x20, 0x9c, 0x91, 0x75, 0x88, 0x54, 0x3a, 0x40, 0x08, 0x1c, 0x13, - 0xd0, 0xf4, 0x5f, 0xdf, 0x92, 0x28, 0xe0, 0x48, 0x1b, 0x21, 0x6b, 0xd9, 0xe1, 0x90, 0x83, 0x46, 0xb9, 0xf9, 0xc3, - 0x2b, 0xd4, 0x69, 0x0e, 0xcc, 0xd3, 0x25, 0xec, 0x39, 0x78, 0xa4, 0x17, 0xc7, 0x47, 0xfa, 0x7f, 0x47, 0x13, 0x35, - 0xfe, 0xf7, 0x35, 0x51, 0x4a, 0x8b, 0xe4, 0xa8, 0x96, 0xbe, 0x4b, 0x1d, 0x05, 0x17, 0x79, 0x47, 0x2d, 0x64, 0xcf, - 0xb2, 0x71, 0xa3, 0x9a, 0xf7, 0xff, 0x6b, 0x65, 0xfe, 0xbf, 0xa6, 0x95, 0x61, 0x4a, 0x76, 0x2c, 0xd5, 0xcc, 0x03, - 0xad, 0x62, 0x98, 0xfd, 0x4c, 0x12, 0x22, 0xc3, 0xa5, 0x01, 0x3f, 0xaa, 0x60, 0x1f, 0xa7, 0xd5, 0x3a, 0x0b, 0x77, - 0xa8, 0x44, 0xbd, 0x15, 0x77, 0x69, 0xfe, 0xa2, 0xfe, 0x97, 0x28, 0x0b, 0x98, 0xda, 0x77, 0x65, 0x1a, 0x07, 0x64, - 0xe1, 0xcf, 0xc2, 0x12, 0x27, 0x37, 0xb6, 0xf1, 0x67, 0x39, 0x9e, 0xf6, 0xab, 0xce, 0xcc, 0x03, 0x09, 0xd4, 0x40, - 0xfc, 0x91, 0x73, 0x59, 0x59, 0x3c, 0x20, 0x74, 0xf3, 0x8f, 0x65, 0x59, 0x94, 0x5e, 0xef, 0x73, 0x92, 0x56, 0x67, - 0x2b, 0x51, 0x27, 0x45, 0xac, 0xa0, 0x6c, 0x52, 0x80, 0xd1, 0x87, 0x95, 0x27, 0xe2, 0xe0, 0x0c, 0x81, 0x1a, 0xce, - 0xea, 0x24, 0x04, 0xa0, 0x61, 0x85, 0xb0, 0x7f, 0x06, 0x2d, 0x3c, 0x0b, 0xe3, 0x70, 0x0d, 0x30, 0x39, 0x69, 0x75, - 0xb6, 0x2e, 0x8b, 0xfb, 0x34, 0x16, 0xf1, 0xa8, 0xa7, 0x28, 0x59, 0xde, 0xe4, 0xae, 0x9c, 0xeb, 0xef, 0xff, 0xa0, - 0x00, 0x76, 0x03, 0x66, 0xdb, 0x02, 0x3b, 0x00, 0x48, 0x50, 0x20, 0x5b, 0xa8, 0xd3, 0xe8, 0x4c, 0x2d, 0x15, 0x78, - 0xcf, 0xf5, 0x00, 0x7f, 0x93, 0x03, 0x96, 0x71, 0x5d, 0xc8, 0x80, 0x11, 0x04, 0x30, 0x02, 0x07, 0x25, 0x60, 0xe8, - 0x0c, 0x71, 0x5b, 0x95, 0xb3, 0x16, 0x9a, 0x2b, 0xdd, 0x96, 0xdc, 0x34, 0xca, 0xd9, 0x4a, 0x04, 0xd0, 0x57, 0x37, - 0x25, 0x4e, 0x97, 0xcb, 0x56, 0x12, 0xf6, 0xed, 0x87, 0x76, 0xaa, 0xc8, 0xe3, 0xa3, 0x34, 0xe4, 0x15, 0x78, 0x92, - 0x71, 0x24, 0x89, 0x12, 0xc1, 0x9b, 0xbc, 0x31, 0xe3, 0xf0, 0xa2, 0x4d, 0x39, 0xb5, 0x37, 0xeb, 0x05, 0xe0, 0x3c, - 0x41, 0x5b, 0x06, 0x18, 0x0b, 0x18, 0x9c, 0x0b, 0xb1, 0xe4, 0x29, 0x82, 0x5f, 0x3a, 0x91, 0xc2, 0xb8, 0xcb, 0x61, - 0x98, 0x07, 0x45, 0xef, 0x92, 0xfa, 0xa3, 0xdf, 0x47, 0x6d, 0x32, 0x18, 0x82, 0x4a, 0x00, 0x95, 0x75, 0x83, 0xc4, - 0xc0, 0xaa, 0xb4, 0x90, 0xb8, 0x84, 0x78, 0x99, 0xaf, 0xa6, 0x75, 0x14, 0x7c, 0xa8, 0x27, 0x84, 0x70, 0x82, 0xf1, - 0x21, 0x6e, 0x80, 0x80, 0xc1, 0x2a, 0x2e, 0x30, 0x48, 0x9e, 0x4b, 0x74, 0x7f, 0x3c, 0xdf, 0x31, 0xc0, 0x95, 0xf3, - 0x9e, 0x6a, 0x57, 0x0f, 0xec, 0xe5, 0x2a, 0x5d, 0x32, 0x42, 0x58, 0xf1, 0x7f, 0x11, 0x79, 0xdf, 0x0e, 0x13, 0x50, - 0xdb, 0xc8, 0x1f, 0x83, 0xc4, 0x5c, 0x26, 0x8a, 0x20, 0x1e, 0x65, 0x05, 0x4b, 0xd2, 0x60, 0x33, 0x4a, 0x52, 0xd0, - 0x68, 0x62, 0x0c, 0x99, 0x0a, 0xed, 0x90, 0x34, 0x9a, 0x8d, 0xc9, 0x3e, 0x86, 0xbc, 0x86, 0x8b, 0xc5, 0x02, 0xef, - 0xfb, 0x59, 0xa8, 0x0e, 0xb6, 0xa5, 0x39, 0x04, 0x9c, 0x24, 0xd8, 0x53, 0x57, 0xa4, 0x24, 0xcc, 0x46, 0x9f, 0x42, - 0xce, 0x0d, 0xe8, 0x38, 0x69, 0x0c, 0xd5, 0x07, 0x26, 0xe1, 0x55, 0x84, 0x4e, 0xca, 0x0a, 0x61, 0x01, 0xf7, 0x8d, - 0x8c, 0x46, 0x2b, 0x69, 0x10, 0x78, 0x9b, 0x61, 0x2b, 0xb0, 0x09, 0x0d, 0x7f, 0x91, 0x79, 0x98, 0x56, 0xb3, 0x12, - 0xcc, 0xf9, 0x06, 0x2a, 0x31, 0x9e, 0x2c, 0xaf, 0xf8, 0xc6, 0xc5, 0x4a, 0x4c, 0x66, 0xcb, 0xf9, 0x64, 0x2d, 0xa9, - 0xe6, 0x72, 0x6f, 0xcd, 0x32, 0xb6, 0x84, 0xfd, 0xc3, 0xc0, 0x50, 0x3a, 0xb0, 0xa3, 0xa9, 0xa6, 0x4d, 0x02, 0x4c, - 0xa6, 0x73, 0xce, 0x87, 0x97, 0x88, 0x26, 0xab, 0x53, 0x77, 0x32, 0x55, 0xed, 0xe0, 0x9a, 0x9c, 0xc9, 0xe9, 0x91, - 0x7a, 0xaa, 0x75, 0x2f, 0xf9, 0x68, 0x3b, 0xac, 0x46, 0x5b, 0x3f, 0x00, 0xb7, 0x4e, 0x61, 0xa7, 0xef, 0x86, 0xd5, - 0x68, 0xe7, 0x6b, 0xd8, 0x5d, 0x52, 0x08, 0x54, 0x7f, 0x96, 0x35, 0x99, 0x8b, 0xd7, 0xc5, 0x83, 0x57, 0xb0, 0xe7, - 0xfe, 0x40, 0xff, 0x2a, 0xd9, 0x73, 0xdf, 0x66, 0x72, 0xfd, 0x33, 0xed, 0x1a, 0x8d, 0x99, 0x8e, 0xd7, 0xae, 0xc0, - 0x0a, 0x0d, 0x90, 0x5f, 0xb0, 0xa3, 0xbd, 0xcd, 0x41, 0x20, 0x40, 0xf7, 0x12, 0x1c, 0x45, 0x01, 0x51, 0xd3, 0xaa, - 0xf2, 0xe8, 0x74, 0xef, 0xef, 0xf1, 0x8d, 0x10, 0xb0, 0xc9, 0x53, 0xeb, 0xde, 0x32, 0xf6, 0x0f, 0x07, 0x08, 0xa1, - 0x97, 0xd3, 0x6f, 0xb4, 0x65, 0xf5, 0x68, 0xc7, 0x72, 0xdf, 0x30, 0xea, 0x29, 0x18, 0xc3, 0xd0, 0x85, 0x55, 0x8c, - 0xe4, 0x19, 0x90, 0x35, 0x7e, 0x83, 0xe8, 0x02, 0x16, 0xbd, 0xde, 0xeb, 0x23, 0x1a, 0x44, 0x40, 0xa5, 0xd7, 0xfc, - 0xa5, 0xc8, 0xe7, 0xaa, 0x10, 0xbd, 0xf7, 0xd6, 0xce, 0x9b, 0x19, 0xc9, 0x32, 0x69, 0xa4, 0xda, 0xad, 0x2c, 0xd6, - 0x95, 0x37, 0x3b, 0x21, 0x5d, 0xcc, 0x31, 0x54, 0x06, 0x8f, 0x03, 0x50, 0x7a, 0xfe, 0x25, 0xf4, 0x4a, 0x86, 0x4c, - 0xb3, 0x44, 0x33, 0xbb, 0x6b, 0xfc, 0xc9, 0x2a, 0xf5, 0x62, 0x44, 0xcc, 0x06, 0xb6, 0x10, 0xb7, 0x45, 0xa5, 0xdb, - 0xa2, 0x50, 0xb6, 0x28, 0xd2, 0x87, 0xda, 0x99, 0xee, 0xcc, 0xc2, 0x67, 0x95, 0x69, 0xdf, 0xdb, 0xcc, 0x8c, 0x0d, - 0xd0, 0x76, 0x11, 0xbe, 0x81, 0x0e, 0x54, 0x08, 0xf9, 0x8f, 0x88, 0x88, 0x44, 0xc0, 0x2e, 0xa7, 0xee, 0xc4, 0xa6, - 0x43, 0x32, 0x0f, 0x31, 0x2b, 0xd4, 0x28, 0x2f, 0x79, 0x72, 0x34, 0x20, 0x15, 0xa1, 0x6e, 0xf7, 0xfb, 0xe7, 0x4b, - 0x17, 0xd4, 0x7e, 0x4d, 0xb1, 0x63, 0x74, 0x53, 0xc0, 0xb9, 0xe0, 0x51, 0xde, 0x73, 0xef, 0x1c, 0xd0, 0x1c, 0xdb, - 0x53, 0x64, 0x0d, 0x38, 0xbd, 0xed, 0x42, 0x80, 0xed, 0xb3, 0x66, 0x6b, 0x7f, 0xb2, 0xba, 0x8a, 0xa6, 0x5e, 0xc9, - 0x67, 0xba, 0x8b, 0x12, 0xb7, 0x8b, 0x62, 0xd9, 0x45, 0x9b, 0x06, 0x82, 0x1d, 0x57, 0x7e, 0x00, 0xbc, 0xa1, 0x51, - 0xbf, 0x5f, 0xb6, 0x7a, 0xf6, 0xe4, 0x6b, 0xc7, 0x3d, 0x9b, 0xf9, 0xac, 0x34, 0x3d, 0xfb, 0x6b, 0xea, 0xf6, 0xac, - 0x9c, 0xec, 0x45, 0xe7, 0x64, 0x9f, 0xce, 0xe6, 0x81, 0xe0, 0x72, 0xe7, 0x3e, 0xcf, 0xa7, 0x7a, 0xda, 0x55, 0x7e, - 0xd0, 0x1a, 0x22, 0xf3, 0x85, 0xcf, 0x55, 0xf7, 0xba, 0x82, 0x05, 0x2c, 0xc1, 0xdd, 0x7a, 0x69, 0xfe, 0x2b, 0x76, - 0x7f, 0x2f, 0xe8, 0xa5, 0xf9, 0x6f, 0xf4, 0x27, 0x05, 0x70, 0x00, 0x1a, 0x53, 0xbb, 0x05, 0x1e, 0x62, 0xa8, 0xa0, - 0x70, 0x37, 0x2b, 0xe7, 0x5e, 0x0d, 0x70, 0x98, 0xa4, 0x6f, 0x68, 0xf5, 0x4a, 0x8b, 0x5d, 0x2f, 0x93, 0xbd, 0x02, - 0x3c, 0x54, 0x21, 0x0f, 0x0f, 0x87, 0xa8, 0x63, 0xd8, 0x41, 0x1d, 0x01, 0xc3, 0x1e, 0x42, 0x63, 0x0b, 0x3c, 0x1f, - 0x3f, 0x67, 0x7c, 0x2f, 0x40, 0x6d, 0x84, 0xf0, 0x78, 0xb5, 0x28, 0x43, 0x6c, 0xd9, 0x5b, 0xa4, 0x92, 0xfa, 0x59, - 0x20, 0xca, 0x68, 0x15, 0xd0, 0x56, 0x7b, 0xcc, 0xd2, 0x78, 0x03, 0xa1, 0x62, 0xa9, 0x8f, 0x21, 0x34, 0x70, 0xf8, - 0x1d, 0x0e, 0x20, 0xc1, 0x97, 0x5c, 0x93, 0xcd, 0xbd, 0xcd, 0xef, 0x69, 0x9f, 0x3f, 0x1c, 0xce, 0x2f, 0x11, 0x94, - 0x2e, 0x85, 0x8f, 0x54, 0x22, 0xaa, 0xa7, 0xb8, 0x29, 0x21, 0x9b, 0x25, 0x2b, 0xfd, 0xe0, 0x57, 0xf5, 0x0b, 0x00, - 0x64, 0x21, 0xd0, 0x26, 0x32, 0xfb, 0xd3, 0x99, 0x8a, 0x2e, 0x00, 0x0e, 0xf1, 0xc7, 0x4f, 0x10, 0x7d, 0x43, 0xcb, - 0xb4, 0x7c, 0x9c, 0xf0, 0x10, 0xb4, 0xb6, 0xa4, 0x93, 0x88, 0x95, 0x02, 0x1b, 0x22, 0xe1, 0xfb, 0xfd, 0xf3, 0x58, - 0xd2, 0x81, 0x46, 0xad, 0xee, 0x8d, 0x5b, 0xdd, 0x2b, 0x5f, 0xd7, 0x9d, 0xdc, 0xf8, 0xa0, 0x68, 0x9f, 0xcd, 0x1b, - 0x95, 0xef, 0xfb, 0x3a, 0x67, 0x77, 0xba, 0x77, 0xe4, 0x9c, 0xf8, 0xfe, 0x1e, 0x42, 0xd1, 0x43, 0x53, 0x64, 0x59, - 0x12, 0x06, 0xb4, 0xd6, 0xae, 0x3d, 0xcb, 0xe8, 0xe0, 0xb5, 0x6f, 0x08, 0x11, 0x79, 0x8a, 0x4f, 0x42, 0x6e, 0x71, - 0x7c, 0x50, 0xa0, 0x7f, 0x66, 0xfc, 0x99, 0x13, 0x3f, 0x6c, 0xf5, 0x0b, 0xe0, 0xdc, 0x74, 0xef, 0xdd, 0x89, 0x59, - 0x8f, 0xa1, 0x94, 0x8d, 0xff, 0xfb, 0x7d, 0x22, 0x0b, 0x74, 0x3a, 0xa2, 0x61, 0x20, 0xb8, 0x8b, 0xea, 0xff, 0x5e, - 0xf1, 0xba, 0x67, 0xad, 0xce, 0x97, 0x9f, 0x3a, 0x3d, 0xe9, 0xd5, 0xcb, 0xb8, 0x07, 0x54, 0xe8, 0x00, 0xe1, 0xbc, - 0xee, 0x37, 0x6c, 0xf7, 0xdd, 0x2f, 0xef, 0x8e, 0x5e, 0x06, 0x36, 0x29, 0x12, 0xdb, 0x4a, 0x3e, 0xeb, 0x81, 0xc2, - 0xaf, 0xc7, 0x7a, 0x75, 0xb1, 0xee, 0xb1, 0x1e, 0x6a, 0x01, 0xd1, 0xc3, 0x02, 0xd4, 0x7f, 0x3d, 0xfb, 0x34, 0x14, - 0x0e, 0xb2, 0x71, 0xaa, 0x40, 0x91, 0x05, 0xbf, 0x16, 0xa3, 0x75, 0x41, 0x80, 0xc8, 0x96, 0x90, 0x56, 0x9d, 0xcc, - 0x1e, 0x97, 0x5a, 0x92, 0xc1, 0x37, 0x01, 0x99, 0x1d, 0x58, 0x39, 0x41, 0xe9, 0xb8, 0x35, 0xe0, 0xca, 0x16, 0x8f, - 0x76, 0xfb, 0xd3, 0x20, 0x3b, 0x6b, 0x4e, 0x1a, 0xed, 0xc3, 0x3e, 0xcd, 0x03, 0x04, 0x22, 0x99, 0x8a, 0x20, 0xd7, - 0xdc, 0x5b, 0xd2, 0x47, 0x87, 0x73, 0x5e, 0xc8, 0x3f, 0xa7, 0x52, 0x87, 0x38, 0x94, 0x58, 0x03, 0x81, 0xca, 0x33, - 0x54, 0x39, 0x6c, 0x90, 0xe3, 0x9f, 0x1d, 0xc9, 0x4c, 0x62, 0xb2, 0xc8, 0xdd, 0x9a, 0xa9, 0xf0, 0x03, 0xc1, 0xc7, - 0x2c, 0xe7, 0xc0, 0x05, 0x36, 0x9b, 0xfb, 0x6a, 0x8a, 0x8b, 0x2b, 0xf0, 0xc7, 0x14, 0x7e, 0xc5, 0x53, 0xd8, 0x69, - 0xf7, 0xeb, 0xa2, 0x4a, 0x51, 0xb7, 0x51, 0x58, 0x54, 0xb2, 0x60, 0x5a, 0x43, 0x9a, 0xe8, 0x30, 0xfa, 0x83, 0x9c, - 0x81, 0x82, 0x90, 0x5f, 0x36, 0x0d, 0x30, 0x52, 0xc9, 0xe5, 0x41, 0x95, 0x04, 0x5e, 0x80, 0x6d, 0x50, 0xb1, 0x75, - 0x01, 0x41, 0xb6, 0x49, 0x51, 0xa6, 0x5f, 0x8b, 0xbc, 0x0e, 0xb3, 0xa0, 0x1a, 0xa5, 0xd5, 0x4f, 0xfa, 0x27, 0x30, - 0x6f, 0x53, 0x31, 0xaa, 0x55, 0x4c, 0x7e, 0xa3, 0xdf, 0x2f, 0x06, 0xad, 0x0f, 0x19, 0x7c, 0xf4, 0xda, 0x34, 0xf8, - 0x93, 0xd3, 0x60, 0x87, 0x89, 0x46, 0x00, 0x24, 0x73, 0x6a, 0xc9, 0x43, 0xd1, 0x1f, 0x41, 0x8e, 0x35, 0xaa, 0x9c, - 0x82, 0xc1, 0xfa, 0x8f, 0x47, 0x3b, 0x30, 0xf5, 0xe2, 0x68, 0x4b, 0x76, 0xd0, 0xca, 0x37, 0xc0, 0xfd, 0x1a, 0xd9, - 0x62, 0x96, 0x03, 0x34, 0x7b, 0x8d, 0xc8, 0xf8, 0xe4, 0x05, 0x30, 0x66, 0xeb, 0x2c, 0x8c, 0x44, 0x1c, 0x8c, 0x55, - 0x63, 0xc6, 0x0c, 0x0c, 0x5c, 0xa0, 0x6b, 0x99, 0x94, 0xa4, 0x21, 0x1d, 0x0c, 0x58, 0x29, 0x5b, 0x38, 0xe0, 0x45, - 0x73, 0xdc, 0x8e, 0x37, 0x2d, 0x1a, 0x0f, 0x6c, 0x17, 0xdb, 0xdf, 0xbf, 0x2c, 0xb6, 0xef, 0xc2, 0x2d, 0xe9, 0x15, - 0x72, 0x96, 0xd0, 0xcf, 0x9f, 0x64, 0x9f, 0x35, 0x9c, 0x9c, 0x0a, 0xcd, 0xd0, 0x52, 0x24, 0x94, 0xe2, 0x9d, 0x9e, - 0x14, 0x18, 0xcb, 0x58, 0xf8, 0x7b, 0xe0, 0x9c, 0x2e, 0x14, 0x91, 0x3b, 0x70, 0x1c, 0xdf, 0x40, 0x05, 0xa3, 0x86, - 0x83, 0x97, 0x31, 0x6c, 0x8b, 0x62, 0x16, 0x12, 0x4e, 0x21, 0x5c, 0xac, 0xb2, 0x7e, 0x5f, 0xfe, 0xa2, 0x2e, 0xba, - 0xc8, 0x64, 0xdd, 0x27, 0xe1, 0xc8, 0x8c, 0xe5, 0xd4, 0x0b, 0xc9, 0xf3, 0x9e, 0x27, 0xd3, 0xe4, 0x59, 0x1e, 0x44, - 0x00, 0xf9, 0x1c, 0xde, 0x87, 0x69, 0x06, 0x56, 0x69, 0x52, 0x7e, 0x84, 0xd2, 0x17, 0x9f, 0x57, 0x7e, 0xa0, 0xb3, - 0xe7, 0x26, 0x19, 0xde, 0xac, 0x5a, 0x6f, 0x52, 0xeb, 0xba, 0x78, 0xc0, 0xdf, 0x3b, 0x83, 0x8d, 0x73, 0x9d, 0x09, - 0x0e, 0xbc, 0x48, 0x6a, 0xbd, 0x66, 0xfc, 0x3a, 0xc3, 0x75, 0xa9, 0xda, 0xe8, 0xa3, 0x10, 0x9d, 0x43, 0xa6, 0x02, - 0x14, 0x8a, 0xb4, 0x7f, 0x50, 0x6a, 0x65, 0x52, 0x69, 0x23, 0x01, 0x74, 0x0f, 0x93, 0x06, 0x5b, 0x0c, 0x65, 0x2c, - 0x4d, 0xa2, 0xdc, 0x69, 0x10, 0x57, 0xf6, 0xe7, 0x4a, 0xe2, 0xd0, 0xb2, 0x48, 0xfe, 0xbd, 0xeb, 0xe9, 0x2b, 0xa4, - 0xee, 0x64, 0x81, 0xcc, 0x18, 0x2f, 0xf2, 0xf8, 0x33, 0x10, 0x66, 0x83, 0x36, 0x2a, 0x0a, 0x21, 0x64, 0x83, 0x18, - 0x34, 0x5e, 0xe4, 0xf1, 0x4b, 0x45, 0xe3, 0x21, 0x1f, 0x45, 0xbe, 0xfa, 0xab, 0xd4, 0x7f, 0x85, 0x3e, 0x33, 0xc1, - 0x23, 0x54, 0x13, 0xfd, 0xbb, 0xe7, 0xb3, 0x7b, 0x50, 0x1b, 0x46, 0x61, 0x66, 0xca, 0xaf, 0x7c, 0x53, 0x9c, 0xbd, - 0xfe, 0x8a, 0xae, 0xb2, 0xad, 0xfb, 0xd1, 0xa7, 0x23, 0x02, 0x6b, 0x63, 0x74, 0xc5, 0x8d, 0x01, 0xe4, 0x30, 0x79, - 0xbf, 0xa2, 0xb4, 0x1c, 0xd2, 0x20, 0x74, 0xd0, 0x10, 0xf4, 0x4a, 0xa2, 0x0f, 0x24, 0x16, 0x31, 0x86, 0x17, 0xe2, - 0x19, 0xa9, 0xc9, 0x44, 0x43, 0xbc, 0x22, 0xf6, 0x43, 0xb4, 0xe4, 0xd4, 0x44, 0x37, 0xc2, 0x14, 0x03, 0x89, 0x9d, - 0x41, 0x72, 0x92, 0xd4, 0xca, 0x2f, 0x9e, 0x49, 0xc2, 0x12, 0x3b, 0x0f, 0x31, 0x98, 0xd4, 0xd2, 0x9d, 0xde, 0x54, - 0xe9, 0xeb, 0x91, 0x96, 0x83, 0xf6, 0x01, 0xd8, 0xa5, 0xa4, 0xf7, 0x4f, 0x0a, 0x45, 0x7c, 0x0c, 0xe3, 0x18, 0xc2, - 0xb7, 0x88, 0xea, 0x0a, 0x9c, 0x6b, 0x05, 0x1a, 0xab, 0x81, 0x87, 0x66, 0x56, 0xcd, 0x87, 0x9c, 0x7e, 0x2a, 0x2d, - 0x7f, 0x8c, 0x68, 0x6c, 0xb4, 0x6e, 0x0e, 0x87, 0x3d, 0xad, 0x7a, 0xe9, 0x1c, 0x74, 0xd9, 0x4c, 0x62, 0xe2, 0x06, - 0xd2, 0xf5, 0xa3, 0xdf, 0x4c, 0xd8, 0x8b, 0xa8, 0x90, 0x4b, 0x21, 0x28, 0x68, 0x75, 0x20, 0x70, 0x28, 0xbc, 0x45, - 0x99, 0x2f, 0x62, 0xda, 0x40, 0x18, 0x7c, 0x7e, 0x20, 0x3f, 0xdf, 0x14, 0xa4, 0x62, 0xc7, 0xba, 0xf6, 0xfb, 0x9b, - 0xd2, 0x03, 0x3c, 0x39, 0x93, 0xe4, 0x69, 0x33, 0x84, 0x15, 0x01, 0x34, 0x66, 0x35, 0x59, 0x9c, 0x70, 0x65, 0x0e, - 0x3f, 0x55, 0x5e, 0xc9, 0x52, 0xa6, 0xce, 0x53, 0xbd, 0x00, 0xa2, 0x8e, 0x37, 0x68, 0x45, 0xea, 0x57, 0xe8, 0xec, - 0x35, 0x2b, 0x21, 0xe3, 0xe1, 0x39, 0xe7, 0xe9, 0xe8, 0x81, 0x25, 0x3c, 0xc2, 0xbf, 0x92, 0x89, 0x3e, 0xfc, 0x1e, - 0x38, 0xdc, 0x8c, 0x13, 0x1e, 0xb9, 0xcd, 0xde, 0x57, 0xe1, 0x0a, 0x6e, 0xa6, 0x05, 0x20, 0xb9, 0x05, 0x49, 0x13, - 0x50, 0x42, 0x22, 0x13, 0x32, 0x6b, 0x4a, 0x7e, 0x69, 0x69, 0x1b, 0xac, 0x61, 0xd2, 0x79, 0xc0, 0x8b, 0x56, 0x1f, - 0xad, 0x26, 0xda, 0x65, 0x96, 0xcf, 0x87, 0x38, 0x43, 0x35, 0xc7, 0xdd, 0x19, 0xfc, 0x1c, 0xf0, 0x8a, 0x55, 0x4d, - 0x3a, 0xda, 0x0d, 0xb8, 0xf0, 0xe4, 0x3a, 0x4f, 0x47, 0x5b, 0xfc, 0x25, 0xf7, 0x07, 0x80, 0x0e, 0xa6, 0x2e, 0x81, - 0x3f, 0x55, 0x5b, 0x4d, 0xa5, 0x5e, 0xb6, 0xf6, 0xeb, 0xba, 0xb3, 0x5a, 0xb9, 0x67, 0x5d, 0x86, 0xf6, 0xc8, 0x90, - 0x33, 0x66, 0xc0, 0x9f, 0x33, 0x96, 0xfc, 0x39, 0x63, 0xc5, 0x9f, 0x33, 0x6e, 0x8c, 0x0c, 0xa0, 0x04, 0xf7, 0x92, - 0x5f, 0xef, 0x11, 0x33, 0xc4, 0x6a, 0x50, 0x09, 0xac, 0x2c, 0xe5, 0xdc, 0x47, 0x4e, 0x31, 0xe5, 0x94, 0xe1, 0xa5, - 0xd3, 0x99, 0x3b, 0x90, 0xf3, 0x60, 0xe6, 0x0e, 0x93, 0xb3, 0x3e, 0xc5, 0xb1, 0x34, 0x26, 0x45, 0x05, 0xe9, 0x9c, - 0x0e, 0x37, 0xaf, 0x8e, 0xf3, 0x84, 0x65, 0x7c, 0xdc, 0x3e, 0x53, 0x20, 0xc4, 0x16, 0xcf, 0x90, 0x48, 0xa9, 0x9a, - 0xe5, 0x36, 0x7f, 0x38, 0xd4, 0xa3, 0x07, 0xbd, 0xd3, 0xc3, 0xaf, 0x84, 0xbd, 0xcc, 0x3c, 0xfb, 0x04, 0x01, 0x4c, - 0x12, 0x79, 0x26, 0xe1, 0xe8, 0xc7, 0x72, 0xf4, 0x37, 0x0d, 0xff, 0x9a, 0xa1, 0xba, 0x3b, 0x04, 0x26, 0xb6, 0xec, - 0xc0, 0x21, 0x38, 0x5d, 0x55, 0x22, 0x01, 0x07, 0x9b, 0x0d, 0x8b, 0xf4, 0x1e, 0x0f, 0x71, 0x3e, 0x28, 0x7c, 0x84, - 0x86, 0x19, 0xbd, 0xdf, 0xdf, 0x08, 0xaf, 0x92, 0xad, 0x3c, 0x1c, 0x12, 0xeb, 0x2e, 0xec, 0xe8, 0xe3, 0x68, 0x8f, - 0x12, 0x6a, 0x3f, 0xaa, 0xf5, 0xa6, 0x52, 0x0f, 0x72, 0xb3, 0x0b, 0x89, 0x41, 0xc5, 0x52, 0x7d, 0x7a, 0xa5, 0xfa, - 0x50, 0xb3, 0xce, 0xef, 0xea, 0xb8, 0x4f, 0xc5, 0x68, 0x2d, 0x27, 0x04, 0xb8, 0x0e, 0x12, 0x8d, 0x0e, 0x80, 0x71, - 0xb6, 0xd9, 0xf2, 0x52, 0x5b, 0x27, 0x4a, 0xc7, 0x71, 0xae, 0x8f, 0xe3, 0xc3, 0x41, 0x8a, 0x19, 0x97, 0x47, 0x62, - 0xc6, 0x65, 0x03, 0xf0, 0x66, 0x9d, 0x07, 0xf5, 0xe1, 0x70, 0x49, 0x97, 0x22, 0xd3, 0xd9, 0x46, 0xf9, 0x59, 0x8f, - 0x1e, 0x9e, 0x25, 0x68, 0xee, 0xad, 0xb0, 0xf7, 0x22, 0xd9, 0x9e, 0xc9, 0x3a, 0xf5, 0x32, 0xf2, 0xe9, 0x85, 0x7b, - 0x76, 0xc9, 0xd5, 0x0f, 0xab, 0xaf, 0xa7, 0xbf, 0x0a, 0x2f, 0x62, 0x15, 0xed, 0xd6, 0x25, 0x13, 0xf6, 0x96, 0x52, - 0x49, 0xab, 0xbc, 0x7c, 0xba, 0xf1, 0x03, 0xcc, 0x4c, 0x7b, 0xfa, 0x20, 0x1b, 0x51, 0xfd, 0x59, 0x89, 0x5a, 0x19, - 0x26, 0x0b, 0xe7, 0x25, 0x53, 0x4f, 0x06, 0x3c, 0x66, 0x25, 0x8f, 0x64, 0xa7, 0x37, 0x06, 0x41, 0x00, 0xeb, 0x9c, - 0xb4, 0xea, 0x8c, 0xa3, 0xd1, 0xaa, 0x72, 0x71, 0xba, 0xca, 0x05, 0x86, 0xdb, 0xad, 0xd9, 0x46, 0xd5, 0x59, 0x6e, - 0x6a, 0x95, 0xf2, 0x1d, 0xc0, 0xc7, 0xb2, 0xca, 0x05, 0x1d, 0x53, 0xa6, 0xce, 0x1b, 0x08, 0xc6, 0x56, 0x35, 0x2e, - 0x9c, 0x1a, 0x17, 0x3c, 0xa2, 0x76, 0x37, 0x4d, 0x3d, 0xda, 0x02, 0x4b, 0xe9, 0x68, 0xc7, 0x4b, 0x54, 0x29, 0xfc, - 0x4d, 0xf0, 0x7d, 0x18, 0xc7, 0x2f, 0x8b, 0xad, 0x3a, 0x10, 0x6f, 0x8b, 0x2d, 0xd2, 0xbe, 0xc8, 0xbf, 0x10, 0x07, - 0xbc, 0xd6, 0x35, 0xe5, 0xb5, 0x35, 0xa7, 0x81, 0xad, 0x61, 0xa4, 0xa4, 0x70, 0x6e, 0xfe, 0x3c, 0x1c, 0x68, 0x65, - 0xd7, 0xea, 0xae, 0x50, 0xeb, 0x31, 0x87, 0x0d, 0x7b, 0x91, 0x85, 0x3b, 0x51, 0x82, 0x23, 0x97, 0xfc, 0xeb, 0x70, - 0xd0, 0x2a, 0x4b, 0x75, 0xa4, 0xcf, 0xf6, 0x5f, 0x83, 0x31, 0x43, 0x97, 0x26, 0x60, 0xd9, 0x18, 0xc9, 0xbf, 0x9a, - 0x66, 0xde, 0x30, 0x59, 0x33, 0x85, 0xe3, 0xd0, 0x30, 0x42, 0x1a, 0xd0, 0x6d, 0x50, 0x1b, 0x9e, 0xcc, 0x37, 0x55, - 0xf9, 0xd5, 0x1d, 0xa9, 0xf6, 0x83, 0xe1, 0xe5, 0x44, 0x9c, 0xd3, 0x25, 0x49, 0x3d, 0x95, 0x50, 0x12, 0x82, 0x5d, - 0xfa, 0x40, 0x4e, 0xac, 0x80, 0xac, 0x65, 0x2c, 0xbf, 0xd5, 0x03, 0x42, 0xff, 0x69, 0xb7, 0x5e, 0xe8, 0x3f, 0x4d, - 0xb3, 0x85, 0xba, 0xfe, 0x30, 0xb9, 0xef, 0xe8, 0xf5, 0x07, 0x87, 0x77, 0xea, 0xaa, 0xe2, 0x2a, 0x1e, 0xd5, 0x86, - 0x49, 0x6e, 0x94, 0x85, 0xbb, 0x62, 0x53, 0xab, 0xe5, 0xe9, 0x38, 0x8c, 0xc0, 0x8c, 0xa0, 0x00, 0x59, 0xd7, 0x6d, - 0x44, 0x0c, 0x2b, 0xb9, 0x4c, 0xc8, 0x27, 0x04, 0x64, 0x51, 0x6a, 0x9c, 0x8f, 0x5b, 0xa0, 0x12, 0xc1, 0xe0, 0x34, - 0xb4, 0x56, 0xdd, 0xe4, 0x27, 0x95, 0x8d, 0xdd, 0x01, 0x39, 0x24, 0x99, 0x2c, 0xee, 0x46, 0xb7, 0x62, 0x59, 0x94, - 0xe2, 0x67, 0xac, 0x87, 0x6b, 0xb6, 0x70, 0x9f, 0x01, 0xa1, 0xfd, 0x44, 0x69, 0x6f, 0x22, 0x4d, 0xd0, 0x7d, 0xc7, - 0x56, 0x00, 0x32, 0x80, 0xa2, 0xae, 0x76, 0xeb, 0x73, 0x7e, 0x8e, 0xa4, 0x19, 0x0e, 0xa3, 0xdb, 0xa7, 0x77, 0xc1, - 0xdd, 0xe0, 0x12, 0xb5, 0xd2, 0x97, 0x2c, 0x6e, 0x61, 0x50, 0xed, 0xcd, 0x12, 0x0e, 0x6a, 0x66, 0xad, 0x8d, 0x40, - 0x30, 0xd9, 0x43, 0x41, 0xc5, 0x5c, 0xc1, 0x3e, 0x28, 0x58, 0x4b, 0x5e, 0x07, 0x87, 0x5b, 0xfb, 0xb2, 0x52, 0x5c, - 0x3c, 0xbf, 0x48, 0x5a, 0x17, 0x96, 0xf2, 0xe2, 0x79, 0x03, 0x06, 0x97, 0x23, 0x6c, 0xaa, 0xca, 0x9f, 0x6c, 0x00, - 0x74, 0x2b, 0xa2, 0x88, 0x17, 0xa5, 0xb0, 0x6d, 0xe5, 0x33, 0x27, 0x6c, 0xb0, 0x61, 0x0f, 0x70, 0xaf, 0x0c, 0x4a, - 0x06, 0x17, 0x62, 0xdc, 0x6e, 0x76, 0x01, 0xae, 0x60, 0x28, 0x8c, 0xad, 0xf9, 0x9b, 0xcc, 0x8b, 0x94, 0x80, 0x9b, - 0x21, 0xca, 0xd7, 0x06, 0x4e, 0x26, 0x3d, 0xb9, 0x96, 0x2c, 0x06, 0x2c, 0x68, 0xf0, 0x1d, 0xb5, 0xfe, 0xce, 0xe4, - 0xdf, 0x78, 0x7a, 0xe8, 0x07, 0x5f, 0x32, 0x6f, 0xe9, 0xb3, 0x37, 0x95, 0x8c, 0xd6, 0x24, 0x51, 0x5e, 0x3d, 0x5c, - 0x82, 0xdc, 0xb0, 0x1c, 0x3d, 0xb0, 0x25, 0x88, 0x13, 0xcb, 0x51, 0x42, 0x19, 0x5d, 0xe1, 0x5e, 0x65, 0xb6, 0x4c, - 0x04, 0x52, 0x1c, 0x58, 0x4a, 0xb9, 0xb7, 0x58, 0x07, 0x4b, 0xdc, 0x9f, 0x48, 0x2e, 0xa0, 0xe4, 0x01, 0x94, 0x2b, - 0x05, 0x04, 0x7c, 0x3a, 0x80, 0xf2, 0xa5, 0xbc, 0x08, 0x7f, 0xe2, 0x44, 0x0d, 0x96, 0xa3, 0x87, 0x86, 0xfd, 0xe4, - 0x85, 0x96, 0xfd, 0xe1, 0x4e, 0x6b, 0x1a, 0x56, 0xfc, 0x0e, 0xa6, 0xc5, 0xc4, 0xed, 0xcb, 0x95, 0x5d, 0x15, 0x9f, - 0xad, 0xd4, 0xd9, 0x4d, 0x0d, 0x49, 0xd8, 0x37, 0x64, 0x15, 0xe0, 0x60, 0x55, 0xc4, 0x3d, 0xcb, 0x72, 0x1f, 0x46, - 0x7f, 0x6e, 0xd2, 0x52, 0x58, 0xa8, 0x92, 0xfe, 0xbe, 0x29, 0x05, 0x52, 0x99, 0xe8, 0x44, 0x0b, 0xc1, 0x15, 0x18, - 0x04, 0xee, 0x45, 0x5e, 0x03, 0x60, 0x0c, 0xb8, 0x14, 0x28, 0xcb, 0xb6, 0x84, 0x90, 0xea, 0x7e, 0x06, 0x6a, 0x3b, - 0x71, 0x9f, 0x46, 0x64, 0x2d, 0x44, 0x5f, 0x05, 0x63, 0xe6, 0xbc, 0x94, 0x6e, 0xb1, 0xe9, 0x6a, 0xb3, 0xba, 0x41, - 0xe7, 0xd2, 0x96, 0x9b, 0x9f, 0xb0, 0xc5, 0x5a, 0x81, 0xb2, 0x09, 0x49, 0xdb, 0x39, 0xcf, 0x51, 0x36, 0xa1, 0xa5, - 0xbd, 0xa7, 0x1e, 0x15, 0xaa, 0x93, 0xad, 0x97, 0xaa, 0xa9, 0x45, 0x58, 0x2d, 0x2e, 0x2a, 0x3f, 0x00, 0xdd, 0x54, - 0x5a, 0xbd, 0xa8, 0x6b, 0x34, 0x85, 0x5a, 0x2d, 0x1c, 0x37, 0xda, 0xd9, 0x74, 0x99, 0xde, 0x21, 0xce, 0xaa, 0xb4, - 0x43, 0xff, 0x92, 0x69, 0xd7, 0xcb, 0x8e, 0x7e, 0x33, 0xae, 0x2e, 0x70, 0x21, 0x36, 0xe0, 0x73, 0xee, 0x2f, 0xaf, - 0xf7, 0x3c, 0xee, 0xf9, 0x87, 0x03, 0xb2, 0x27, 0xb5, 0x3f, 0x54, 0x1f, 0xbb, 0x82, 0x21, 0x0b, 0xa3, 0xd4, 0x5f, - 0xa4, 0xbc, 0xf7, 0x04, 0xc7, 0xfd, 0x4b, 0xd5, 0x63, 0x3f, 0x65, 0x7c, 0x5f, 0x17, 0x9b, 0x28, 0xa1, 0xa8, 0x86, - 0xde, 0xaa, 0xd8, 0x54, 0x22, 0x2e, 0x1e, 0xf2, 0x1e, 0xc3, 0x64, 0x18, 0x0b, 0x99, 0x0a, 0x7f, 0xca, 0x54, 0xf0, - 0x08, 0xa1, 0xc4, 0xcd, 0xba, 0x47, 0xda, 0x4d, 0x88, 0x53, 0xaa, 0x45, 0x29, 0x93, 0xf1, 0x6f, 0xfd, 0x04, 0xca, - 0x73, 0x8a, 0x96, 0xe9, 0x47, 0x85, 0xcb, 0xf4, 0xcd, 0xfa, 0xb8, 0xf4, 0x4c, 0x84, 0x3a, 0x73, 0xb1, 0xa9, 0x75, - 0x3a, 0xc6, 0x4e, 0xe9, 0xd4, 0x86, 0xbd, 0xaf, 0x14, 0x97, 0x15, 0x85, 0x7f, 0x23, 0x91, 0x55, 0xcf, 0x88, 0xe3, - 0xff, 0xcc, 0xda, 0x67, 0x58, 0x05, 0x7e, 0x19, 0xc8, 0xfb, 0x05, 0xc0, 0xc7, 0x75, 0x5d, 0xa6, 0xb7, 0x1b, 0xa0, - 0x0d, 0xa1, 0xe1, 0xef, 0xf9, 0xc8, 0x80, 0xe9, 0x3e, 0xc2, 0x19, 0xd2, 0x43, 0x9d, 0x73, 0x3a, 0x2b, 0xd3, 0x39, - 0x57, 0x61, 0x2d, 0xc1, 0x5e, 0x4e, 0x9a, 0x5c, 0xae, 0x4b, 0x50, 0x33, 0x81, 0xdb, 0x87, 0xf6, 0x88, 0x10, 0x6a, - 0x53, 0x56, 0xd3, 0x4b, 0xa8, 0x79, 0x27, 0xa7, 0x1d, 0x4d, 0x4a, 0x70, 0xd5, 0xd0, 0x59, 0xb9, 0xfe, 0xeb, 0x70, - 0xe8, 0xdd, 0x66, 0x45, 0xf4, 0x47, 0x0f, 0xfd, 0x1d, 0xb7, 0x37, 0xe9, 0x57, 0x88, 0x96, 0xb1, 0xfe, 0x86, 0x0c, - 0xe8, 0x78, 0x32, 0xbc, 0x2d, 0xb6, 0x3d, 0xf6, 0x1e, 0x35, 0x58, 0xfa, 0xfa, 0xf1, 0x07, 0x48, 0xa8, 0xba, 0xf6, - 0x85, 0xc5, 0x13, 0xe6, 0x29, 0xd1, 0xb6, 0xf0, 0x21, 0x2c, 0xf4, 0x3d, 0x44, 0x46, 0x42, 0xb8, 0xa9, 0xec, 0x1e, - 0x25, 0xed, 0x42, 0x5f, 0xfa, 0x5a, 0xf6, 0x95, 0xef, 0x5c, 0x00, 0xac, 0xec, 0x73, 0x1b, 0xee, 0x49, 0x7f, 0x4a, - 0xf5, 0x61, 0xfb, 0x5b, 0xb2, 0x80, 0x42, 0x0b, 0xeb, 0xa9, 0x9c, 0x9d, 0xeb, 0x92, 0xa7, 0xd9, 0x74, 0xbf, 0x86, - 0x3d, 0xea, 0x1e, 0xbd, 0xa6, 0x82, 0xf3, 0x4b, 0x33, 0x7a, 0xff, 0x30, 0x14, 0xaa, 0xa3, 0xce, 0x1d, 0x64, 0x5d, - 0x5a, 0x97, 0x9c, 0xdf, 0xac, 0xdc, 0x51, 0x98, 0xdf, 0x87, 0xe0, 0x19, 0xd6, 0xbd, 0xbb, 0x38, 0xef, 0xfd, 0xd9, - 0x9a, 0x23, 0x3f, 0x65, 0xb3, 0x14, 0xb1, 0x48, 0xe6, 0x60, 0xf5, 0x43, 0x3f, 0x8f, 0xfd, 0x36, 0xc8, 0xe1, 0xb8, - 0x69, 0x40, 0x87, 0x0d, 0x99, 0xb5, 0x2f, 0x11, 0x38, 0xd5, 0x08, 0xd2, 0xd4, 0x04, 0x35, 0xcb, 0x43, 0x24, 0xb6, - 0x4b, 0xd9, 0x36, 0xc8, 0x75, 0x17, 0x4c, 0x73, 0xa4, 0x3d, 0x83, 0xf7, 0x4d, 0x9a, 0xa4, 0x42, 0xb3, 0x48, 0x5b, - 0x25, 0xe3, 0xdf, 0x91, 0x36, 0x53, 0xb2, 0xc7, 0xd6, 0xc0, 0x7b, 0x09, 0xca, 0xc9, 0x30, 0xc5, 0xf0, 0x1d, 0x5f, - 0xef, 0x3c, 0xe6, 0x9e, 0x73, 0xcc, 0x36, 0x29, 0x3b, 0x82, 0x49, 0xb2, 0xf1, 0x0d, 0xc5, 0x1b, 0x7e, 0xb8, 0xad, - 0x44, 0x09, 0xa0, 0x97, 0x05, 0xbf, 0x96, 0x36, 0x57, 0xe8, 0x76, 0xf7, 0x8e, 0x52, 0xf8, 0x25, 0x2f, 0x0f, 0x87, - 0x6d, 0xea, 0x85, 0xd0, 0xf9, 0x22, 0x7e, 0x0f, 0xe6, 0x30, 0x86, 0xd8, 0x8c, 0x00, 0x61, 0x8e, 0x0f, 0xa8, 0x83, - 0xf5, 0x23, 0x00, 0x8d, 0x13, 0x28, 0xc0, 0xe8, 0xab, 0x6d, 0x41, 0xdf, 0xf2, 0xe2, 0x22, 0x42, 0xd4, 0x28, 0xc0, - 0x44, 0x49, 0xb3, 0x18, 0x86, 0x03, 0x9d, 0xdf, 0x37, 0xb7, 0x75, 0x29, 0x70, 0xe8, 0x1d, 0xcb, 0xf0, 0xdf, 0xfe, - 0xc7, 0xda, 0xd2, 0xaa, 0xb2, 0xdd, 0x1a, 0xa7, 0x99, 0xff, 0xed, 0xb6, 0xd0, 0xf7, 0x5f, 0x09, 0xc5, 0xf3, 0x8e, - 0xd7, 0xed, 0xaf, 0x10, 0xbd, 0xaf, 0x5b, 0x79, 0x57, 0x6a, 0x37, 0xcc, 0x94, 0x3f, 0xa4, 0x79, 0x5c, 0x3c, 0x8c, - 0xe2, 0xd6, 0x91, 0x37, 0x49, 0xcf, 0x39, 0xff, 0x5a, 0xf5, 0xfb, 0xde, 0x57, 0x20, 0xe3, 0x7d, 0x25, 0x8c, 0x23, - 0x26, 0x71, 0xf0, 0xed, 0xc5, 0x28, 0xda, 0x94, 0xb0, 0x21, 0xb7, 0x4f, 0x4b, 0xd0, 0xcc, 0xf4, 0xfb, 0x28, 0x51, - 0x5a, 0xf3, 0xfd, 0x2f, 0x72, 0xbe, 0xbf, 0x12, 0xf2, 0x66, 0x25, 0x3f, 0x7c, 0xb4, 0xc2, 0xc0, 0xf7, 0x38, 0xfd, - 0x2a, 0x7a, 0xec, 0xae, 0xf4, 0xe1, 0xbb, 0xd2, 0xd2, 0x67, 0x15, 0xf5, 0x77, 0x54, 0xd4, 0xbc, 0x12, 0x23, 0x22, - 0x1e, 0x04, 0xed, 0x6c, 0xbb, 0xd4, 0xae, 0x25, 0x68, 0x17, 0x6c, 0x0a, 0xfb, 0xd7, 0xa3, 0x43, 0xde, 0xef, 0x7f, - 0xca, 0xbd, 0x16, 0xaf, 0xbb, 0x0e, 0x4d, 0xf9, 0x6b, 0xe1, 0x21, 0x04, 0xb0, 0x96, 0x81, 0x32, 0x8e, 0x30, 0xe9, - 0x22, 0xaf, 0x51, 0x36, 0x9d, 0x08, 0x7c, 0xcc, 0xb2, 0x2b, 0x27, 0x99, 0x06, 0x98, 0x51, 0x4d, 0x61, 0x26, 0xc0, - 0x48, 0x7d, 0xc2, 0xba, 0xe9, 0x69, 0x15, 0x5a, 0xbe, 0x86, 0x60, 0x5d, 0x64, 0x19, 0x47, 0x31, 0x13, 0x00, 0x6c, - 0x3e, 0x81, 0x7c, 0x45, 0x57, 0x87, 0xa4, 0x95, 0x2a, 0xef, 0xd7, 0x19, 0x91, 0xd1, 0x24, 0x44, 0xf3, 0x5b, 0x78, - 0x60, 0xdf, 0x36, 0x33, 0xaa, 0xd4, 0x33, 0xaa, 0xf2, 0x19, 0x0e, 0x4b, 0xe1, 0x18, 0xf1, 0xff, 0x96, 0xaa, 0x1e, - 0x11, 0xe8, 0x55, 0x99, 0x56, 0x51, 0x91, 0xe7, 0x22, 0x42, 0x84, 0x6a, 0xe9, 0x1c, 0x0e, 0xfd, 0xd8, 0xef, 0xe3, - 0x40, 0x98, 0x17, 0xff, 0xfa, 0x58, 0x57, 0xfe, 0xb5, 0xc0, 0xb5, 0x92, 0x02, 0xa7, 0xa2, 0x46, 0x88, 0x10, 0xde, - 0x9f, 0xc0, 0xb3, 0x9a, 0xfa, 0x7e, 0x63, 0x99, 0xe8, 0xfe, 0x91, 0x01, 0xe5, 0x0f, 0xc8, 0xd7, 0x95, 0x14, 0x67, - 0xea, 0xe4, 0x31, 0x71, 0xc6, 0x01, 0x88, 0xf9, 0xb6, 0x44, 0xa3, 0xb1, 0xff, 0x01, 0x09, 0x86, 0xea, 0x07, 0x3b, - 0xdd, 0xd4, 0xfb, 0x67, 0x26, 0x71, 0x14, 0x7d, 0xda, 0x26, 0x8f, 0x25, 0x4b, 0xa3, 0x85, 0xa3, 0xf7, 0x88, 0x61, - 0x1c, 0x4e, 0xe7, 0x63, 0x92, 0x6d, 0x4c, 0x56, 0x01, 0xa4, 0x93, 0x99, 0x3a, 0xa6, 0xd4, 0xd1, 0x38, 0xd7, 0x0b, - 0xaa, 0xd0, 0x63, 0x5d, 0xf2, 0x1c, 0xac, 0x27, 0x3f, 0x7a, 0xa5, 0x3f, 0x15, 0x72, 0x0e, 0x1b, 0x89, 0xa0, 0xf0, - 0x03, 0x5c, 0x0d, 0x56, 0x0a, 0x18, 0x4c, 0x7d, 0x0b, 0x5f, 0x13, 0xcf, 0x51, 0xf0, 0x28, 0xec, 0x62, 0x6c, 0xad, - 0x7c, 0xe7, 0x93, 0x82, 0x72, 0xcf, 0x8a, 0x39, 0xaf, 0x80, 0x73, 0x19, 0x14, 0xc2, 0x74, 0x3c, 0xcb, 0xff, 0x99, - 0xe4, 0xf5, 0xc4, 0x86, 0x00, 0x19, 0xfc, 0x29, 0x71, 0x5a, 0xba, 0x43, 0x77, 0x1e, 0x7a, 0x16, 0x71, 0xd8, 0xe8, - 0xc9, 0xba, 0x2c, 0xb6, 0x29, 0xea, 0x25, 0xcc, 0x0f, 0xe4, 0xe7, 0x2d, 0xf9, 0x3e, 0x44, 0xf1, 0x36, 0xf8, 0x35, - 0x63, 0xb1, 0xc0, 0xbf, 0xfe, 0x96, 0x31, 0x9a, 0x68, 0xc1, 0xbf, 0xb2, 0x06, 0x89, 0x8a, 0xff, 0x9a, 0x4d, 0x00, - 0xd6, 0x91, 0xab, 0x0f, 0x9f, 0x12, 0xe3, 0xad, 0xd9, 0xf0, 0xc8, 0x37, 0x2b, 0xd0, 0xa9, 0xcf, 0xdd, 0x95, 0xed, - 0xa9, 0x6a, 0xfc, 0x2d, 0xd5, 0xd5, 0x48, 0x55, 0x35, 0xfe, 0x96, 0x52, 0x35, 0x7e, 0xcb, 0x28, 0x7e, 0xa7, 0xf2, - 0x19, 0x32, 0x27, 0x9b, 0x98, 0xa4, 0xd3, 0xf7, 0x86, 0x13, 0xbb, 0xec, 0x37, 0x6f, 0x13, 0x99, 0x89, 0x14, 0x72, - 0x6f, 0x00, 0xda, 0x7e, 0x97, 0x1b, 0x4e, 0x89, 0xf3, 0x73, 0x0f, 0x57, 0x6c, 0x5a, 0xbd, 0xa2, 0x05, 0x0b, 0x6c, - 0x5e, 0x66, 0x79, 0x8a, 0x04, 0xb6, 0x4d, 0x99, 0xf5, 0xe7, 0xdc, 0x03, 0x08, 0x66, 0x52, 0x13, 0x00, 0xd2, 0x42, - 0x54, 0x0a, 0x91, 0xbf, 0xc2, 0x59, 0x7d, 0xce, 0x7b, 0x9b, 0x3c, 0x26, 0xd2, 0xea, 0x5e, 0xbf, 0x9f, 0x9e, 0xa5, - 0x39, 0x05, 0x35, 0x1c, 0x67, 0x9d, 0xfe, 0x92, 0x05, 0x75, 0x22, 0x57, 0xe9, 0xdf, 0xdd, 0x20, 0x2f, 0xe3, 0xfb, - 0xba, 0xed, 0xf9, 0x13, 0xf5, 0xf7, 0xce, 0xfa, 0xdb, 0x02, 0xc1, 0x9d, 0x1c, 0xfb, 0xc9, 0xaa, 0x94, 0x27, 0xc6, - 0xa5, 0xbd, 0xe7, 0x37, 0x75, 0x51, 0x64, 0x75, 0xba, 0xfe, 0x28, 0xf5, 0x34, 0xba, 0x2f, 0xf6, 0x60, 0x0c, 0xde, - 0x01, 0xe0, 0x99, 0x0e, 0x0d, 0x90, 0xbe, 0x67, 0xe4, 0xe1, 0x3e, 0xb7, 0xe4, 0x27, 0x95, 0xb5, 0x49, 0xc2, 0x8a, - 0x62, 0x33, 0x8c, 0x11, 0x4a, 0xc6, 0x69, 0x6c, 0xfd, 0x7e, 0x5f, 0xfd, 0xbd, 0xc3, 0x28, 0x2a, 0x2a, 0xee, 0x18, - 0x8d, 0xca, 0xaa, 0x1e, 0x6d, 0x07, 0x87, 0xc3, 0x79, 0x6e, 0xe3, 0x68, 0xeb, 0x15, 0xb0, 0xb7, 0x42, 0xa5, 0xec, - 0x95, 0x08, 0xcb, 0x0f, 0x57, 0x7e, 0xbf, 0x0f, 0xff, 0xca, 0x48, 0x0b, 0xcf, 0x9f, 0xe2, 0xaf, 0x45, 0x5d, 0x60, - 0x78, 0x06, 0xad, 0xd1, 0x0a, 0x82, 0x09, 0xfe, 0xde, 0x81, 0x7a, 0x69, 0xa5, 0x7d, 0x02, 0xdd, 0x0a, 0xf4, 0xa0, - 0x1e, 0xfa, 0x34, 0x69, 0x5f, 0x48, 0xd4, 0xed, 0xad, 0x4e, 0xa3, 0x3f, 0x2a, 0xb8, 0x9c, 0xc2, 0xe4, 0x70, 0x43, - 0x9f, 0x56, 0xe1, 0xf6, 0x33, 0x3c, 0xfd, 0x19, 0x28, 0xb7, 0x0e, 0x87, 0x1c, 0xc4, 0x16, 0x70, 0xf3, 0x58, 0x85, - 0x5f, 0x8a, 0x52, 0x46, 0xd4, 0xc7, 0xd3, 0x02, 0xb4, 0x77, 0x01, 0x3a, 0x60, 0x69, 0x10, 0xaf, 0x90, 0x3c, 0x67, - 0x23, 0x80, 0x65, 0x07, 0x96, 0xb3, 0x8c, 0x53, 0x98, 0x67, 0x79, 0xad, 0x56, 0xda, 0x59, 0x99, 0x78, 0x35, 0xcb, - 0xc0, 0x59, 0xe0, 0xa2, 0xf2, 0x59, 0xa6, 0x55, 0x4f, 0x55, 0x82, 0x3e, 0xaf, 0xe4, 0x04, 0x57, 0x82, 0x93, 0x0d, - 0xc8, 0x2f, 0x40, 0x92, 0xa6, 0x94, 0x35, 0xe5, 0xf5, 0x25, 0xdd, 0x90, 0xd1, 0x73, 0xde, 0xf3, 0xa2, 0x61, 0xe8, - 0x5f, 0x78, 0x25, 0x84, 0x6f, 0xe2, 0xb6, 0x8d, 0x52, 0xd8, 0x5f, 0x04, 0x16, 0x9f, 0xb0, 0x1f, 0xbd, 0xa5, 0x3f, - 0x1d, 0x07, 0xe1, 0x10, 0xb9, 0xa1, 0x62, 0x0e, 0xec, 0x69, 0xc0, 0x62, 0x13, 0x5f, 0x6d, 0x26, 0xf1, 0x60, 0xe0, - 0xeb, 0x8c, 0xc5, 0x2c, 0x06, 0x1a, 0xe4, 0x78, 0x70, 0x39, 0xd7, 0x27, 0x84, 0x7e, 0x18, 0x51, 0x39, 0x2a, 0xd0, - 0x39, 0x88, 0x06, 0x4b, 0xc0, 0x53, 0x6f, 0x65, 0x83, 0x24, 0x63, 0x92, 0x49, 0x5c, 0x6b, 0x92, 0xea, 0x70, 0x42, - 0xeb, 0x40, 0xc7, 0xd5, 0x05, 0x74, 0x3e, 0xae, 0x7b, 0x1f, 0xaf, 0x86, 0x0b, 0x2a, 0xfd, 0x42, 0x0c, 0xbc, 0x7a, - 0x3a, 0x0e, 0x2e, 0xe9, 0x56, 0xb8, 0x58, 0x85, 0xdb, 0x9f, 0xe5, 0x03, 0xc7, 0x1d, 0x95, 0x34, 0x04, 0x06, 0x6f, - 0x0f, 0xdd, 0xcd, 0x0c, 0x0d, 0x75, 0xd2, 0x3e, 0x8c, 0x43, 0x39, 0xc4, 0xaa, 0x15, 0x17, 0xd2, 0x1b, 0xc1, 0xb7, - 0x0b, 0xc5, 0x58, 0x36, 0x76, 0x69, 0x28, 0x0a, 0x7f, 0x05, 0xb0, 0x43, 0xed, 0xaf, 0x54, 0xf2, 0x31, 0x32, 0xaa, - 0x69, 0xa0, 0x63, 0x00, 0x96, 0x2c, 0x4d, 0x24, 0x55, 0xa4, 0x91, 0xf8, 0x23, 0x33, 0xd6, 0x51, 0xd3, 0xf5, 0x05, - 0x53, 0xd5, 0x22, 0xe9, 0x76, 0x26, 0xb1, 0x9c, 0x48, 0x52, 0xdb, 0x7d, 0x44, 0x0c, 0x06, 0x3e, 0xd8, 0x88, 0x69, - 0x26, 0xc2, 0x11, 0x8f, 0x4a, 0x64, 0xd1, 0xe5, 0xb7, 0x51, 0x26, 0x6d, 0x5f, 0x56, 0x64, 0x0b, 0x82, 0xe9, 0x49, - 0xf4, 0x41, 0x92, 0x72, 0x2a, 0x12, 0x69, 0x46, 0x08, 0xf0, 0xe3, 0x49, 0x79, 0xa5, 0x3f, 0x07, 0x4d, 0x2b, 0xc1, - 0x4b, 0x06, 0xc9, 0x23, 0xf1, 0x33, 0x29, 0x98, 0xc5, 0x58, 0x35, 0x18, 0x60, 0x39, 0xd5, 0x33, 0xc7, 0x24, 0xfd, - 0x97, 0x4e, 0x27, 0xec, 0x17, 0x5e, 0x6e, 0x6b, 0x79, 0xd3, 0xdc, 0x7b, 0xe1, 0x55, 0x2c, 0xd5, 0xb0, 0x0c, 0xfa, - 0xaf, 0x89, 0x76, 0xc1, 0xd6, 0x96, 0x31, 0x61, 0xd5, 0x0f, 0x20, 0xed, 0x91, 0x2e, 0xaf, 0x1a, 0xe6, 0x4c, 0xf0, - 0xe8, 0xc2, 0x9a, 0x07, 0xd1, 0x85, 0xf0, 0x91, 0xcb, 0x6e, 0x92, 0x5c, 0x8d, 0x27, 0x7e, 0x38, 0x18, 0x28, 0x00, - 0x5a, 0x5a, 0x27, 0xc5, 0x20, 0x7c, 0x26, 0xe4, 0x40, 0x1a, 0x1d, 0x55, 0x01, 0x16, 0xcb, 0xec, 0xaa, 0x9c, 0x64, - 0x83, 0x81, 0x0f, 0x62, 0x63, 0x62, 0x37, 0x34, 0x9b, 0xfb, 0xec, 0x44, 0x41, 0x56, 0x9b, 0xc3, 0xd6, 0x4c, 0xb7, - 0xc0, 0x00, 0x60, 0x10, 0x11, 0x2c, 0xf7, 0xb9, 0x91, 0x8f, 0xa8, 0xd3, 0x53, 0x18, 0x01, 0xc1, 0x2f, 0x27, 0x02, - 0x91, 0x8b, 0x04, 0xea, 0x01, 0x66, 0x02, 0xcc, 0xa8, 0x62, 0x78, 0x09, 0xec, 0xe2, 0xb9, 0x79, 0xc5, 0xa0, 0x7f, - 0xd1, 0x24, 0x4b, 0x34, 0x95, 0x38, 0x1a, 0x23, 0xa7, 0xd2, 0x18, 0x19, 0x10, 0xbb, 0x38, 0xfe, 0x3d, 0xa5, 0x47, - 0x41, 0xca, 0xbe, 0x54, 0x86, 0x38, 0x1c, 0xc5, 0x57, 0xb0, 0x6a, 0x1c, 0x0e, 0xb5, 0x79, 0x3d, 0x9d, 0xd5, 0xf3, - 0x81, 0x08, 0xe0, 0xbf, 0xa1, 0x60, 0x2f, 0x35, 0x15, 0xb9, 0x41, 0xea, 0x3c, 0x1c, 0x52, 0x90, 0x4f, 0x75, 0x93, - 0x7f, 0xa9, 0xdc, 0xfd, 0x74, 0x36, 0xb7, 0xe6, 0xe8, 0x45, 0x8d, 0xeb, 0xd6, 0xea, 0x86, 0x42, 0xa2, 0x35, 0x4d, - 0x8a, 0xab, 0x6a, 0x52, 0x0c, 0x78, 0xee, 0x0b, 0xd5, 0xc5, 0xd6, 0x08, 0x16, 0xfe, 0xdc, 0x02, 0x61, 0x32, 0xee, - 0xc5, 0x47, 0x0b, 0x39, 0xa5, 0x5d, 0x5b, 0xed, 0xb6, 0x95, 0x0d, 0x29, 0x9a, 0x0f, 0x2f, 0x61, 0x97, 0x4e, 0x11, - 0x6d, 0xbb, 0x24, 0xf8, 0x02, 0xb4, 0xac, 0x2e, 0x44, 0x1e, 0xd3, 0xaf, 0x90, 0x5f, 0x8a, 0xe1, 0x7f, 0x4a, 0xf7, - 0xe6, 0xd4, 0x06, 0x39, 0x80, 0xed, 0xde, 0xc3, 0xed, 0x18, 0x3d, 0x90, 0xc1, 0x1b, 0x21, 0xe7, 0x9c, 0x5f, 0x4e, - 0xad, 0x19, 0x13, 0x0d, 0x0b, 0x56, 0x0e, 0x23, 0x3f, 0x40, 0xc6, 0xcb, 0x29, 0xb0, 0xb2, 0x1f, 0x15, 0x71, 0xe9, - 0x0f, 0x23, 0xff, 0xe2, 0x79, 0x90, 0x71, 0x2f, 0x1a, 0x76, 0x7c, 0x01, 0xf6, 0xea, 0x8b, 0xe7, 0x2c, 0x1a, 0xf0, - 0xea, 0xaa, 0x9e, 0x66, 0xc1, 0x30, 0x63, 0xd1, 0x55, 0x31, 0x04, 0x1f, 0xda, 0xeb, 0x72, 0x10, 0xfa, 0xbe, 0xd9, - 0x39, 0x74, 0x37, 0x24, 0xf2, 0x08, 0xfb, 0x09, 0xdc, 0x76, 0xb5, 0xc4, 0x0c, 0x26, 0x9b, 0xbb, 0x88, 0x19, 0x6c, - 0xf9, 0x8b, 0xe7, 0x86, 0x4b, 0xa8, 0xba, 0x96, 0x9a, 0x8d, 0x02, 0xcd, 0xc9, 0x15, 0x9a, 0x93, 0x95, 0x50, 0x4b, - 0x3e, 0xa9, 0x70, 0xc2, 0xce, 0x27, 0xb9, 0xb2, 0x1b, 0x8d, 0x31, 0x70, 0xd1, 0x9e, 0xdb, 0xc2, 0xc8, 0x4c, 0x67, - 0x29, 0x1a, 0xb0, 0xf0, 0x4c, 0x9c, 0xd2, 0x18, 0xd0, 0xbe, 0x1c, 0x58, 0xda, 0x90, 0x9f, 0xe4, 0xcc, 0x40, 0xdb, - 0x90, 0xd2, 0xa8, 0x19, 0xf8, 0x33, 0x35, 0x61, 0x7e, 0x05, 0x2b, 0x11, 0x44, 0x75, 0x01, 0x26, 0x49, 0x4e, 0x46, - 0x23, 0x65, 0x25, 0x92, 0x73, 0xc0, 0xfb, 0x04, 0x9e, 0x2c, 0x62, 0x5b, 0xfb, 0x53, 0xfa, 0x5f, 0x1d, 0x3e, 0x97, - 0xfe, 0x33, 0x01, 0x2c, 0xe4, 0xd2, 0x20, 0x32, 0x50, 0x38, 0xa4, 0xa6, 0x12, 0x71, 0xe2, 0x78, 0x06, 0xbe, 0x81, - 0x0b, 0x34, 0x05, 0xf4, 0x07, 0x35, 0xa3, 0x88, 0x2c, 0xfc, 0xd5, 0xb3, 0x9b, 0xba, 0xd1, 0xf3, 0xcc, 0x79, 0x0d, - 0x9a, 0x19, 0x08, 0xe9, 0x71, 0xaa, 0xde, 0x86, 0x44, 0xe7, 0xe5, 0xa5, 0x7e, 0x99, 0x10, 0xc9, 0x8a, 0xc8, 0xd3, - 0xf7, 0x39, 0x98, 0x47, 0x14, 0xa1, 0x83, 0x2b, 0xf3, 0x70, 0x38, 0x17, 0x14, 0xbe, 0xa3, 0x3c, 0x1f, 0x70, 0x9a, - 0x45, 0x09, 0x68, 0x03, 0x59, 0x6e, 0xca, 0x5c, 0x27, 0x2d, 0x53, 0xf7, 0x1e, 0xac, 0x04, 0x15, 0xba, 0x39, 0x05, - 0x85, 0x32, 0x12, 0x94, 0xd2, 0x6a, 0x10, 0x4a, 0x75, 0x58, 0x04, 0x91, 0x43, 0x16, 0x02, 0x6e, 0xa6, 0xa2, 0xd1, - 0x92, 0x86, 0x47, 0x38, 0x37, 0x50, 0x08, 0x40, 0x62, 0x4f, 0x15, 0x65, 0x5c, 0x0e, 0x01, 0x1f, 0x25, 0x1c, 0xe2, - 0xac, 0x49, 0x5b, 0x9e, 0x83, 0x38, 0x96, 0x4b, 0xbe, 0xae, 0x10, 0x0c, 0x22, 0xf4, 0x19, 0xf2, 0x27, 0xcb, 0xf9, - 0x77, 0xeb, 0x30, 0xed, 0x08, 0x1f, 0x76, 0xb5, 0x05, 0x17, 0xb3, 0xdb, 0xf9, 0x04, 0xe2, 0x5b, 0x6e, 0xe7, 0xc7, - 0x18, 0x22, 0x0b, 0x7f, 0x70, 0x37, 0x94, 0x5c, 0x51, 0xe8, 0xb2, 0x1e, 0x91, 0x22, 0x7b, 0xba, 0xe6, 0x08, 0x82, - 0x03, 0xad, 0x1a, 0x64, 0x68, 0x24, 0xbe, 0x78, 0x0e, 0x59, 0x83, 0x35, 0xff, 0x52, 0x91, 0xb3, 0xba, 0x3f, 0xd9, - 0x40, 0x35, 0xc9, 0x64, 0xad, 0xa8, 0x9c, 0xbf, 0x5d, 0x95, 0xe5, 0xc9, 0xaa, 0x0c, 0x57, 0x83, 0xae, 0xaa, 0x2c, - 0x39, 0x52, 0x1b, 0xa0, 0x35, 0x5d, 0x21, 0x86, 0x42, 0xd6, 0x60, 0x69, 0x55, 0x65, 0x4d, 0x7d, 0x02, 0x81, 0x3e, - 0xc0, 0x32, 0x6a, 0xf6, 0xd3, 0xe1, 0x3f, 0x83, 0x7f, 0xaa, 0x90, 0xa5, 0x3a, 0xad, 0x33, 0xf1, 0x6b, 0xb0, 0x64, - 0xf8, 0xc7, 0x6f, 0xc1, 0x1a, 0xb0, 0x04, 0xc8, 0x72, 0xb7, 0xb1, 0xd1, 0x7a, 0xe5, 0x15, 0xe2, 0x7d, 0xad, 0x2f, - 0xfa, 0xad, 0xdb, 0x44, 0xad, 0x00, 0x23, 0x14, 0x5a, 0x04, 0xd8, 0xea, 0x81, 0x7b, 0x0a, 0x7e, 0x20, 0x86, 0x73, - 0x4d, 0x5a, 0x53, 0x27, 0xbc, 0xce, 0xc6, 0x91, 0x88, 0xea, 0x2d, 0x5c, 0xdc, 0xeb, 0xad, 0xc5, 0xdf, 0xa8, 0x40, - 0x00, 0x64, 0x31, 0xc5, 0xda, 0x79, 0x43, 0x7a, 0x65, 0xd8, 0x49, 0xe8, 0xbd, 0x61, 0x27, 0x90, 0x17, 0x87, 0x9d, - 0x42, 0x97, 0x68, 0x3b, 0x45, 0x6a, 0xa2, 0xed, 0xa4, 0xc5, 0x2a, 0x2c, 0x21, 0xf8, 0x55, 0x7b, 0xeb, 0x28, 0xdb, - 0x17, 0x59, 0xc2, 0xb4, 0x05, 0x8c, 0x72, 0xab, 0x3e, 0x73, 0x8a, 0x58, 0x29, 0x7b, 0xa7, 0x93, 0x2a, 0x77, 0x91, - 0xcf, 0xad, 0xa6, 0xc8, 0xe4, 0x97, 0xc7, 0x2d, 0x92, 0x4f, 0x7e, 0x6e, 0x37, 0x4c, 0xa6, 0x7f, 0x3a, 0xfa, 0x02, - 0xba, 0x22, 0x3b, 0x7d, 0x02, 0x01, 0x99, 0x0a, 0xaa, 0xd5, 0xad, 0x62, 0x9a, 0xb7, 0xab, 0xec, 0xf6, 0x42, 0x89, - 0xe1, 0x74, 0x76, 0x12, 0x1e, 0x6d, 0x86, 0x0c, 0x1c, 0x82, 0x40, 0x21, 0x54, 0x14, 0xc3, 0x23, 0x50, 0x6b, 0x24, - 0x1f, 0xe0, 0x47, 0xbb, 0x53, 0x41, 0xa4, 0x76, 0x53, 0x71, 0xe3, 0xe4, 0xa6, 0xeb, 0xa5, 0x40, 0xad, 0x53, 0xb2, - 0x02, 0x28, 0x21, 0xea, 0xcf, 0x62, 0x5b, 0xbf, 0x82, 0x2b, 0x36, 0xdf, 0x37, 0x8a, 0x9e, 0x5c, 0x9f, 0xa2, 0x6e, - 0xc5, 0xd5, 0x69, 0xda, 0x6a, 0x8e, 0x1d, 0x67, 0xc8, 0xc1, 0xb3, 0x82, 0x60, 0x3b, 0x2a, 0x51, 0xbe, 0x6b, 0x37, - 0x1d, 0x13, 0x5b, 0xfd, 0xb3, 0xa8, 0x36, 0x77, 0x50, 0x11, 0x11, 0x1f, 0x65, 0x37, 0x4f, 0xda, 0xef, 0x60, 0x8f, - 0xb5, 0x1a, 0x44, 0xf6, 0x19, 0x5c, 0xe5, 0x3a, 0x2d, 0x72, 0x5b, 0x06, 0xe7, 0x1f, 0x5e, 0xed, 0x2a, 0x6c, 0x72, - 0xac, 0xab, 0xab, 0x99, 0xea, 0xa4, 0x62, 0x03, 0x63, 0x4d, 0x6b, 0xa9, 0xe6, 0x31, 0x24, 0xdd, 0x95, 0xc5, 0x59, - 0x95, 0x74, 0xd3, 0x73, 0xe3, 0x4c, 0x21, 0x06, 0xce, 0x56, 0xa3, 0xe5, 0x0c, 0x43, 0x74, 0x7d, 0x98, 0x25, 0x7e, - 0xab, 0xa7, 0xdc, 0xe7, 0xe1, 0xd6, 0xef, 0xea, 0x05, 0x27, 0x93, 0xfd, 0xe4, 0x38, 0x77, 0xbb, 0x48, 0xfb, 0x89, - 0x6f, 0xc3, 0xfc, 0xeb, 0x1b, 0xc4, 0x9d, 0xa8, 0xff, 0x51, 0x01, 0xd0, 0xe0, 0x26, 0x8f, 0x25, 0x4a, 0xfd, 0x5e, - 0x55, 0x3f, 0xa8, 0x99, 0xaa, 0x69, 0x20, 0x98, 0x53, 0x29, 0xe0, 0x0f, 0xb7, 0x0b, 0x57, 0x3c, 0xe2, 0x86, 0x85, - 0xf1, 0x4f, 0xaf, 0x66, 0xa7, 0x82, 0xca, 0xc0, 0xcd, 0xf8, 0x4f, 0x4f, 0xb0, 0x53, 0x58, 0x2b, 0x20, 0x2b, 0xfc, - 0xe9, 0xe5, 0x8f, 0xbc, 0x5f, 0xf1, 0x3f, 0xbd, 0xea, 0x91, 0xf7, 0x11, 0xe7, 0xe5, 0x4f, 0x24, 0x75, 0x42, 0x54, - 0x97, 0x3f, 0x09, 0x53, 0x6c, 0x95, 0xe6, 0xaf, 0x49, 0xe1, 0x13, 0x7c, 0x01, 0xbe, 0xc3, 0x55, 0xb8, 0x35, 0xbf, - 0xc1, 0x63, 0xc7, 0x62, 0xdb, 0xa5, 0xbe, 0x80, 0x72, 0x04, 0x16, 0x91, 0xdb, 0x6f, 0x57, 0xf6, 0xab, 0x85, 0x51, - 0xc6, 0xd8, 0x7d, 0xc9, 0x4a, 0x94, 0xce, 0xfa, 0xfd, 0x42, 0x0a, 0x46, 0x76, 0x61, 0x8d, 0xf6, 0x28, 0x55, 0xaf, - 0xbe, 0x0b, 0xeb, 0x28, 0x49, 0xf3, 0x3b, 0x19, 0x7d, 0x24, 0xc3, 0x8e, 0xf4, 0x95, 0x94, 0x68, 0xaf, 0x55, 0x58, - 0x8e, 0x66, 0xbf, 0x2e, 0x39, 0x50, 0x5e, 0xb7, 0x82, 0xf2, 0x55, 0x13, 0x40, 0xaf, 0x54, 0xfb, 0x0c, 0xb4, 0x82, - 0xc2, 0x52, 0x79, 0xb0, 0x12, 0xe7, 0xa2, 0xcf, 0x8a, 0xc3, 0x41, 0x5d, 0x0c, 0x09, 0x05, 0xaa, 0xc4, 0x49, 0x68, - 0xc4, 0x73, 0xb8, 0x10, 0x8a, 0xeb, 0x1c, 0x63, 0x2b, 0x72, 0xe0, 0x40, 0x86, 0x1f, 0x10, 0x78, 0x2f, 0xfb, 0x57, - 0x30, 0x18, 0x26, 0xb8, 0x91, 0x51, 0x27, 0xe7, 0xec, 0x4f, 0x0c, 0xcc, 0xa0, 0x9e, 0xd4, 0xee, 0xb3, 0x7b, 0x15, - 0xd8, 0x0b, 0x67, 0x40, 0x7b, 0x37, 0x46, 0x3f, 0xab, 0x62, 0xed, 0xa4, 0x7f, 0x2e, 0xd6, 0x90, 0x4c, 0x87, 0xc5, - 0xd1, 0x36, 0x0d, 0x8f, 0xe4, 0xc9, 0x71, 0xbc, 0xe9, 0x1f, 0x0e, 0x63, 0xfc, 0x38, 0xca, 0xaf, 0x2d, 0xe0, 0x55, - 0xdc, 0x42, 0x1a, 0x8b, 0x14, 0xbd, 0x03, 0x31, 0x87, 0xa2, 0x97, 0xec, 0xb7, 0x8c, 0x97, 0x13, 0x41, 0x29, 0x49, - 0x6c, 0x78, 0x47, 0x7a, 0x9a, 0xd6, 0xa3, 0xad, 0x0c, 0xd8, 0xaf, 0x47, 0x3b, 0xfa, 0x0b, 0x14, 0x8f, 0x16, 0xfe, - 0x92, 0xfe, 0x2e, 0xee, 0xe6, 0x9e, 0xf3, 0x4d, 0xe3, 0x3b, 0xe2, 0x02, 0xc5, 0x9a, 0xdd, 0x5f, 0xd3, 0xd2, 0x59, - 0x07, 0x82, 0x03, 0xde, 0x62, 0x17, 0xed, 0xfb, 0x8d, 0xeb, 0xf4, 0xb4, 0xff, 0xde, 0xad, 0x51, 0xbe, 0xf7, 0x0f, - 0x89, 0x72, 0xb0, 0x7f, 0xed, 0xa2, 0xf9, 0xdb, 0x4f, 0x19, 0x92, 0x0a, 0xcd, 0x0d, 0xb6, 0x93, 0x2d, 0xc2, 0xda, - 0x18, 0x07, 0x15, 0xbb, 0x2b, 0xc3, 0x08, 0x18, 0xd4, 0xb1, 0xff, 0xd1, 0x67, 0xd3, 0x86, 0xec, 0x03, 0x40, 0xe5, - 0x2a, 0x04, 0xec, 0x01, 0x38, 0xd1, 0x08, 0x37, 0xc0, 0xad, 0x46, 0x4b, 0x3a, 0xa8, 0xdb, 0x82, 0x81, 0x68, 0x09, - 0x1b, 0x79, 0xdb, 0xd5, 0xe9, 0x1b, 0xc2, 0x87, 0xda, 0x49, 0xe9, 0x50, 0xfe, 0xe6, 0x39, 0xfb, 0xef, 0x1d, 0xd6, - 0xd4, 0x94, 0x1b, 0xc0, 0xcc, 0x59, 0x89, 0xbc, 0x42, 0xe8, 0x14, 0xf9, 0xbd, 0xaa, 0x2b, 0x31, 0x5c, 0xd6, 0xa2, - 0xec, 0xcc, 0x6e, 0x9d, 0xe8, 0x9d, 0x53, 0x50, 0x4b, 0x65, 0x83, 0x9c, 0xa4, 0xda, 0x7c, 0x64, 0xad, 0xa0, 0x44, - 0x5d, 0xa3, 0xc0, 0xf1, 0x29, 0xd7, 0xee, 0xff, 0x9d, 0x33, 0x41, 0xcd, 0x36, 0xaa, 0xfb, 0x6b, 0xfd, 0x54, 0xd5, - 0x24, 0x16, 0xe0, 0x72, 0x92, 0xe6, 0x1d, 0x8f, 0xb0, 0xfa, 0xc7, 0xc9, 0x52, 0x04, 0x7a, 0x1d, 0xd1, 0xae, 0x04, - 0x24, 0x68, 0x27, 0x67, 0xa1, 0x22, 0x50, 0xa0, 0xaf, 0xbf, 0xdc, 0xa4, 0x59, 0x2c, 0x57, 0xb3, 0x3d, 0x4c, 0x94, - 0xc5, 0x7a, 0x88, 0x20, 0x67, 0xa6, 0x0e, 0xf6, 0x7b, 0x9a, 0xd1, 0x2c, 0xbc, 0x32, 0x25, 0xb8, 0x14, 0x57, 0x51, - 0x91, 0x83, 0xcf, 0x21, 0xbe, 0xf0, 0xb9, 0x90, 0x1b, 0x44, 0x34, 0xfd, 0x45, 0xa2, 0xda, 0x91, 0x02, 0x39, 0x94, - 0xfc, 0x84, 0xf8, 0x4b, 0xd6, 0xc6, 0xb8, 0x5f, 0x3a, 0xd5, 0x7e, 0xa5, 0x10, 0xdc, 0x7f, 0xb6, 0xc5, 0x46, 0x95, - 0x27, 0x7a, 0xf4, 0x29, 0xd6, 0xff, 0x64, 0x01, 0xa5, 0xba, 0x6f, 0x83, 0x53, 0xf1, 0x28, 0xdc, 0xd4, 0xc5, 0x0d, - 0x42, 0x0b, 0x94, 0xa3, 0xaa, 0xd8, 0x94, 0x11, 0x71, 0xc2, 0x6e, 0xea, 0xa2, 0xa7, 0x39, 0xd0, 0xa9, 0xc3, 0xd2, - 0x44, 0x9e, 0x08, 0xed, 0x16, 0x74, 0x4f, 0x73, 0xac, 0xc4, 0x0b, 0x59, 0x3a, 0xc8, 0x3a, 0x91, 0x26, 0x54, 0xee, - 0xea, 0xaa, 0xa3, 0x52, 0xa9, 0x1b, 0xde, 0xa4, 0x9a, 0xf1, 0x77, 0x69, 0xfe, 0xc4, 0xb2, 0xdf, 0xb4, 0x7e, 0xab, - 0xd5, 0xde, 0x58, 0x3d, 0x2a, 0x59, 0x73, 0x9c, 0x4d, 0x48, 0x4a, 0x9f, 0xb0, 0xdd, 0x4c, 0xba, 0xd6, 0x81, 0x27, - 0xc1, 0xe5, 0xd0, 0x13, 0x50, 0x31, 0x68, 0xe2, 0xed, 0x2e, 0x50, 0x8f, 0xc0, 0x33, 0x50, 0x3e, 0x51, 0xeb, 0x80, - 0x9f, 0xd7, 0x5a, 0x9e, 0x32, 0xc2, 0xb0, 0xda, 0x59, 0xb4, 0x1c, 0x9c, 0x77, 0x8a, 0xc0, 0xb5, 0x2b, 0x81, 0xe7, - 0x43, 0xf5, 0x5e, 0x08, 0x18, 0xee, 0x9f, 0x0b, 0x95, 0xcd, 0x6e, 0x86, 0xf3, 0xa8, 0x71, 0x7a, 0xa0, 0xbd, 0xed, - 0x5a, 0x0f, 0xf5, 0xae, 0xdb, 0xb9, 0xad, 0x74, 0xef, 0xd7, 0x4e, 0x26, 0x5d, 0x40, 0x6b, 0xf3, 0xd9, 0x77, 0x76, - 0xa5, 0x75, 0xd3, 0x73, 0xf6, 0x60, 0xeb, 0x96, 0xe8, 0x5c, 0x10, 0x4d, 0x7e, 0x3f, 0xf0, 0xac, 0x6d, 0x47, 0xbf, - 0x4d, 0x3b, 0xb6, 0xb9, 0x87, 0xba, 0x57, 0x50, 0xeb, 0x0d, 0xcd, 0xfb, 0x67, 0xae, 0x6d, 0xc7, 0x57, 0xbf, 0xae, - 0x3b, 0x5c, 0xe7, 0x4d, 0x70, 0xdc, 0x74, 0x6d, 0xab, 0x9d, 0xfd, 0xdc, 0xdd, 0x5b, 0x8b, 0x28, 0xcc, 0xb2, 0x9f, - 0x8a, 0xe2, 0x8f, 0x4a, 0xdf, 0x11, 0xe8, 0xe8, 0xce, 0x8b, 0x3a, 0x5d, 0xee, 0x3e, 0x12, 0xc6, 0x93, 0x57, 0x1f, - 0x11, 0xdd, 0xfa, 0x3e, 0x73, 0xbf, 0x02, 0xdc, 0x08, 0xee, 0x20, 0xda, 0xbb, 0xa5, 0x3e, 0xa9, 0xd5, 0xd7, 0x7a, - 0xed, 0x3c, 0x3d, 0xbf, 0xe9, 0xdc, 0x7e, 0xf7, 0xcd, 0xd1, 0xd6, 0x7b, 0x5c, 0x58, 0x2b, 0x4b, 0x4f, 0x55, 0xc1, - 0xde, 0x2c, 0x4f, 0x55, 0xc1, 0xe4, 0x81, 0xd7, 0xec, 0x17, 0x34, 0xb8, 0xd2, 0xd1, 0xc6, 0x7b, 0xa2, 0x06, 0x6e, - 0x51, 0x58, 0x3a, 0xfc, 0x92, 0x9b, 0xc9, 0x2b, 0xdc, 0x5f, 0x2a, 0x72, 0xb1, 0xef, 0x9c, 0xd1, 0x9d, 0x99, 0x75, - 0xaf, 0x2a, 0x5c, 0x2d, 0xc8, 0xd5, 0x81, 0xad, 0x65, 0x17, 0x87, 0x1b, 0x16, 0x51, 0x80, 0x40, 0x4c, 0xaf, 0xd4, - 0xda, 0x1f, 0xd1, 0x20, 0xe4, 0x83, 0x81, 0x5f, 0x60, 0xb0, 0x2a, 0x50, 0xf8, 0x40, 0x91, 0xfc, 0xb5, 0x27, 0x60, - 0x17, 0xcf, 0x00, 0xdd, 0x8a, 0xcd, 0x8a, 0x11, 0x22, 0x64, 0xb2, 0x9c, 0xd5, 0x74, 0x06, 0xf9, 0xd4, 0x17, 0xdf, - 0xd9, 0xaa, 0xd3, 0x79, 0x5b, 0x53, 0xe5, 0xd4, 0xa1, 0xd0, 0xdd, 0x4d, 0xdd, 0xb9, 0x75, 0x91, 0xa7, 0x0e, 0x21, - 0x57, 0x2a, 0x56, 0x62, 0x1a, 0x6a, 0x9e, 0xa4, 0x19, 0xf5, 0xa5, 0xbd, 0xdf, 0x6b, 0x14, 0x4e, 0xf9, 0xd3, 0x31, - 0xa8, 0xc2, 0x55, 0x0d, 0x71, 0x2c, 0x55, 0xf1, 0xc8, 0x06, 0x81, 0xe6, 0xd5, 0xad, 0x4a, 0x9a, 0x90, 0xc9, 0x8d, - 0xf0, 0xa9, 0x49, 0x29, 0x4f, 0xd3, 0x26, 0xad, 0x14, 0xa9, 0x83, 0x0f, 0xea, 0x54, 0xe3, 0xb9, 0x59, 0x5d, 0x03, - 0x98, 0x71, 0x7e, 0xc5, 0x2f, 0x15, 0x97, 0x51, 0x5b, 0x99, 0x49, 0xfb, 0x93, 0xa3, 0xb1, 0x51, 0x97, 0xd3, 0x46, - 0x19, 0x61, 0xa5, 0x34, 0x27, 0xc5, 0x72, 0x3c, 0xff, 0x80, 0xc1, 0x9a, 0x27, 0xb0, 0x83, 0x89, 0x4a, 0x79, 0x1f, - 0x01, 0xf1, 0x75, 0x92, 0xde, 0x25, 0x90, 0x22, 0xfd, 0x4b, 0x97, 0x3c, 0x75, 0x18, 0x1b, 0x88, 0x31, 0x2b, 0x66, - 0x46, 0xff, 0x83, 0xbb, 0xa4, 0x3f, 0x09, 0x01, 0x70, 0x13, 0x4d, 0xa1, 0x53, 0xe7, 0xc9, 0x45, 0x1e, 0x2c, 0x2f, - 0x3c, 0xb4, 0x62, 0xc4, 0x83, 0xff, 0xbc, 0x0e, 0x11, 0xc4, 0x1c, 0x53, 0x3c, 0xfd, 0xc2, 0xe8, 0x3f, 0x82, 0x4b, - 0x8c, 0x20, 0x74, 0xf7, 0xce, 0x61, 0x08, 0x37, 0x7b, 0x90, 0x41, 0xfd, 0xa1, 0x0e, 0x89, 0x1a, 0xfe, 0x54, 0x79, - 0xd0, 0xff, 0x75, 0x26, 0x2c, 0xb5, 0x9f, 0x9e, 0x0e, 0xa0, 0x82, 0xf7, 0x15, 0x6f, 0x23, 0xe2, 0xfb, 0xc4, 0xcf, - 0xe2, 0xc1, 0xe6, 0xd9, 0x06, 0xac, 0x75, 0x4f, 0x72, 0x63, 0x5d, 0x25, 0x6c, 0x20, 0xe0, 0x6b, 0x4c, 0x6b, 0xcf, - 0x6b, 0xb7, 0x7b, 0xf0, 0x9f, 0xfe, 0x45, 0xc8, 0x80, 0x89, 0xd3, 0xf7, 0x99, 0x93, 0x35, 0xba, 0xc8, 0x64, 0xfa, - 0xd0, 0x49, 0xdf, 0xe8, 0x74, 0xdf, 0x09, 0xff, 0xa8, 0x98, 0xc5, 0x87, 0x5b, 0xfa, 0x4a, 0x93, 0xe2, 0x0e, 0x58, - 0xd9, 0x3c, 0x2a, 0x08, 0x75, 0x2e, 0xa2, 0x6f, 0x4c, 0xf9, 0x96, 0x50, 0xb3, 0x6f, 0x2c, 0x29, 0xa5, 0x7b, 0x0d, - 0xbd, 0x49, 0x6b, 0xfd, 0x36, 0x4a, 0x30, 0x26, 0x3a, 0x9e, 0xbc, 0x8c, 0xc7, 0xca, 0xfb, 0x78, 0xdc, 0x48, 0x85, - 0x3c, 0x00, 0x11, 0xa8, 0x18, 0x7f, 0xba, 0xf2, 0xe4, 0xa4, 0x17, 0xc6, 0xab, 0x50, 0x0a, 0x0a, 0x03, 0xba, 0x02, - 0x29, 0xe0, 0x51, 0x7b, 0xa2, 0xb3, 0xb0, 0x4b, 0xb8, 0x47, 0x37, 0x01, 0x63, 0x7d, 0xfe, 0x09, 0xd0, 0xdc, 0x85, - 0x3b, 0xbc, 0x18, 0xa0, 0x36, 0xf5, 0xea, 0xee, 0xe3, 0x5a, 0x9d, 0xc3, 0x21, 0x38, 0x58, 0x0d, 0x22, 0x38, 0x9d, - 0x4f, 0x1d, 0xcd, 0xb2, 0x00, 0x95, 0x93, 0xe5, 0x46, 0xde, 0x3c, 0x5a, 0xf4, 0xea, 0xbe, 0xb7, 0x4c, 0xcb, 0xaa, - 0x0e, 0x32, 0x96, 0x85, 0x15, 0xe0, 0xea, 0xd0, 0xfa, 0x41, 0xb8, 0x2c, 0x9c, 0x3f, 0x10, 0x82, 0xd8, 0xbd, 0xda, - 0x96, 0x3c, 0x57, 0x73, 0xf8, 0xd9, 0x73, 0xb6, 0xe6, 0x12, 0x75, 0xd2, 0x99, 0x08, 0x40, 0xec, 0xa9, 0x59, 0x45, - 0xd7, 0x40, 0x52, 0xa7, 0x59, 0x45, 0xd7, 0xd4, 0x6c, 0x63, 0x1c, 0xc8, 0x47, 0xab, 0x14, 0xb0, 0xef, 0xa6, 0xe3, - 0x60, 0xf5, 0x2c, 0x96, 0xd7, 0xa1, 0xbb, 0x67, 0x1b, 0xe5, 0x33, 0xa8, 0x5b, 0x6d, 0x8c, 0x89, 0xed, 0xe6, 0xcb, - 0xb9, 0x7e, 0x3b, 0x58, 0xfa, 0x76, 0xd0, 0x9c, 0x53, 0xf6, 0x9d, 0x2e, 0x7b, 0x65, 0x97, 0x4d, 0x3d, 0x77, 0x54, - 0xb4, 0x1a, 0x03, 0x7a, 0x03, 0x0b, 0xd6, 0xe7, 0x22, 0xcd, 0x56, 0xa5, 0x2a, 0x01, 0x2f, 0x8c, 0x15, 0xbb, 0xf3, - 0x1b, 0x99, 0x21, 0x09, 0xf3, 0x38, 0x13, 0xef, 0xe8, 0x5e, 0x0b, 0x93, 0xe3, 0x58, 0x24, 0x53, 0x42, 0xa7, 0x74, - 0x67, 0x1b, 0x3a, 0x57, 0x61, 0x14, 0xd1, 0x5a, 0x49, 0xa5, 0x91, 0xc0, 0xd4, 0x0c, 0x50, 0x32, 0x57, 0xe0, 0x94, - 0x2e, 0xf7, 0xbf, 0x23, 0x31, 0xce, 0x7c, 0x51, 0x32, 0x03, 0xba, 0xe5, 0xd7, 0xc5, 0xba, 0x95, 0x22, 0x23, 0xcc, - 0x9b, 0xe3, 0xf6, 0xba, 0x3e, 0x04, 0x72, 0xb5, 0xec, 0x51, 0x34, 0x0e, 0x0a, 0x1d, 0x2e, 0x55, 0x02, 0xec, 0x8b, - 0xc4, 0xcf, 0x08, 0x5b, 0xda, 0x03, 0xb9, 0x3d, 0x3a, 0x13, 0xe6, 0x9c, 0x93, 0xb2, 0xec, 0x5c, 0x9a, 0xc1, 0xe5, - 0xc4, 0x95, 0xe0, 0x22, 0xbd, 0x6d, 0x4f, 0x93, 0x96, 0xb6, 0x8f, 0x0d, 0xe7, 0x68, 0x68, 0x1b, 0x74, 0xc7, 0xfe, - 0xd0, 0x5c, 0x2c, 0x62, 0xeb, 0x62, 0x31, 0xec, 0xcc, 0x7e, 0xb4, 0x58, 0x80, 0x1c, 0x00, 0x8e, 0xba, 0x0d, 0x1f, - 0xb3, 0x25, 0x70, 0x5a, 0x4d, 0xb3, 0xa9, 0xb7, 0xe1, 0xd5, 0x33, 0xd5, 0xd3, 0x4b, 0x9e, 0x3f, 0x13, 0x66, 0x2c, - 0x36, 0x3c, 0x7f, 0x66, 0x1d, 0x39, 0xd5, 0x33, 0xa1, 0x44, 0xeb, 0x02, 0x9a, 0x81, 0xd7, 0x14, 0x30, 0x62, 0xc9, - 0x64, 0x4a, 0x15, 0x79, 0xdc, 0x9b, 0x6e, 0xd4, 0xe0, 0x05, 0x85, 0x43, 0x20, 0xa5, 0xd3, 0x2f, 0x9e, 0x33, 0xfd, - 0xde, 0xc5, 0xf3, 0x0e, 0x59, 0xdb, 0x30, 0x5d, 0x6e, 0x86, 0xc9, 0xa0, 0xf4, 0x9f, 0x99, 0x89, 0x71, 0x61, 0x4d, - 0x12, 0x40, 0xfc, 0x1b, 0xfb, 0x1d, 0x52, 0xb8, 0x79, 0x7f, 0x39, 0x8c, 0x1f, 0x79, 0x3f, 0x46, 0xf6, 0x24, 0xcd, - 0x10, 0x6b, 0x26, 0x15, 0x72, 0xf7, 0xd5, 0xfa, 0xc7, 0xc4, 0x6e, 0xb2, 0x07, 0x16, 0x80, 0xd8, 0x9a, 0xb6, 0xba, - 0xe5, 0xfd, 0xbe, 0x67, 0x8a, 0x00, 0x3f, 0x28, 0xff, 0xe8, 0xce, 0x90, 0x0c, 0xca, 0xae, 0x1b, 0x42, 0x3c, 0x28, - 0x9b, 0xa6, 0xbd, 0xde, 0xf6, 0xce, 0x3c, 0x56, 0xd7, 0x69, 0x67, 0x71, 0xb5, 0xc8, 0x20, 0xad, 0x3e, 0x64, 0xc7, - 0x99, 0x7d, 0x76, 0xb4, 0x54, 0xba, 0xdf, 0x87, 0x88, 0xb8, 0xa3, 0xac, 0xed, 0xb7, 0x5b, 0x70, 0x0d, 0x47, 0x83, - 0xd0, 0x95, 0xbd, 0x5d, 0x46, 0x1b, 0x17, 0xe2, 0xb8, 0x67, 0x3a, 0x5f, 0xf0, 0xe5, 0x51, 0xda, 0x79, 0x70, 0xaa, - 0x27, 0xfa, 0xdc, 0x74, 0x57, 0x99, 0x5c, 0xeb, 0xb0, 0x1a, 0x83, 0xda, 0x2c, 0x6c, 0xe1, 0x2e, 0x6c, 0xa3, 0x83, - 0xd6, 0xbe, 0x2c, 0xf8, 0xa7, 0x0c, 0xc0, 0x97, 0x9e, 0x2d, 0xdb, 0x5e, 0x93, 0x56, 0x6f, 0x64, 0x14, 0x62, 0x4b, - 0xdb, 0xab, 0x4f, 0x47, 0xf9, 0xb8, 0x39, 0xa1, 0xb8, 0x90, 0xa3, 0xfc, 0xe8, 0x35, 0x44, 0x5d, 0xeb, 0x3a, 0x2e, - 0x16, 0x1d, 0x6e, 0x5c, 0x75, 0xdb, 0x8d, 0xeb, 0x47, 0xc4, 0x5b, 0xa3, 0x4d, 0x0a, 0xb5, 0x32, 0x76, 0x04, 0x2f, - 0xcb, 0x87, 0x43, 0x26, 0x86, 0x43, 0x09, 0x99, 0xfa, 0xd8, 0xbd, 0xa1, 0x69, 0x9f, 0x9f, 0xb6, 0x7e, 0xc4, 0x52, - 0xe3, 0x28, 0x36, 0xbc, 0xd3, 0x77, 0x1e, 0x5b, 0xe3, 0x4a, 0xbe, 0x0c, 0x66, 0xbb, 0x82, 0x6a, 0x6b, 0xbc, 0x61, - 0x2f, 0xe7, 0xbf, 0x54, 0x52, 0xc9, 0xdf, 0xfe, 0x0c, 0xd7, 0xf0, 0xd6, 0x96, 0x0e, 0x9a, 0x6a, 0x96, 0xb3, 0x5c, - 0xdf, 0x0b, 0x8e, 0x3f, 0xee, 0x5e, 0x11, 0x0c, 0x7e, 0x4f, 0x47, 0x41, 0x2e, 0x96, 0x6a, 0x0d, 0x28, 0x48, 0x47, - 0x76, 0x4c, 0x65, 0x81, 0x61, 0x00, 0x6f, 0xc8, 0x00, 0x79, 0x4c, 0xe1, 0x6e, 0xa8, 0xf0, 0xc2, 0x97, 0x15, 0xd9, - 0x25, 0xb0, 0xad, 0x19, 0x1f, 0x33, 0xdc, 0x41, 0xc8, 0x3f, 0x82, 0xdd, 0xb1, 0x15, 0xbb, 0x65, 0x0b, 0x86, 0x64, - 0xe3, 0x38, 0x8c, 0x31, 0x1f, 0x4f, 0xe2, 0x2b, 0x31, 0x89, 0x07, 0x3c, 0x42, 0xc7, 0x88, 0x35, 0xaf, 0x67, 0xb1, - 0x1c, 0x40, 0x76, 0xc7, 0x95, 0x0e, 0x08, 0xa1, 0xb1, 0xa1, 0x25, 0x6f, 0x0a, 0x83, 0x8b, 0x1d, 0xfb, 0x8c, 0x44, - 0x32, 0x0e, 0xc1, 0xa2, 0x55, 0x0d, 0x2c, 0x4c, 0xec, 0x96, 0x17, 0xb3, 0xd5, 0x1c, 0xff, 0x39, 0x1c, 0x10, 0x00, - 0x3b, 0xd8, 0x37, 0xec, 0x2e, 0x42, 0xa4, 0xb7, 0x05, 0xbf, 0xb3, 0x3c, 0x5d, 0xd8, 0x3d, 0x7f, 0xc7, 0xc7, 0xec, - 0xfc, 0x47, 0x0f, 0x22, 0x67, 0xcf, 0x3f, 0x01, 0x1a, 0xe2, 0x3d, 0xbf, 0x4d, 0xbd, 0x8a, 0xdd, 0x12, 0x05, 0xe1, - 0x2d, 0x38, 0x03, 0xdd, 0x43, 0x04, 0xec, 0x3b, 0xbe, 0xc0, 0x58, 0xb1, 0xb3, 0x74, 0xe9, 0x61, 0x46, 0xa8, 0x3d, - 0x9d, 0x2f, 0x6b, 0x35, 0x09, 0x37, 0x57, 0xcb, 0xc9, 0x60, 0xb0, 0xf1, 0x77, 0x7c, 0x0d, 0x7c, 0x30, 0xe7, 0x3f, - 0x7a, 0x3b, 0x2a, 0x17, 0xfe, 0xf3, 0x3a, 0x4b, 0xde, 0xf9, 0xec, 0xdd, 0x80, 0x2f, 0x00, 0x6f, 0x09, 0x1d, 0xb8, - 0xee, 0x7d, 0x26, 0xf1, 0xda, 0xde, 0xe9, 0x6b, 0x04, 0x12, 0xf9, 0x02, 0x30, 0x62, 0x62, 0x7e, 0xbf, 0x83, 0x08, - 0x8c, 0x04, 0x7c, 0x5b, 0xb5, 0x47, 0xfc, 0x96, 0x1b, 0xc0, 0xaf, 0xcc, 0x67, 0x0f, 0x3c, 0xd4, 0x3f, 0x13, 0x9f, - 0xdd, 0xf0, 0x0f, 0xfc, 0xda, 0x93, 0x92, 0x74, 0x39, 0xfb, 0x30, 0x87, 0xeb, 0xa1, 0x94, 0xa7, 0x43, 0xfa, 0xd9, - 0x18, 0x0c, 0x20, 0x14, 0x32, 0x6f, 0x3c, 0x60, 0x4d, 0x0a, 0xf1, 0x2f, 0xe0, 0xdb, 0x51, 0xc2, 0xe6, 0x8d, 0xb7, - 0xf5, 0xb5, 0xbc, 0x79, 0xe3, 0x3d, 0xf8, 0x14, 0x05, 0x58, 0x05, 0xa5, 0x2c, 0xb0, 0x0a, 0xc2, 0x46, 0x1b, 0x61, - 0x0c, 0x5c, 0xbd, 0x6b, 0x0c, 0x75, 0x3d, 0x47, 0x6c, 0x5b, 0xe9, 0xfb, 0xf0, 0x3d, 0x64, 0xc0, 0x07, 0x6f, 0x8a, - 0x92, 0xe8, 0x73, 0x6a, 0x8a, 0xa4, 0x75, 0xcf, 0xfd, 0xd6, 0xba, 0xa3, 0x35, 0xa5, 0x3e, 0x72, 0x35, 0x3e, 0x1c, - 0xea, 0x6b, 0xa1, 0x45, 0x82, 0x29, 0x68, 0x5c, 0x83, 0xb6, 0x00, 0x41, 0x9f, 0x07, 0xc8, 0x5a, 0x52, 0x2c, 0xf8, - 0xf6, 0x57, 0x88, 0xc1, 0x2b, 0xd3, 0x3b, 0x97, 0xab, 0x8c, 0x84, 0xed, 0x85, 0x5f, 0x0e, 0x6b, 0x7f, 0xe2, 0xd4, - 0xc2, 0xd2, 0x6a, 0x0e, 0xea, 0x67, 0xb6, 0x1c, 0xa7, 0xaa, 0xf6, 0x2f, 0x49, 0x52, 0xed, 0x2a, 0x2d, 0xa7, 0xf7, - 0xf6, 0x4d, 0x97, 0x09, 0x36, 0xf6, 0x03, 0xaa, 0x8e, 0xac, 0x86, 0xdd, 0x17, 0xea, 0x8b, 0x9e, 0x92, 0x09, 0xcd, - 0x47, 0x15, 0xcd, 0xb3, 0xfb, 0xcd, 0x8e, 0xfa, 0x4f, 0x2f, 0x87, 0x22, 0x40, 0xb2, 0x4a, 0x8b, 0xa5, 0xc8, 0xd9, - 0xd8, 0x8f, 0x87, 0x49, 0xa6, 0xc2, 0x0b, 0xd2, 0xd1, 0xdd, 0x6f, 0xdc, 0xdf, 0x72, 0x03, 0x59, 0xa1, 0x55, 0x1b, - 0x8c, 0x95, 0xa2, 0x65, 0xb0, 0xbe, 0x1a, 0xf7, 0xfb, 0xe2, 0x6a, 0x3c, 0x15, 0x41, 0x0d, 0xc4, 0x45, 0xe2, 0x7a, - 0x3c, 0xad, 0x89, 0x25, 0xb5, 0x2b, 0x30, 0x46, 0x8f, 0xab, 0xa2, 0xf6, 0xa9, 0xaf, 0x21, 0x14, 0xa9, 0xd6, 0xcc, - 0xb1, 0xc6, 0x8d, 0x11, 0x71, 0x87, 0x95, 0x6b, 0xa7, 0xf6, 0x3a, 0x00, 0xcb, 0xab, 0x71, 0x41, 0xd8, 0x24, 0xc7, - 0xce, 0x05, 0xac, 0x46, 0x43, 0xaa, 0xdd, 0x70, 0xeb, 0x65, 0xe7, 0x37, 0x8f, 0x13, 0x5b, 0x1b, 0xe1, 0x96, 0x02, - 0xca, 0x28, 0xbf, 0xb1, 0x9c, 0xb0, 0x3b, 0xd5, 0x3b, 0x52, 0xb5, 0x23, 0x4e, 0x5c, 0xc0, 0x72, 0xc3, 0x53, 0xab, - 0x6f, 0x62, 0x70, 0x22, 0x54, 0xad, 0x74, 0xb8, 0x93, 0x09, 0xc4, 0xfd, 0xea, 0xbe, 0xee, 0x95, 0xe0, 0x27, 0x21, - 0xaf, 0xdf, 0xf2, 0x0e, 0x00, 0x2b, 0x3e, 0xe4, 0xc5, 0xb4, 0x70, 0xb4, 0x2e, 0x83, 0x32, 0x40, 0x84, 0x66, 0x00, - 0x74, 0x72, 0x75, 0x10, 0xa5, 0x81, 0x2b, 0xee, 0x10, 0xe1, 0xa7, 0xd1, 0xb3, 0xfc, 0x3a, 0x7c, 0x56, 0x4d, 0xc3, - 0x8b, 0x3c, 0x88, 0x2e, 0xaa, 0x20, 0x7a, 0x56, 0x5d, 0x85, 0xcf, 0xf2, 0x69, 0x74, 0x91, 0x07, 0xe1, 0x45, 0xd5, - 0xd8, 0x77, 0xed, 0xee, 0x9e, 0x90, 0xb7, 0x5d, 0xfd, 0x91, 0x73, 0x65, 0x4f, 0x99, 0x9e, 0x9f, 0xd7, 0x7a, 0xa5, - 0x76, 0x9b, 0xeb, 0x35, 0x6a, 0xa6, 0x3e, 0xca, 0xfe, 0x62, 0x1b, 0x0b, 0x8f, 0xe6, 0x10, 0xfa, 0x8c, 0xb4, 0x98, - 0x7b, 0x9c, 0xeb, 0xcd, 0x9e, 0x14, 0x06, 0x46, 0x4c, 0x2a, 0x19, 0x39, 0xbd, 0xc0, 0x45, 0xa8, 0x42, 0x0c, 0x6b, - 0xe9, 0x6a, 0x9f, 0x75, 0xe9, 0x0d, 0xd4, 0x35, 0xc5, 0xbe, 0x86, 0x0c, 0xbc, 0x68, 0x7a, 0x19, 0x8c, 0x01, 0x39, - 0x02, 0xef, 0xf8, 0x6c, 0x09, 0x07, 0xe6, 0x1a, 0xa0, 0x6f, 0x1e, 0xf5, 0x75, 0xb9, 0xe3, 0x6b, 0xd5, 0x37, 0xd3, - 0xf5, 0x48, 0x29, 0x3f, 0x56, 0xfc, 0xee, 0xe2, 0x39, 0xbb, 0xe5, 0x1a, 0x15, 0xe5, 0xa5, 0x5e, 0xac, 0xf7, 0xc0, - 0x55, 0xf7, 0x12, 0x6e, 0xb3, 0x78, 0xec, 0xca, 0x03, 0x96, 0x6d, 0xd9, 0x03, 0xbb, 0x61, 0x1f, 0xd8, 0x13, 0xf6, - 0x96, 0x7d, 0x65, 0x35, 0x42, 0x94, 0x97, 0x4a, 0xca, 0xf3, 0x17, 0xfc, 0x56, 0xda, 0x1e, 0x25, 0x2c, 0xd9, 0x83, - 0x6d, 0xa7, 0x19, 0x6e, 0xd8, 0x07, 0xbe, 0x18, 0xae, 0xd8, 0x5b, 0xc8, 0x86, 0x42, 0xf1, 0x60, 0xc5, 0x6a, 0xb8, - 0xc2, 0x52, 0x06, 0x7d, 0x1a, 0x96, 0x96, 0xb0, 0x68, 0x0a, 0x45, 0x29, 0xfa, 0x2d, 0xaf, 0x09, 0x3b, 0xad, 0xc6, - 0x42, 0xe4, 0x87, 0x86, 0x2b, 0xf6, 0xc0, 0x17, 0x83, 0x15, 0xfb, 0xa0, 0x6d, 0x44, 0x83, 0x8d, 0x5b, 0x1c, 0x81, - 0x59, 0xe9, 0xc2, 0xa4, 0x40, 0xbd, 0xb5, 0x6f, 0x82, 0x1b, 0x76, 0x83, 0xf5, 0x7b, 0x82, 0x45, 0xa3, 0xcc, 0x3f, - 0x58, 0xb1, 0xaf, 0x5c, 0x62, 0xa8, 0xb9, 0xe5, 0x49, 0xc7, 0x50, 0x5d, 0x20, 0x5d, 0x11, 0x9e, 0x70, 0x7a, 0x91, - 0x7d, 0xc5, 0x32, 0xe8, 0x2b, 0xc3, 0x15, 0xdb, 0x62, 0xed, 0x6e, 0x8c, 0x71, 0xcb, 0xaa, 0x9e, 0x04, 0x05, 0x46, - 0x59, 0xa5, 0xb4, 0x5c, 0x1c, 0xb1, 0x6c, 0xea, 0xa8, 0x41, 0x6d, 0x18, 0xd0, 0x07, 0xa3, 0xff, 0xf0, 0xf5, 0xbb, - 0x1f, 0xbd, 0x52, 0xdf, 0x7c, 0x5f, 0x3a, 0xde, 0x95, 0x25, 0x7a, 0x57, 0xfe, 0xca, 0xcb, 0xd9, 0xcb, 0xf9, 0x44, - 0xd7, 0x92, 0x36, 0x19, 0x72, 0x37, 0x9d, 0xbd, 0xec, 0xf0, 0xb7, 0xfc, 0xd5, 0xf7, 0x1b, 0xab, 0x8f, 0xd5, 0x77, - 0x75, 0xf7, 0x3e, 0x0c, 0x36, 0x8d, 0x53, 0xf1, 0xdd, 0xe9, 0x8a, 0x63, 0x3b, 0x6b, 0xed, 0x9d, 0xf9, 0x3f, 0x5c, - 0xeb, 0x2d, 0x8e, 0xdd, 0x0d, 0xdf, 0x0e, 0x37, 0xf6, 0x30, 0xc8, 0xef, 0x4b, 0xc5, 0x71, 0x56, 0xf3, 0x17, 0x5e, - 0xa7, 0x24, 0x0b, 0xa8, 0x46, 0x9f, 0x8d, 0x34, 0x74, 0xc9, 0x4c, 0x4c, 0x43, 0x7c, 0x91, 0x01, 0x3a, 0x17, 0x88, - 0x67, 0xf7, 0x7c, 0x3c, 0xb9, 0xbf, 0x8a, 0x27, 0xf7, 0x03, 0xfe, 0xd9, 0xb4, 0xa0, 0xbd, 0xe0, 0xee, 0x7d, 0xf6, - 0x2b, 0x2f, 0xec, 0x25, 0xf9, 0xd2, 0x67, 0xef, 0x85, 0xbb, 0x4a, 0x5f, 0xfa, 0xec, 0xab, 0xe0, 0xbf, 0x8e, 0x34, - 0x59, 0x06, 0xfb, 0x5a, 0xf3, 0x5f, 0x47, 0xc8, 0xfa, 0xc1, 0xbe, 0x08, 0xfe, 0x1e, 0xfc, 0xbf, 0xab, 0x04, 0x2d, - 0xe3, 0x5f, 0x6a, 0xf5, 0xf3, 0x83, 0x8c, 0xcd, 0x81, 0x37, 0xa1, 0x15, 0xf4, 0xe6, 0x6d, 0x2d, 0x7f, 0x12, 0x17, - 0x47, 0xaa, 0x9e, 0x1a, 0x0e, 0x5a, 0x2c, 0x66, 0x51, 0x1f, 0xa5, 0x53, 0x79, 0x93, 0x77, 0x3c, 0x93, 0x16, 0xe6, - 0x7b, 0x08, 0x07, 0x7e, 0x67, 0xc3, 0x14, 0xec, 0x38, 0x6e, 0x06, 0xef, 0x18, 0x40, 0x48, 0x66, 0xd3, 0x2d, 0xbf, - 0xe1, 0x4f, 0xf8, 0x57, 0xbe, 0x0b, 0x1e, 0xf8, 0x07, 0xfe, 0x96, 0xd7, 0x35, 0xdf, 0xb1, 0xa5, 0x84, 0x3c, 0xad, - 0xb7, 0x97, 0xc1, 0x96, 0xd5, 0xbb, 0xcb, 0xe0, 0x81, 0xd5, 0xdb, 0xe7, 0xc1, 0x0d, 0xab, 0x77, 0xcf, 0x83, 0x0f, - 0x6c, 0x7b, 0x19, 0x3c, 0x61, 0xbb, 0xcb, 0xe0, 0x2d, 0xdb, 0x3e, 0x0f, 0xbe, 0xb2, 0xdd, 0xf3, 0xa0, 0x56, 0x48, - 0x0f, 0x5f, 0x85, 0x64, 0x3a, 0xf9, 0x5a, 0x33, 0xc3, 0xaa, 0x1b, 0x7c, 0x11, 0xd6, 0x2f, 0xaa, 0x65, 0xf0, 0xa5, - 0x66, 0xba, 0xcd, 0x81, 0x10, 0x4c, 0xb7, 0x38, 0xb8, 0xa5, 0x27, 0xa6, 0x5d, 0x41, 0x2a, 0x58, 0x57, 0x4b, 0x83, - 0x45, 0xdd, 0xb4, 0x4e, 0x66, 0xc7, 0x3b, 0x31, 0xee, 0xf0, 0x4e, 0x5c, 0xb0, 0x65, 0xd3, 0xe9, 0xaa, 0x73, 0xfa, - 0x3c, 0xd0, 0x47, 0x80, 0xde, 0xfb, 0x2b, 0xe9, 0x41, 0x53, 0x34, 0x3c, 0x57, 0xba, 0xe3, 0xd6, 0x7e, 0x1f, 0x5a, - 0xfb, 0x3d, 0x93, 0x8a, 0xb4, 0x88, 0x45, 0x65, 0x51, 0x55, 0xc8, 0x27, 0x1e, 0x64, 0x5a, 0xab, 0x96, 0x30, 0x52, - 0x67, 0x02, 0x26, 0x7d, 0x41, 0x87, 0x41, 0x4e, 0x76, 0x05, 0xb6, 0xe4, 0x9b, 0x41, 0xc2, 0xd6, 0x3c, 0x9e, 0x0e, - 0x93, 0x60, 0xc9, 0xee, 0xf8, 0xb0, 0x5b, 0x2c, 0x58, 0xa9, 0x30, 0x26, 0x7d, 0x7d, 0x3a, 0xda, 0xdd, 0x79, 0x6f, - 0x95, 0xc6, 0x71, 0x26, 0x50, 0xe7, 0x56, 0xe9, 0x6d, 0x7e, 0xeb, 0xec, 0xea, 0x6b, 0xb5, 0xcb, 0x83, 0xc0, 0xf0, - 0x2b, 0x10, 0xed, 0x10, 0xef, 0x1d, 0xd4, 0x18, 0xe9, 0x96, 0xcc, 0xba, 0xaf, 0xec, 0x7d, 0x7d, 0x6b, 0xb6, 0xea, - 0x7f, 0xb7, 0x08, 0xda, 0xcb, 0x65, 0xef, 0x7f, 0x36, 0xaf, 0xfe, 0xd6, 0xf1, 0xea, 0xc6, 0x9f, 0x3c, 0xf0, 0xcf, - 0x18, 0x9d, 0x80, 0x89, 0x6c, 0xc7, 0x3f, 0x8f, 0xb6, 0x8d, 0x53, 0x9e, 0xdc, 0xcb, 0xff, 0xaf, 0x14, 0x68, 0xef, - 0xe6, 0x95, 0xbd, 0x29, 0x6e, 0x79, 0xc7, 0x5e, 0xbe, 0xb4, 0xf6, 0x44, 0x83, 0x50, 0xf2, 0x99, 0xbb, 0x41, 0xd1, - 0xb0, 0x27, 0xbe, 0xe4, 0xd5, 0xec, 0xf3, 0x7c, 0xb2, 0xe5, 0xc7, 0x3b, 0xe2, 0xe7, 0x8e, 0x1d, 0xf1, 0xa5, 0x3f, - 0x58, 0x36, 0xdf, 0xea, 0xd5, 0xce, 0x9d, 0xdc, 0xa9, 0xf4, 0x8e, 0x1f, 0xef, 0xe3, 0xc3, 0x7f, 0xbb, 0xd2, 0xbb, - 0xef, 0xae, 0xb4, 0x5d, 0xe5, 0xee, 0xce, 0x37, 0x1d, 0xdf, 0xc8, 0x5a, 0x63, 0xb8, 0x99, 0x51, 0x30, 0xc2, 0xb4, - 0x85, 0x69, 0x1a, 0x44, 0x96, 0x62, 0x11, 0x12, 0x35, 0x4a, 0xe7, 0x44, 0x9f, 0x05, 0x9d, 0x82, 0x2e, 0x6e, 0xf4, - 0xb7, 0x7c, 0xcc, 0x16, 0xc6, 0x65, 0xf3, 0xf6, 0x6a, 0x31, 0x19, 0x0c, 0x6e, 0xfd, 0xfd, 0x3d, 0x0f, 0x67, 0xb7, - 0x73, 0xf6, 0x8e, 0xdf, 0xd3, 0x7a, 0x9a, 0xa8, 0xc6, 0x17, 0x8f, 0x49, 0x60, 0xb7, 0xbe, 0x3f, 0xb1, 0x88, 0x60, - 0xed, 0x1b, 0xe7, 0xad, 0x3f, 0x90, 0x66, 0x69, 0xb9, 0xb5, 0x7f, 0x78, 0x5c, 0x43, 0x71, 0x0b, 0x42, 0xc6, 0x07, - 0x5b, 0xe5, 0xf0, 0x96, 0x7f, 0xf2, 0xde, 0xf9, 0xd3, 0x77, 0x3a, 0xf8, 0x66, 0xa2, 0xce, 0xa5, 0xb7, 0x17, 0xcf, - 0xd9, 0xaf, 0xfc, 0xb3, 0x3c, 0x53, 0xde, 0x0b, 0x39, 0x6d, 0x6f, 0x90, 0xc4, 0x89, 0x8e, 0x8a, 0xaf, 0x6e, 0x22, - 0x81, 0x42, 0x20, 0x1e, 0x47, 0xcd, 0x1f, 0x26, 0xe5, 0xd4, 0xdb, 0x01, 0xc9, 0x2b, 0xb7, 0x15, 0xd1, 0xb7, 0x9c, - 0xf3, 0xc5, 0xf0, 0x72, 0xfa, 0xb5, 0xdb, 0xb7, 0x47, 0x85, 0xb5, 0xa9, 0x88, 0xb7, 0x5b, 0x0c, 0xc2, 0x3a, 0x99, - 0x59, 0xe6, 0x92, 0x2f, 0x7d, 0xad, 0xcd, 0xdc, 0x63, 0x7a, 0xc7, 0x99, 0x66, 0xc8, 0xe8, 0x0b, 0xcc, 0x4c, 0x87, - 0xc3, 0xdd, 0x39, 0x96, 0xc7, 0x87, 0x6f, 0x9f, 0x3d, 0x19, 0x3c, 0xc1, 0x10, 0x2e, 0x2b, 0x2c, 0xe4, 0x2b, 0x1f, - 0x66, 0x75, 0xeb, 0xda, 0x71, 0xf1, 0x7c, 0xf8, 0x12, 0xf2, 0x06, 0x5d, 0x0f, 0x4d, 0x11, 0xad, 0xf2, 0x3b, 0x8a, - 0x3e, 0x51, 0x72, 0xd0, 0xf1, 0x04, 0x6a, 0x87, 0x5c, 0xb8, 0x5f, 0x9f, 0x71, 0x50, 0x74, 0x60, 0xa9, 0xfd, 0xfe, - 0xf9, 0x67, 0x22, 0x94, 0x86, 0xf1, 0x7e, 0x19, 0x46, 0x7f, 0xc4, 0x65, 0xb1, 0x86, 0x23, 0x76, 0x00, 0x9f, 0x7b, - 0xa6, 0xaf, 0x61, 0x77, 0xbe, 0xef, 0x07, 0xde, 0x96, 0xdf, 0xb0, 0xaf, 0xdc, 0xbb, 0x1c, 0xbe, 0xf5, 0x9f, 0x3d, - 0x01, 0xf9, 0x09, 0xc6, 0xe5, 0x0b, 0x86, 0xc4, 0x76, 0x14, 0xa3, 0xd6, 0xe1, 0x97, 0x1a, 0x62, 0xb5, 0x3e, 0x23, - 0x75, 0x17, 0xa4, 0x7f, 0x54, 0xc8, 0x7e, 0x42, 0x60, 0x35, 0x49, 0x9f, 0x02, 0x93, 0xf8, 0xb6, 0x86, 0x04, 0xd2, - 0xb4, 0x40, 0x0c, 0x0e, 0x14, 0x9f, 0x0a, 0xfe, 0x75, 0xf8, 0x85, 0xe4, 0xbf, 0x45, 0xcd, 0xc7, 0xf0, 0x37, 0x0c, - 0xcd, 0xa4, 0x7a, 0x48, 0xeb, 0x28, 0xf1, 0x6a, 0x38, 0xf5, 0xc2, 0x4a, 0xa8, 0x93, 0x21, 0x48, 0xc5, 0x90, 0x0b, - 0x71, 0xf1, 0x7c, 0x72, 0x5b, 0x8a, 0xf0, 0x8f, 0x09, 0x3e, 0x93, 0x2b, 0x4d, 0x3e, 0xa3, 0x27, 0x8d, 0x2c, 0xe0, - 0x41, 0xbe, 0x2f, 0x7b, 0x35, 0x58, 0xd4, 0x43, 0x7e, 0x5b, 0xbb, 0xef, 0xcb, 0x39, 0x41, 0x8f, 0xec, 0x07, 0x34, - 0x07, 0x03, 0x35, 0x03, 0x29, 0x43, 0x70, 0x0b, 0x97, 0x7e, 0x4f, 0x15, 0xe4, 0xcb, 0xef, 0x7d, 0x11, 0x32, 0x70, - 0x65, 0x41, 0x98, 0x72, 0xa9, 0x90, 0x02, 0xc7, 0x6d, 0x3d, 0xf8, 0xa2, 0xd1, 0x49, 0x24, 0xf8, 0x94, 0x80, 0x24, - 0x69, 0x79, 0x20, 0x69, 0xc4, 0x74, 0x20, 0x2e, 0x94, 0xa6, 0x59, 0x49, 0x11, 0x87, 0xd8, 0x55, 0xdf, 0x21, 0xe1, - 0x59, 0xf0, 0x81, 0xc1, 0xda, 0x91, 0xa2, 0xc5, 0x57, 0x63, 0x3a, 0xd6, 0x61, 0x43, 0x77, 0xb2, 0xb8, 0x5f, 0x25, - 0x75, 0x1a, 0x89, 0x2b, 0xef, 0x85, 0xfc, 0xf9, 0x4f, 0x25, 0x02, 0xe9, 0x5d, 0x0d, 0xc4, 0x20, 0xf8, 0x01, 0xfa, - 0x0f, 0x58, 0xe4, 0x20, 0x28, 0xd5, 0x65, 0x98, 0x57, 0x19, 0x15, 0x38, 0xdb, 0xb1, 0xed, 0x9c, 0xa9, 0xba, 0x05, - 0x5f, 0x84, 0x61, 0x48, 0x3b, 0x5b, 0x35, 0x27, 0xb7, 0x7a, 0x03, 0xf5, 0x4c, 0xe2, 0x48, 0x2d, 0xc5, 0x91, 0xb6, - 0xe6, 0x3e, 0x5d, 0x7a, 0xdd, 0xf2, 0x82, 0x86, 0x0b, 0xd0, 0x8b, 0xd2, 0x5d, 0xe7, 0x13, 0x0a, 0x5d, 0x56, 0xe3, - 0x6a, 0x28, 0xea, 0x50, 0x8e, 0xb1, 0xf6, 0xe7, 0x4a, 0x9e, 0xdf, 0x81, 0xf5, 0x08, 0x0d, 0x5f, 0x95, 0x3a, 0x88, - 0xed, 0x27, 0x7a, 0xd7, 0xa9, 0xd4, 0xdf, 0x00, 0x30, 0x70, 0xea, 0x78, 0xa8, 0x8f, 0xda, 0x29, 0x64, 0x3b, 0xf7, - 0x96, 0x18, 0x95, 0x2b, 0xe1, 0xa9, 0xd2, 0xf2, 0x94, 0xb2, 0xea, 0x6b, 0xc1, 0xad, 0xec, 0x3e, 0x1b, 0x40, 0x46, - 0x1b, 0x14, 0xc8, 0x33, 0x6a, 0x6b, 0x3c, 0x48, 0x35, 0xcd, 0x12, 0xc7, 0xf0, 0x41, 0x91, 0x66, 0x15, 0x58, 0xbc, - 0xcc, 0x25, 0x73, 0x50, 0xb0, 0x5c, 0x6f, 0x36, 0xd3, 0x4c, 0xf5, 0x45, 0x6e, 0x6f, 0x34, 0x5e, 0xa6, 0xff, 0x66, - 0xc9, 0x80, 0x47, 0x17, 0xcf, 0xfd, 0x00, 0xd2, 0x24, 0xc5, 0x03, 0x24, 0xc1, 0xf6, 0x60, 0x17, 0x3b, 0x0c, 0x5b, - 0xc5, 0xca, 0x9e, 0x3c, 0x5d, 0xee, 0xd0, 0x94, 0x4b, 0x70, 0xc9, 0x89, 0xb9, 0x9c, 0xfa, 0xbe, 0x64, 0xbd, 0xa1, - 0x38, 0x65, 0xd3, 0x04, 0x94, 0x04, 0xda, 0x2d, 0xf8, 0x2f, 0x7c, 0x6a, 0xe8, 0xb4, 0x00, 0x4b, 0x6d, 0x37, 0xe0, - 0xbf, 0xd0, 0x2f, 0xb6, 0xbb, 0xa8, 0x1f, 0x98, 0x07, 0x7b, 0xb3, 0xb8, 0x32, 0x06, 0x9c, 0x24, 0xae, 0x34, 0x8f, - 0x5c, 0x3f, 0x28, 0xfa, 0x74, 0x59, 0x3b, 0x70, 0xa6, 0xb8, 0xb0, 0x4a, 0x6d, 0x92, 0x5e, 0xfb, 0x2d, 0x35, 0xf1, - 0x26, 0x4a, 0xaa, 0xc2, 0x76, 0x48, 0xfb, 0x97, 0x94, 0x33, 0x55, 0xdc, 0x21, 0x7a, 0xb2, 0x9b, 0xb8, 0x0a, 0xbc, - 0xb0, 0xaa, 0xd8, 0x08, 0xb5, 0x19, 0x59, 0x4e, 0xe0, 0x74, 0x8f, 0xd5, 0x05, 0x1f, 0xdb, 0xd5, 0xec, 0x82, 0x95, - 0x6c, 0xcd, 0xa4, 0xfb, 0xbc, 0x1d, 0x73, 0x21, 0xaf, 0xf4, 0xb2, 0x68, 0x05, 0xb4, 0x07, 0x81, 0xc3, 0x2f, 0x35, - 0xdd, 0xa3, 0x67, 0x9b, 0x6d, 0x6a, 0xb3, 0xb1, 0xb5, 0x08, 0x21, 0x03, 0xd1, 0xd0, 0x17, 0x72, 0x46, 0x91, 0xaf, - 0xd2, 0x72, 0xad, 0x36, 0x56, 0x19, 0x2f, 0x30, 0x11, 0x64, 0x38, 0x0b, 0xef, 0xd1, 0xd3, 0x7a, 0xa4, 0x29, 0x26, - 0xc1, 0x49, 0x17, 0x7f, 0x01, 0x36, 0x94, 0x27, 0xb9, 0x39, 0x20, 0x07, 0x50, 0xb9, 0x14, 0xa5, 0x52, 0x06, 0xff, - 0xac, 0xee, 0xc8, 0xb6, 0xea, 0xbf, 0xd3, 0x40, 0x06, 0x77, 0xa0, 0x6f, 0x7b, 0xa1, 0xb5, 0xa3, 0x9d, 0x2b, 0x5b, - 0xd3, 0xb6, 0x4c, 0xf3, 0x18, 0x59, 0x6c, 0x00, 0xf9, 0x44, 0x3a, 0x07, 0x22, 0xaf, 0x89, 0xc6, 0x3b, 0xbb, 0xe6, - 0xe3, 0xa9, 0x78, 0x4c, 0xde, 0xab, 0x7c, 0xdf, 0xdc, 0xeb, 0x83, 0x31, 0xf6, 0x2d, 0x28, 0x13, 0x1f, 0xad, 0xb6, - 0xd6, 0x25, 0xd6, 0x5b, 0xa5, 0x49, 0x74, 0xc3, 0x15, 0x74, 0x1c, 0x89, 0x1b, 0xc4, 0xe0, 0x98, 0xf1, 0xda, 0x2a, - 0x4b, 0x5f, 0x61, 0x99, 0xeb, 0x98, 0x25, 0x43, 0x26, 0x75, 0x9e, 0x28, 0x78, 0xf2, 0xf3, 0x84, 0x64, 0x44, 0xd4, - 0x6c, 0xcb, 0x51, 0xca, 0x4d, 0x0b, 0xb8, 0xcc, 0xc8, 0x00, 0xbe, 0x49, 0x13, 0x80, 0x72, 0xf9, 0x12, 0xa4, 0xd2, - 0x10, 0xc1, 0x35, 0xdb, 0x4b, 0x46, 0xb7, 0x8e, 0xd6, 0x41, 0x95, 0x64, 0xee, 0xe0, 0xdc, 0xce, 0x22, 0xa5, 0xde, - 0x7c, 0x84, 0x61, 0x27, 0x1f, 0xc3, 0x3a, 0xc1, 0x6f, 0x03, 0x6a, 0xd2, 0xe7, 0xc2, 0x8b, 0x46, 0x80, 0xa6, 0xbe, - 0x53, 0x65, 0x7c, 0x2e, 0xbc, 0x6c, 0xb4, 0x65, 0x19, 0xa5, 0x50, 0x5d, 0x30, 0xbb, 0x35, 0x5d, 0x88, 0x79, 0x55, - 0x0d, 0xb4, 0x41, 0x6e, 0xd7, 0x31, 0x03, 0x1a, 0xb5, 0x5d, 0x79, 0x64, 0x01, 0x6e, 0xcd, 0x44, 0x60, 0xe4, 0xfc, - 0x87, 0xfc, 0x95, 0x0a, 0xe7, 0xe9, 0xf7, 0x43, 0x6f, 0xbf, 0x0d, 0xa2, 0xd1, 0xf6, 0x92, 0xed, 0x82, 0x68, 0xb4, - 0xbb, 0x6c, 0x18, 0xfd, 0x7e, 0x4e, 0xbf, 0x9f, 0x37, 0xa0, 0x2a, 0x11, 0x26, 0xe2, 0x5e, 0xbf, 0x51, 0xcb, 0x57, - 0x6a, 0xfd, 0x4e, 0x2d, 0x5f, 0xaa, 0xe1, 0xad, 0x3d, 0x89, 0x04, 0x91, 0xa5, 0xb1, 0x79, 0x90, 0x6c, 0xa9, 0x96, - 0x4a, 0xc7, 0xa8, 0x32, 0xa2, 0x96, 0xce, 0xe6, 0x58, 0x31, 0xd2, 0xce, 0x41, 0xc9, 0x80, 0x4c, 0x8b, 0xab, 0x1a, - 0xd3, 0xcd, 0x8a, 0x96, 0x98, 0x8c, 0xb0, 0xb2, 0x2d, 0x6f, 0x37, 0xa9, 0x9a, 0xce, 0xc9, 0xcd, 0xad, 0x52, 0x6e, - 0x6e, 0x05, 0xcf, 0xbf, 0xa1, 0x5b, 0x2e, 0xb9, 0xf6, 0x32, 0x9b, 0x16, 0x4a, 0xb7, 0x8c, 0x6b, 0xb0, 0xb5, 0x6f, - 0x02, 0x59, 0xe6, 0x23, 0x45, 0x8d, 0xed, 0x45, 0xa3, 0x7c, 0x83, 0x6c, 0x45, 0x8c, 0x3a, 0x65, 0xc1, 0xf8, 0xdb, - 0x1d, 0x3d, 0x90, 0x81, 0xaa, 0xaa, 0x36, 0x0e, 0xee, 0xac, 0xf4, 0x87, 0xe5, 0xc5, 0x73, 0x96, 0x58, 0xe9, 0xe4, - 0x42, 0x15, 0xfa, 0x83, 0x10, 0xdd, 0x54, 0x36, 0x1c, 0x1c, 0xea, 0x62, 0x2b, 0x03, 0x42, 0x0f, 0xd3, 0x7b, 0x1b, - 0x2b, 0x59, 0xee, 0x9a, 0xf2, 0xc5, 0x8c, 0x27, 0x1c, 0x47, 0x5f, 0xae, 0x16, 0x61, 0xad, 0x16, 0xd9, 0x09, 0xf0, - 0xd0, 0x5a, 0x2d, 0x85, 0x5c, 0x2d, 0xc2, 0x99, 0xe9, 0x42, 0xcd, 0xf4, 0x0c, 0x14, 0x90, 0x42, 0xcd, 0xf2, 0x04, - 0x60, 0xe1, 0x85, 0x99, 0xe1, 0xc2, 0xcc, 0x70, 0x1c, 0x52, 0xe3, 0xff, 0xa0, 0xf7, 0x3a, 0xf7, 0xdc, 0x72, 0x37, - 0x3a, 0x8d, 0xf8, 0x76, 0xb4, 0xc1, 0x1c, 0x1f, 0x84, 0x13, 0x08, 0x15, 0x2c, 0x11, 0xab, 0x47, 0x23, 0xec, 0xa8, - 0xa1, 0x72, 0xb4, 0x5f, 0x16, 0x96, 0x64, 0x49, 0x58, 0x92, 0x7b, 0x35, 0xce, 0xa5, 0xe5, 0xe2, 0x55, 0x12, 0x88, - 0x44, 0xc6, 0x4b, 0x69, 0x82, 0x4f, 0x78, 0x39, 0x32, 0x52, 0xf3, 0x64, 0x91, 0x7a, 0x39, 0xcb, 0xd8, 0x18, 0x31, - 0x8c, 0x42, 0xbf, 0xa9, 0xfa, 0xfd, 0xb4, 0xf4, 0x72, 0x6a, 0xe7, 0x67, 0x70, 0xbd, 0x3c, 0x75, 0x16, 0x39, 0x42, - 0x5e, 0x8d, 0xa4, 0xc2, 0xf2, 0x5a, 0xa9, 0xa7, 0x2f, 0xc1, 0x07, 0x75, 0xf7, 0x46, 0x01, 0x10, 0x17, 0xb9, 0xf4, - 0xaf, 0x2d, 0xe1, 0xd2, 0x94, 0x1b, 0x18, 0xf4, 0x90, 0xe7, 0x24, 0x84, 0x4a, 0x10, 0x92, 0xc2, 0xba, 0x71, 0x5f, - 0x3c, 0x9f, 0xb8, 0xee, 0x2c, 0x36, 0x30, 0xc1, 0xe1, 0x00, 0x88, 0x07, 0x53, 0x2f, 0x1a, 0xf0, 0x52, 0xcd, 0x99, - 0x4f, 0x5e, 0x4e, 0x30, 0x19, 0xa0, 0xaa, 0x18, 0x38, 0x65, 0x3d, 0x93, 0x8f, 0x8c, 0x9b, 0x99, 0xef, 0x07, 0xf8, - 0x6e, 0x5d, 0x48, 0xf4, 0x07, 0x05, 0x50, 0x90, 0x29, 0x80, 0x82, 0xc4, 0x00, 0x14, 0xc4, 0x06, 0xa0, 0x60, 0xd3, - 0xf0, 0xb5, 0xd4, 0xe1, 0x46, 0x40, 0x17, 0xe1, 0x43, 0xcf, 0xc2, 0xc6, 0x0a, 0xc5, 0xb3, 0x31, 0x1b, 0xb3, 0x42, - 0xed, 0x3c, 0xb9, 0x9c, 0x8a, 0x9d, 0xc5, 0x58, 0x57, 0x91, 0x65, 0xe2, 0x85, 0x84, 0x22, 0xe7, 0xdc, 0x48, 0xd4, - 0xdd, 0xcf, 0xbd, 0x97, 0x64, 0x2c, 0x99, 0x37, 0x34, 0x6a, 0x30, 0x2f, 0xbb, 0x0e, 0x60, 0x5a, 0xf2, 0x6d, 0x41, - 0x83, 0xe9, 0x54, 0x79, 0x44, 0x9a, 0x04, 0xb5, 0x73, 0x99, 0x14, 0x39, 0x21, 0x4c, 0x82, 0x5e, 0x09, 0x7e, 0x23, - 0x51, 0xfe, 0xbf, 0xe9, 0x04, 0x0f, 0x70, 0x4c, 0xb4, 0x4a, 0xbe, 0x82, 0x01, 0x33, 0xe7, 0x2f, 0xa4, 0x53, 0x36, - 0x42, 0x31, 0x96, 0x69, 0x3c, 0xfa, 0xca, 0x86, 0x08, 0x6d, 0xf5, 0x02, 0x4d, 0x4c, 0x50, 0x07, 0x78, 0x44, 0x7f, - 0x8d, 0xbe, 0x1a, 0x0a, 0x95, 0xae, 0x46, 0xea, 0x9a, 0x9d, 0x73, 0xfe, 0xbe, 0x36, 0x9c, 0xc8, 0x98, 0x36, 0x05, - 0xbe, 0x01, 0x81, 0x7c, 0x03, 0x01, 0xe0, 0xaa, 0xe9, 0xcc, 0x5e, 0x01, 0x9c, 0x03, 0x01, 0x3c, 0xce, 0x3b, 0x1e, - 0x3f, 0xd2, 0x5f, 0xc5, 0x71, 0xef, 0x34, 0x0d, 0xdb, 0x7f, 0x05, 0xc6, 0x62, 0x28, 0xc7, 0xf3, 0x9d, 0x82, 0x64, - 0x8f, 0x52, 0x96, 0xae, 0x9a, 0xc8, 0x0e, 0xc5, 0xfa, 0x34, 0xa7, 0x8c, 0xa5, 0x6d, 0x39, 0x46, 0x1b, 0xaf, 0x1f, - 0xe3, 0xf1, 0xcd, 0x8d, 0x9e, 0x7c, 0xd0, 0x83, 0xdb, 0xdb, 0xdb, 0xd7, 0x3d, 0x66, 0xf3, 0xad, 0x58, 0x3c, 0x2b, - 0xe2, 0xc4, 0x69, 0x1d, 0x72, 0x80, 0x83, 0x9c, 0x84, 0x40, 0x3a, 0xc6, 0xa5, 0x16, 0x1d, 0xd4, 0x2c, 0xe7, 0x35, - 0xb0, 0xcc, 0x22, 0xc8, 0x06, 0x88, 0x6a, 0x9a, 0x8a, 0xd5, 0xf0, 0xa0, 0x54, 0xcd, 0x29, 0x95, 0xda, 0x37, 0x9c, - 0xad, 0x4e, 0x9f, 0x58, 0xb5, 0x09, 0xb7, 0xfe, 0xb5, 0xf6, 0x04, 0x6d, 0x25, 0x0d, 0x84, 0x7a, 0xbe, 0x4e, 0xef, - 0x28, 0x8a, 0xc7, 0x99, 0x89, 0xa7, 0x2a, 0x30, 0xf6, 0xad, 0x1d, 0x41, 0x41, 0xd2, 0x74, 0x1d, 0x70, 0x98, 0x46, - 0x27, 0x2c, 0xfe, 0x29, 0x7d, 0x28, 0x2f, 0x6a, 0x05, 0x4e, 0xf2, 0x77, 0xe1, 0x22, 0x92, 0x58, 0xe8, 0x97, 0x04, - 0x40, 0x22, 0x83, 0x57, 0xa3, 0x62, 0x2d, 0x54, 0x80, 0x9c, 0xa2, 0xf4, 0x56, 0xf1, 0x71, 0x29, 0x4a, 0x95, 0x52, - 0x99, 0x1b, 0x95, 0x02, 0xc2, 0xda, 0xc0, 0xd1, 0x05, 0x7c, 0x01, 0x41, 0x6b, 0xb9, 0x5b, 0xdb, 0x9e, 0x37, 0x32, - 0x9f, 0x99, 0xe6, 0x69, 0xf5, 0x51, 0xfd, 0xfd, 0x61, 0x89, 0x61, 0x36, 0x9e, 0xfe, 0xbe, 0xcd, 0x10, 0x6e, 0xfe, - 0x86, 0x21, 0xba, 0x03, 0x70, 0xcc, 0xd2, 0x1e, 0x0a, 0x59, 0x30, 0xc1, 0x1a, 0xaa, 0xf2, 0x94, 0xcf, 0x5e, 0x3e, - 0xb9, 0x05, 0x34, 0x35, 0x74, 0x71, 0xa3, 0x53, 0x5d, 0x95, 0x20, 0x7c, 0xdf, 0x15, 0xea, 0xb1, 0x39, 0xe0, 0xd4, - 0x00, 0x50, 0x2c, 0xf2, 0x5a, 0x8f, 0xed, 0x1f, 0xf4, 0x46, 0xbd, 0x01, 0xe2, 0xe9, 0x9c, 0x17, 0xfe, 0x11, 0xfd, - 0x3a, 0xf5, 0x67, 0x5c, 0x08, 0xa2, 0x5e, 0x4f, 0xc2, 0x7b, 0x71, 0x96, 0xc6, 0xc1, 0x59, 0x6f, 0x60, 0x2e, 0x02, - 0xc5, 0x59, 0x9a, 0x9f, 0x81, 0x58, 0x8e, 0xf0, 0x88, 0x35, 0xbb, 0x03, 0xc4, 0xc0, 0x52, 0x87, 0x24, 0xab, 0x8e, - 0xed, 0xf7, 0xdf, 0x8c, 0x0c, 0x6f, 0x3a, 0x22, 0xc2, 0xe8, 0xdf, 0x15, 0x08, 0x50, 0xb0, 0xcc, 0x6c, 0x67, 0x26, - 0x5d, 0xed, 0x59, 0x3d, 0x6f, 0x36, 0x79, 0x57, 0xef, 0x58, 0x4d, 0xcb, 0xa9, 0x69, 0x95, 0xd5, 0xb4, 0x49, 0x0e, - 0x35, 0x13, 0xfd, 0xbe, 0xc6, 0x47, 0xcd, 0xe7, 0x80, 0xcb, 0x86, 0xc9, 0x6f, 0x66, 0xd5, 0xbc, 0xdf, 0xf7, 0xe4, - 0x23, 0xf8, 0x85, 0xc4, 0x65, 0x6e, 0x8d, 0xe5, 0xd3, 0xd7, 0xc4, 0x67, 0x66, 0x10, 0x8f, 0xee, 0x8e, 0xa0, 0xbe, - 0x6e, 0x84, 0xd7, 0x31, 0x57, 0xd8, 0x4c, 0x4c, 0xdf, 0xc0, 0xe0, 0x79, 0xc2, 0x07, 0x6f, 0x39, 0xfa, 0x1b, 0xe9, - 0xcc, 0x14, 0x2c, 0xe4, 0xdc, 0x9f, 0xbc, 0x41, 0xe8, 0x64, 0x44, 0x7a, 0xd0, 0xe9, 0x04, 0x0d, 0xd9, 0xef, 0xaf, - 0xa0, 0x33, 0x5b, 0xa9, 0x94, 0xad, 0x8a, 0xca, 0x74, 0x5d, 0x17, 0x65, 0x05, 0x1d, 0x4b, 0x3f, 0x6f, 0x85, 0xcc, - 0xac, 0x9f, 0x59, 0xc8, 0x4f, 0x2b, 0x89, 0x35, 0x65, 0xdb, 0x27, 0x6a, 0x83, 0x34, 0xeb, 0x42, 0x75, 0x81, 0x73, - 0x67, 0xed, 0xf5, 0x46, 0xa8, 0x7f, 0xce, 0x47, 0xeb, 0x62, 0xed, 0x81, 0x4b, 0xcc, 0x2c, 0x9d, 0x2b, 0x0e, 0x8d, - 0xdc, 0x1f, 0x7d, 0x29, 0xd2, 0x9c, 0xf2, 0x00, 0x0d, 0xa2, 0x98, 0xdb, 0x6f, 0x81, 0xf4, 0x43, 0x6f, 0x81, 0xec, - 0xa3, 0x73, 0x4e, 0xde, 0x00, 0x38, 0x1d, 0x22, 0xe2, 0x56, 0x24, 0xe8, 0x58, 0x35, 0xbc, 0xb5, 0x70, 0x4f, 0x7b, - 0x69, 0xdc, 0x4b, 0xf3, 0xb3, 0xb4, 0xdf, 0x37, 0x00, 0x9a, 0x29, 0x22, 0xc3, 0xe3, 0x8c, 0x5c, 0x24, 0x2d, 0x04, - 0x53, 0xda, 0x7f, 0x35, 0x86, 0x04, 0x81, 0x80, 0xff, 0x5d, 0x78, 0x4f, 0x00, 0x6d, 0x93, 0x36, 0xe0, 0xaa, 0xc7, - 0x74, 0x60, 0xb6, 0xe4, 0x6c, 0xd5, 0xd9, 0x00, 0x94, 0x53, 0xa5, 0xf5, 0x94, 0xc7, 0x35, 0x45, 0x44, 0xaa, 0x2c, - 0xd4, 0x6f, 0xac, 0x27, 0x93, 0x55, 0x2e, 0x32, 0xe4, 0xa8, 0x4c, 0xef, 0x6b, 0x46, 0x88, 0x5d, 0xfa, 0xf9, 0x02, - 0x96, 0x6c, 0xfc, 0x09, 0x27, 0x6f, 0x09, 0x90, 0xb6, 0xb3, 0x76, 0x55, 0xed, 0x72, 0xdc, 0xda, 0xcd, 0x01, 0xc9, - 0xd7, 0x1b, 0x8d, 0x46, 0xda, 0x4f, 0x4e, 0xc0, 0x50, 0xf5, 0xd4, 0x52, 0xe8, 0xb1, 0x5a, 0x61, 0xeb, 0x76, 0xe4, - 0x32, 0x4b, 0x06, 0xf3, 0x85, 0x71, 0xfc, 0xca, 0x7c, 0xf4, 0xf1, 0x52, 0x59, 0xbb, 0x8e, 0xf8, 0xfa, 0x8f, 0xb2, - 0x5a, 0xdf, 0xf3, 0xae, 0x6a, 0x02, 0xbe, 0xa8, 0x62, 0x4b, 0xbf, 0xe3, 0x3d, 0xd9, 0xbb, 0xf8, 0xda, 0x0d, 0x76, - 0xc9, 0xf7, 0xbc, 0x45, 0x9d, 0xe7, 0x2b, 0x5f, 0x37, 0xaa, 0x74, 0x7b, 0x2f, 0x59, 0xe0, 0xda, 0x3b, 0x6a, 0x1a, - 0xeb, 0x99, 0x1f, 0x3d, 0x2c, 0x42, 0xb6, 0xf3, 0xb1, 0xf7, 0x55, 0xf3, 0xf4, 0xac, 0xa1, 0x37, 0xa9, 0xa1, 0x8f, - 0xbd, 0x28, 0xdb, 0xa7, 0xa6, 0x11, 0xbd, 0x86, 0x0d, 0x7d, 0xec, 0x2d, 0x39, 0x39, 0x24, 0x18, 0x9c, 0x1a, 0xf3, - 0xc7, 0x87, 0xd3, 0x19, 0xfe, 0x8e, 0x01, 0x95, 0x98, 0xcc, 0xa7, 0xc7, 0xb4, 0xa3, 0x00, 0x33, 0xaa, 0xf4, 0xf6, - 0xe9, 0x81, 0xed, 0x78, 0x59, 0x0f, 0x2d, 0xbd, 0x7b, 0x72, 0x74, 0x3b, 0x5e, 0x55, 0xe3, 0x4b, 0x39, 0xe4, 0x79, - 0x3e, 0x1b, 0x8d, 0x46, 0xc2, 0xa0, 0x73, 0x57, 0x7a, 0x03, 0x2b, 0x90, 0xc1, 0x45, 0xf5, 0xa1, 0x5c, 0x7a, 0x3b, - 0x75, 0x68, 0x57, 0xfe, 0x24, 0x3f, 0x1c, 0x8a, 0x91, 0x39, 0xc6, 0x01, 0xe7, 0xa4, 0x50, 0x72, 0x94, 0xac, 0x25, - 0x88, 0x4e, 0x69, 0x3c, 0x95, 0xf5, 0xda, 0x8a, 0xc8, 0xab, 0x11, 0xf2, 0x21, 0xf8, 0xc9, 0x03, 0xb5, 0xf8, 0xb5, - 0x16, 0xc4, 0x1e, 0xfb, 0x54, 0x29, 0x1d, 0xe2, 0x55, 0x01, 0x21, 0xc2, 0x80, 0x37, 0xd0, 0x0e, 0x4a, 0x70, 0xd8, - 0xe1, 0x3e, 0x22, 0x42, 0xf4, 0x6b, 0x2f, 0x9f, 0xc9, 0x70, 0xe5, 0xde, 0xa0, 0x9a, 0x33, 0x40, 0xac, 0xf4, 0x19, - 0xb8, 0x60, 0x02, 0xea, 0x29, 0x3e, 0x45, 0xff, 0x7a, 0xf3, 0xb0, 0xe9, 0xfa, 0xb4, 0x04, 0x54, 0x44, 0xcf, 0x7e, - 0x3e, 0x06, 0xf0, 0xce, 0xae, 0xcd, 0x48, 0x7b, 0xf9, 0x1b, 0x60, 0x58, 0x29, 0x49, 0xb4, 0x73, 0x4a, 0x04, 0xee, - 0x7c, 0x64, 0x4b, 0x3f, 0x4a, 0x81, 0x98, 0x3b, 0x9e, 0x24, 0xb2, 0x07, 0x1b, 0x39, 0x81, 0x5b, 0x0c, 0x78, 0x74, - 0x00, 0x2a, 0x57, 0x0a, 0x72, 0xaf, 0x39, 0x92, 0x3b, 0x7e, 0xe8, 0xfd, 0x30, 0xa8, 0x07, 0x3f, 0xf4, 0xce, 0x52, - 0x92, 0x3b, 0xc2, 0x33, 0x35, 0x25, 0x44, 0x7c, 0xf6, 0xc3, 0x20, 0x1f, 0xe0, 0x59, 0xa2, 0x45, 0x5a, 0xe4, 0x56, - 0x13, 0x35, 0x6e, 0xc2, 0x8b, 0x44, 0xd2, 0x10, 0xed, 0x3a, 0x8f, 0x88, 0x05, 0x80, 0x64, 0xf1, 0xd9, 0xbc, 0xa1, - 0xa8, 0x77, 0x13, 0xbe, 0x45, 0x77, 0x59, 0xec, 0xf7, 0xb7, 0x79, 0x5a, 0xf7, 0x74, 0xa8, 0x0c, 0xbe, 0x20, 0xd5, - 0x04, 0x78, 0xb4, 0xbf, 0x36, 0xc7, 0xab, 0x57, 0x9b, 0x23, 0x65, 0xa1, 0x4a, 0xd4, 0x6f, 0xb1, 0x9a, 0xf5, 0x10, - 0x91, 0x3b, 0xcb, 0x8c, 0xbd, 0xbd, 0xe0, 0x95, 0x9c, 0x55, 0xb1, 0x5d, 0x8e, 0xaf, 0x08, 0x6b, 0x2b, 0x09, 0xd0, - 0xd1, 0x7a, 0xac, 0x4d, 0x31, 0xf2, 0x2b, 0x85, 0x04, 0x5c, 0x74, 0x6c, 0x2d, 0x14, 0x1b, 0x2f, 0x40, 0x5f, 0xb2, - 0x33, 0x0d, 0xb0, 0xde, 0xe8, 0x55, 0xc4, 0x6d, 0xf9, 0x48, 0x85, 0x37, 0xb9, 0xa9, 0x32, 0x2b, 0x9b, 0x45, 0xbb, - 0x9f, 0x2a, 0x5e, 0x21, 0x6e, 0xbd, 0x51, 0x7b, 0x14, 0xa0, 0xf6, 0xd0, 0x42, 0x19, 0xa0, 0x4b, 0xd3, 0x0c, 0x00, - 0x19, 0x00, 0x64, 0xaa, 0x88, 0xcf, 0x04, 0xa8, 0xb4, 0xd5, 0x8d, 0x02, 0x27, 0xd2, 0x6b, 0x60, 0x5c, 0x60, 0xa5, - 0x8f, 0x6c, 0x64, 0xb0, 0xd8, 0x22, 0xc0, 0x2d, 0x47, 0xfa, 0x30, 0x0d, 0x27, 0xdb, 0x68, 0x0e, 0x93, 0x34, 0xbf, - 0x0f, 0xb3, 0x54, 0x42, 0x4b, 0xfc, 0x28, 0x6b, 0x8c, 0x58, 0x40, 0xfa, 0x3e, 0xbd, 0x28, 0xb2, 0x98, 0x20, 0xe1, - 0xac, 0xa7, 0x0e, 0xa0, 0x9a, 0x9c, 0x6b, 0x4d, 0xab, 0x67, 0xb5, 0xc9, 0x43, 0x16, 0xe8, 0xec, 0xc1, 0x98, 0xd4, - 0x72, 0x43, 0x8f, 0xec, 0xaf, 0x1c, 0xcf, 0x08, 0xdf, 0xf5, 0x0c, 0xa7, 0xfe, 0xbb, 0xa9, 0x81, 0x94, 0x29, 0x01, - 0x04, 0x19, 0x1c, 0x4d, 0x08, 0xe5, 0xe9, 0x98, 0x4c, 0x6d, 0x7e, 0x04, 0xc2, 0x11, 0xc1, 0x2b, 0x78, 0x6e, 0x68, - 0xdd, 0x72, 0x63, 0x67, 0x91, 0xa7, 0x09, 0x20, 0x8b, 0x17, 0x7c, 0x0b, 0xc8, 0x9c, 0x7a, 0x55, 0xc8, 0x9e, 0x3d, - 0x17, 0xd3, 0xd9, 0x3c, 0x78, 0x48, 0x68, 0xff, 0x62, 0xc2, 0x6f, 0xba, 0xab, 0xe4, 0xca, 0xd4, 0xba, 0x37, 0xd1, - 0x63, 0x2e, 0x77, 0xfa, 0xb4, 0xe2, 0x18, 0xf1, 0x0c, 0x56, 0x01, 0x39, 0x67, 0x43, 0x7e, 0x7d, 0x0e, 0xd8, 0x2d, - 0x2b, 0xe1, 0x45, 0xfc, 0x3a, 0x94, 0xd5, 0x02, 0xe4, 0x47, 0xce, 0x23, 0xf3, 0xcb, 0x57, 0xdb, 0xa1, 0x9c, 0x53, - 0x14, 0xd1, 0x72, 0x6a, 0x5a, 0x52, 0xc8, 0x0e, 0x3d, 0x05, 0x93, 0xa9, 0x2d, 0x7f, 0x6f, 0x13, 0x97, 0xe4, 0x9b, - 0x49, 0x64, 0x5f, 0x07, 0x58, 0xb3, 0x56, 0xdd, 0x43, 0x37, 0x04, 0x03, 0x44, 0x46, 0x28, 0xb3, 0xb9, 0xbe, 0x5b, - 0x0f, 0x06, 0x0a, 0xe6, 0x57, 0xd0, 0x4d, 0x8b, 0x4e, 0x71, 0x80, 0x9c, 0xb5, 0xae, 0x51, 0xa9, 0x2a, 0x0e, 0x1d, - 0xe6, 0xdd, 0xb2, 0x2a, 0xbb, 0x2c, 0xbd, 0x10, 0xa4, 0x46, 0x5d, 0x05, 0x8b, 0x94, 0x8a, 0x28, 0xde, 0x93, 0x5f, - 0x03, 0x13, 0xcf, 0xac, 0x1c, 0xa5, 0xf1, 0x1c, 0x10, 0x83, 0x14, 0x10, 0xa7, 0xfc, 0x0a, 0xd0, 0x44, 0x17, 0x51, - 0x98, 0xbd, 0x8d, 0xab, 0xa0, 0xb6, 0x9a, 0x7e, 0xef, 0x40, 0xc6, 0x9e, 0xd7, 0xfd, 0x7e, 0x4a, 0x8c, 0x7e, 0x18, - 0x85, 0x81, 0x7f, 0x8f, 0xa7, 0xfb, 0x26, 0x48, 0xcd, 0x2b, 0x0f, 0xf0, 0x8a, 0x2e, 0xb7, 0x36, 0xe5, 0x8a, 0xc6, - 0x85, 0xbf, 0x46, 0x70, 0xf8, 0xd4, 0x51, 0x6c, 0xb7, 0xa9, 0x72, 0x6a, 0x63, 0x30, 0x08, 0xe1, 0xbe, 0x95, 0xf1, - 0xfb, 0xc4, 0xcb, 0x67, 0xd1, 0x1c, 0x14, 0xa5, 0x99, 0xe6, 0x0b, 0x29, 0xa4, 0x9b, 0x00, 0x7d, 0x34, 0x08, 0xb5, - 0xba, 0xf2, 0x8f, 0xc4, 0x4b, 0xd5, 0xb4, 0x36, 0x4f, 0xb1, 0x46, 0x81, 0x98, 0x45, 0xf3, 0x86, 0x65, 0x74, 0x48, - 0xaa, 0xcb, 0xa5, 0x69, 0xc6, 0x1f, 0x56, 0x33, 0x54, 0x2b, 0x8e, 0x9a, 0xa0, 0x46, 0xe9, 0x06, 0x2e, 0x80, 0x7f, - 0xa3, 0x3b, 0x8e, 0x6a, 0x14, 0x29, 0x1a, 0xf0, 0x09, 0x62, 0xc4, 0x9a, 0xcd, 0x13, 0xd6, 0x9a, 0xba, 0x66, 0xf4, - 0xfb, 0x32, 0x64, 0xc8, 0x24, 0x21, 0x4f, 0x1f, 0x2e, 0xd7, 0x4f, 0xa4, 0xba, 0x00, 0x7e, 0xe5, 0x8a, 0xcd, 0x7a, - 0xbd, 0x39, 0xc0, 0xf5, 0xc2, 0xfa, 0x85, 0x8d, 0x2b, 0x38, 0xbf, 0x24, 0xf8, 0x5d, 0xf5, 0x23, 0xcc, 0x32, 0xa8, - 0x02, 0x32, 0xfe, 0x58, 0x48, 0xe7, 0xb9, 0x8b, 0x49, 0xfd, 0x66, 0xa4, 0x2e, 0x28, 0xb3, 0x74, 0x6e, 0x71, 0x82, - 0x80, 0xf3, 0xb0, 0x7a, 0x02, 0xc9, 0xbe, 0x7c, 0xec, 0xd3, 0x8c, 0x02, 0xd5, 0x11, 0xe0, 0xb3, 0x59, 0x3f, 0x84, - 0xfd, 0x03, 0x22, 0x0b, 0xf5, 0x37, 0x6f, 0xe4, 0xac, 0x21, 0x79, 0x20, 0xd5, 0xdc, 0xc7, 0x70, 0x6a, 0x2c, 0xf0, - 0xa5, 0x45, 0x6f, 0x2a, 0x78, 0x4d, 0xc8, 0xdc, 0x0b, 0xb4, 0xf6, 0x2d, 0xe0, 0x08, 0x11, 0x5c, 0x46, 0x29, 0x4e, - 0x7b, 0xbb, 0x5e, 0x80, 0xdc, 0xe6, 0x16, 0xe4, 0xf5, 0x3b, 0x17, 0xbf, 0x38, 0x45, 0x7a, 0x16, 0x5d, 0x60, 0xa0, - 0x0b, 0x32, 0x6f, 0xfc, 0xb3, 0x82, 0x95, 0x0b, 0xe8, 0xbd, 0x54, 0xac, 0xe4, 0x64, 0xdb, 0xa9, 0x3f, 0x4a, 0x65, - 0xbf, 0x3d, 0xb3, 0x26, 0xf0, 0xab, 0xc4, 0x7e, 0x89, 0x4c, 0xbe, 0xe9, 0xb1, 0xc9, 0x57, 0x86, 0x45, 0xa7, 0x96, - 0xc1, 0x39, 0x3d, 0x32, 0x38, 0xf7, 0x76, 0x56, 0x6d, 0x42, 0x18, 0x0a, 0x92, 0x40, 0xd3, 0xa5, 0x87, 0x75, 0xd3, - 0x9f, 0x9f, 0xb4, 0xa8, 0xb6, 0x6a, 0xdf, 0xba, 0x1f, 0x87, 0xd8, 0xc5, 0xaf, 0x12, 0xcf, 0x10, 0x91, 0xfa, 0x40, - 0x07, 0x26, 0x83, 0x27, 0x2e, 0xfb, 0x7d, 0x28, 0x6c, 0x36, 0x9e, 0x8f, 0xea, 0xe2, 0xe7, 0xe2, 0x01, 0x50, 0x1d, - 0x2a, 0xb0, 0xcb, 0xa1, 0x0c, 0x65, 0xc4, 0xa6, 0xb6, 0xdc, 0xf3, 0xfb, 0xab, 0x30, 0x07, 0x79, 0x47, 0xc3, 0xe3, - 0x9c, 0x81, 0x18, 0x06, 0x5f, 0xff, 0xe1, 0xc9, 0x3e, 0x6d, 0x7e, 0x38, 0x83, 0xef, 0x8e, 0xce, 0x3e, 0x22, 0xdd, - 0xcd, 0xd9, 0xba, 0x2c, 0xee, 0xd3, 0x58, 0x9c, 0xfd, 0x00, 0xa9, 0x3f, 0x9c, 0x15, 0xe5, 0xd9, 0x0f, 0xaa, 0x32, - 0x3f, 0x9c, 0xd1, 0x82, 0x1b, 0xfd, 0x6e, 0x4d, 0xbc, 0x7f, 0x54, 0x9a, 0x01, 0x6d, 0x09, 0x91, 0x59, 0x5a, 0xfd, - 0x08, 0x4a, 0x44, 0xc5, 0x8f, 0x2a, 0xa3, 0x5a, 0xad, 0x1d, 0xe7, 0x43, 0xa2, 0x91, 0xb2, 0x69, 0x42, 0xe2, 0x6a, - 0x09, 0xeb, 0x50, 0xcf, 0x4e, 0x9b, 0x6f, 0xc7, 0x79, 0xa0, 0x0e, 0x88, 0x9c, 0x5f, 0xe7, 0xa3, 0x2d, 0x7d, 0x0d, - 0xbe, 0x75, 0x38, 0xe4, 0xa3, 0x9d, 0xf9, 0xe9, 0x93, 0xb5, 0x52, 0xc6, 0x1d, 0x29, 0x72, 0x21, 0xe4, 0x8c, 0xdb, - 0xf6, 0x18, 0x70, 0x00, 0xf8, 0x87, 0x03, 0xfd, 0xde, 0xc9, 0xdf, 0x6a, 0xb7, 0xb4, 0xea, 0xf9, 0xb1, 0xc5, 0x9d, - 0xf1, 0xba, 0x36, 0x44, 0x6d, 0x7b, 0x89, 0x2d, 0xbd, 0x6f, 0x1a, 0xd4, 0x14, 0xd1, 0x4f, 0x58, 0x4d, 0xac, 0xe2, - 0xb0, 0x20, 0x25, 0x24, 0x31, 0x1c, 0xa3, 0x1d, 0x7a, 0x9c, 0x2e, 0x96, 0x9e, 0xdc, 0x77, 0x78, 0xb9, 0xf5, 0x7d, - 0x40, 0xd2, 0x2a, 0x9c, 0x7f, 0xf4, 0x42, 0x03, 0x8f, 0x5e, 0xe4, 0x55, 0x91, 0x89, 0x91, 0xa0, 0x51, 0x7e, 0x4b, - 0xe2, 0xcc, 0x19, 0xd6, 0xe2, 0x4c, 0x81, 0x85, 0x85, 0x04, 0xd0, 0x5d, 0x94, 0x94, 0x1e, 0x9c, 0x3d, 0xd9, 0x97, - 0xcd, 0xef, 0x04, 0x0f, 0x31, 0x5a, 0x00, 0x23, 0xce, 0xae, 0x5d, 0xde, 0x43, 0x58, 0xe6, 0xde, 0xef, 0x6f, 0xef, - 0xf2, 0x02, 0x42, 0x34, 0xcf, 0xa4, 0x62, 0xb5, 0x3c, 0x03, 0xc6, 0x3c, 0x11, 0x9f, 0x85, 0x95, 0x9c, 0x06, 0x55, - 0x47, 0xb1, 0x7a, 0x1b, 0xcf, 0x3d, 0xa0, 0xf8, 0xfe, 0x90, 0x00, 0x97, 0xbb, 0xcf, 0xde, 0x28, 0xd7, 0x54, 0xd2, - 0x23, 0xcf, 0x31, 0x5a, 0x32, 0x01, 0x8a, 0x67, 0x88, 0x93, 0x14, 0x56, 0xcf, 0x4d, 0x90, 0x8a, 0x7c, 0x7d, 0x42, - 0xf1, 0x45, 0xf3, 0x28, 0x6a, 0x58, 0xc8, 0x12, 0x38, 0x1e, 0x92, 0x59, 0x36, 0x47, 0x96, 0xf2, 0xb4, 0x3d, 0x45, - 0x3a, 0x3a, 0xb1, 0xc4, 0x6f, 0x6b, 0x7e, 0xbd, 0x48, 0x45, 0x60, 0xd2, 0xce, 0x56, 0xe6, 0x5e, 0x08, 0x43, 0x95, - 0x70, 0xef, 0x75, 0x3d, 0x0b, 0xe5, 0xa6, 0x68, 0x55, 0xcc, 0x1e, 0xa6, 0xc4, 0x0c, 0x53, 0xac, 0xbf, 0xb0, 0xe1, - 0x37, 0x89, 0x17, 0x83, 0xe1, 0x7a, 0xc9, 0xcb, 0xd9, 0xc6, 0x2c, 0x84, 0xc3, 0x61, 0x33, 0x29, 0x66, 0x4b, 0x08, - 0x73, 0x5d, 0xce, 0x0f, 0x87, 0xae, 0x96, 0xad, 0x85, 0x07, 0x0f, 0x55, 0x0b, 0x37, 0x0d, 0xcb, 0xe1, 0x67, 0x32, - 0x8b, 0xb1, 0x7d, 0x8d, 0xcf, 0xec, 0xcf, 0x17, 0xdd, 0xb3, 0x04, 0xc9, 0x37, 0xd6, 0x40, 0x3b, 0x36, 0x6b, 0x77, - 0xb8, 0x1a, 0x01, 0x49, 0xe9, 0x6e, 0xf4, 0x77, 0x65, 0x27, 0x4f, 0x09, 0x72, 0x47, 0x2b, 0xb0, 0xdf, 0x7d, 0xe3, - 0x4f, 0xb4, 0xd8, 0x83, 0x76, 0x1b, 0x5b, 0x42, 0x54, 0xd3, 0x9e, 0xcb, 0x95, 0x62, 0x69, 0xde, 0x4a, 0x1b, 0x3d, - 0x1f, 0xd6, 0xe7, 0xbe, 0x91, 0x03, 0x05, 0x63, 0xc4, 0x53, 0xeb, 0x20, 0x9a, 0xcd, 0x81, 0x06, 0x03, 0xcd, 0x23, - 0x3c, 0xb5, 0xd0, 0x41, 0x99, 0xb5, 0x61, 0x3f, 0x49, 0x4e, 0x96, 0xc7, 0xe1, 0x5b, 0xf8, 0x97, 0xcf, 0xb0, 0x49, - 0x4c, 0xb1, 0x3d, 0xfe, 0x56, 0x29, 0x2a, 0x3c, 0xb6, 0x20, 0xae, 0xb5, 0x1b, 0x51, 0x1b, 0x2a, 0x87, 0x7f, 0x09, - 0xfb, 0x08, 0xfb, 0x0d, 0x4d, 0x10, 0x06, 0xbb, 0xfe, 0x4c, 0x20, 0x44, 0x2c, 0xc4, 0x0b, 0xfe, 0x56, 0x49, 0x2a, - 0x3a, 0xe1, 0xb3, 0x45, 0x09, 0xbc, 0x75, 0x18, 0xd0, 0x27, 0x14, 0x29, 0x85, 0x30, 0x34, 0x13, 0x7a, 0x47, 0xff, - 0x8d, 0xd8, 0xc9, 0x26, 0xb9, 0x15, 0xf2, 0x81, 0xa4, 0x92, 0x60, 0x82, 0x95, 0x17, 0xca, 0x17, 0xee, 0x85, 0x52, - 0x6b, 0x2d, 0x68, 0xfd, 0xf2, 0x27, 0x89, 0x67, 0xf0, 0xf7, 0x40, 0xc6, 0xa0, 0xdb, 0x88, 0x6a, 0x92, 0x63, 0xfa, - 0x28, 0x9d, 0x67, 0xa0, 0x02, 0x3a, 0x5b, 0x67, 0x61, 0xbd, 0x2c, 0xca, 0x55, 0x2b, 0x52, 0x54, 0x96, 0x3e, 0x52, - 0x8f, 0x31, 0x2f, 0xcc, 0x93, 0x13, 0xf9, 0xe0, 0x11, 0x00, 0xe3, 0x51, 0x9e, 0x56, 0x1d, 0xa5, 0xf5, 0x03, 0xcb, - 0x80, 0x11, 0x38, 0x51, 0x06, 0x3c, 0xc2, 0x32, 0x30, 0x4f, 0xbb, 0x0c, 0x35, 0x88, 0x35, 0xaa, 0xae, 0xd4, 0x06, - 0x73, 0xa2, 0x28, 0xf9, 0x14, 0x4b, 0x2b, 0x8c, 0xa1, 0xa9, 0x2b, 0x8f, 0xac, 0x97, 0x9c, 0xb0, 0x27, 0xbb, 0x81, - 0x74, 0x0b, 0x1b, 0x85, 0x33, 0xe8, 0x5a, 0x96, 0x28, 0x17, 0xdd, 0x32, 0xa2, 0x4c, 0x84, 0xd4, 0xcf, 0x1e, 0xce, - 0xb4, 0xda, 0x6f, 0xec, 0xa4, 0x7d, 0x7b, 0xa4, 0xe8, 0x05, 0x83, 0xf6, 0x69, 0x8f, 0x94, 0x7a, 0xd6, 0xc8, 0x65, - 0x60, 0x4b, 0x97, 0xaa, 0x9e, 0xff, 0x02, 0xe5, 0x3b, 0x98, 0x19, 0x67, 0xb3, 0xdf, 0xf5, 0xe6, 0xf6, 0x64, 0x5f, - 0x37, 0xbf, 0xb3, 0x5e, 0x0f, 0xb6, 0x06, 0x99, 0xf8, 0x42, 0xb1, 0x50, 0x59, 0x85, 0x58, 0x41, 0xda, 0xff, 0x12, - 0xde, 0xef, 0xf0, 0xd6, 0x08, 0xcd, 0xca, 0x78, 0x98, 0x8f, 0x9e, 0xec, 0x45, 0xf3, 0x7b, 0x67, 0xd9, 0x56, 0xae, - 0x4a, 0x66, 0xfb, 0xfd, 0x28, 0x69, 0xce, 0x1e, 0xaf, 0x91, 0xd4, 0x01, 0x3e, 0x5e, 0x9f, 0xe1, 0x23, 0x95, 0x50, - 0x6a, 0x41, 0x55, 0x83, 0xd6, 0xc7, 0x7e, 0x6f, 0x3d, 0xa7, 0x8f, 0x1f, 0xcb, 0xe9, 0x96, 0x14, 0x61, 0xfc, 0xc0, - 0x60, 0xca, 0x4e, 0x9c, 0xba, 0xe4, 0xcd, 0x90, 0xde, 0x75, 0xab, 0xa4, 0x2e, 0x7b, 0x94, 0x08, 0x42, 0x1d, 0xac, - 0x5f, 0xec, 0x87, 0x30, 0xb3, 0x45, 0x7f, 0xd8, 0xac, 0xe6, 0x04, 0x88, 0x08, 0x68, 0xad, 0xf2, 0x3e, 0x70, 0xcc, - 0x17, 0x66, 0xcd, 0x0d, 0xe9, 0xd6, 0x9b, 0x2b, 0xed, 0x95, 0x14, 0xd0, 0xcf, 0x41, 0xe6, 0xf6, 0xd1, 0x2d, 0x57, - 0x2d, 0xf3, 0x5c, 0xda, 0x72, 0xc0, 0xa2, 0x85, 0x40, 0xcd, 0xce, 0xa5, 0xc3, 0x81, 0x82, 0x50, 0x57, 0xa2, 0x8a, - 0xb8, 0x3a, 0x8a, 0x16, 0xa2, 0x56, 0xab, 0x76, 0x39, 0xd9, 0x54, 0xc8, 0x96, 0x44, 0x90, 0x51, 0xb2, 0x57, 0x42, - 0x7d, 0x94, 0xab, 0x3d, 0xd3, 0x70, 0x80, 0x26, 0x60, 0xd3, 0x06, 0x7f, 0x0b, 0xdc, 0xcb, 0xe0, 0xcc, 0xb4, 0x4f, - 0xc3, 0x08, 0x38, 0xcd, 0x21, 0xe6, 0xcf, 0xef, 0x7a, 0x50, 0xc1, 0x83, 0x8e, 0xf4, 0xd7, 0xf5, 0xac, 0xc0, 0x33, - 0xf7, 0xc4, 0xf3, 0x37, 0x27, 0xd2, 0x8b, 0x1c, 0x1e, 0x68, 0x1a, 0xc4, 0x8c, 0xbf, 0x28, 0xcb, 0x70, 0x37, 0x5a, - 0x96, 0xc5, 0xca, 0x8b, 0xf4, 0x3e, 0x9e, 0x49, 0x31, 0x90, 0x98, 0x31, 0x33, 0xba, 0x8a, 0x75, 0x9c, 0xc3, 0xb8, - 0xb7, 0x27, 0x61, 0x85, 0xf6, 0xcf, 0x12, 0x7b, 0x5d, 0x00, 0x96, 0x43, 0xd6, 0xa0, 0x15, 0xde, 0xe9, 0xf6, 0x76, - 0x8f, 0x4b, 0x76, 0x14, 0x37, 0x80, 0x7e, 0x56, 0x43, 0xcb, 0x04, 0xb5, 0xcc, 0xba, 0x93, 0xc9, 0x14, 0xc9, 0xe5, - 0xdb, 0xb0, 0x37, 0xac, 0xc8, 0xe7, 0x8d, 0xdc, 0x1e, 0xde, 0x87, 0x2b, 0x11, 0x6b, 0x0b, 0x3a, 0xe9, 0xc8, 0x38, - 0xdc, 0x0b, 0xcd, 0x8d, 0x74, 0xff, 0xa4, 0x4a, 0xc2, 0x52, 0xc4, 0x70, 0x0b, 0x64, 0x7b, 0xb5, 0xad, 0x04, 0x25, - 0xf0, 0xc1, 0x7e, 0x2c, 0xc5, 0x32, 0xdd, 0x0a, 0xc0, 0x75, 0xe0, 0x7f, 0x4a, 0x44, 0x42, 0x77, 0xe7, 0x21, 0x8a, - 0x35, 0xf2, 0xbe, 0x41, 0x34, 0xf6, 0xd7, 0x20, 0xa7, 0x01, 0x99, 0x48, 0x31, 0x92, 0x05, 0x03, 0x1f, 0x40, 0xce, - 0xd7, 0x60, 0x92, 0x9b, 0xe6, 0x9e, 0x1f, 0xe4, 0xba, 0x83, 0x69, 0x1f, 0x74, 0x2f, 0xae, 0x35, 0xcb, 0xc1, 0x2b, - 0x26, 0xe2, 0x7f, 0xab, 0xbd, 0x92, 0xe5, 0x2c, 0xf3, 0x1b, 0x73, 0xd1, 0xc9, 0xe0, 0xaa, 0x21, 0xfc, 0x62, 0x96, - 0xcd, 0x79, 0x34, 0xcb, 0x74, 0xd4, 0x7f, 0xd1, 0x1c, 0x95, 0x02, 0x70, 0xea, 0x78, 0x01, 0xd6, 0xd0, 0x57, 0xba, - 0x69, 0xc5, 0x23, 0x8d, 0x31, 0x0a, 0x2a, 0x74, 0x10, 0xfa, 0x5b, 0x0d, 0x48, 0x1b, 0x4c, 0xd2, 0x24, 0x54, 0x3e, - 0xb8, 0xa0, 0x1b, 0xe6, 0xe5, 0xca, 0xe5, 0xaa, 0x49, 0xd5, 0xf2, 0xcb, 0x11, 0xf5, 0x5d, 0x2d, 0xb9, 0x54, 0x9b, - 0x4f, 0x8d, 0xb2, 0x46, 0x90, 0xc9, 0x51, 0xfa, 0x7d, 0xca, 0x85, 0x5b, 0x19, 0x93, 0xf5, 0xe1, 0xe0, 0x15, 0xdc, - 0xd4, 0xf8, 0x75, 0x4e, 0x84, 0xa2, 0xf6, 0x90, 0x08, 0x5b, 0xbb, 0x15, 0xba, 0xf7, 0xb8, 0x51, 0x9a, 0x47, 0xd9, - 0x26, 0x16, 0x95, 0xd7, 0x4b, 0xc0, 0x5a, 0xdc, 0x03, 0x5e, 0x54, 0x5a, 0xfa, 0x15, 0x2b, 0x00, 0x3d, 0x40, 0x0a, - 0x1b, 0x3f, 0x22, 0x03, 0xd6, 0x47, 0x2f, 0xf5, 0xfb, 0x7d, 0x63, 0xca, 0xff, 0xf0, 0x90, 0x03, 0x49, 0xa1, 0x28, - 0xeb, 0x1d, 0x4c, 0x20, 0xb8, 0x76, 0x92, 0xf6, 0xac, 0xe6, 0xd7, 0xeb, 0xda, 0x03, 0x7e, 0x2b, 0xdf, 0x22, 0xb1, - 0x7a, 0x6d, 0x5f, 0x6c, 0xf6, 0x69, 0x75, 0x63, 0x34, 0x0e, 0x82, 0xa5, 0xd5, 0x5b, 0xad, 0x72, 0xc8, 0x1b, 0x5e, - 0x81, 0x48, 0x65, 0x5d, 0x5d, 0x2b, 0xe7, 0xea, 0x5a, 0x70, 0xe4, 0x92, 0x2d, 0x79, 0x0e, 0xff, 0x85, 0xdc, 0x2b, - 0x0f, 0x87, 0xc2, 0xef, 0xf7, 0xd3, 0x19, 0x69, 0x65, 0x81, 0x3d, 0x6d, 0x5d, 0x7b, 0xa1, 0x7f, 0x38, 0xfc, 0x08, - 0x5e, 0x23, 0xfe, 0xe1, 0x50, 0xf6, 0xfb, 0x9f, 0xcc, 0x4d, 0xe6, 0x7c, 0xac, 0x94, 0xb2, 0x97, 0xa8, 0x74, 0x7f, - 0x9b, 0xf0, 0xde, 0xff, 0x1e, 0xfd, 0xef, 0xd1, 0x65, 0x4f, 0x85, 0x80, 0x25, 0x7c, 0x86, 0x37, 0x74, 0xa6, 0x2e, - 0xe7, 0x4c, 0xba, 0xbb, 0x2b, 0x3f, 0xf4, 0x9e, 0x86, 0x8a, 0xef, 0xcd, 0x4d, 0x1b, 0x7f, 0xad, 0x8e, 0x34, 0x09, - 0x1d, 0x17, 0xfd, 0xc3, 0xe1, 0x73, 0xa2, 0xf5, 0x69, 0xa9, 0xd2, 0xa7, 0x29, 0x1c, 0x25, 0x43, 0x8c, 0xeb, 0x16, - 0xa6, 0x03, 0xfb, 0x71, 0xf3, 0x55, 0xf2, 0xe2, 0x2c, 0x85, 0x6b, 0x6f, 0x3e, 0x4b, 0xe7, 0x53, 0xb0, 0xae, 0x0c, - 0xf3, 0x59, 0x3d, 0x0f, 0x20, 0x75, 0x08, 0x69, 0xd6, 0x34, 0xfc, 0x4b, 0xe5, 0x0a, 0xde, 0xda, 0xe3, 0xdd, 0xc0, - 0x45, 0xa9, 0x23, 0x7d, 0xd2, 0x46, 0xd3, 0x25, 0x95, 0xfc, 0x27, 0x91, 0xc7, 0x18, 0xb3, 0xf1, 0x9a, 0x78, 0x3f, - 0x8b, 0xfc, 0x55, 0x01, 0xd8, 0x45, 0x00, 0x86, 0x9c, 0xce, 0x1d, 0x49, 0xfc, 0xe7, 0xe4, 0xfb, 0x3f, 0xa6, 0x4b, - 0xfb, 0x58, 0x16, 0x77, 0xa5, 0xa8, 0xaa, 0xa3, 0xd2, 0x76, 0xb6, 0x5c, 0x0f, 0x4c, 0xa2, 0xfd, 0xbe, 0x64, 0x12, - 0x4d, 0x31, 0x14, 0x05, 0x6e, 0x8d, 0xbd, 0x69, 0xca, 0x15, 0x63, 0xf5, 0xc8, 0x58, 0x3f, 0x5f, 0xee, 0xde, 0xc6, - 0x5e, 0xea, 0x07, 0x29, 0x08, 0xc2, 0x1a, 0x4a, 0x29, 0x45, 0x3e, 0x38, 0x9f, 0x61, 0x2a, 0x51, 0xeb, 0x52, 0xaa, - 0xfc, 0x61, 0xa4, 0xf9, 0x30, 0x05, 0xbd, 0xec, 0xdf, 0x2b, 0x98, 0xff, 0xba, 0x3d, 0x58, 0x9f, 0xd6, 0x65, 0x1a, - 0x55, 0x44, 0x95, 0x17, 0xa6, 0xda, 0x04, 0x22, 0xf8, 0xb5, 0xb0, 0xf8, 0x7e, 0x7d, 0x72, 0x24, 0x68, 0xcc, 0x64, - 0xf9, 0x74, 0xe4, 0x7e, 0x61, 0x5f, 0xb9, 0x8e, 0xe7, 0x7f, 0x6e, 0xe6, 0xff, 0x00, 0x9d, 0x21, 0x8b, 0x6b, 0x6e, - 0x19, 0x2c, 0x70, 0xf6, 0x4b, 0x57, 0x0f, 0xf8, 0x9b, 0x79, 0xe2, 0x1a, 0xe8, 0x98, 0xaf, 0xd1, 0x55, 0x31, 0x9d, - 0x15, 0x03, 0xe0, 0xb2, 0xf5, 0x1b, 0x6b, 0x4e, 0xbc, 0xb1, 0x28, 0xaf, 0xe4, 0x82, 0xd0, 0xd7, 0x55, 0x98, 0x8d, - 0xab, 0x62, 0x53, 0x89, 0x62, 0x53, 0xf7, 0x48, 0x2d, 0x9b, 0x4f, 0x6b, 0x5b, 0x21, 0xfb, 0x57, 0xd1, 0x62, 0xf0, - 0x32, 0xac, 0x93, 0x51, 0x96, 0xae, 0xa7, 0xc0, 0xaf, 0x17, 0xc0, 0x59, 0x64, 0x5e, 0x79, 0xef, 0xec, 0x01, 0x5b, - 0x34, 0x9e, 0x02, 0x39, 0x2a, 0xfd, 0x91, 0x37, 0x46, 0xa7, 0x27, 0xfa, 0xfd, 0x7c, 0x4a, 0x31, 0x5f, 0x7f, 0x05, - 0x78, 0xae, 0x5a, 0x2e, 0x40, 0x5f, 0x86, 0x3a, 0xa8, 0x44, 0xa9, 0x15, 0xc3, 0x88, 0x85, 0xbf, 0x0a, 0x24, 0x72, - 0xa6, 0xc0, 0x66, 0x15, 0x25, 0xa1, 0x12, 0x95, 0x92, 0xad, 0x09, 0x6a, 0xe9, 0x7d, 0x51, 0xd6, 0xfb, 0x0a, 0x1c, - 0x25, 0x23, 0x6d, 0x96, 0x93, 0x66, 0x5c, 0x81, 0x32, 0x17, 0xfd, 0x60, 0xff, 0xaa, 0x3c, 0xbf, 0x91, 0xf9, 0x2c, - 0xf7, 0x1d, 0x9d, 0xd3, 0x76, 0x5c, 0xa0, 0xcc, 0x2d, 0xa7, 0xad, 0x96, 0x3c, 0x26, 0xef, 0x59, 0xb0, 0xed, 0xbf, - 0x48, 0x90, 0x62, 0x11, 0xe6, 0x13, 0xaa, 0x6c, 0xfe, 0x0e, 0xa1, 0xb6, 0x38, 0xb0, 0xc7, 0x2e, 0x4c, 0xc4, 0x7f, - 0x0b, 0x96, 0xc4, 0x30, 0x2b, 0x45, 0x18, 0xef, 0xc0, 0xfb, 0x67, 0x53, 0x89, 0xd1, 0x19, 0x3a, 0xb9, 0x9f, 0x3d, - 0xa4, 0x75, 0x72, 0xf6, 0xf6, 0xf5, 0xd9, 0x0f, 0xbd, 0x41, 0x31, 0x4a, 0xe3, 0x41, 0xef, 0x87, 0xb3, 0xd5, 0x06, - 0xd0, 0x32, 0xc5, 0x59, 0x4c, 0xa6, 0x34, 0x11, 0x9f, 0x91, 0x61, 0xf0, 0xac, 0x4e, 0xc4, 0x19, 0x4d, 0x4c, 0xf7, - 0x35, 0x4a, 0x93, 0x6f, 0x47, 0x61, 0x0e, 0x2f, 0x97, 0x62, 0x53, 0x89, 0x18, 0xec, 0x94, 0x6a, 0x9e, 0xe5, 0xed, - 0xb3, 0x38, 0x1f, 0x75, 0xc8, 0x2a, 0x1d, 0xf8, 0xdb, 0x13, 0x69, 0x57, 0xa5, 0x2b, 0x20, 0xf4, 0x00, 0x38, 0xe9, - 0xca, 0x9f, 0x87, 0x83, 0x48, 0x20, 0xd4, 0x82, 0x39, 0x99, 0x46, 0x74, 0x43, 0x7a, 0x85, 0x7d, 0x06, 0x66, 0x21, - 0xa5, 0x79, 0x70, 0x73, 0xb5, 0x18, 0xba, 0x2b, 0x56, 0x8e, 0xc2, 0x6a, 0x2d, 0xa2, 0x1a, 0x59, 0x8f, 0xc1, 0x79, - 0x07, 0x22, 0x00, 0x14, 0x39, 0x78, 0xc6, 0xa3, 0x7e, 0x3f, 0x52, 0x41, 0x39, 0x09, 0xfd, 0xa2, 0xd0, 0x2f, 0x0d, - 0x47, 0x19, 0xf3, 0xaf, 0xa1, 0xe6, 0x08, 0xa8, 0xff, 0x0f, 0x6f, 0xdf, 0xc2, 0xdd, 0xb6, 0x8d, 0xad, 0xfb, 0x57, - 0x2c, 0xde, 0x54, 0x25, 0x22, 0x48, 0x96, 0xdc, 0xa4, 0x33, 0xa5, 0x0c, 0xeb, 0xb8, 0x79, 0xb4, 0xe9, 0x34, 0x8f, - 0xc6, 0x69, 0xa7, 0x53, 0x5d, 0x1d, 0x97, 0x26, 0x61, 0x8b, 0x0d, 0x0d, 0xa8, 0x24, 0xe5, 0x47, 0x25, 0xfe, 0xf7, - 0xbb, 0xf6, 0xc6, 0x93, 0x14, 0xe5, 0x64, 0xe6, 0x9e, 0x7b, 0x57, 0xd6, 0x8a, 0x45, 0x10, 0xc4, 0x1b, 0x1b, 0x1b, - 0xfb, 0xf1, 0xed, 0x3b, 0x16, 0x9b, 0x70, 0x01, 0xb8, 0x9d, 0x13, 0xea, 0x8c, 0xf7, 0x58, 0x13, 0x98, 0xd3, 0x84, - 0xa0, 0x30, 0xd7, 0xc1, 0xc2, 0x00, 0xd0, 0xbb, 0xf6, 0x68, 0xcb, 0x49, 0x97, 0x60, 0xf1, 0xdc, 0xc0, 0xe2, 0xd5, - 0xc5, 0xa2, 0xba, 0xe6, 0x5a, 0x6e, 0x61, 0x53, 0xca, 0x2a, 0x86, 0x00, 0x02, 0xcd, 0x98, 0x61, 0x77, 0xdc, 0xe5, - 0x48, 0xd6, 0x45, 0xc1, 0xc5, 0x4e, 0x60, 0xe8, 0x66, 0x5c, 0x32, 0x73, 0x70, 0x35, 0xc3, 0x3a, 0xa9, 0x28, 0xc0, - 0xae, 0x2e, 0x40, 0xf6, 0xc2, 0x50, 0xd7, 0xcd, 0x6c, 0xb9, 0x0e, 0x7c, 0x5d, 0xba, 0xf0, 0x25, 0x05, 0x2f, 0x57, - 0x52, 0x94, 0xd9, 0x0d, 0xff, 0xd1, 0xbe, 0x6c, 0xc6, 0x92, 0x42, 0x3b, 0xd2, 0xd7, 0xed, 0xee, 0x68, 0x31, 0x8e, - 0x2d, 0xc7, 0xb7, 0x54, 0xba, 0xd7, 0xa3, 0xea, 0x85, 0xd0, 0xd6, 0xb9, 0x96, 0x59, 0x9a, 0x72, 0xf1, 0x4a, 0xa4, - 0x59, 0xe2, 0x25, 0xc7, 0x3a, 0x56, 0xb5, 0x0b, 0x82, 0xe5, 0xc2, 0x24, 0x3f, 0xcf, 0x4a, 0x8c, 0x1d, 0xdc, 0x68, - 0x54, 0x2b, 0xea, 0x94, 0x89, 0x81, 0x21, 0xdf, 0x63, 0xf0, 0x6d, 0x56, 0x26, 0xc0, 0xf0, 0x63, 0xa2, 0xbe, 0xa4, - 0xa7, 0x10, 0xf0, 0x41, 0x85, 0xe6, 0x7e, 0xce, 0x11, 0xfc, 0xda, 0xaa, 0xcc, 0x81, 0xc9, 0xd6, 0x2a, 0x48, 0xc4, - 0xbd, 0xcb, 0xe6, 0x7a, 0x11, 0x2d, 0xd4, 0x5d, 0xa8, 0x17, 0x6f, 0xb7, 0xbd, 0x44, 0xd1, 0x01, 0x27, 0x3f, 0x0d, - 0x5e, 0xc6, 0x59, 0xce, 0xd3, 0x83, 0x4a, 0x1e, 0xa8, 0x0d, 0x75, 0xa0, 0x9c, 0x39, 0x60, 0xe7, 0x7d, 0x59, 0x1d, - 0xe8, 0x35, 0x7d, 0xa0, 0xdb, 0x79, 0x00, 0x17, 0x0c, 0xdc, 0xb9, 0x57, 0xd9, 0x0d, 0x17, 0x07, 0xa0, 0x0c, 0xb4, - 0xc6, 0x03, 0x75, 0x59, 0x8d, 0xd4, 0xc4, 0xe8, 0x18, 0xd6, 0x89, 0x3e, 0x98, 0x03, 0xfa, 0x1d, 0x84, 0xb5, 0x6f, - 0xbd, 0x5d, 0xe9, 0x83, 0x36, 0xa0, 0x3f, 0x2e, 0x4d, 0x1f, 0x74, 0xe0, 0x78, 0x15, 0x1d, 0xb8, 0x31, 0xa4, 0x1a, - 0xb4, 0xd5, 0xc8, 0x2a, 0x50, 0xbc, 0xe1, 0x2d, 0xde, 0x9d, 0x6b, 0xc9, 0xc6, 0x7b, 0x89, 0x18, 0x5f, 0x99, 0xa8, - 0xe2, 0x4c, 0x9c, 0x7a, 0xa9, 0xbc, 0xd6, 0x4e, 0x32, 0xc2, 0xf8, 0x96, 0x95, 0xd4, 0xdf, 0x21, 0xe6, 0x16, 0x69, - 0x0e, 0x83, 0x17, 0x61, 0x45, 0x66, 0xbc, 0xdf, 0x97, 0x33, 0x19, 0x95, 0x33, 0x71, 0x58, 0x46, 0x0a, 0xac, 0x6d, - 0x9f, 0x08, 0xe8, 0x41, 0x09, 0x90, 0x2f, 0x00, 0xaa, 0x1e, 0x12, 0xfe, 0x3c, 0x24, 0xf5, 0xe9, 0x14, 0xfa, 0x14, - 0xda, 0x7a, 0xc5, 0x15, 0xc4, 0xab, 0xba, 0x31, 0xb2, 0x8d, 0x0a, 0x5a, 0x3c, 0x96, 0x67, 0xb5, 0x61, 0x6c, 0x4e, - 0xad, 0x7f, 0xbd, 0xd9, 0x60, 0xca, 0xe6, 0x42, 0xad, 0xc2, 0x90, 0x44, 0xb7, 0xa5, 0x17, 0x49, 0xc4, 0xc2, 0x66, - 0xb5, 0x36, 0xbf, 0x09, 0x03, 0x92, 0x89, 0x14, 0xf7, 0xb3, 0x25, 0xce, 0x5d, 0x3c, 0x9e, 0x57, 0x7d, 0xad, 0xa5, - 0x45, 0xa6, 0xcd, 0xf7, 0xfa, 0x32, 0xa4, 0xa9, 0xa8, 0x21, 0x8d, 0x3a, 0x33, 0xe8, 0xbe, 0x5d, 0xde, 0xb2, 0x1a, - 0x61, 0x02, 0xbc, 0xd2, 0x19, 0x74, 0xa3, 0xf1, 0x40, 0x2c, 0xab, 0x51, 0xb1, 0x16, 0x02, 0x81, 0x87, 0x21, 0xc7, - 0xcc, 0x12, 0x92, 0xec, 0x2f, 0xfe, 0xad, 0x8a, 0xb3, 0x50, 0xc4, 0xb7, 0x06, 0xd9, 0xbb, 0xb2, 0xae, 0xdd, 0x75, - 0xe4, 0xe7, 0xc4, 0xc2, 0x6a, 0xff, 0xa1, 0x79, 0xd4, 0x1a, 0x67, 0x01, 0x6d, 0x4d, 0xab, 0x1b, 0x0e, 0xf7, 0xa8, - 0x8e, 0x45, 0x69, 0xb0, 0x89, 0x3d, 0xb2, 0x5c, 0xb4, 0x8e, 0x19, 0x34, 0xa0, 0xbf, 0xcb, 0xae, 0xd7, 0xd7, 0x08, - 0xe0, 0x56, 0x22, 0xeb, 0x24, 0x95, 0x7f, 0x49, 0x7b, 0xd4, 0xb5, 0x3d, 0x95, 0xff, 0x6d, 0x9b, 0x2a, 0x87, 0x16, - 0x53, 0x1e, 0xbb, 0x39, 0x0b, 0x54, 0x47, 0x82, 0x28, 0x50, 0x5b, 0x2f, 0x98, 0x7a, 0xa7, 0x4c, 0xd1, 0x01, 0x02, - 0x5d, 0x98, 0x33, 0xec, 0x33, 0x8e, 0x18, 0xb3, 0x54, 0x62, 0x30, 0xf5, 0x31, 0x46, 0x35, 0xad, 0x15, 0xa0, 0xeb, - 0xa7, 0x1b, 0xf8, 0x13, 0x15, 0x35, 0x1a, 0x6a, 0x8d, 0xa4, 0x50, 0x34, 0x51, 0xa1, 0xc8, 0xd2, 0x42, 0xc7, 0x55, - 0xe8, 0x24, 0x12, 0x96, 0x80, 0x86, 0x09, 0xd1, 0x49, 0x05, 0xde, 0x1a, 0xc0, 0x99, 0x8f, 0x8b, 0x72, 0x5d, 0x68, - 0x83, 0xb9, 0xef, 0xe3, 0x1b, 0xfe, 0xea, 0xb9, 0x33, 0xaa, 0x6f, 0x59, 0xeb, 0x7b, 0x5a, 0x90, 0xef, 0x43, 0x4e, - 0xd1, 0x81, 0x89, 0x9d, 0x6c, 0xd0, 0x18, 0xa3, 0xac, 0x75, 0xd4, 0x8b, 0xb7, 0x3a, 0x14, 0x8b, 0x36, 0xc1, 0x7b, - 0xc0, 0x53, 0x44, 0x1b, 0x1e, 0x0a, 0x63, 0x55, 0x8d, 0x4f, 0x25, 0x6b, 0xe9, 0xc1, 0x0a, 0x9e, 0xae, 0x13, 0x1e, - 0x82, 0x1e, 0x89, 0xb0, 0x93, 0xb0, 0x98, 0xc7, 0x0b, 0x38, 0x4e, 0x0a, 0x02, 0x6a, 0x07, 0x7d, 0x05, 0x9f, 0x2f, - 0xd0, 0xfd, 0x55, 0xa2, 0x07, 0x18, 0x5a, 0x10, 0x37, 0xa3, 0xa0, 0x8e, 0xae, 0xe3, 0x55, 0x43, 0x45, 0xc2, 0xe7, - 0x05, 0xd8, 0x0e, 0x29, 0xf5, 0x14, 0x68, 0xa1, 0x12, 0xa5, 0x1f, 0x06, 0xbe, 0x43, 0x63, 0x60, 0x6b, 0x1d, 0xa0, - 0xa1, 0x9f, 0x31, 0x4d, 0xad, 0x33, 0x54, 0x3e, 0xf3, 0xee, 0x99, 0xd1, 0x72, 0x66, 0xd1, 0x18, 0xf4, 0x6d, 0x34, - 0x45, 0x71, 0x4e, 0x3e, 0x0b, 0x8a, 0x38, 0xcd, 0xe2, 0x1c, 0xfc, 0x36, 0xe3, 0x02, 0x33, 0x26, 0x71, 0xc5, 0xaf, - 0x64, 0x01, 0xda, 0xee, 0x5c, 0xa5, 0xd6, 0x35, 0x08, 0xc8, 0xbe, 0x07, 0xab, 0x97, 0x86, 0x8e, 0xca, 0x79, 0x77, - 0x69, 0x53, 0x88, 0x58, 0x84, 0x60, 0xd3, 0x4c, 0x97, 0xec, 0x34, 0x54, 0xda, 0x1c, 0x08, 0x75, 0x84, 0xc6, 0xfd, - 0xd3, 0x30, 0xb6, 0x9a, 0x62, 0x6b, 0xf7, 0xb6, 0xdd, 0xfe, 0x5a, 0x7a, 0xe9, 0x34, 0x27, 0x3d, 0xc6, 0x7e, 0x2d, - 0xc3, 0x62, 0x64, 0x3b, 0x42, 0x60, 0xc9, 0x79, 0x9f, 0xfa, 0xaf, 0x68, 0x39, 0x4f, 0xc0, 0x74, 0x44, 0x07, 0xcb, - 0x05, 0xca, 0x8e, 0x01, 0xdd, 0x81, 0xc1, 0x15, 0xfd, 0x3e, 0x58, 0x65, 0x98, 0x0b, 0xc9, 0x92, 0xa4, 0x0c, 0x9e, - 0xa7, 0x1e, 0x1c, 0xfc, 0x9a, 0x29, 0x73, 0x17, 0x65, 0x7d, 0xba, 0x24, 0xd3, 0x14, 0x19, 0x88, 0x75, 0xb8, 0xc9, - 0xd2, 0x28, 0x51, 0x22, 0xb2, 0x25, 0xfa, 0x47, 0x1a, 0x8a, 0xa5, 0x23, 0xf7, 0x22, 0x55, 0x22, 0x54, 0xcc, 0x53, - 0x3c, 0xa9, 0xd3, 0x3a, 0x1d, 0x61, 0xe8, 0x49, 0x50, 0xca, 0xd5, 0x30, 0x50, 0x25, 0xd5, 0x4b, 0x61, 0x53, 0x6c, - 0xb7, 0xfa, 0x62, 0x25, 0xe6, 0xf1, 0x02, 0x5f, 0x0a, 0x1c, 0xc5, 0x7f, 0x70, 0x2f, 0xec, 0x94, 0xda, 0x1e, 0xd4, - 0x8e, 0x28, 0xa1, 0xff, 0xe0, 0x70, 0x91, 0xf8, 0x56, 0xea, 0x10, 0x80, 0x68, 0x11, 0x72, 0xae, 0x0e, 0x52, 0xc3, - 0x0d, 0xed, 0x08, 0xff, 0x0d, 0xd7, 0x67, 0x9c, 0xd1, 0x9b, 0x6a, 0x46, 0x0d, 0xe5, 0xeb, 0x41, 0x1b, 0xa3, 0x3e, - 0x1b, 0x38, 0xac, 0x10, 0x85, 0x36, 0xec, 0xa4, 0x54, 0xa2, 0x85, 0xa1, 0x54, 0x7f, 0x09, 0x15, 0x27, 0xdc, 0x99, - 0x51, 0x96, 0x8c, 0x4f, 0xcb, 0x63, 0x31, 0x1d, 0x0c, 0x4a, 0x52, 0x19, 0x0b, 0x3d, 0xb8, 0x1e, 0x78, 0xfe, 0x3d, - 0x70, 0x0b, 0xf1, 0x90, 0x91, 0xc5, 0x90, 0x1b, 0x9c, 0xfc, 0x16, 0x27, 0x57, 0x8d, 0x4a, 0x15, 0xc7, 0x9a, 0xa8, - 0x16, 0xfc, 0xa3, 0x0c, 0x03, 0xf4, 0x49, 0x0a, 0xc0, 0x64, 0x30, 0xe5, 0x77, 0x20, 0x51, 0x3a, 0x57, 0x37, 0xa4, - 0x9f, 0x45, 0xc1, 0x2f, 0x79, 0xc1, 0x45, 0xe2, 0x0a, 0xb0, 0xbc, 0x83, 0xed, 0x75, 0x54, 0x51, 0x85, 0xc9, 0x6b, - 0x7a, 0x1c, 0x71, 0xe3, 0xfd, 0x67, 0x7a, 0x6c, 0x31, 0x5b, 0xad, 0x63, 0x83, 0xcf, 0x1c, 0x83, 0x0b, 0xba, 0x96, - 0xd8, 0x1a, 0xaa, 0x61, 0x45, 0x60, 0xe0, 0x02, 0x0e, 0xc2, 0x12, 0xc5, 0xb1, 0x95, 0xbc, 0x22, 0x0d, 0x29, 0xed, - 0x03, 0xc3, 0xd1, 0x26, 0x39, 0xbe, 0xcd, 0xb2, 0x9b, 0xc0, 0xf9, 0xa2, 0x73, 0xd2, 0x4c, 0x58, 0x1b, 0xbc, 0xcf, - 0x9b, 0xf3, 0x6b, 0xff, 0x90, 0x50, 0x15, 0xf7, 0x86, 0xb7, 0xe3, 0xde, 0x38, 0xe1, 0xd7, 0x5c, 0x2c, 0x74, 0xa8, - 0x16, 0x73, 0xc9, 0xf2, 0x5b, 0xeb, 0xdd, 0x92, 0xa4, 0x56, 0x40, 0xfb, 0x2c, 0x0b, 0x6a, 0x22, 0x00, 0xe4, 0x0f, - 0x7f, 0x81, 0xd0, 0x19, 0xfe, 0xf6, 0x18, 0x5c, 0x91, 0xc2, 0xbd, 0x43, 0x20, 0xac, 0xe9, 0xe6, 0x4e, 0x6d, 0xc0, - 0x17, 0xe3, 0xfe, 0x8c, 0xa9, 0xa7, 0xdf, 0x66, 0x72, 0x57, 0xd7, 0xed, 0x91, 0x65, 0xf8, 0x08, 0x57, 0x0a, 0xe0, - 0x66, 0xc2, 0x5f, 0x0c, 0x33, 0xa9, 0x3e, 0x01, 0x4c, 0x35, 0x1d, 0xdc, 0x27, 0x08, 0x0c, 0xa0, 0x12, 0x2d, 0x46, - 0x37, 0xca, 0x11, 0xcd, 0xc0, 0xad, 0xe9, 0x56, 0x18, 0x6f, 0x3d, 0x68, 0xa1, 0x67, 0x1a, 0x4e, 0xfc, 0x07, 0xcd, - 0xbc, 0x2a, 0x20, 0x80, 0x56, 0x46, 0xf0, 0xd6, 0xfa, 0x68, 0x8e, 0x10, 0x9f, 0xb0, 0x24, 0x9a, 0xb0, 0x78, 0xa6, - 0xf8, 0x31, 0xa1, 0x9b, 0xa6, 0xb6, 0xe9, 0x03, 0xd2, 0x5f, 0x5c, 0xb3, 0x7e, 0xca, 0xb2, 0xf6, 0xed, 0xa1, 0xe2, - 0xc5, 0xb4, 0x19, 0x07, 0x31, 0x51, 0xc5, 0xf8, 0x5f, 0x70, 0x5f, 0x6a, 0x05, 0x88, 0xcc, 0x5d, 0xf5, 0xf4, 0xfb, - 0xcd, 0x6c, 0x39, 0x10, 0x2a, 0xbf, 0x33, 0x48, 0xfa, 0x74, 0x68, 0x3f, 0xb0, 0x49, 0xd4, 0x16, 0x7a, 0xfe, 0xb8, - 0xd4, 0x4d, 0xbc, 0xbc, 0x36, 0x35, 0xa2, 0x15, 0x32, 0x54, 0xb6, 0x0e, 0x58, 0xdf, 0xdf, 0x87, 0xbb, 0x8b, 0x9a, - 0x86, 0x5a, 0xf7, 0xdc, 0xb5, 0x28, 0x38, 0xf1, 0x07, 0x18, 0x8b, 0x0b, 0x49, 0xad, 0xe3, 0x31, 0xe9, 0x47, 0x0b, - 0x99, 0xdc, 0xa8, 0xab, 0x93, 0x33, 0xc5, 0x3c, 0x81, 0x0b, 0x70, 0xd9, 0xf6, 0x57, 0x54, 0xea, 0x52, 0x6e, 0xaf, - 0x28, 0x4d, 0x0f, 0x69, 0x7b, 0x15, 0xe7, 0x6d, 0xc1, 0x05, 0xff, 0x4c, 0xc1, 0x85, 0x75, 0xb0, 0xee, 0xb8, 0x53, - 0xf6, 0x84, 0x27, 0xca, 0xb4, 0x36, 0xb8, 0xeb, 0x06, 0x63, 0x62, 0xec, 0x77, 0x97, 0x3c, 0xf9, 0x88, 0x2c, 0xf8, - 0xb7, 0x99, 0x00, 0xcf, 0x64, 0xf7, 0x4a, 0xe5, 0xff, 0xde, 0xbf, 0xda, 0xda, 0x77, 0xd6, 0xfc, 0xd3, 0xb3, 0x1e, - 0xee, 0x1c, 0x26, 0x3f, 0x56, 0x67, 0x40, 0x37, 0xd7, 0x32, 0xe5, 0x80, 0x0c, 0x60, 0x2d, 0x92, 0xd1, 0x80, 0x0f, - 0xad, 0x2c, 0xdb, 0xbe, 0xd3, 0xea, 0x82, 0xb0, 0x97, 0xc0, 0x4d, 0xf7, 0xd7, 0x66, 0x66, 0x4e, 0xd7, 0x4a, 0x34, - 0x5d, 0x1a, 0x5b, 0xcb, 0x52, 0x85, 0xf1, 0xbe, 0xf7, 0x24, 0x9b, 0xe6, 0xc7, 0xcb, 0x69, 0x6e, 0xa9, 0xdb, 0xc6, - 0x2d, 0x1b, 0x40, 0x43, 0xec, 0x5a, 0x5b, 0x39, 0xe0, 0xe5, 0xf6, 0x20, 0x9a, 0xaf, 0x15, 0xa1, 0xa7, 0x4a, 0x84, - 0x3e, 0x4d, 0x9b, 0x7d, 0xb0, 0xab, 0x6a, 0xdd, 0x08, 0x79, 0x34, 0x48, 0x35, 0x23, 0xff, 0xf6, 0x86, 0x17, 0x97, - 0xb9, 0xbc, 0x05, 0x38, 0x64, 0x52, 0x1b, 0x85, 0xe5, 0x35, 0xb8, 0xf3, 0xa3, 0xe3, 0x38, 0x13, 0xa3, 0x1c, 0xe3, - 0xb6, 0x22, 0x52, 0xb2, 0x4e, 0x9c, 0x01, 0x1e, 0xb2, 0x3f, 0x69, 0x3a, 0xb4, 0x6b, 0x81, 0xe1, 0x7d, 0x81, 0xbb, - 0xca, 0xd9, 0xc9, 0x26, 0xb7, 0x8b, 0xbe, 0x39, 0xc3, 0xba, 0x23, 0xa5, 0xb5, 0xb1, 0xe8, 0xba, 0x83, 0xb5, 0x66, - 0xd0, 0x16, 0xa1, 0xe4, 0x43, 0xee, 0xa4, 0xfd, 0x2b, 0xa0, 0xc1, 0x79, 0x96, 0xde, 0x59, 0xab, 0xfc, 0x8d, 0x16, - 0xe2, 0x44, 0x31, 0x75, 0xe2, 0x9b, 0x28, 0xd1, 0xe7, 0x67, 0x62, 0xdc, 0x40, 0x20, 0xf5, 0x7b, 0x8c, 0xaf, 0x51, - 0x84, 0x09, 0x5c, 0x07, 0xa2, 0xd8, 0x9e, 0xa8, 0x8d, 0xe5, 0x08, 0x3a, 0x21, 0xc4, 0x3b, 0x28, 0xc3, 0x58, 0x5d, - 0x1c, 0x68, 0x83, 0xa5, 0xaf, 0x5b, 0xeb, 0xdc, 0x10, 0x0a, 0xe3, 0x04, 0xa6, 0x18, 0x24, 0x75, 0xd6, 0x59, 0x26, - 0xa8, 0xb2, 0x63, 0xd2, 0x79, 0x1f, 0xa0, 0xbb, 0x6b, 0xd1, 0x14, 0x5f, 0x77, 0xee, 0xa0, 0x7d, 0x5c, 0xbf, 0xd6, - 0x22, 0x37, 0xf8, 0xf3, 0x96, 0x08, 0x8b, 0xc0, 0x59, 0x6b, 0xf2, 0x55, 0x23, 0x1c, 0x98, 0x92, 0x4c, 0xc3, 0x5e, - 0xa2, 0x6c, 0xba, 0xb7, 0xdb, 0x5e, 0x6f, 0xaf, 0x88, 0xab, 0xc7, 0x58, 0xe5, 0xdd, 0xcc, 0xed, 0x9d, 0x6a, 0x2d, - 0x76, 0x6f, 0xda, 0x7e, 0x8a, 0x1d, 0xb5, 0xd6, 0x6e, 0x37, 0x9c, 0x50, 0x43, 0xbe, 0x15, 0x55, 0x5a, 0x9d, 0x6e, - 0x0c, 0xda, 0x21, 0xb4, 0xb5, 0xc8, 0xe0, 0x46, 0xf9, 0xdc, 0x09, 0x9d, 0x54, 0xc8, 0x55, 0xa7, 0x2e, 0xd8, 0x5c, - 0xf3, 0x6a, 0x29, 0xd3, 0x48, 0x50, 0xb4, 0x39, 0x8f, 0x4a, 0x9a, 0xc8, 0xb5, 0xa8, 0x22, 0x59, 0xa3, 0x5e, 0xd4, - 0x6a, 0x0c, 0x10, 0x90, 0xe9, 0xbc, 0xe9, 0x41, 0x15, 0xcc, 0x86, 0x32, 0x92, 0xd3, 0xf7, 0x60, 0x69, 0x8f, 0x1c, - 0x6b, 0xbd, 0xaf, 0xce, 0x16, 0xdf, 0xea, 0x09, 0xc1, 0x14, 0x66, 0x0f, 0x44, 0x84, 0x6b, 0x1a, 0x43, 0x4e, 0xbb, - 0xc4, 0x65, 0x4d, 0xb7, 0x84, 0x3d, 0xdc, 0xae, 0x64, 0x27, 0x6e, 0x9e, 0x34, 0x37, 0x57, 0xb0, 0x93, 0x62, 0x3e, - 0x06, 0xed, 0x97, 0x54, 0xd7, 0x2e, 0xcd, 0xad, 0xc7, 0x83, 0x80, 0x06, 0x83, 0xc2, 0xf0, 0xaf, 0x13, 0xe3, 0xe1, - 0x49, 0x03, 0x82, 0xa4, 0x5c, 0x84, 0x63, 0xdf, 0x88, 0x7e, 0x32, 0x95, 0xc7, 0x1c, 0x2d, 0xde, 0xa1, 0xd5, 0x09, - 0x04, 0xf4, 0x12, 0xa1, 0x24, 0x46, 0x55, 0x68, 0x44, 0x50, 0x9e, 0x96, 0xbf, 0x54, 0xd5, 0x21, 0xa0, 0x90, 0xf6, - 0x15, 0x85, 0xb2, 0x4d, 0x62, 0x68, 0x86, 0x5f, 0xce, 0x27, 0x0b, 0x3d, 0x03, 0x03, 0x39, 0x3f, 0x5a, 0xe8, 0x59, - 0x18, 0xc8, 0xf9, 0x57, 0x8b, 0xda, 0xad, 0x03, 0x4d, 0x40, 0x3c, 0x17, 0x8e, 0x4e, 0x4a, 0xab, 0xb2, 0x05, 0x74, - 0xf3, 0x10, 0x41, 0xff, 0x87, 0x3d, 0x04, 0x9d, 0x5c, 0x68, 0x47, 0x6e, 0x40, 0xdb, 0x21, 0x09, 0xec, 0x15, 0x93, - 0x0a, 0x13, 0x8b, 0xe8, 0x98, 0x8d, 0xc1, 0x10, 0x5b, 0x7d, 0x70, 0xcc, 0xc6, 0x53, 0x9f, 0x04, 0x01, 0xa3, 0xfb, - 0xbd, 0x01, 0x07, 0xbf, 0xc3, 0xab, 0xf4, 0xc9, 0x46, 0xa0, 0x9b, 0xbe, 0xbb, 0x1b, 0x7a, 0x17, 0x57, 0x70, 0xaa, - 0x76, 0xf7, 0x24, 0x74, 0x93, 0x69, 0xc7, 0xea, 0x35, 0xc4, 0x0d, 0xf9, 0x95, 0xd1, 0x68, 0x64, 0x53, 0x42, 0x42, - 0x0c, 0xe7, 0xd0, 0xcc, 0x69, 0xb9, 0x7c, 0x75, 0xeb, 0xd9, 0x80, 0x0c, 0x33, 0xbd, 0x63, 0xb2, 0x7e, 0x80, 0xb2, - 0xea, 0x31, 0xb4, 0x43, 0xef, 0x91, 0xe3, 0x87, 0x07, 0xdf, 0x64, 0xfc, 0xc4, 0xe1, 0xda, 0xc3, 0xb9, 0xf0, 0x5d, - 0xd6, 0x8c, 0xcc, 0xa1, 0xf3, 0xec, 0xe3, 0x78, 0x0f, 0xe3, 0xe4, 0xd3, 0x2c, 0x94, 0x37, 0x5e, 0xd3, 0xff, 0xa8, - 0xf4, 0x66, 0x87, 0x43, 0x4e, 0x57, 0xb0, 0xe2, 0x66, 0x55, 0x68, 0xf8, 0x59, 0xe4, 0x8d, 0x23, 0x5e, 0x93, 0xa8, - 0xea, 0x3e, 0xef, 0x6d, 0xc4, 0xd2, 0x8e, 0x71, 0x00, 0x70, 0xa2, 0x56, 0x0d, 0xbb, 0xd2, 0xb8, 0x56, 0x07, 0x31, - 0x22, 0x25, 0x6c, 0x95, 0x38, 0x12, 0xca, 0xdf, 0x00, 0x84, 0xc5, 0x50, 0x1c, 0x6f, 0x0d, 0xeb, 0x03, 0xec, 0x87, - 0x2e, 0xd0, 0x34, 0xa7, 0x54, 0x33, 0x00, 0x48, 0x02, 0xfe, 0xe8, 0xe9, 0xa6, 0xa1, 0xb2, 0xcd, 0xf3, 0xd0, 0xb2, - 0xba, 0x82, 0x07, 0x7a, 0xea, 0x4a, 0x06, 0xc6, 0x55, 0x1d, 0x7b, 0x9b, 0xfd, 0xed, 0xd1, 0x2a, 0xf2, 0x9d, 0x4d, - 0x6a, 0x9a, 0x05, 0x90, 0xa2, 0x71, 0xe9, 0x0b, 0x3d, 0x9d, 0x00, 0xad, 0xd7, 0x96, 0x8a, 0xf6, 0xfb, 0x28, 0x46, - 0x8d, 0x0b, 0x05, 0x56, 0x61, 0x82, 0xc2, 0x21, 0xc2, 0x08, 0xa1, 0xdf, 0x95, 0xe1, 0xc6, 0x17, 0x64, 0x10, 0x0d, - 0xd7, 0xa2, 0x43, 0x11, 0x39, 0x5e, 0xb4, 0x2d, 0x55, 0x35, 0x27, 0x4d, 0x5b, 0x02, 0x6f, 0x22, 0x03, 0xb6, 0xf3, - 0x4f, 0x1b, 0x22, 0x57, 0xe1, 0x02, 0x86, 0xef, 0x88, 0x6b, 0x41, 0x74, 0x53, 0x9b, 0x7a, 0x1b, 0x76, 0x88, 0x8e, - 0xa6, 0x78, 0x74, 0xc8, 0x3d, 0x77, 0xcf, 0x6d, 0x11, 0xdf, 0x7e, 0x82, 0xdc, 0x35, 0x9d, 0xbd, 0x14, 0x61, 0x50, - 0xb7, 0x6c, 0xa0, 0x58, 0xc7, 0x4e, 0x50, 0x80, 0x01, 0x5c, 0xfe, 0x02, 0x3a, 0x36, 0x18, 0x54, 0x04, 0x9f, 0x14, - 0xb6, 0x4d, 0x83, 0xfc, 0x11, 0xef, 0x86, 0x0e, 0xaf, 0x2d, 0x79, 0x20, 0x5e, 0x61, 0x9f, 0x28, 0x61, 0xff, 0x82, - 0x82, 0xee, 0x28, 0x2f, 0x57, 0x85, 0xab, 0xd2, 0x00, 0x54, 0xd9, 0xf1, 0x5c, 0x6b, 0x4a, 0x5a, 0xc0, 0x4a, 0x49, - 0xdd, 0xf9, 0x4d, 0x70, 0xdc, 0x92, 0xa9, 0xf0, 0xad, 0xba, 0x51, 0xe5, 0xb1, 0x44, 0x91, 0x8e, 0x3d, 0xdb, 0x39, - 0x58, 0x03, 0xe0, 0x29, 0x6c, 0x2f, 0xce, 0x04, 0x7c, 0xee, 0xb4, 0xcb, 0x96, 0xb9, 0x04, 0x8a, 0xfa, 0x61, 0x9c, - 0x97, 0x1d, 0x5f, 0xee, 0x8e, 0xb6, 0xf7, 0xd0, 0x1b, 0xb1, 0x31, 0x5e, 0x9f, 0x47, 0x4d, 0x3f, 0x7b, 0x86, 0x2b, - 0x4b, 0x41, 0x1e, 0x68, 0xaa, 0x47, 0x18, 0x1d, 0x02, 0xd3, 0x94, 0x9f, 0xb0, 0xf1, 0x74, 0x38, 0x34, 0x64, 0xd0, - 0x6b, 0x26, 0x86, 0x02, 0xfb, 0x0c, 0x5a, 0x67, 0x26, 0xae, 0xf1, 0x69, 0xfb, 0x0a, 0x5a, 0xdd, 0xa1, 0x4c, 0xee, - 0x1c, 0x0c, 0x1f, 0x68, 0xc9, 0x14, 0x4c, 0x15, 0xde, 0x10, 0xa9, 0x64, 0x6f, 0x96, 0xd6, 0x61, 0xdf, 0x2e, 0x14, - 0x5a, 0x68, 0xe2, 0x57, 0x19, 0xe2, 0xa7, 0xae, 0x33, 0xff, 0x36, 0xed, 0x53, 0x83, 0x58, 0x38, 0x12, 0x83, 0x88, - 0x5f, 0x9c, 0x2a, 0xdb, 0x09, 0xa1, 0x62, 0xe3, 0xa1, 0x6b, 0xdd, 0x38, 0x92, 0x2a, 0x0c, 0xa5, 0xd0, 0x78, 0x6a, - 0xb8, 0xef, 0x85, 0x0e, 0x5f, 0x87, 0x59, 0xdc, 0x66, 0x8d, 0xa4, 0xc6, 0x38, 0x15, 0x26, 0x4e, 0xa5, 0x5c, 0x45, - 0x02, 0x03, 0xe5, 0xd9, 0xc2, 0x20, 0xc0, 0x24, 0x26, 0x19, 0x5b, 0x0b, 0x61, 0xc2, 0xd8, 0xb9, 0xc2, 0x34, 0x75, - 0x91, 0xfa, 0xcd, 0xc0, 0x64, 0x41, 0x43, 0x7e, 0x8f, 0x46, 0x6b, 0xaa, 0xa6, 0x00, 0xc3, 0x38, 0x4a, 0x35, 0xfe, - 0x2d, 0x42, 0x6d, 0x86, 0x01, 0x80, 0x6d, 0xde, 0xc9, 0x4c, 0x54, 0xaf, 0x04, 0x42, 0xa0, 0x39, 0xfb, 0xa9, 0xb8, - 0xda, 0x99, 0x05, 0xa3, 0x68, 0xb7, 0x57, 0x3e, 0x1f, 0x38, 0xa1, 0x3c, 0x55, 0x17, 0xa8, 0x97, 0xb2, 0x78, 0x2d, - 0x53, 0xde, 0x0a, 0x91, 0x79, 0x20, 0xd9, 0x87, 0x7c, 0x04, 0xe7, 0x15, 0x3a, 0x95, 0x9b, 0x6d, 0xa2, 0xcc, 0x92, - 0x24, 0x63, 0x81, 0xb1, 0x79, 0x09, 0x66, 0x52, 0x33, 0x63, 0xf8, 0x35, 0xc4, 0x19, 0xdb, 0x39, 0x09, 0x37, 0xfb, - 0x79, 0x60, 0x88, 0x52, 0x2e, 0x5a, 0xa2, 0x61, 0x6b, 0xc7, 0xeb, 0xc9, 0x35, 0xe1, 0x3e, 0x6c, 0xc4, 0x9a, 0x8c, - 0x31, 0xae, 0xcd, 0x8d, 0xac, 0x1f, 0x2d, 0xf0, 0x60, 0x4c, 0x59, 0x7f, 0x02, 0x99, 0x56, 0x52, 0xd6, 0xf9, 0xc2, - 0x88, 0x99, 0x54, 0xa2, 0x77, 0xfb, 0xc6, 0x67, 0x75, 0x17, 0x51, 0xbf, 0xb5, 0xdf, 0x93, 0x7a, 0xb8, 0xf7, 0x1f, - 0x14, 0xd6, 0xa0, 0x32, 0xe2, 0x32, 0xa2, 0x3c, 0x73, 0xa0, 0x9b, 0x26, 0x45, 0x9c, 0x9e, 0xaf, 0xe2, 0xa2, 0xe4, - 0x29, 0x54, 0xaa, 0xa9, 0x5b, 0xd4, 0x9b, 0x80, 0xbd, 0x21, 0x92, 0x24, 0x6b, 0x69, 0x6c, 0xc5, 0x2e, 0x0d, 0xd2, - 0xb3, 0x37, 0xe2, 0xd2, 0xcb, 0x0a, 0x0d, 0x69, 0xa9, 0x77, 0x16, 0x2a, 0x99, 0xbf, 0xe2, 0x3f, 0x83, 0x5a, 0x81, - 0x8e, 0x36, 0x29, 0xc6, 0x33, 0x60, 0xc4, 0x77, 0x83, 0x59, 0x3d, 0x40, 0x5c, 0x34, 0x41, 0xa9, 0x77, 0xc4, 0x8e, - 0x9f, 0x9a, 0x3c, 0xbc, 0x0b, 0x39, 0x67, 0xf0, 0xe9, 0xc3, 0x2c, 0x51, 0x6b, 0x1d, 0x89, 0x91, 0x9a, 0x01, 0x34, - 0x1d, 0x94, 0x39, 0x8f, 0x45, 0x30, 0xeb, 0x99, 0xc4, 0xa8, 0xc7, 0xf5, 0x2f, 0xd0, 0x50, 0xfb, 0xcd, 0xca, 0xf2, - 0xac, 0xba, 0xff, 0x1c, 0x0e, 0x6c, 0x6a, 0x2b, 0xe8, 0xf1, 0xba, 0x92, 0x57, 0x57, 0xaa, 0xdb, 0x7e, 0x21, 0x46, - 0x4e, 0xd7, 0xb8, 0x96, 0xce, 0xab, 0x05, 0xeb, 0x75, 0xa7, 0x9b, 0xc5, 0xdd, 0x2c, 0xa3, 0x81, 0xb0, 0xb6, 0xf3, - 0x89, 0xe6, 0xcf, 0x9a, 0x6d, 0xf7, 0xf1, 0x16, 0xc4, 0x2c, 0x00, 0x88, 0xf4, 0x20, 0x0a, 0x96, 0x59, 0xca, 0x03, - 0x2a, 0xf7, 0x71, 0x94, 0x85, 0xd2, 0xcb, 0x59, 0xc6, 0x4f, 0x9b, 0xc6, 0x5a, 0x67, 0x85, 0x32, 0xb4, 0x36, 0xba, - 0xd3, 0x55, 0x86, 0xd8, 0x7e, 0x12, 0x67, 0x0b, 0x70, 0x7f, 0xcc, 0x50, 0x68, 0xe8, 0x2c, 0x23, 0x4d, 0x34, 0x7c, - 0xd7, 0x9e, 0x41, 0x46, 0x71, 0xb2, 0xce, 0x2b, 0xe9, 0x46, 0x9f, 0xb5, 0x91, 0x30, 0xf7, 0x10, 0xfd, 0x2a, 0x06, - 0x8f, 0x72, 0x9f, 0xd7, 0x46, 0x27, 0xd3, 0x32, 0xd2, 0xee, 0xfc, 0xa4, 0x5e, 0x66, 0xa9, 0xd6, 0x61, 0xfb, 0x0c, - 0x7b, 0x6b, 0x4c, 0x7a, 0x13, 0x52, 0xc3, 0x48, 0x7c, 0x3a, 0xa3, 0x46, 0x08, 0x68, 0xcb, 0xf1, 0x77, 0xf8, 0x0c, - 0x43, 0x53, 0x60, 0xa9, 0xe2, 0x16, 0x76, 0xc3, 0xd7, 0x7c, 0xb2, 0x6a, 0x01, 0x08, 0x66, 0xe5, 0xeb, 0x5d, 0xbc, - 0x12, 0xea, 0x73, 0x6d, 0x06, 0x80, 0x2c, 0x28, 0xe5, 0x8e, 0x9f, 0x52, 0xe9, 0x60, 0x89, 0xa2, 0xed, 0xe5, 0xf4, - 0x8d, 0x8e, 0x8d, 0x1f, 0xd2, 0x73, 0x01, 0xdb, 0x85, 0xfc, 0xd6, 0x5e, 0xbd, 0x44, 0x45, 0x6a, 0xdb, 0xac, 0x07, - 0xf8, 0x72, 0x83, 0x26, 0x61, 0x04, 0x65, 0xca, 0x14, 0xc0, 0xe0, 0xa6, 0x1a, 0x05, 0x93, 0x56, 0x23, 0x61, 0x4b, - 0x3d, 0xc9, 0x72, 0xd3, 0x07, 0xa7, 0xda, 0x23, 0xe8, 0xd1, 0x0e, 0x27, 0x2d, 0xfb, 0xb5, 0x82, 0xa3, 0x93, 0xab, - 0x21, 0x6a, 0xe6, 0xbd, 0xb6, 0x23, 0x43, 0xca, 0x65, 0x18, 0x08, 0xa6, 0x1c, 0xf3, 0xf4, 0xd8, 0x7a, 0x46, 0x44, - 0x0f, 0x9c, 0x7d, 0xa6, 0x5b, 0x75, 0x25, 0x01, 0xd1, 0xf1, 0xeb, 0x27, 0xaf, 0xae, 0xe3, 0x2b, 0x83, 0xa2, 0xd4, - 0xb0, 0x88, 0x51, 0xa6, 0x7d, 0x95, 0x84, 0xc1, 0xfb, 0xf9, 0xfd, 0x8f, 0x2a, 0x4b, 0xed, 0xf7, 0x60, 0x63, 0x45, - 0x55, 0x3f, 0x97, 0xbc, 0x68, 0x0a, 0xb0, 0xf6, 0x59, 0xa2, 0x40, 0xee, 0xf7, 0x36, 0xcd, 0x7c, 0x13, 0x35, 0x6e, - 0x36, 0xac, 0x37, 0xae, 0xdb, 0xa5, 0xb6, 0x64, 0x47, 0x56, 0x22, 0x67, 0x16, 0x83, 0x19, 0x3f, 0x2a, 0x0c, 0x4a, - 0xc3, 0x06, 0x55, 0xa9, 0xf8, 0xbd, 0x11, 0xc1, 0xa9, 0x63, 0x55, 0x61, 0x4c, 0x03, 0x66, 0x5b, 0x51, 0x6b, 0x50, - 0x07, 0xa5, 0xb4, 0x35, 0x01, 0xd9, 0x7e, 0x65, 0x05, 0x35, 0xbf, 0xff, 0x65, 0x0c, 0xf9, 0x9a, 0x52, 0x50, 0x49, - 0xc0, 0xce, 0xa0, 0xd1, 0x53, 0x25, 0x0c, 0xa4, 0x20, 0x78, 0x02, 0x94, 0x2f, 0xa2, 0xc6, 0x6a, 0xb7, 0xaf, 0x4e, - 0x8d, 0xd1, 0x16, 0x10, 0x5a, 0x48, 0x8f, 0x2e, 0xfb, 0xb8, 0x8d, 0x75, 0x20, 0xf1, 0xe0, 0x04, 0xdb, 0xb9, 0xba, - 0x46, 0x23, 0xa1, 0xf9, 0x43, 0xa3, 0x01, 0xaf, 0x69, 0x05, 0x0a, 0xf5, 0x1c, 0x47, 0x43, 0x67, 0x87, 0x14, 0x44, - 0x6c, 0xd0, 0xc2, 0xbe, 0x3d, 0x1f, 0x9a, 0x7d, 0x3d, 0x4f, 0x16, 0xa4, 0xa6, 0xd2, 0x7d, 0xee, 0x96, 0x90, 0xb5, - 0xea, 0x50, 0x56, 0x1e, 0xe0, 0x78, 0xa1, 0x64, 0xfe, 0x0e, 0x93, 0x1a, 0xa5, 0x31, 0xa1, 0x31, 0x62, 0x01, 0x4b, - 0x82, 0xf6, 0x7a, 0xa0, 0x7e, 0x19, 0x84, 0x0a, 0x67, 0x7a, 0x22, 0xf1, 0x29, 0xe5, 0xea, 0xd3, 0x82, 0xd4, 0xd3, - 0x82, 0x39, 0xd0, 0x4b, 0xdf, 0xca, 0xaf, 0x6c, 0x7c, 0xb4, 0xbb, 0x77, 0xcd, 0x85, 0x75, 0x0c, 0x71, 0xb1, 0x85, - 0xdf, 0x9c, 0x9a, 0x02, 0xb0, 0xe1, 0xa9, 0x2e, 0xcb, 0x37, 0x6a, 0x22, 0xb3, 0x38, 0x24, 0x11, 0x48, 0xb6, 0x9b, - 0x9b, 0xdb, 0x08, 0xb6, 0xbd, 0x85, 0xda, 0x50, 0x7f, 0x79, 0xdb, 0x7d, 0xcf, 0xf0, 0x72, 0x4f, 0xee, 0xdd, 0xb4, - 0xa1, 0xfc, 0x7e, 0xff, 0x2a, 0xf9, 0xbf, 0xaa, 0x64, 0xbf, 0x55, 0x66, 0xdd, 0x16, 0xef, 0x77, 0x1d, 0xb7, 0x1c, - 0xa3, 0x41, 0x60, 0x4d, 0x81, 0x81, 0xf4, 0xa4, 0x31, 0x4d, 0x74, 0x74, 0x65, 0xc6, 0x0c, 0x1e, 0x5d, 0x80, 0xe6, - 0x30, 0x9d, 0xe7, 0x31, 0x00, 0x07, 0xf8, 0x47, 0x1e, 0xa1, 0xfe, 0xe9, 0x3c, 0x0f, 0xce, 0x83, 0x41, 0x39, 0x08, - 0xf4, 0x27, 0xae, 0x39, 0xc1, 0x02, 0x74, 0x6e, 0x31, 0x83, 0xb8, 0x93, 0xd6, 0xcc, 0x21, 0x3e, 0x4e, 0xa6, 0x83, - 0x41, 0x4c, 0x36, 0x00, 0xd2, 0x17, 0x2f, 0xac, 0x73, 0x50, 0xa1, 0x17, 0x64, 0xab, 0xee, 0xa2, 0x59, 0xb1, 0x57, - 0xed, 0x34, 0xef, 0xf7, 0xf3, 0x79, 0x39, 0x08, 0x1a, 0x15, 0x16, 0xc6, 0xfb, 0x8f, 0x36, 0xbf, 0x34, 0x3a, 0x69, - 0x82, 0x11, 0x6b, 0x4f, 0x51, 0xbd, 0xe2, 0x69, 0x46, 0x1b, 0xb7, 0x63, 0xa5, 0x7c, 0x01, 0x51, 0x3c, 0x30, 0x64, - 0xad, 0xbc, 0x3b, 0x07, 0xaf, 0xcb, 0x8d, 0x37, 0x47, 0x14, 0x60, 0x37, 0x85, 0x71, 0x52, 0x73, 0xd1, 0x45, 0x4d, - 0x3c, 0x83, 0x9d, 0xae, 0xde, 0x4a, 0xb4, 0x1a, 0xef, 0xc5, 0xbb, 0x66, 0xe3, 0x6f, 0xe4, 0x81, 0x2e, 0xf3, 0xe0, - 0x12, 0x10, 0x67, 0x0f, 0xe2, 0xea, 0x00, 0x4b, 0x3d, 0x08, 0x06, 0x16, 0x39, 0xa4, 0x5d, 0xad, 0x1e, 0x8a, 0x48, - 0x9d, 0xc7, 0x60, 0xc0, 0x64, 0x1a, 0x52, 0x93, 0x69, 0xaf, 0x50, 0x90, 0x36, 0xd6, 0x5a, 0x40, 0x1b, 0x0e, 0x8b, - 0x1d, 0xbb, 0x61, 0x77, 0xba, 0x75, 0x28, 0x94, 0x30, 0x90, 0x75, 0xdd, 0x3c, 0xd4, 0x1a, 0x9e, 0x08, 0x7a, 0x50, - 0x8d, 0xf6, 0xd3, 0x43, 0x79, 0xd2, 0x1e, 0x0b, 0x70, 0xd1, 0xc3, 0x97, 0x2f, 0x04, 0x5e, 0xb4, 0x77, 0x90, 0xe7, - 0xcc, 0xa7, 0xca, 0x07, 0xb1, 0xe1, 0x96, 0xe1, 0x43, 0xfb, 0xf8, 0x56, 0x20, 0x93, 0xba, 0xa3, 0xa9, 0xad, 0xdd, - 0xd1, 0x38, 0x26, 0xd0, 0x6f, 0xca, 0x51, 0xca, 0xc4, 0xd4, 0xb2, 0x64, 0x27, 0xbd, 0x5c, 0x79, 0x43, 0xa5, 0xec, - 0x64, 0xd9, 0xe6, 0xfc, 0xd2, 0x46, 0x42, 0xbf, 0xaf, 0xdd, 0x81, 0xf0, 0x8d, 0x5a, 0x6f, 0xc8, 0xcb, 0x86, 0x88, - 0xe5, 0x10, 0x33, 0x70, 0xbc, 0x90, 0xca, 0xb5, 0xbb, 0x68, 0xaa, 0xea, 0x76, 0xb6, 0x72, 0x41, 0x4b, 0xbc, 0x95, - 0x02, 0xab, 0x48, 0x9d, 0x5e, 0x4f, 0x25, 0xee, 0xfb, 0x28, 0xb6, 0x1f, 0x01, 0xdb, 0xd8, 0x38, 0x1a, 0x1b, 0xb7, - 0x88, 0x0d, 0xbe, 0x8a, 0x2a, 0x5a, 0x70, 0x80, 0xe0, 0x6e, 0x4b, 0x6a, 0x69, 0xe6, 0x10, 0xf7, 0x15, 0x0f, 0xd0, - 0xbe, 0x8b, 0x23, 0x4e, 0x05, 0xd8, 0xd6, 0xb5, 0xce, 0x59, 0x2d, 0x07, 0x6c, 0x26, 0x7a, 0xfe, 0x69, 0xd5, 0x48, - 0xc4, 0xb0, 0xca, 0x46, 0xca, 0x0a, 0xed, 0x41, 0xe9, 0x12, 0x2e, 0xbe, 0x00, 0x2f, 0xdb, 0xfb, 0x95, 0xdd, 0xe7, - 0x4b, 0xec, 0x1f, 0xe6, 0x55, 0x13, 0x3c, 0xf2, 0x1a, 0x6f, 0xef, 0x61, 0xe2, 0x73, 0xa5, 0x10, 0x5e, 0xa5, 0x34, - 0x94, 0x00, 0x0c, 0x92, 0xa0, 0x86, 0x2b, 0x6d, 0x9b, 0x41, 0x2a, 0x63, 0xd8, 0xdd, 0xea, 0xad, 0xfe, 0x4f, 0xab, - 0x70, 0x51, 0xc9, 0x62, 0x4c, 0x02, 0x9d, 0x53, 0x2d, 0x37, 0x81, 0x05, 0xcf, 0x77, 0xc9, 0x11, 0x28, 0xec, 0x04, - 0x70, 0x43, 0x09, 0xfb, 0x19, 0x6f, 0x43, 0x39, 0x7b, 0x69, 0x25, 0x4f, 0x6e, 0x5f, 0x52, 0x41, 0x13, 0x32, 0x15, - 0x76, 0xff, 0xb6, 0x36, 0xec, 0xf3, 0x50, 0x8e, 0xa4, 0xc0, 0xc5, 0x41, 0xe7, 0x00, 0xf6, 0x07, 0xb9, 0x8c, 0xcd, - 0x67, 0xd2, 0xef, 0xab, 0xf7, 0xcf, 0xf2, 0x2c, 0xf9, 0xb8, 0xf3, 0xde, 0xf0, 0x34, 0x4b, 0x06, 0x54, 0x22, 0xa6, - 0xd6, 0x55, 0x31, 0x5c, 0x6a, 0x17, 0xe3, 0x06, 0xc9, 0x88, 0xf7, 0x52, 0x87, 0x18, 0x31, 0xbe, 0xc8, 0x0e, 0x49, - 0xc9, 0xe9, 0xb2, 0xee, 0xec, 0xb9, 0x16, 0xcd, 0xa0, 0x31, 0xdc, 0x8e, 0xf7, 0x92, 0x5e, 0x01, 0x2a, 0x40, 0x74, - 0xcf, 0x02, 0xd7, 0xf0, 0xe6, 0x92, 0x68, 0x6c, 0xe9, 0x69, 0x4b, 0x34, 0xb0, 0x57, 0x26, 0x24, 0xd5, 0xc6, 0x01, - 0x16, 0xb1, 0xae, 0x3f, 0x86, 0x05, 0x00, 0xb5, 0x1a, 0xa4, 0x57, 0xfa, 0x92, 0x50, 0x95, 0x84, 0x60, 0x74, 0x22, - 0xe1, 0x65, 0x40, 0xe3, 0xcc, 0x24, 0x5a, 0xd8, 0xe0, 0x80, 0x3e, 0xaf, 0x4c, 0xa2, 0xb1, 0x21, 0x0f, 0x28, 0xb7, - 0x69, 0x00, 0x83, 0x0f, 0x92, 0x24, 0xfa, 0x6a, 0x69, 0x92, 0x40, 0x50, 0x82, 0xf2, 0x0d, 0xfa, 0x53, 0xe9, 0xf9, - 0x58, 0xfe, 0xe6, 0x1d, 0x4a, 0xdf, 0x87, 0x05, 0xc8, 0x14, 0x75, 0xc5, 0x34, 0x63, 0x27, 0x59, 0xb7, 0x31, 0x89, - 0xe7, 0x69, 0x77, 0x57, 0x28, 0x97, 0x2e, 0xf0, 0x2b, 0xcb, 0x10, 0xc7, 0xfa, 0x59, 0xbc, 0x62, 0xa7, 0x21, 0xd7, - 0x78, 0xe9, 0xcf, 0xe2, 0x15, 0xce, 0x10, 0xad, 0x5a, 0x09, 0x44, 0xf9, 0xaf, 0xda, 0xc0, 0x21, 0xee, 0x13, 0x0c, - 0x72, 0x51, 0x79, 0x0f, 0x04, 0xf2, 0xb6, 0x82, 0x88, 0x34, 0xb3, 0xeb, 0x30, 0x22, 0xd5, 0x4e, 0x92, 0xf9, 0xf2, - 0x07, 0x99, 0x09, 0xef, 0x1b, 0x78, 0x6c, 0x36, 0xcb, 0xa6, 0x98, 0x2f, 0x54, 0x30, 0x07, 0xf7, 0x89, 0x8a, 0x4b, - 0x51, 0xf9, 0x4f, 0xd8, 0x05, 0x2f, 0xc6, 0x83, 0xd7, 0x6b, 0x04, 0xd8, 0xaf, 0xfc, 0x27, 0x6f, 0xcc, 0xfe, 0xb2, - 0x6e, 0x7c, 0x99, 0x89, 0xf8, 0xc0, 0x47, 0x77, 0x94, 0x8f, 0xee, 0xbd, 0x4c, 0x7f, 0x34, 0xa0, 0x44, 0x46, 0x65, - 0xc5, 0x57, 0x2b, 0x9e, 0xce, 0xee, 0x92, 0x28, 0x1b, 0x55, 0x5c, 0xc0, 0xf4, 0x82, 0xe3, 0x5d, 0xb2, 0xbe, 0xc8, - 0x92, 0x57, 0x10, 0x7b, 0x60, 0x25, 0x15, 0x16, 0x3f, 0x2c, 0x33, 0xb5, 0x98, 0x85, 0xac, 0xa4, 0xe0, 0xc1, 0xec, - 0x26, 0x89, 0xfe, 0x5a, 0x7a, 0x48, 0x6a, 0x66, 0xca, 0x36, 0xb5, 0x23, 0xd4, 0xc6, 0xd7, 0x91, 0x6e, 0xb4, 0x05, - 0x00, 0xdc, 0xb3, 0x45, 0x1a, 0x49, 0x26, 0x86, 0x93, 0x9a, 0x71, 0x93, 0x5e, 0x60, 0x6a, 0x5c, 0xb3, 0x8a, 0x26, - 0xce, 0x42, 0x06, 0xf4, 0xfe, 0x80, 0x97, 0x83, 0xcf, 0x19, 0xdc, 0x7f, 0xd0, 0x1a, 0xb8, 0x3c, 0x2e, 0xfa, 0x7d, - 0x79, 0x5c, 0x6c, 0xb7, 0xe5, 0x49, 0xdc, 0xef, 0xcb, 0x93, 0xd8, 0xf0, 0x0f, 0x4a, 0xb1, 0x6d, 0xcc, 0x0d, 0x12, - 0x9a, 0x4b, 0x88, 0x5a, 0x34, 0x82, 0x3f, 0x34, 0xcb, 0xb9, 0x88, 0xf2, 0xe3, 0xa4, 0xdf, 0xef, 0x2d, 0x67, 0x62, - 0x90, 0x0f, 0x93, 0x28, 0x1f, 0x26, 0x9e, 0x13, 0xe2, 0xb7, 0x9e, 0x13, 0xa2, 0xa2, 0x81, 0x2b, 0x38, 0x33, 0x00, - 0x51, 0xc0, 0xa7, 0x7f, 0x54, 0xd7, 0x52, 0xe8, 0x5a, 0x62, 0x55, 0x4b, 0xa2, 0x2b, 0xa8, 0xd9, 0x4d, 0x11, 0x96, - 0x58, 0x0a, 0x5d, 0xb2, 0x3f, 0x96, 0xc0, 0x13, 0xe5, 0xbc, 0xda, 0x00, 0x03, 0x1b, 0xe1, 0x9d, 0xc3, 0x84, 0x93, - 0x58, 0xd7, 0x80, 0x76, 0xba, 0xa9, 0xe9, 0x25, 0x5d, 0xd1, 0x2b, 0xe4, 0x67, 0x2f, 0xc1, 0x60, 0xe9, 0x98, 0xe5, - 0xd3, 0xc1, 0xe0, 0x92, 0xac, 0x58, 0x39, 0x0f, 0xe3, 0x41, 0xb8, 0x9e, 0xe5, 0xc3, 0xcb, 0xe8, 0x92, 0x90, 0x2f, - 0x8a, 0x05, 0xed, 0xad, 0x46, 0xe5, 0xc7, 0x0c, 0xc2, 0xfb, 0xa5, 0xb3, 0x30, 0x33, 0x71, 0x3e, 0x56, 0xa3, 0x3b, - 0xba, 0x82, 0xf8, 0x35, 0x70, 0x23, 0x21, 0x11, 0x74, 0xe4, 0x8a, 0xae, 0xe8, 0x9a, 0x4a, 0x33, 0xc3, 0x18, 0xad, - 0xdb, 0x1e, 0x27, 0x09, 0x38, 0x26, 0xbb, 0xe2, 0xa3, 0xb1, 0x2a, 0xbc, 0xeb, 0x3b, 0x42, 0x7b, 0xbd, 0xc4, 0x0d, - 0xd2, 0x2f, 0xed, 0x41, 0x02, 0x46, 0x64, 0xa4, 0x06, 0xca, 0x8c, 0x8c, 0xa4, 0x66, 0x52, 0x71, 0x48, 0x62, 0x7f, - 0x48, 0xd4, 0x38, 0x24, 0xfe, 0x38, 0xe4, 0x7a, 0x1c, 0x90, 0xbb, 0x5f, 0xb2, 0x31, 0x4d, 0xd9, 0x98, 0xae, 0xd5, - 0xa8, 0xd0, 0x6b, 0x7a, 0xa1, 0xa9, 0xe3, 0x39, 0x7b, 0x0d, 0x07, 0xf6, 0x20, 0xcc, 0x67, 0xf1, 0xf0, 0x75, 0xf4, - 0x9a, 0x90, 0x2f, 0x24, 0xbd, 0x51, 0x97, 0x32, 0x08, 0x84, 0x78, 0x0d, 0xce, 0xa5, 0x2e, 0xd4, 0xc9, 0xb5, 0xd9, - 0x71, 0xf8, 0x74, 0xd5, 0x78, 0xba, 0x80, 0x88, 0x3e, 0x68, 0xa5, 0xd2, 0xef, 0x87, 0x97, 0xac, 0x9c, 0x9f, 0x87, - 0x63, 0x02, 0x38, 0x3c, 0x7a, 0x38, 0x2f, 0x47, 0x77, 0xf4, 0x72, 0x74, 0x4f, 0xc0, 0xc2, 0x6b, 0x3c, 0x5d, 0x1f, - 0xb3, 0x78, 0x3a, 0x18, 0xac, 0x91, 0xaa, 0xab, 0xdc, 0x6b, 0xb2, 0xa0, 0x97, 0x38, 0x11, 0x04, 0x18, 0xfa, 0x4c, - 0xac, 0x0d, 0x0d, 0x7f, 0xcd, 0xe0, 0xe3, 0x7b, 0x76, 0x39, 0xba, 0xa7, 0x77, 0xec, 0xf5, 0x76, 0x3c, 0x05, 0x66, - 0x6a, 0x35, 0x0b, 0xef, 0x8f, 0xaf, 0x66, 0x57, 0xec, 0x3e, 0xba, 0x3f, 0x81, 0x86, 0x5e, 0xb3, 0x7b, 0x04, 0x5c, - 0x4a, 0x1f, 0x2f, 0x07, 0xaf, 0xc9, 0xe1, 0x60, 0x90, 0x92, 0x28, 0xbc, 0x09, 0xbd, 0x56, 0xbe, 0xa6, 0xf7, 0x84, - 0xae, 0xd8, 0x1d, 0x8e, 0xc6, 0x15, 0xc3, 0x0f, 0x2e, 0xd8, 0x7d, 0x7d, 0x13, 0x7a, 0xbb, 0x39, 0x11, 0x9d, 0x20, - 0x46, 0xe8, 0x6b, 0xe0, 0x68, 0x96, 0x0b, 0x33, 0x01, 0x4f, 0xe6, 0x22, 0xa3, 0x45, 0xa1, 0x19, 0x88, 0xb3, 0x12, - 0x10, 0x4b, 0xa2, 0xee, 0x37, 0x1b, 0x9d, 0xc3, 0x72, 0xee, 0xf7, 0x7b, 0x95, 0xa1, 0x07, 0x88, 0x9c, 0xd9, 0x49, - 0x0f, 0x7a, 0x3e, 0x3d, 0xc0, 0x4f, 0xf4, 0xaa, 0x41, 0x9c, 0xcc, 0x5f, 0x96, 0xd1, 0xb7, 0x1e, 0x7d, 0xf8, 0xbe, - 0x9b, 0xf2, 0x88, 0xfc, 0xdf, 0xa7, 0x3c, 0x65, 0x1e, 0xbd, 0xae, 0x3c, 0x10, 0x3c, 0x6f, 0x4d, 0x2a, 0x8d, 0x44, - 0x35, 0x3a, 0x5f, 0xc5, 0xa0, 0x8d, 0x44, 0x6d, 0x83, 0x7e, 0x42, 0x0b, 0x2b, 0x88, 0x90, 0x73, 0xf4, 0x1c, 0x0c, - 0x52, 0x21, 0x54, 0x8e, 0x5a, 0x94, 0x68, 0x08, 0x92, 0xcb, 0x92, 0xab, 0xf0, 0x39, 0x84, 0xaa, 0xd3, 0xc7, 0x99, - 0x08, 0x1b, 0x7a, 0x1c, 0xfa, 0x00, 0xf0, 0x3f, 0xef, 0x90, 0x8b, 0x92, 0x5f, 0xe1, 0xd9, 0xdc, 0x26, 0x18, 0x05, - 0x4b, 0x44, 0x33, 0xb4, 0x0d, 0x62, 0x3f, 0x96, 0x04, 0xeb, 0x91, 0x34, 0x1e, 0x95, 0xe6, 0x88, 0xf0, 0xa3, 0xf8, - 0x28, 0x7a, 0x1a, 0x1b, 0x12, 0xc9, 0x91, 0x44, 0xf2, 0x01, 0x10, 0x4e, 0x82, 0xfe, 0xe2, 0xae, 0xc9, 0xae, 0x85, - 0xc4, 0xa0, 0x3f, 0x2d, 0x99, 0x96, 0xdd, 0xab, 0x1e, 0xfb, 0x8a, 0x20, 0x77, 0x4c, 0xff, 0xe9, 0xf5, 0xe1, 0x5f, - 0x4b, 0x9c, 0x41, 0xeb, 0xf9, 0xa2, 0x3a, 0x33, 0xf3, 0x06, 0x37, 0xf2, 0xba, 0xac, 0x5d, 0x97, 0x2f, 0xf9, 0x01, - 0xbf, 0xab, 0xb8, 0x48, 0xcb, 0x83, 0x9f, 0xaa, 0x36, 0x9e, 0x53, 0xb9, 0x5e, 0xb9, 0x38, 0x2b, 0xca, 0x38, 0xd5, - 0x93, 0xba, 0x18, 0x6b, 0xd8, 0x86, 0xdf, 0x23, 0xea, 0x4a, 0x5a, 0x8e, 0x9e, 0x52, 0xae, 0x9a, 0x29, 0x97, 0xeb, - 0x3c, 0xff, 0x71, 0x27, 0x15, 0xa7, 0xb8, 0x99, 0x82, 0x54, 0xa9, 0xe5, 0x02, 0xaa, 0xe7, 0xa8, 0xe5, 0x6e, 0x69, - 0x76, 0x80, 0x73, 0xdb, 0x54, 0x1f, 0x2b, 0xb3, 0x0b, 0x2f, 0xb9, 0x71, 0x7f, 0x32, 0x65, 0x58, 0x30, 0x0a, 0x6d, - 0x56, 0x5d, 0x69, 0xfb, 0x42, 0xeb, 0x34, 0x0c, 0x57, 0x7e, 0xbc, 0x80, 0x74, 0x01, 0xe3, 0x78, 0x51, 0x32, 0x31, - 0x6e, 0x8f, 0xde, 0x0a, 0xe2, 0x73, 0xb6, 0x02, 0xe9, 0xf7, 0x7b, 0xc2, 0xdb, 0x75, 0x1d, 0x6d, 0xf7, 0xc4, 0x29, - 0xa3, 0x72, 0x15, 0x8b, 0xef, 0xe2, 0x95, 0x81, 0x4c, 0x56, 0xc7, 0x63, 0x63, 0x4c, 0xa7, 0xff, 0x48, 0x42, 0xbf, - 0x10, 0x0a, 0x3e, 0xeb, 0xa5, 0x95, 0x27, 0xb7, 0x87, 0x65, 0x5c, 0xa3, 0x57, 0xe2, 0x4a, 0xf7, 0xcd, 0x48, 0x21, - 0xf5, 0xc8, 0x57, 0x4d, 0x01, 0xbd, 0x19, 0xfb, 0x66, 0x2a, 0xcc, 0xdb, 0x9e, 0x31, 0x57, 0x08, 0x56, 0xaa, 0xec, - 0xf6, 0x9d, 0x1a, 0x53, 0x31, 0x83, 0x29, 0xb6, 0x9d, 0xc5, 0xa4, 0x5b, 0xf9, 0xa7, 0x9d, 0xfb, 0x65, 0xde, 0xe1, - 0xae, 0xa8, 0xdf, 0x02, 0x17, 0x9a, 0x15, 0x65, 0xd5, 0x96, 0x0d, 0xdb, 0xc6, 0x1b, 0x59, 0x28, 0x36, 0xc0, 0xb2, - 0xe7, 0xbe, 0x85, 0x07, 0x88, 0x9b, 0x70, 0xcf, 0x2e, 0x6a, 0xb8, 0x31, 0x7c, 0x5e, 0x49, 0xbe, 0x2b, 0x8d, 0xb9, - 0xf4, 0xa9, 0xd2, 0xc4, 0x70, 0xb2, 0x18, 0x71, 0x91, 0x2e, 0xea, 0xcc, 0xae, 0x85, 0x4f, 0x78, 0x19, 0xce, 0xf9, - 0xc2, 0xe8, 0xa6, 0x74, 0xe9, 0x05, 0x8b, 0x75, 0xa7, 0x37, 0x2b, 0x8d, 0x95, 0x12, 0x71, 0x6b, 0x96, 0x09, 0x94, - 0xa5, 0xac, 0x95, 0xf0, 0xa6, 0x68, 0xd9, 0x4a, 0x1a, 0x79, 0xcf, 0x1c, 0xdc, 0xc7, 0xbe, 0x47, 0x4c, 0x64, 0x13, - 0x98, 0x14, 0x0d, 0x1d, 0xd0, 0xae, 0xba, 0xf0, 0xcd, 0xa8, 0x07, 0x83, 0xdc, 0x92, 0x44, 0xac, 0x20, 0xc5, 0x0a, - 0xd6, 0x35, 0x2b, 0xe6, 0xf9, 0x82, 0x5e, 0x32, 0x39, 0x4f, 0x17, 0x74, 0xc5, 0xe4, 0x7c, 0x8d, 0x37, 0xa1, 0x4b, - 0x38, 0x21, 0xc9, 0x26, 0x56, 0x0a, 0xd8, 0x4b, 0xbc, 0xbc, 0xe1, 0x99, 0xaa, 0x69, 0xd9, 0x95, 0xe2, 0x00, 0xe3, - 0x8b, 0x32, 0x0c, 0xcb, 0xe1, 0x25, 0x58, 0x4b, 0x1c, 0x86, 0xab, 0x39, 0x5f, 0xa8, 0xdf, 0x10, 0x75, 0x3e, 0x09, - 0x15, 0xbb, 0x60, 0xf7, 0x02, 0x99, 0x5e, 0xcf, 0xf9, 0x42, 0x8d, 0x84, 0x2e, 0xf8, 0xda, 0x1a, 0x9b, 0xc4, 0x9e, - 0xa0, 0x65, 0x16, 0xcf, 0xc7, 0x8b, 0x28, 0xae, 0x61, 0x19, 0x9e, 0xa9, 0x99, 0x69, 0xc9, 0x7f, 0x12, 0xb5, 0xa1, - 0x89, 0xbe, 0xc1, 0x2a, 0xf2, 0x87, 0xc7, 0x47, 0x97, 0x40, 0xc6, 0xce, 0xae, 0x64, 0xe6, 0x43, 0xdf, 0x47, 0x06, - 0xf7, 0xdc, 0x94, 0x33, 0xae, 0x82, 0x44, 0x19, 0xb8, 0x7b, 0x35, 0x4b, 0xc6, 0x5a, 0x84, 0xef, 0x1e, 0x15, 0x45, - 0x9f, 0x49, 0xd3, 0x80, 0xee, 0x23, 0xc1, 0x1c, 0xe8, 0xbd, 0x42, 0x87, 0xcb, 0x6a, 0x9b, 0x09, 0xf8, 0x8b, 0x04, - 0xf9, 0xad, 0xd0, 0xab, 0x1a, 0x83, 0x2a, 0xda, 0x45, 0x2c, 0xfd, 0xfb, 0x88, 0x1f, 0x65, 0xf3, 0x4f, 0x73, 0x8f, - 0x57, 0x12, 0x06, 0x3f, 0xa4, 0x66, 0x93, 0xcc, 0xdb, 0x2b, 0xf6, 0x1e, 0x3a, 0xea, 0x51, 0x6b, 0xbc, 0xaf, 0x5e, - 0x72, 0x0a, 0x31, 0x4a, 0x28, 0x3a, 0x09, 0x06, 0x70, 0xbb, 0x84, 0x14, 0x77, 0x83, 0xdd, 0x34, 0xaf, 0x79, 0x51, - 0x70, 0xb1, 0xae, 0xaa, 0xc0, 0x0f, 0x68, 0x38, 0x5f, 0xec, 0x86, 0x30, 0x1c, 0xd3, 0xd6, 0x35, 0x0c, 0xc2, 0x8c, - 0x61, 0x24, 0x04, 0xaf, 0x7f, 0xd1, 0x57, 0x34, 0x89, 0x57, 0xdf, 0xf2, 0xbf, 0x32, 0x5e, 0x28, 0x22, 0x0d, 0x22, - 0xa4, 0x6e, 0xe2, 0x1b, 0x99, 0x26, 0x05, 0x14, 0x02, 0x8c, 0x02, 0x2a, 0xb1, 0xa1, 0xa9, 0xf8, 0x5b, 0x2d, 0x3e, - 0xf8, 0xa9, 0xe9, 0x78, 0x34, 0xae, 0x5b, 0x9d, 0x51, 0x41, 0x67, 0xa0, 0x47, 0xad, 0xa8, 0xa7, 0x41, 0x2b, 0xc1, - 0x34, 0xd2, 0xbc, 0x75, 0x0f, 0x81, 0x57, 0xa6, 0xc5, 0x3b, 0x0f, 0xe8, 0xe6, 0xdc, 0x07, 0x4f, 0x1e, 0xd3, 0x73, - 0x87, 0x9e, 0x5c, 0xb1, 0x93, 0xaa, 0x87, 0xda, 0x7b, 0x33, 0x42, 0x41, 0xbf, 0x8f, 0x29, 0xd0, 0x8d, 0xa0, 0xf6, - 0xae, 0xee, 0x3f, 0x94, 0xbb, 0x1c, 0xbe, 0xe3, 0x2c, 0x37, 0x80, 0xa5, 0x22, 0x6b, 0x05, 0x1e, 0x05, 0xa8, 0x4b, - 0x65, 0x08, 0x5b, 0xcc, 0xe1, 0x50, 0xd9, 0xad, 0x5a, 0x0d, 0x25, 0x39, 0x2e, 0x47, 0xe0, 0x10, 0xba, 0x2e, 0x07, - 0xe5, 0x68, 0x99, 0x55, 0xef, 0xf1, 0xb7, 0x66, 0x1d, 0x92, 0x6c, 0x1f, 0xeb, 0xc0, 0x2d, 0xeb, 0x30, 0xfd, 0x68, - 0x90, 0x02, 0xd0, 0x64, 0x23, 0x70, 0x09, 0xc0, 0x7b, 0xfb, 0x8f, 0x08, 0xb5, 0x32, 0xdd, 0xcb, 0x58, 0xa8, 0xef, - 0x1b, 0x49, 0x50, 0x42, 0x33, 0xa1, 0x72, 0x2c, 0x05, 0xef, 0x3c, 0xd2, 0x39, 0xa9, 0x33, 0xf1, 0x1e, 0xc4, 0x69, - 0xe1, 0x03, 0x7b, 0x0b, 0x82, 0x73, 0x16, 0xf4, 0x1e, 0x6f, 0xb3, 0x5a, 0x6a, 0xa3, 0x07, 0x0a, 0xe0, 0x77, 0x83, - 0x7b, 0x04, 0xf9, 0x6a, 0x0c, 0xd7, 0x4a, 0xde, 0x86, 0x7c, 0x58, 0xd0, 0x23, 0x32, 0xb0, 0xcf, 0x62, 0x18, 0xd3, - 0x23, 0x72, 0x6c, 0x9f, 0xa5, 0x1b, 0xc0, 0x81, 0xd4, 0xa3, 0x4a, 0x8f, 0xa0, 0x41, 0xbf, 0xda, 0x16, 0x59, 0x92, - 0xf5, 0x43, 0x69, 0x14, 0x31, 0x50, 0x25, 0x88, 0xa8, 0xc5, 0xbf, 0x1e, 0xcc, 0x75, 0x8f, 0xb9, 0x40, 0x98, 0x83, - 0x01, 0x07, 0x71, 0x1b, 0x84, 0xe6, 0x80, 0xd9, 0xdc, 0x45, 0x82, 0xde, 0x5b, 0xc3, 0xcc, 0x8e, 0xfe, 0x70, 0x2b, - 0xc1, 0x37, 0x59, 0x6b, 0xd4, 0x79, 0x71, 0x08, 0x04, 0xc1, 0x9b, 0x42, 0x55, 0x7b, 0xd5, 0x03, 0x1b, 0x6f, 0xd5, - 0x8f, 0xed, 0x76, 0x3c, 0x15, 0xee, 0xda, 0x2f, 0x28, 0x9c, 0x7c, 0x4a, 0xfe, 0xf5, 0xde, 0x64, 0x70, 0x60, 0x64, - 0xf8, 0xd2, 0xdb, 0xbf, 0xf0, 0xb5, 0x96, 0xee, 0x89, 0x41, 0x49, 0x1e, 0x1f, 0x29, 0xfa, 0xb7, 0x57, 0x56, 0x3e, - 0xb5, 0xd3, 0xbf, 0xdd, 0x9a, 0xf5, 0x79, 0x3c, 0x9a, 0x6c, 0xb7, 0xbd, 0xb8, 0xd2, 0x1e, 0x6b, 0x7a, 0x41, 0xa0, - 0x73, 0x3d, 0x39, 0x3c, 0x82, 0xa8, 0x08, 0xcd, 0xb8, 0x9b, 0x65, 0x43, 0x22, 0xe3, 0xc7, 0xe9, 0x2c, 0x1b, 0x82, - 0x1d, 0xee, 0x45, 0x25, 0x2e, 0x47, 0xad, 0x0d, 0x4e, 0xcf, 0x93, 0x10, 0x42, 0x39, 0x60, 0x65, 0x77, 0xea, 0xcf, - 0xbd, 0x32, 0x13, 0x52, 0x93, 0xd5, 0xed, 0x94, 0xee, 0x61, 0x9a, 0x1f, 0x98, 0x11, 0x1c, 0x70, 0x6f, 0x7f, 0xd5, - 0x1f, 0xc3, 0x24, 0xd3, 0xe4, 0x14, 0xc9, 0x2f, 0xd2, 0x53, 0x48, 0xda, 0xa1, 0xa7, 0x8a, 0x00, 0x4e, 0xa8, 0xfd, - 0x18, 0x7e, 0xc3, 0xb8, 0x7f, 0xdb, 0x7c, 0xed, 0xa6, 0x22, 0x7a, 0x42, 0xb1, 0x4c, 0x4d, 0x4e, 0x93, 0xac, 0x48, - 0x20, 0x6a, 0xa3, 0x6a, 0x46, 0xf4, 0x95, 0x8b, 0xf9, 0xa8, 0x08, 0x9f, 0x57, 0xeb, 0xff, 0x0c, 0xe1, 0x33, 0x0a, - 0x37, 0x80, 0xcb, 0x2b, 0xae, 0x2e, 0xc2, 0xa7, 0x4f, 0xe8, 0xc1, 0xe4, 0xeb, 0x23, 0x7a, 0x70, 0xf4, 0xd5, 0x53, - 0x02, 0xb0, 0x68, 0x57, 0x17, 0xe1, 0xd1, 0xd3, 0xa7, 0xf4, 0xe0, 0x9b, 0x6f, 0xe8, 0xc1, 0xe4, 0xab, 0xa3, 0x46, - 0xda, 0xe4, 0xe9, 0x37, 0xf4, 0xe0, 0xeb, 0x27, 0x8d, 0xb4, 0xa3, 0xf1, 0x53, 0x7a, 0xf0, 0xf7, 0xaf, 0x4d, 0xda, - 0xdf, 0x20, 0xdb, 0x37, 0x47, 0xf8, 0x9f, 0x49, 0x9b, 0x3c, 0xfd, 0x8a, 0x1e, 0x4c, 0xc6, 0x50, 0xc9, 0x53, 0x57, - 0xc9, 0x78, 0x02, 0x1f, 0x7f, 0x05, 0xff, 0xfd, 0x8d, 0x04, 0x0b, 0x5a, 0x49, 0x96, 0x0b, 0xd4, 0x9f, 0xa1, 0x88, - 0x13, 0x55, 0x13, 0x09, 0x0f, 0x31, 0xb3, 0xfa, 0x26, 0x0e, 0x03, 0xe2, 0xd2, 0xa1, 0x20, 0x7a, 0x30, 0x1e, 0x3d, - 0x25, 0x81, 0x0f, 0x4f, 0x77, 0xeb, 0x83, 0x8c, 0xe5, 0x62, 0x9e, 0x7d, 0x91, 0x9b, 0xd8, 0x0a, 0x1e, 0x80, 0xd5, - 0x47, 0x3f, 0x57, 0x25, 0xe7, 0xd9, 0x17, 0x95, 0xdc, 0xcd, 0xf5, 0x6b, 0x0b, 0x50, 0xde, 0x5f, 0xb5, 0xec, 0xb6, - 0x50, 0xa1, 0xd3, 0x5a, 0xa3, 0xcf, 0x3e, 0x62, 0xfa, 0x60, 0xe0, 0xdd, 0xb0, 0xff, 0xb1, 0x53, 0x4e, 0xeb, 0x1b, - 0x8d, 0x42, 0x8d, 0xca, 0x43, 0xc2, 0x4e, 0xa0, 0xe8, 0xc1, 0x00, 0x78, 0x02, 0x0f, 0xf7, 0xed, 0xdf, 0x2c, 0xe3, - 0x63, 0x47, 0x19, 0x3f, 0xa1, 0x0c, 0x01, 0x8d, 0x7a, 0x98, 0xdd, 0xf4, 0xb0, 0xd1, 0xad, 0x5e, 0xb2, 0x54, 0x27, - 0x53, 0xd3, 0x33, 0xd8, 0xd7, 0xba, 0x96, 0x07, 0x46, 0x14, 0x2d, 0x2f, 0x0f, 0x52, 0x3e, 0xab, 0xd8, 0x3f, 0x96, - 0xa8, 0xde, 0x8a, 0x1a, 0x6f, 0x64, 0x36, 0xab, 0xd8, 0x77, 0xe6, 0x0d, 0x70, 0x33, 0xec, 0x57, 0xf5, 0xe4, 0x07, - 0xce, 0xe0, 0xd2, 0xb6, 0x47, 0x99, 0x18, 0x01, 0x56, 0x40, 0x06, 0x0e, 0x3c, 0x00, 0x3a, 0xe8, 0x8f, 0xf6, 0x76, - 0xab, 0x52, 0x9a, 0x7d, 0xb6, 0x30, 0x80, 0x86, 0x79, 0x9b, 0xb8, 0xb2, 0x7f, 0x6b, 0xc8, 0x4b, 0x50, 0xb8, 0xd5, - 0x2c, 0x6f, 0xa7, 0x30, 0x84, 0x10, 0xfc, 0x61, 0xc9, 0x00, 0x70, 0x20, 0xc0, 0x60, 0xac, 0x65, 0x40, 0xcd, 0x96, - 0x8f, 0x36, 0x5c, 0xa9, 0x27, 0x81, 0x33, 0xb8, 0x94, 0x45, 0xc2, 0xdf, 0x6a, 0xb1, 0x3f, 0x5a, 0x3f, 0xfa, 0xbe, - 0x3d, 0x1e, 0xac, 0x7d, 0x8f, 0x8f, 0xf4, 0x67, 0x8d, 0xeb, 0xc0, 0xa6, 0xe5, 0x1b, 0x2f, 0x6a, 0x2b, 0xf1, 0x28, - 0x81, 0x37, 0x30, 0x11, 0x29, 0x0c, 0x52, 0x2d, 0x70, 0x0c, 0xca, 0x1b, 0x0b, 0xb1, 0x54, 0x5d, 0xdd, 0x60, 0x0b, - 0x22, 0x43, 0xf0, 0x70, 0xfb, 0x6d, 0xa9, 0x02, 0x47, 0xf5, 0xfb, 0x5c, 0xfa, 0x6e, 0x4f, 0xc6, 0x8e, 0x1c, 0xa7, - 0x7e, 0x2a, 0x1c, 0xfc, 0x37, 0xa9, 0x6b, 0x63, 0xb9, 0x92, 0x32, 0xcb, 0xb2, 0xb0, 0x93, 0x50, 0xcb, 0x3d, 0x2a, - 0x0f, 0x92, 0x2f, 0xe4, 0x10, 0xc9, 0x02, 0xa3, 0x50, 0x90, 0xe1, 0x84, 0x8a, 0xd1, 0x5a, 0x94, 0xcb, 0xec, 0xb2, - 0x0a, 0x37, 0x4a, 0xa1, 0xcc, 0x29, 0xfa, 0x76, 0x83, 0x03, 0x09, 0x89, 0xb2, 0xf2, 0x4d, 0xfc, 0x26, 0x44, 0xb0, - 0x3a, 0xae, 0x6d, 0xa1, 0xb8, 0xb7, 0x3f, 0x79, 0xda, 0xc5, 0x1f, 0x19, 0x17, 0x50, 0x17, 0x8b, 0x69, 0x38, 0xb1, - 0xb1, 0x6f, 0xdc, 0x17, 0x56, 0xd3, 0x03, 0x50, 0xdf, 0xa5, 0x12, 0x23, 0xa8, 0xaf, 0x8c, 0x7d, 0x6c, 0x8f, 0x31, - 0x39, 0x83, 0x58, 0xc3, 0x2a, 0x67, 0xa6, 0xfa, 0x46, 0xd8, 0x09, 0x00, 0x37, 0x42, 0x6b, 0x74, 0x64, 0x92, 0x2a, - 0xc4, 0xf3, 0x52, 0x85, 0x6f, 0xcd, 0x08, 0x1d, 0x83, 0x37, 0x95, 0x6d, 0x64, 0x26, 0x7d, 0xc1, 0xa0, 0x39, 0xb6, - 0x75, 0x14, 0x56, 0x5b, 0x59, 0x76, 0x02, 0x70, 0x03, 0xd9, 0xb1, 0xb9, 0x78, 0xce, 0xaa, 0x79, 0xb6, 0x88, 0x4c, - 0x50, 0xc0, 0xa5, 0xb0, 0x0c, 0xda, 0x9b, 0x3d, 0xb2, 0x1d, 0x87, 0xd0, 0x0d, 0xf7, 0x11, 0x8c, 0xa7, 0xdd, 0x14, - 0xac, 0x20, 0x1a, 0x21, 0x1e, 0x66, 0xcc, 0xe2, 0x7b, 0xa5, 0x29, 0x4f, 0x55, 0x4b, 0x20, 0x70, 0x14, 0x42, 0x5d, - 0xec, 0x1a, 0x25, 0xb8, 0x4c, 0x8d, 0x60, 0x06, 0x3b, 0x76, 0xa4, 0xb6, 0x4b, 0xce, 0xe9, 0x50, 0x4d, 0x69, 0xa9, - 0xa7, 0x54, 0xfb, 0x1a, 0x8a, 0x79, 0x89, 0x1e, 0x7a, 0xe0, 0x7a, 0xa0, 0x1d, 0xf2, 0x4a, 0x3a, 0x31, 0x11, 0x74, - 0x5a, 0x6d, 0xc2, 0xce, 0x8d, 0x74, 0xcb, 0x6a, 0xe4, 0x1d, 0x43, 0xb3, 0x23, 0x5e, 0xf8, 0x81, 0xba, 0x00, 0x22, - 0x64, 0x6f, 0x8b, 0xcc, 0x11, 0xcd, 0xb2, 0xf2, 0x25, 0x94, 0xc5, 0x11, 0x5b, 0x57, 0xc0, 0xb5, 0x14, 0x4c, 0x2e, - 0x79, 0xc4, 0x53, 0x44, 0x04, 0x3c, 0x55, 0xda, 0xf5, 0x9d, 0x96, 0x10, 0x9a, 0xa5, 0x40, 0xdc, 0x5c, 0x14, 0xe7, - 0xda, 0x06, 0xb2, 0x00, 0xfa, 0xf6, 0x63, 0x76, 0xed, 0x85, 0x83, 0xdd, 0x5c, 0x67, 0xe2, 0x39, 0xbf, 0xcc, 0x04, - 0x4f, 0x11, 0xec, 0xea, 0xce, 0x3c, 0x70, 0xc7, 0xb6, 0x81, 0xe5, 0xdb, 0xb7, 0xb0, 0x60, 0xca, 0x50, 0x2b, 0x25, - 0x32, 0x11, 0x09, 0xc8, 0xec, 0x33, 0x77, 0xaf, 0x33, 0xf1, 0x3a, 0xbe, 0x03, 0x6f, 0x8a, 0x06, 0x3f, 0x3d, 0xba, - 0xc0, 0x2f, 0x11, 0x49, 0x14, 0x62, 0xd8, 0x62, 0x44, 0x2c, 0x44, 0x8e, 0x1d, 0x13, 0xca, 0x95, 0xa0, 0xb5, 0x35, - 0x04, 0x5e, 0xfc, 0x69, 0xd5, 0xbd, 0xeb, 0x4c, 0x18, 0xfb, 0x8c, 0xeb, 0xf8, 0x8e, 0x95, 0x0a, 0xcc, 0x02, 0xe3, - 0xdc, 0xb7, 0xa5, 0x24, 0xd7, 0x99, 0x30, 0x02, 0x92, 0xeb, 0xf8, 0x8e, 0x36, 0x65, 0x1c, 0xda, 0x8a, 0xce, 0x8b, - 0xf3, 0xbb, 0x3b, 0xfc, 0x12, 0x43, 0xad, 0x8c, 0xfb, 0x7d, 0x90, 0x98, 0x49, 0xdb, 0x94, 0x99, 0x8c, 0xa4, 0x46, - 0x0b, 0xa9, 0x28, 0x1f, 0x4c, 0xc8, 0xee, 0x4a, 0xb5, 0x8c, 0xa8, 0xfd, 0x2a, 0x14, 0xb3, 0x71, 0x34, 0x21, 0x74, - 0xd2, 0xb1, 0xde, 0x4d, 0x6b, 0x21, 0xd3, 0xe8, 0x69, 0xe4, 0xf9, 0x74, 0x16, 0xac, 0x9a, 0x16, 0xc7, 0x8c, 0x4f, - 0x8b, 0xc1, 0x80, 0x68, 0x97, 0xc2, 0x0d, 0xd6, 0x03, 0xa6, 0x34, 0x2e, 0xde, 0x9a, 0x69, 0xf5, 0x4b, 0xa9, 0x42, - 0xd2, 0x7b, 0x06, 0x24, 0x99, 0x74, 0xc1, 0x6e, 0x41, 0xa2, 0xe8, 0xf9, 0xdf, 0xa9, 0x2d, 0xb8, 0xeb, 0xc1, 0xd8, - 0x8c, 0xee, 0xeb, 0x19, 0xff, 0xa1, 0xb6, 0x05, 0x51, 0x9f, 0x4a, 0xd6, 0xeb, 0x48, 0x54, 0x21, 0x17, 0xe1, 0x67, - 0x47, 0x43, 0x0c, 0x51, 0xed, 0xb1, 0x40, 0xac, 0xaf, 0x2f, 0x78, 0x81, 0xd3, 0xcf, 0xdc, 0xe5, 0x0a, 0xb6, 0x05, - 0xad, 0x0c, 0x8d, 0x7a, 0x13, 0xbf, 0x89, 0xec, 0x65, 0x41, 0x17, 0xf9, 0x1c, 0x85, 0xac, 0x79, 0x18, 0x56, 0xc3, - 0xf6, 0x20, 0x92, 0xc3, 0xf6, 0x24, 0x34, 0x1a, 0x03, 0x0b, 0x64, 0x87, 0x46, 0xe0, 0x22, 0xb4, 0xf2, 0xb7, 0x63, - 0x70, 0xe1, 0xb2, 0x88, 0x2c, 0x43, 0x1d, 0xbf, 0xa9, 0xdd, 0x04, 0xd5, 0x2b, 0x74, 0x9a, 0xc2, 0xaa, 0x94, 0x49, - 0x3e, 0xfc, 0x7a, 0x29, 0x0b, 0xcc, 0xe4, 0x75, 0xd9, 0xa3, 0xaf, 0xed, 0xf6, 0x0e, 0x4c, 0xc1, 0xba, 0x4f, 0xde, - 0xd7, 0x8f, 0x3b, 0x7b, 0x02, 0x46, 0xb1, 0x2a, 0x47, 0x53, 0x48, 0xa9, 0x7d, 0x50, 0xea, 0x8f, 0xe1, 0x52, 0x68, - 0x8e, 0xdd, 0x02, 0x26, 0x01, 0xfb, 0x0c, 0xa9, 0x1e, 0xd3, 0x8e, 0x7d, 0x8e, 0x36, 0xb0, 0x24, 0xe0, 0xf0, 0x8f, - 0x32, 0x59, 0xfb, 0x57, 0x77, 0x91, 0x36, 0x43, 0xb6, 0xcc, 0x17, 0xc0, 0xe7, 0xc3, 0xae, 0x8d, 0x4a, 0x94, 0x4d, - 0x44, 0x92, 0xc2, 0x96, 0xc7, 0x20, 0xed, 0x51, 0x4c, 0x57, 0x05, 0x4f, 0x32, 0x94, 0x52, 0x24, 0xda, 0x27, 0x38, - 0x87, 0x37, 0xb8, 0x1f, 0x55, 0x40, 0x78, 0x15, 0x72, 0x3a, 0x4a, 0xa9, 0xb6, 0x80, 0x51, 0xd4, 0x03, 0x44, 0x79, - 0x19, 0xc8, 0xf1, 0xb6, 0xdb, 0x09, 0x5d, 0xb1, 0xe5, 0x70, 0x42, 0x91, 0x94, 0x5c, 0x61, 0xb9, 0xd7, 0xa0, 0xf3, - 0xb8, 0x60, 0xbd, 0x17, 0x80, 0x45, 0x70, 0x0e, 0x7f, 0x63, 0x42, 0x6f, 0xe0, 0x6f, 0x4e, 0xe8, 0x6b, 0x16, 0x5e, - 0x0f, 0xaf, 0xc8, 0x61, 0x98, 0x0e, 0x26, 0x4a, 0x30, 0x76, 0xcf, 0x96, 0x65, 0xa8, 0x12, 0x57, 0x87, 0x97, 0xe4, - 0xf1, 0x25, 0xbd, 0xa3, 0xb7, 0xf4, 0x8c, 0xbe, 0x05, 0xc2, 0x7f, 0x7f, 0x3c, 0xe1, 0xc3, 0xc9, 0x93, 0x7e, 0xbf, - 0x77, 0xd1, 0xef, 0xf7, 0xce, 0x8d, 0x01, 0x85, 0xde, 0x45, 0x57, 0x35, 0xd5, 0xbf, 0xae, 0xeb, 0xc5, 0xf4, 0xad, - 0xda, 0xb8, 0x09, 0xcf, 0xf2, 0xf0, 0xfa, 0xf0, 0x9e, 0x0c, 0xf1, 0xf1, 0x32, 0x97, 0xb2, 0x08, 0xaf, 0x0e, 0xef, - 0x09, 0x7d, 0x7b, 0x02, 0x7a, 0x53, 0xac, 0xef, 0xed, 0xe3, 0x7b, 0x5d, 0x1b, 0xa1, 0x2f, 0xc2, 0x04, 0xb6, 0xc9, - 0x1d, 0xb3, 0x77, 0xed, 0xc9, 0x18, 0x62, 0x99, 0xdc, 0x7b, 0xe5, 0xdd, 0x3f, 0xbe, 0x23, 0x87, 0x77, 0xe0, 0x29, - 0x6a, 0xc9, 0xdf, 0x2c, 0xbc, 0x65, 0xad, 0x1a, 0x1e, 0xdf, 0xd3, 0xb3, 0x56, 0x23, 0x1e, 0xdf, 0x93, 0x28, 0xbc, - 0x65, 0x57, 0xf4, 0x8c, 0x5d, 0x13, 0x7a, 0xd1, 0xef, 0x9f, 0xf7, 0xfb, 0xb2, 0xdf, 0xff, 0x47, 0x1c, 0x86, 0xf1, - 0xb0, 0x20, 0x87, 0x92, 0xde, 0x1f, 0x4e, 0xf8, 0x57, 0x64, 0x16, 0xea, 0xe6, 0xab, 0x05, 0x67, 0x55, 0xde, 0x2a, - 0xd7, 0x3d, 0x05, 0x6b, 0x85, 0x7b, 0xa6, 0x9e, 0xde, 0xd2, 0x5b, 0x56, 0xd0, 0x33, 0x16, 0x93, 0xe8, 0x06, 0x5a, - 0x71, 0x31, 0x2b, 0xa2, 0x5b, 0x7a, 0xc6, 0xce, 0x67, 0x71, 0x74, 0x46, 0xdf, 0xb2, 0x7c, 0x38, 0x81, 0xbc, 0x67, - 0xc3, 0x5b, 0x72, 0xf8, 0x96, 0x44, 0xe1, 0x5b, 0xfd, 0xfb, 0x9e, 0x5e, 0xf1, 0xf0, 0x2d, 0xf5, 0xaa, 0x79, 0x4b, - 0x4c, 0xf5, 0x8d, 0xda, 0xdf, 0x92, 0xc8, 0x1f, 0xcc, 0xb7, 0xd6, 0x9e, 0xe6, 0x91, 0xa3, 0x8d, 0x69, 0x19, 0x82, - 0xbe, 0xb9, 0x0c, 0x6f, 0x09, 0x99, 0x36, 0xc7, 0x0e, 0x06, 0x74, 0xf6, 0x28, 0x4a, 0x08, 0xbd, 0xf5, 0x4b, 0xbd, - 0xc5, 0x31, 0x34, 0x23, 0xa4, 0xd2, 0xce, 0x30, 0x0d, 0xd7, 0xc1, 0x2b, 0x0d, 0xd6, 0x71, 0xd1, 0xef, 0x87, 0xeb, - 0x7e, 0x1f, 0x22, 0xdd, 0x17, 0x33, 0x13, 0xdb, 0xcd, 0x91, 0x4d, 0x7a, 0x0b, 0xda, 0xff, 0x57, 0x83, 0x01, 0x74, - 0xc6, 0x2b, 0x29, 0xbc, 0x1d, 0xbc, 0x7a, 0x7c, 0x4f, 0x54, 0x1d, 0x05, 0x15, 0x32, 0x2c, 0xe8, 0x6b, 0x9a, 0x01, - 0xe0, 0xd7, 0xab, 0xc1, 0x80, 0x44, 0xe6, 0x33, 0x32, 0x7d, 0x75, 0xfc, 0x76, 0x3a, 0x18, 0xbc, 0x32, 0xdb, 0xe4, - 0x2f, 0xb6, 0xa7, 0x14, 0x58, 0x7f, 0xe7, 0xfd, 0xfe, 0x5f, 0x27, 0x31, 0xb9, 0x28, 0x78, 0xfc, 0x71, 0xda, 0x6c, - 0xcb, 0x5f, 0x2e, 0xaa, 0xda, 0x79, 0xbf, 0xbf, 0xee, 0xf7, 0xcf, 0x00, 0xbb, 0x68, 0xe6, 0x7c, 0x3d, 0x41, 0xda, - 0x32, 0x77, 0x14, 0x49, 0x93, 0x1c, 0x1a, 0x43, 0xdb, 0x62, 0xd5, 0xb6, 0x59, 0x47, 0x06, 0x16, 0x47, 0xcd, 0x8a, - 0xe2, 0x9a, 0x44, 0x61, 0xef, 0x7c, 0xbb, 0x3d, 0x63, 0x8c, 0xc5, 0x04, 0xa4, 0x1f, 0xfe, 0xeb, 0xb3, 0xba, 0x11, - 0x43, 0x4c, 0x48, 0x64, 0x36, 0x37, 0x4b, 0x7b, 0x08, 0x44, 0x1c, 0x36, 0xfd, 0x7b, 0x73, 0x2f, 0x17, 0xb5, 0xe3, - 0x5b, 0x7f, 0x03, 0x10, 0x22, 0xc9, 0x42, 0x3e, 0xc3, 0x31, 0x28, 0x33, 0x00, 0x32, 0x8f, 0xd4, 0xcc, 0x4b, 0x00, - 0x01, 0x26, 0xdb, 0xed, 0x68, 0x3c, 0x9e, 0xd0, 0x82, 0x8d, 0xfe, 0xf6, 0xf4, 0x71, 0xf5, 0x38, 0x0c, 0x82, 0x41, - 0x46, 0x5a, 0x7a, 0x0a, 0xbb, 0x58, 0xab, 0x43, 0x30, 0x82, 0xd7, 0xec, 0xe3, 0x4d, 0xf6, 0xd9, 0xec, 0x23, 0x12, - 0xd6, 0x06, 0xe3, 0xc8, 0x45, 0xda, 0xd2, 0xdb, 0xed, 0x61, 0x30, 0xb9, 0x48, 0x3f, 0xc1, 0x76, 0xfa, 0xfc, 0x9b, - 0x07, 0xe3, 0x09, 0x07, 0xa3, 0xbb, 0x28, 0xe8, 0x33, 0x6d, 0xbb, 0xad, 0xfc, 0x4b, 0xe0, 0x1b, 0x4c, 0x05, 0x1d, - 0x9b, 0x65, 0xe1, 0x06, 0x15, 0x51, 0x47, 0xcb, 0xa0, 0xaa, 0x95, 0xed, 0x1c, 0x50, 0x4b, 0xac, 0xca, 0xc4, 0x2d, - 0x30, 0x0c, 0x19, 0xea, 0x72, 0x4f, 0xab, 0xdf, 0x78, 0x21, 0x0d, 0x7c, 0x86, 0x13, 0x11, 0x7a, 0xdc, 0x1a, 0xf7, - 0xb9, 0x35, 0xf1, 0x09, 0x6e, 0xad, 0x44, 0x12, 0x6b, 0x60, 0x49, 0xcd, 0xe5, 0x28, 0x61, 0x27, 0x25, 0xe3, 0xb3, - 0x32, 0x4a, 0x68, 0x0c, 0x0f, 0x92, 0x89, 0x99, 0x8c, 0x12, 0xb4, 0x4f, 0x74, 0x11, 0x06, 0xff, 0x04, 0xcc, 0x7e, - 0x9a, 0xc3, 0x5f, 0x49, 0xa6, 0xc9, 0x31, 0x04, 0x84, 0x38, 0x1e, 0xcf, 0xe2, 0x70, 0x4c, 0xa2, 0xe4, 0x04, 0x9e, - 0xe0, 0xbf, 0x22, 0x1c, 0x93, 0x5a, 0xdf, 0x61, 0xa4, 0xba, 0xdc, 0x26, 0x0c, 0xe0, 0xca, 0xc6, 0xb3, 0x49, 0x64, - 0xa5, 0xbb, 0xf2, 0xf1, 0x68, 0xfc, 0x94, 0x4c, 0xe3, 0x50, 0x0e, 0x12, 0x42, 0xc1, 0xbb, 0x37, 0x2c, 0x87, 0x89, - 0x86, 0x67, 0x03, 0x36, 0xaf, 0x74, 0x6c, 0x9e, 0x84, 0x13, 0x10, 0x86, 0x09, 0x39, 0xd6, 0x3d, 0x48, 0x29, 0xfa, - 0x3c, 0xc7, 0x7e, 0xea, 0x23, 0x08, 0xb3, 0xa3, 0x96, 0x8a, 0xaf, 0x00, 0xe8, 0x12, 0x07, 0x87, 0xda, 0x33, 0x5f, - 0xcc, 0xc2, 0xd2, 0xa3, 0x52, 0xa6, 0xba, 0x43, 0xd1, 0xa0, 0xfc, 0xa6, 0x41, 0x87, 0x82, 0x0c, 0x26, 0xb4, 0x3c, - 0x99, 0xf0, 0xaf, 0x20, 0x80, 0x47, 0x23, 0xe2, 0x97, 0xc2, 0x89, 0x81, 0xf0, 0x2a, 0xc8, 0x40, 0xa5, 0xb5, 0x6a, - 0xcc, 0xc8, 0x56, 0x7c, 0x00, 0x61, 0x52, 0x0e, 0x6e, 0xe5, 0x3a, 0x4f, 0x21, 0x2a, 0xd8, 0x3a, 0xaf, 0x0e, 0xae, - 0xc0, 0x92, 0x3d, 0xae, 0x20, 0x4e, 0xd8, 0x7a, 0x05, 0xd8, 0xb9, 0x8f, 0x36, 0x65, 0x7d, 0xa0, 0xbe, 0x3b, 0xc0, - 0x96, 0xc3, 0xab, 0x4a, 0x1e, 0x4c, 0xc6, 0xe3, 0xf1, 0xe8, 0x77, 0x38, 0x3a, 0x80, 0xd0, 0x92, 0xc8, 0xf0, 0xc9, - 0x00, 0x8d, 0xbb, 0xae, 0xb8, 0x37, 0x2e, 0x14, 0x65, 0xa5, 0x93, 0x09, 0x01, 0xf1, 0xb3, 0xe9, 0x1b, 0xec, 0x2b, - 0xae, 0xe3, 0x9f, 0xec, 0x7e, 0x62, 0x56, 0xb4, 0x5a, 0xa9, 0xa3, 0x77, 0x6f, 0xcf, 0x5e, 0x7d, 0x78, 0xf5, 0xcb, - 0x8b, 0xf3, 0x57, 0x6f, 0x5e, 0xbe, 0x7a, 0xf3, 0xea, 0xc3, 0xbf, 0x1e, 0x60, 0xb0, 0x7d, 0x5b, 0x11, 0x3b, 0xf6, - 0xde, 0x3d, 0xc6, 0xab, 0xc5, 0x17, 0xce, 0x1e, 0xb9, 0x5b, 0x2c, 0xc0, 0x26, 0x18, 0x6e, 0x41, 0x50, 0xcd, 0x68, - 0x54, 0xfa, 0x9e, 0x80, 0x8c, 0x46, 0x85, 0x6c, 0x3c, 0xac, 0xd8, 0x0a, 0xb9, 0x78, 0xc7, 0x70, 0xf0, 0x91, 0xfd, - 0xad, 0x38, 0x13, 0x6e, 0x47, 0x5b, 0xb3, 0x22, 0xe0, 0xf3, 0xb5, 0x16, 0x95, 0xc7, 0x85, 0xa8, 0xbd, 0x6d, 0x9f, - 0x43, 0x42, 0x3d, 0x22, 0xd7, 0xc1, 0xfb, 0x36, 0xc8, 0x1e, 0x1f, 0x79, 0x4f, 0xca, 0x33, 0xd4, 0xe7, 0x68, 0xf8, - 0xa8, 0xf1, 0x8c, 0x4e, 0xcc, 0xb5, 0xd1, 0xa1, 0x9e, 0x17, 0xb0, 0xbf, 0x95, 0x18, 0x1b, 0xa2, 0x3d, 0xa4, 0x88, - 0xf5, 0xe1, 0x74, 0xbf, 0xbb, 0x37, 0xa3, 0xef, 0xe0, 0xf8, 0x51, 0xaa, 0x09, 0xa4, 0x45, 0x81, 0xd2, 0x95, 0x21, - 0xb7, 0x3d, 0x0b, 0x0b, 0xf3, 0x33, 0x6c, 0x10, 0x40, 0x7b, 0xd9, 0xb1, 0x24, 0xd0, 0x2c, 0x5e, 0xeb, 0xfa, 0xe7, - 0xe5, 0xcb, 0x44, 0x3b, 0x5f, 0x7c, 0x07, 0x21, 0x86, 0xfd, 0x2b, 0x42, 0x63, 0xc2, 0xdd, 0x24, 0xbb, 0x4b, 0x8b, - 0xb9, 0x57, 0x5d, 0xc7, 0x78, 0xdc, 0xed, 0xb9, 0x52, 0x34, 0x6f, 0x5d, 0x60, 0x0f, 0xd4, 0xbc, 0x8e, 0x97, 0x2c, - 0x04, 0x6c, 0xc6, 0x43, 0xbb, 0x48, 0x9c, 0xdf, 0x3b, 0x9d, 0x90, 0xc3, 0xa3, 0x29, 0x1f, 0xb2, 0x92, 0x8a, 0x01, - 0x2b, 0xeb, 0x1d, 0x6a, 0xce, 0xdb, 0x84, 0x5c, 0xec, 0xd2, 0x70, 0x31, 0xe4, 0x0f, 0x5d, 0x92, 0x3e, 0xf0, 0x86, - 0x43, 0xb5, 0x6d, 0x2e, 0x86, 0x34, 0xe5, 0x74, 0x97, 0xca, 0x80, 0x10, 0xe9, 0x3a, 0xae, 0x48, 0xad, 0x8f, 0xaa, - 0xd4, 0x49, 0x3a, 0x6e, 0xb2, 0xcd, 0x27, 0x2e, 0xd9, 0xea, 0x76, 0xed, 0x5f, 0xab, 0xdb, 0x17, 0x66, 0x20, 0x7f, - 0x7f, 0x20, 0xaa, 0x89, 0x81, 0xe8, 0x02, 0x2a, 0xf8, 0x07, 0x78, 0x79, 0xf2, 0x48, 0x2b, 0x40, 0xf7, 0x9d, 0x1d, - 0x5d, 0x7b, 0xbc, 0x31, 0x8b, 0xad, 0x25, 0xce, 0x59, 0xe5, 0x3b, 0xcb, 0xab, 0xb2, 0x15, 0xba, 0x8e, 0x60, 0xbf, - 0x85, 0x1d, 0x7d, 0xf7, 0xb6, 0x01, 0x10, 0xa5, 0xb0, 0x72, 0x67, 0xbf, 0xf0, 0xce, 0x7e, 0x61, 0xcf, 0x7e, 0xbb, - 0x09, 0x94, 0x0f, 0x2b, 0xb4, 0xec, 0xa5, 0x14, 0x95, 0x69, 0xf2, 0xb8, 0xa9, 0xcb, 0x42, 0x5a, 0xcc, 0x0f, 0x2d, - 0xed, 0x7a, 0x32, 0xa6, 0x12, 0xd5, 0x23, 0xdf, 0x63, 0xab, 0x0e, 0x4b, 0xf2, 0xf0, 0x3d, 0xf3, 0x7f, 0xf6, 0x06, - 0xb9, 0xef, 0x6e, 0xf7, 0x7f, 0x73, 0xa1, 0x83, 0xdb, 0x5a, 0x2a, 0x3c, 0x75, 0x75, 0x5c, 0xe0, 0x5d, 0x2d, 0x7d, - 0xf8, 0xae, 0xf6, 0x2e, 0xd3, 0xcb, 0xae, 0x02, 0xd4, 0x20, 0xb1, 0xbe, 0xe6, 0x45, 0x96, 0xd4, 0x56, 0xa1, 0xf1, - 0x96, 0x43, 0x68, 0x0f, 0xef, 0xe0, 0x02, 0x39, 0x2c, 0x21, 0xf4, 0x63, 0x65, 0x04, 0x80, 0x3e, 0x8b, 0xfd, 0x96, - 0x87, 0x19, 0x19, 0xf8, 0x12, 0xbf, 0x52, 0xfa, 0xe2, 0xe2, 0xc3, 0x9d, 0xcc, 0x04, 0xbd, 0x4a, 0x6c, 0x76, 0x29, - 0xdb, 0x31, 0x3f, 0xfc, 0x2f, 0x30, 0x1a, 0x84, 0xd7, 0x96, 0xec, 0x50, 0x74, 0xcc, 0x72, 0x05, 0x47, 0x6d, 0xe9, - 0xca, 0x2c, 0x5b, 0xd7, 0xcf, 0x6a, 0x98, 0xe9, 0x33, 0xe5, 0x2d, 0xc8, 0xbe, 0x90, 0xbb, 0x9f, 0xea, 0x8a, 0x05, - 0x39, 0x99, 0x8c, 0xa7, 0x44, 0x0c, 0x06, 0xad, 0xe4, 0x63, 0x4c, 0x1e, 0x0e, 0x77, 0x98, 0x4b, 0xa1, 0xfb, 0xe1, - 0xf5, 0x01, 0xea, 0x6b, 0x6c, 0x49, 0xb2, 0xa9, 0xd8, 0x9f, 0x60, 0x16, 0x0b, 0xc4, 0xd1, 0xc1, 0x2f, 0xce, 0x17, - 0x00, 0xb2, 0x0c, 0xcb, 0x4c, 0x0b, 0x8b, 0xca, 0x54, 0xf9, 0xc8, 0x16, 0x4c, 0x1e, 0x8f, 0x67, 0x7e, 0xcf, 0x1d, - 0x83, 0x43, 0x48, 0x34, 0xb1, 0xc6, 0x2f, 0x7e, 0x16, 0x8c, 0xe3, 0x50, 0x9e, 0xc8, 0xc6, 0x77, 0x25, 0x89, 0xc6, - 0xc6, 0x54, 0x59, 0x5f, 0x25, 0xaa, 0x61, 0x42, 0x1e, 0x17, 0xe4, 0xb0, 0xa0, 0x4b, 0x7f, 0x2c, 0x31, 0xfd, 0x30, - 0x3e, 0x9c, 0x8c, 0xc9, 0xe3, 0xf8, 0xf1, 0xc4, 0xc0, 0x0d, 0xfb, 0x39, 0xf2, 0xe1, 0x92, 0x1c, 0x36, 0xab, 0x04, - 0x53, 0x54, 0xd3, 0x33, 0xbf, 0x92, 0x64, 0xb0, 0x1c, 0xa4, 0x8f, 0x5b, 0x79, 0xb1, 0x56, 0x3d, 0xde, 0xeb, 0x63, - 0x3e, 0x25, 0xa2, 0x71, 0x63, 0x58, 0xd3, 0xeb, 0xf8, 0x0f, 0x59, 0x44, 0xa5, 0x04, 0x44, 0x42, 0x50, 0x6f, 0x67, - 0x97, 0x59, 0x12, 0x8b, 0x34, 0x4a, 0x6b, 0x42, 0xd3, 0x13, 0x36, 0x19, 0xcf, 0x52, 0x96, 0x1e, 0x4f, 0x9e, 0xce, - 0x26, 0x4f, 0xa3, 0xa3, 0x71, 0x94, 0x0e, 0x06, 0x90, 0x7c, 0x34, 0x06, 0x17, 0x3b, 0xf8, 0xcd, 0x8e, 0x60, 0xe8, - 0x4e, 0x90, 0x25, 0x2c, 0xa0, 0x69, 0x9f, 0xd7, 0x24, 0x3d, 0x9c, 0x97, 0xaa, 0x27, 0xf1, 0x1d, 0x5d, 0x7b, 0x0e, - 0x2e, 0x7e, 0x0b, 0x2f, 0x5d, 0x0b, 0x2f, 0x77, 0x5b, 0x28, 0x4c, 0xdc, 0x14, 0xf9, 0xff, 0xe3, 0x86, 0xb1, 0xef, - 0x2e, 0x61, 0x16, 0xd7, 0x4d, 0x36, 0x5a, 0x15, 0xb2, 0x92, 0x70, 0x9b, 0x50, 0xa2, 0xb0, 0x51, 0xbc, 0x5a, 0xe5, - 0xda, 0x45, 0x6c, 0x5e, 0x51, 0x00, 0x77, 0x81, 0x38, 0xc5, 0xc0, 0x42, 0x1b, 0x03, 0xb9, 0xbf, 0x78, 0x21, 0x99, - 0x55, 0xfb, 0x98, 0x7b, 0xe4, 0x1f, 0x21, 0x18, 0xa3, 0x8a, 0x93, 0xf1, 0x4c, 0x61, 0x5d, 0x7c, 0x4a, 0xde, 0xfb, - 0x6f, 0x1c, 0x45, 0xf6, 0x68, 0x06, 0x3d, 0x41, 0xe4, 0x3c, 0xe2, 0xec, 0xc9, 0xe4, 0x65, 0xe0, 0x7e, 0x06, 0x2b, - 0xfd, 0x75, 0xb7, 0x19, 0x6b, 0xdb, 0xa3, 0x7b, 0x61, 0x84, 0xa2, 0x7f, 0xe1, 0x3b, 0x53, 0x2f, 0xe0, 0x12, 0xaa, - 0x81, 0x5d, 0x5f, 0x5d, 0xf1, 0x12, 0x40, 0x84, 0x32, 0xd1, 0xef, 0xf7, 0xfe, 0x30, 0xd0, 0xa4, 0x25, 0x2f, 0x5e, - 0x67, 0xc2, 0x3a, 0xe3, 0x40, 0x53, 0x81, 0xfa, 0x7f, 0xac, 0xec, 0x33, 0x1d, 0x93, 0x99, 0xff, 0x38, 0x9c, 0x90, - 0xa8, 0xf9, 0x9a, 0x7c, 0xe2, 0x34, 0xfd, 0xc4, 0x15, 0xed, 0x3f, 0x90, 0x99, 0x1b, 0x0e, 0x19, 0xea, 0x2f, 0x1d, - 0xf3, 0x64, 0xf4, 0x3a, 0x31, 0x3b, 0x11, 0xac, 0x9a, 0x41, 0x14, 0xf6, 0x02, 0x1e, 0xd4, 0xb5, 0x2c, 0x9e, 0xc2, - 0xec, 0x83, 0x1a, 0x51, 0x1c, 0xb3, 0xf1, 0x2c, 0x94, 0xe1, 0x04, 0xec, 0x7b, 0x27, 0x63, 0xb8, 0x0f, 0xc8, 0xf0, - 0x63, 0x15, 0x62, 0xe7, 0x20, 0xed, 0x63, 0x85, 0x8a, 0x09, 0x80, 0x08, 0x84, 0xbc, 0xfd, 0xbe, 0x54, 0x49, 0xf8, - 0xba, 0xc4, 0x94, 0x42, 0x7d, 0xf0, 0x9f, 0x48, 0xd5, 0x1d, 0xd3, 0xaf, 0xd6, 0x8f, 0x3f, 0x13, 0x8a, 0x4f, 0x77, - 0x29, 0xf1, 0x1d, 0x04, 0x77, 0x96, 0xa0, 0x83, 0xa8, 0xd0, 0x8c, 0xed, 0x61, 0x7e, 0x57, 0xec, 0xe7, 0x77, 0xc5, - 0xff, 0x3b, 0x7e, 0x57, 0x3c, 0xc4, 0x18, 0x56, 0x16, 0x1a, 0x7e, 0x16, 0x8c, 0x83, 0xe8, 0x3f, 0xe7, 0x13, 0xf7, - 0xf2, 0xd4, 0xd7, 0x99, 0x98, 0xee, 0x61, 0x9a, 0x7d, 0x82, 0x82, 0xb0, 0x8a, 0xbb, 0xf4, 0x64, 0x5d, 0xd9, 0x5b, - 0x2b, 0x19, 0x62, 0x9e, 0x07, 0x58, 0xa3, 0xb0, 0xf2, 0x80, 0xee, 0x51, 0xb5, 0x41, 0x9c, 0x08, 0x1e, 0xc6, 0xcc, - 0x4a, 0xdf, 0xb7, 0x5b, 0xa3, 0xc2, 0x7c, 0x90, 0x8b, 0x82, 0xec, 0xe6, 0xe3, 0xd9, 0x38, 0x0a, 0xb1, 0x01, 0xff, - 0x31, 0x63, 0xd5, 0x90, 0xcd, 0x77, 0x32, 0x52, 0x3b, 0x26, 0x4f, 0x93, 0x5d, 0xd2, 0x3b, 0xe0, 0x1d, 0xf2, 0x73, - 0x70, 0x67, 0x93, 0x86, 0xdf, 0x92, 0x57, 0x71, 0x91, 0x55, 0xcb, 0xeb, 0x2c, 0x41, 0xa6, 0x0b, 0x5e, 0x7c, 0x36, - 0xd3, 0xe5, 0x7d, 0xac, 0x0f, 0x18, 0x4f, 0x29, 0x5e, 0x37, 0x44, 0xe9, 0xeb, 0x96, 0x67, 0x85, 0xba, 0x3c, 0xa9, - 0x98, 0xed, 0x59, 0x09, 0x4e, 0xa7, 0x60, 0x82, 0xaf, 0x7f, 0xba, 0xde, 0x27, 0x80, 0x0b, 0x0a, 0x35, 0xa7, 0x85, - 0x5c, 0x19, 0x2c, 0x27, 0x0b, 0xdd, 0x09, 0x98, 0xa1, 0x52, 0xe0, 0x05, 0x0a, 0xfe, 0xa2, 0x81, 0x11, 0x7d, 0xe9, - 0x7e, 0x93, 0x81, 0x41, 0xba, 0x34, 0x27, 0xc2, 0xd8, 0x71, 0x3b, 0x45, 0xda, 0x8a, 0x72, 0xc6, 0xd9, 0x7b, 0x75, - 0xa5, 0x00, 0x03, 0xbc, 0xcd, 0x6d, 0x74, 0x91, 0xa0, 0xd7, 0x82, 0xd2, 0x79, 0x03, 0x77, 0xb3, 0x8c, 0x8c, 0x70, - 0xf1, 0x71, 0xe5, 0xb1, 0xe0, 0x9e, 0xfd, 0x42, 0x2c, 0x8d, 0x66, 0x1a, 0x8c, 0xd9, 0xbc, 0x60, 0x81, 0x42, 0x05, - 0x0a, 0x2c, 0x67, 0xda, 0xd2, 0xb4, 0x1a, 0xf2, 0xc3, 0x23, 0xb4, 0x36, 0xad, 0x06, 0xfc, 0xf0, 0xa8, 0x8e, 0xb2, - 0x63, 0xc8, 0x72, 0xe2, 0x67, 0x50, 0xaf, 0xeb, 0xc8, 0xa4, 0x98, 0xec, 0x7e, 0x7d, 0xa9, 0x3f, 0xaa, 0x1b, 0x70, - 0xfd, 0x00, 0x04, 0xb0, 0x01, 0x38, 0x04, 0xaa, 0xc1, 0xd2, 0x88, 0x60, 0x51, 0xa6, 0xd0, 0xbe, 0x86, 0xde, 0x1b, - 0x0d, 0xff, 0x05, 0xee, 0x22, 0x72, 0xe5, 0x7f, 0x82, 0xc0, 0x5f, 0x51, 0xa6, 0x95, 0x29, 0xfe, 0x27, 0x5a, 0xbd, - 0x42, 0x39, 0x6b, 0x5a, 0xf3, 0x41, 0xb4, 0x26, 0x42, 0x35, 0x63, 0x08, 0xfe, 0xad, 0x2c, 0xd3, 0x96, 0xaa, 0x4a, - 0x7d, 0x68, 0xbc, 0xd6, 0x0a, 0x67, 0xf9, 0x38, 0xf2, 0x5e, 0x63, 0xe8, 0xd8, 0xc4, 0x59, 0xca, 0xa9, 0xd4, 0xd9, - 0x9b, 0x43, 0x19, 0x39, 0xc0, 0xe9, 0x84, 0x8d, 0xa7, 0xc9, 0xb1, 0x9c, 0x26, 0x0e, 0x32, 0x3f, 0x67, 0x18, 0x59, - 0xd5, 0x80, 0xb0, 0x28, 0x1b, 0x4a, 0x5b, 0x80, 0x49, 0x4e, 0x08, 0x99, 0x62, 0x28, 0x8a, 0x7c, 0xa4, 0xfb, 0x61, - 0xbd, 0x59, 0xdd, 0x17, 0xef, 0x34, 0xc0, 0x69, 0x98, 0x40, 0x20, 0xf0, 0x22, 0xbe, 0xcd, 0xc4, 0x15, 0x78, 0x0c, - 0x0f, 0xe0, 0x4b, 0x70, 0x93, 0x4b, 0xd9, 0xaf, 0x55, 0x98, 0xe3, 0xda, 0x02, 0x06, 0x0d, 0x56, 0x0f, 0xa2, 0xc3, - 0xa5, 0xb4, 0xd9, 0x55, 0x80, 0xd8, 0x98, 0x42, 0x2c, 0x0b, 0xb6, 0xb6, 0xec, 0xd9, 0x4f, 0xaa, 0x69, 0x68, 0x9d, - 0x70, 0x2a, 0xae, 0x72, 0x88, 0xa2, 0x32, 0x88, 0xc1, 0x1d, 0xc9, 0xe3, 0xf3, 0x1e, 0x89, 0xf0, 0x92, 0x80, 0x5b, - 0x59, 0x2c, 0xc3, 0x15, 0x5d, 0x8e, 0xee, 0xe8, 0x7a, 0x74, 0x4b, 0xc7, 0x74, 0xf2, 0xf7, 0x31, 0x58, 0x64, 0xeb, - 0xd4, 0x7b, 0xba, 0x1e, 0x2d, 0xe9, 0x37, 0x63, 0x7a, 0xf4, 0x37, 0x30, 0xe1, 0xc3, 0xc3, 0x84, 0x5e, 0x82, 0x63, - 0x17, 0xa9, 0xd1, 0x53, 0xd3, 0x37, 0x38, 0xac, 0x46, 0xf9, 0x90, 0x8f, 0x72, 0xca, 0x47, 0xc5, 0xb0, 0x1a, 0x81, - 0xa7, 0x63, 0x35, 0xe4, 0xa3, 0x8a, 0xf2, 0xd1, 0xc5, 0xb0, 0x1a, 0x5d, 0x90, 0x66, 0xd3, 0x5f, 0x55, 0xfc, 0xba, - 0x64, 0x29, 0x6c, 0x0b, 0x58, 0xbe, 0x9e, 0x57, 0x54, 0xea, 0xaf, 0x6a, 0x73, 0x32, 0x5b, 0xce, 0xde, 0x5e, 0x77, - 0x39, 0xb1, 0x78, 0xdc, 0x36, 0x1d, 0xae, 0xbe, 0x9c, 0xa8, 0x93, 0x5e, 0x21, 0x3f, 0x8c, 0xa7, 0x42, 0x9d, 0x43, - 0x60, 0x26, 0x31, 0x0b, 0x63, 0x86, 0xcd, 0xd4, 0x69, 0xa0, 0xc0, 0xc9, 0x46, 0x9e, 0x8b, 0x62, 0x36, 0xca, 0x29, - 0xbc, 0x8f, 0x09, 0x89, 0x04, 0x9c, 0x55, 0x27, 0xd5, 0xa8, 0x80, 0x98, 0x23, 0x2c, 0xc4, 0x47, 0xe8, 0x97, 0xfa, - 0xc8, 0x43, 0x02, 0xcf, 0xb0, 0xaf, 0xc5, 0x20, 0x86, 0x23, 0xde, 0x56, 0x56, 0xcd, 0xc2, 0x04, 0x2a, 0xab, 0x86, - 0xa5, 0xa9, 0xac, 0xa0, 0xd9, 0xa8, 0xf2, 0x2b, 0xab, 0x70, 0x8c, 0x12, 0x42, 0xa2, 0x52, 0x57, 0x06, 0xea, 0x93, - 0x84, 0x85, 0xa5, 0xae, 0xec, 0x42, 0x7d, 0x74, 0xe1, 0x57, 0x76, 0x01, 0x2e, 0xa4, 0x83, 0xc4, 0xbf, 0x4a, 0xe5, - 0x69, 0xfb, 0x3a, 0xd8, 0x58, 0x55, 0x74, 0xc3, 0xef, 0xaa, 0x22, 0x8e, 0x4a, 0xea, 0x62, 0x40, 0xe3, 0xc2, 0x88, - 0x24, 0xd5, 0x6b, 0x14, 0xfc, 0x21, 0x41, 0x54, 0x1a, 0x83, 0x57, 0x67, 0xd2, 0xb5, 0x52, 0x2b, 0x2a, 0x06, 0xe5, - 0xa0, 0x80, 0xfb, 0x53, 0xde, 0x5a, 0x48, 0x3f, 0x41, 0x44, 0x65, 0x28, 0x6f, 0xf0, 0x4f, 0x0c, 0x9e, 0xcc, 0x56, - 0x69, 0x98, 0x8c, 0xee, 0x69, 0x3c, 0x5a, 0x22, 0x1c, 0x0c, 0x5b, 0xa7, 0x0a, 0x6f, 0xfd, 0x12, 0xd2, 0xef, 0x68, - 0x3c, 0xba, 0xa5, 0xa9, 0xb5, 0x39, 0x35, 0x50, 0x57, 0xbd, 0x31, 0xbd, 0x8b, 0xe0, 0xf5, 0x7d, 0xb4, 0xa4, 0xb0, - 0x95, 0x4e, 0xf3, 0xec, 0x4a, 0x44, 0x29, 0x45, 0x04, 0xc2, 0x35, 0x22, 0x07, 0x2e, 0x35, 0xda, 0xe0, 0x7a, 0x00, - 0x65, 0x68, 0xb8, 0xc0, 0xe5, 0x20, 0x1e, 0x2d, 0x3d, 0x32, 0xb5, 0xd4, 0x17, 0x59, 0x84, 0x8f, 0x76, 0x36, 0x5a, - 0x8a, 0x67, 0xc4, 0xc2, 0xb8, 0x82, 0x21, 0xd4, 0x85, 0x95, 0xa6, 0x20, 0xe9, 0x02, 0x47, 0xf6, 0xc2, 0xb8, 0x0a, - 0x37, 0x60, 0x5a, 0x74, 0x0f, 0xe6, 0x51, 0xa0, 0x70, 0x70, 0x09, 0xd2, 0x4f, 0x28, 0xdb, 0x39, 0x4a, 0x93, 0xc3, - 0x9b, 0xa0, 0x74, 0x67, 0x82, 0x90, 0x76, 0x75, 0x93, 0x2d, 0xe9, 0x1b, 0x6c, 0xef, 0xd0, 0xa9, 0xa8, 0xa0, 0xfa, - 0xdc, 0x82, 0xc9, 0x92, 0x0d, 0xc2, 0x96, 0x30, 0x3d, 0xd3, 0x6b, 0xc0, 0x9e, 0x3e, 0x3c, 0xda, 0x99, 0xef, 0x62, - 0xf6, 0xe6, 0xb0, 0x8c, 0xc6, 0xca, 0x82, 0x37, 0xb7, 0xc4, 0x6e, 0xc9, 0xc6, 0xd3, 0xe5, 0x71, 0x39, 0x5d, 0x22, - 0xb1, 0x33, 0x74, 0x8b, 0xf1, 0xf9, 0x72, 0x41, 0x13, 0x3c, 0xdb, 0x58, 0x35, 0x5f, 0x1a, 0xb4, 0x94, 0x94, 0xe1, - 0x7a, 0x5b, 0xa2, 0xff, 0xbf, 0xba, 0xf8, 0xa5, 0x00, 0x2f, 0xc1, 0x58, 0x00, 0x08, 0xf7, 0x60, 0x5a, 0x90, 0xda, - 0x28, 0x1b, 0xcb, 0x34, 0x4c, 0x71, 0x11, 0x98, 0x94, 0x7e, 0x3f, 0xcc, 0x59, 0x4a, 0x3c, 0xe8, 0x50, 0x77, 0x6a, - 0xa7, 0xbe, 0x10, 0x04, 0x78, 0x24, 0x75, 0x8e, 0x4d, 0xfe, 0x3e, 0x9e, 0x05, 0x6a, 0x20, 0x82, 0x28, 0x3b, 0xc6, - 0x47, 0x0c, 0x5c, 0x14, 0xe9, 0xb8, 0x9d, 0xae, 0x88, 0xcb, 0xdd, 0x63, 0x16, 0xe2, 0x24, 0x61, 0xae, 0x59, 0x36, - 0x64, 0x55, 0x84, 0x09, 0xba, 0x30, 0x30, 0xcb, 0x1b, 0xb2, 0xea, 0xf0, 0x08, 0x22, 0xb5, 0xda, 0x32, 0x56, 0x5d, - 0x65, 0x7c, 0x03, 0x40, 0xd6, 0x8c, 0xb1, 0xa3, 0xbf, 0x8d, 0x67, 0xea, 0x9b, 0x28, 0xe4, 0x27, 0x47, 0x7f, 0x83, - 0xe4, 0xe3, 0x6f, 0x90, 0x99, 0x83, 0xe4, 0x46, 0x41, 0x57, 0xcd, 0x59, 0xd7, 0x50, 0x9a, 0xb8, 0xf6, 0x4a, 0xbd, - 0xf6, 0xa4, 0x59, 0x7b, 0x05, 0xba, 0x53, 0x1b, 0xde, 0x43, 0xd9, 0xce, 0x82, 0x09, 0x3a, 0x9a, 0xdd, 0x81, 0x0e, - 0xde, 0x29, 0x82, 0x5e, 0x24, 0xa1, 0xf1, 0x08, 0x55, 0x46, 0xbd, 0x18, 0x0f, 0xaa, 0x93, 0x75, 0xc9, 0x3c, 0x03, - 0xe6, 0xd8, 0x9e, 0x43, 0x62, 0x98, 0xab, 0x83, 0x3a, 0x65, 0xe5, 0x30, 0xc7, 0x03, 0x78, 0xcd, 0xe4, 0x50, 0x0c, - 0x72, 0x8d, 0xf2, 0x7d, 0xc9, 0x8a, 0x61, 0x39, 0xc8, 0x35, 0x37, 0x33, 0x6d, 0xc6, 0xa6, 0x4d, 0x74, 0x78, 0xe6, - 0x15, 0x3b, 0x59, 0xf5, 0x80, 0x8f, 0x05, 0x4f, 0x66, 0xdf, 0xf3, 0xf1, 0x35, 0x70, 0x32, 0x9b, 0xbb, 0x68, 0x49, - 0xef, 0xa3, 0x94, 0xde, 0x46, 0x6b, 0xba, 0x8c, 0x2e, 0x8d, 0x89, 0x71, 0x52, 0xc3, 0x39, 0x00, 0xad, 0x02, 0x48, - 0x3c, 0xf5, 0xeb, 0x3d, 0x4f, 0xaa, 0x70, 0x49, 0x53, 0x70, 0x1b, 0xf6, 0xed, 0x33, 0xaf, 0x7d, 0x89, 0xd4, 0x06, - 0x31, 0xd6, 0xac, 0xa1, 0xe2, 0xc6, 0x5b, 0xf7, 0x91, 0xa8, 0x61, 0xe7, 0xba, 0xd8, 0x44, 0xd5, 0x70, 0x32, 0x2d, - 0x01, 0xb1, 0xb5, 0x1c, 0x0e, 0xdd, 0x11, 0xb2, 0x7b, 0xfc, 0xe8, 0x40, 0xcf, 0x3d, 0x69, 0xb1, 0x6d, 0x5b, 0xfe, - 0xc0, 0x10, 0xa6, 0xf4, 0xd3, 0x47, 0x3e, 0x20, 0x56, 0x5c, 0xc1, 0xd9, 0x08, 0xd4, 0xd1, 0x0a, 0x9d, 0x7e, 0xad, - 0xc2, 0x42, 0x1f, 0xe0, 0x9b, 0xbb, 0x28, 0xa1, 0xf7, 0x51, 0xee, 0x91, 0xb5, 0x65, 0xcd, 0xe4, 0xf4, 0x3c, 0x0b, - 0x79, 0xfb, 0x40, 0x2f, 0x17, 0x00, 0xa2, 0x35, 0x88, 0x7d, 0xa9, 0xeb, 0x11, 0x38, 0x0d, 0xa1, 0x49, 0x68, 0x04, - 0x57, 0x15, 0x84, 0x11, 0x70, 0x25, 0xe1, 0x6f, 0x30, 0x51, 0x81, 0x2f, 0xc0, 0x45, 0x26, 0x4d, 0x73, 0x1e, 0xd4, - 0xfe, 0x48, 0x9e, 0x15, 0x6d, 0x6f, 0x57, 0x18, 0x4d, 0x30, 0xf6, 0x44, 0xfb, 0x3c, 0x52, 0x8e, 0xe2, 0x22, 0x09, - 0xb3, 0xd1, 0x9d, 0x3a, 0xcf, 0x69, 0x36, 0xba, 0xd7, 0xbf, 0x2a, 0x3a, 0xa6, 0xbf, 0xe8, 0x80, 0x36, 0x4a, 0xfa, - 0xd6, 0x71, 0x36, 0xa0, 0xf5, 0x62, 0x69, 0xfc, 0xaf, 0xe5, 0xe8, 0x8e, 0xca, 0xd1, 0xbd, 0x6f, 0x49, 0x35, 0x99, - 0x16, 0xc7, 0x02, 0x0d, 0xa9, 0x3a, 0xbf, 0x2f, 0x80, 0x9f, 0x2b, 0x8d, 0xef, 0xb4, 0xf9, 0xde, 0x6b, 0xff, 0x45, - 0x27, 0x4f, 0xa0, 0x58, 0xa2, 0x82, 0x55, 0x23, 0xb0, 0x63, 0x5f, 0xe7, 0x71, 0x61, 0x46, 0x29, 0xa6, 0xd6, 0xa4, - 0x1f, 0x03, 0x57, 0x4c, 0x7b, 0x05, 0xb8, 0x5a, 0x82, 0x93, 0x00, 0xc4, 0xd0, 0x84, 0x3d, 0x3b, 0x86, 0xa8, 0xe7, - 0xc6, 0x31, 0x4a, 0x36, 0xdc, 0x03, 0x62, 0x2d, 0xf3, 0x56, 0x2e, 0x01, 0x09, 0xbc, 0xf5, 0x30, 0x29, 0x00, 0x63, - 0xb0, 0x5c, 0x12, 0x9d, 0xc7, 0x43, 0x9f, 0x50, 0x2f, 0x34, 0xea, 0x84, 0x6c, 0x6c, 0x09, 0x1c, 0x7f, 0x58, 0x1f, - 0x02, 0xc1, 0xab, 0x3c, 0xd7, 0x5f, 0x69, 0x5d, 0x7f, 0xa9, 0xf4, 0xdc, 0xb1, 0x5c, 0xd7, 0xcf, 0xdb, 0xd4, 0xe8, - 0x25, 0x58, 0xf8, 0x6e, 0x94, 0x79, 0x24, 0xb7, 0x08, 0xa9, 0x0a, 0xac, 0xd4, 0x2d, 0x24, 0x98, 0x7f, 0x25, 0x67, - 0xab, 0x32, 0x5f, 0x3d, 0xf2, 0xa0, 0x9c, 0x4d, 0x4f, 0x7f, 0x43, 0x82, 0x76, 0xdf, 0x91, 0xe6, 0xf1, 0x16, 0x1d, - 0x3e, 0xbb, 0xd6, 0x12, 0x73, 0x27, 0x51, 0xf1, 0x7c, 0x0a, 0xd8, 0xea, 0x79, 0x76, 0xad, 0x7c, 0xac, 0x76, 0x71, - 0xfc, 0xcc, 0xf9, 0x93, 0x54, 0xe1, 0x5a, 0x34, 0x94, 0x20, 0xe0, 0xcd, 0x61, 0xec, 0x0a, 0x55, 0x40, 0x43, 0x73, - 0x03, 0xc7, 0xb9, 0x1a, 0x56, 0x9a, 0x80, 0x69, 0x29, 0x8f, 0x0e, 0x70, 0x68, 0xf2, 0xa8, 0xdd, 0x34, 0xac, 0x0c, - 0x5d, 0x6b, 0xf4, 0xb9, 0xad, 0x74, 0xc6, 0x9b, 0x0d, 0x3f, 0x3c, 0x1a, 0x54, 0xf8, 0x93, 0x34, 0x47, 0xa3, 0x9d, - 0x1b, 0xee, 0x34, 0x02, 0x33, 0x57, 0x72, 0x45, 0x76, 0x47, 0xc9, 0xcb, 0xef, 0xe9, 0x85, 0x05, 0xf4, 0xe7, 0x3f, - 0x17, 0x13, 0x4e, 0x5a, 0x62, 0x42, 0xb4, 0x74, 0xd0, 0xa2, 0x83, 0x1d, 0xe5, 0x95, 0x7d, 0x89, 0x97, 0xce, 0xf1, - 0xbf, 0xaf, 0xc7, 0xda, 0x55, 0x20, 0xb4, 0x3a, 0x79, 0xd8, 0x9e, 0x2c, 0x10, 0x35, 0xa0, 0x9a, 0x5d, 0x95, 0xa3, - 0x4c, 0x3b, 0x2b, 0xb2, 0x69, 0xc8, 0x5c, 0x77, 0xb3, 0x34, 0x6c, 0x26, 0x3b, 0x16, 0x96, 0x19, 0x06, 0x6b, 0xa7, - 0x8a, 0x3e, 0x07, 0x2d, 0x3f, 0x82, 0xe7, 0x4d, 0xe5, 0x99, 0xcf, 0x66, 0x19, 0xf1, 0x02, 0x9d, 0x73, 0x2a, 0x16, - 0x4d, 0xe9, 0x58, 0xb9, 0xdd, 0x96, 0x68, 0x2c, 0x51, 0x46, 0x41, 0x50, 0xdb, 0x20, 0xec, 0xba, 0x74, 0x4f, 0xfa, - 0xb4, 0x8b, 0x4f, 0x2b, 0xd0, 0xf7, 0x78, 0x9f, 0x81, 0xc4, 0xd4, 0x93, 0x3c, 0x54, 0x8d, 0xe6, 0xe8, 0xe4, 0x59, - 0x9c, 0x6a, 0x7c, 0x7e, 0x25, 0x3b, 0x6b, 0xde, 0xad, 0xc6, 0x14, 0xff, 0x91, 0xba, 0x7d, 0xe7, 0x32, 0x34, 0xd1, - 0x5f, 0xcb, 0x83, 0x96, 0xc2, 0x82, 0xe3, 0xb6, 0xf1, 0xd7, 0x6f, 0x33, 0x87, 0x18, 0x96, 0x2e, 0x87, 0x37, 0xa1, - 0x43, 0x77, 0x57, 0xd9, 0x99, 0xeb, 0x23, 0xea, 0xd4, 0xc5, 0xba, 0x0d, 0x28, 0x59, 0xf2, 0x6e, 0x9d, 0x9e, 0x58, - 0xe9, 0x97, 0xc3, 0x70, 0x67, 0x1e, 0x35, 0xbb, 0xbb, 0xdd, 0x4e, 0x48, 0xdb, 0x3e, 0x18, 0xef, 0x4b, 0x58, 0x88, - 0xf3, 0x0e, 0x3b, 0xf8, 0x29, 0xac, 0x1e, 0xf3, 0xc1, 0x6f, 0x38, 0xce, 0x30, 0xfa, 0x99, 0x32, 0xf4, 0x79, 0x59, - 0xc8, 0x6b, 0xd5, 0x29, 0x5f, 0xe8, 0xd6, 0x32, 0xf5, 0x7e, 0x13, 0xbf, 0x69, 0x05, 0x88, 0xf1, 0xba, 0x62, 0xa5, - 0x78, 0x43, 0x2b, 0x8c, 0x6b, 0xe0, 0x36, 0x39, 0xd4, 0x52, 0x2d, 0x10, 0x75, 0xf9, 0xc9, 0x63, 0x1e, 0x19, 0x75, - 0x26, 0x7c, 0xf7, 0x98, 0xfb, 0xd2, 0xb5, 0xdd, 0x26, 0x7e, 0xaa, 0x69, 0x87, 0xbb, 0x03, 0xdd, 0xd1, 0xba, 0x87, - 0x9b, 0x67, 0xf3, 0xf3, 0xc8, 0x7c, 0x31, 0xc0, 0x66, 0xed, 0x32, 0x2e, 0x3b, 0x86, 0xfb, 0xde, 0xf4, 0x60, 0x2c, - 0x20, 0x90, 0x98, 0xa1, 0x97, 0x81, 0x0b, 0x5c, 0xe0, 0xae, 0x30, 0x60, 0x88, 0x6b, 0x5a, 0x72, 0xae, 0xad, 0x6c, - 0x7d, 0xe4, 0x6d, 0x54, 0x08, 0xd6, 0x75, 0xc7, 0x4d, 0x92, 0x43, 0x70, 0xc2, 0x96, 0x7b, 0x5f, 0x7b, 0xed, 0x0c, - 0xff, 0x39, 0x10, 0xce, 0x2d, 0xd1, 0x33, 0x6a, 0x7b, 0xac, 0xd5, 0xbd, 0x86, 0x57, 0xb9, 0x8f, 0x3c, 0xeb, 0x37, - 0xf3, 0xd2, 0xb0, 0x2f, 0x78, 0x2d, 0x05, 0x87, 0xc6, 0x76, 0x2b, 0xdc, 0x62, 0xf1, 0x8e, 0x56, 0x2b, 0x6b, 0x6d, - 0xb5, 0xd7, 0x4a, 0x45, 0xf7, 0xaf, 0x39, 0x4e, 0x9c, 0xa5, 0xb0, 0xfd, 0xf0, 0xe1, 0x82, 0x5d, 0x13, 0xc0, 0xa0, - 0xc5, 0x64, 0x81, 0x12, 0x54, 0xb2, 0x56, 0xb5, 0xdb, 0x29, 0xf1, 0xcb, 0xfd, 0xac, 0xcb, 0x6c, 0xe7, 0xf1, 0xeb, - 0x26, 0xed, 0x13, 0x9f, 0xa3, 0x1f, 0xe6, 0xb7, 0xd6, 0x49, 0xc9, 0x19, 0xc6, 0xb5, 0xfc, 0xff, 0x2a, 0x7a, 0x55, - 0x64, 0x69, 0xb4, 0x31, 0x3c, 0x98, 0x0d, 0xb5, 0xe9, 0x43, 0x63, 0x54, 0x6e, 0xd9, 0x28, 0x22, 0x5a, 0xdd, 0x81, - 0x60, 0x46, 0x71, 0x5f, 0xa2, 0xcd, 0x2b, 0x55, 0x16, 0xde, 0xe1, 0x13, 0x1b, 0xbd, 0x61, 0x7b, 0x42, 0x28, 0xdf, - 0x3d, 0x2d, 0xcc, 0xaa, 0xa5, 0xa2, 0xc1, 0x76, 0x09, 0xef, 0x62, 0x54, 0xe9, 0x27, 0x4c, 0xb6, 0x2c, 0x98, 0xea, - 0xff, 0x77, 0x45, 0x96, 0xb6, 0x29, 0x3a, 0x30, 0x9d, 0x4d, 0x9f, 0x4e, 0xba, 0xc1, 0x75, 0x06, 0x2c, 0x22, 0xd8, - 0x52, 0xe1, 0x78, 0x94, 0xda, 0x0d, 0x12, 0x26, 0x82, 0x9b, 0xa8, 0x97, 0x1d, 0x2d, 0x53, 0xb2, 0x2a, 0xe0, 0xf9, - 0x95, 0xab, 0x4c, 0xc7, 0xd1, 0xd0, 0xef, 0x5f, 0xa7, 0x26, 0xf4, 0x2b, 0xf5, 0x52, 0x15, 0xe7, 0x61, 0x54, 0x1d, - 0x2a, 0x8c, 0xd1, 0x92, 0xa6, 0x70, 0x0c, 0x66, 0x97, 0x61, 0x8a, 0x97, 0xb3, 0x4d, 0xc2, 0x3e, 0x63, 0x20, 0x97, - 0xda, 0xa0, 0x5e, 0x53, 0xa2, 0x35, 0x6b, 0x6f, 0xe6, 0x94, 0xd0, 0x4b, 0x56, 0xfa, 0x77, 0xa1, 0x35, 0x08, 0x14, - 0x65, 0x33, 0x65, 0x7a, 0xa1, 0xdb, 0x79, 0x49, 0x13, 0x5a, 0xd0, 0x15, 0xa9, 0x41, 0xdf, 0xeb, 0xe4, 0xec, 0xe8, - 0x64, 0x67, 0x66, 0x3d, 0x66, 0xc5, 0x70, 0x32, 0x8d, 0xe1, 0x9a, 0x16, 0xbb, 0x6b, 0xda, 0xb2, 0x79, 0xe3, 0x6a, - 0x6c, 0x9c, 0x06, 0xed, 0x02, 0x69, 0x9b, 0xe6, 0xf6, 0x53, 0x8f, 0xdb, 0x5f, 0xd7, 0x6c, 0x39, 0xed, 0xad, 0xb7, - 0xdb, 0x5e, 0x0a, 0x36, 0xa2, 0x1e, 0x1f, 0xbf, 0x56, 0xd2, 0x75, 0xcb, 0xe5, 0xa7, 0xf0, 0xec, 0xf1, 0xf5, 0x4b, - 0x1f, 0x5c, 0x8e, 0x56, 0x6d, 0xee, 0x7e, 0xb9, 0x8b, 0x2c, 0xf7, 0x59, 0x43, 0xcb, 0xf5, 0x0c, 0x35, 0xc9, 0xb3, - 0xd1, 0xde, 0xa1, 0x16, 0x2c, 0x67, 0xdd, 0x84, 0x27, 0x06, 0x3b, 0xf6, 0xaa, 0xb1, 0x39, 0x2a, 0x73, 0xc9, 0x6a, - 0x90, 0x40, 0x9f, 0xe4, 0x99, 0xa6, 0x7f, 0x90, 0x61, 0x3e, 0xba, 0xa3, 0x39, 0xe0, 0x8a, 0x55, 0xf6, 0x92, 0x41, - 0xea, 0xaa, 0xbd, 0xc4, 0x95, 0xaf, 0x70, 0x48, 0x36, 0xf8, 0x64, 0x98, 0xaa, 0x4f, 0x2e, 0x79, 0xf0, 0xff, 0xb6, - 0x6a, 0x95, 0x9e, 0x9b, 0xe4, 0x86, 0xe3, 0x5f, 0x27, 0x6d, 0x1f, 0x13, 0x83, 0x04, 0x3c, 0xb5, 0x8b, 0xa1, 0x1a, - 0x55, 0x45, 0x2c, 0xca, 0xdc, 0xc4, 0x1c, 0xdb, 0xdb, 0x35, 0x74, 0x50, 0x06, 0xbf, 0x6e, 0xf8, 0xc4, 0xdc, 0x81, - 0xad, 0x40, 0x47, 0x27, 0x9a, 0xcb, 0x30, 0x33, 0x97, 0x61, 0xda, 0xb5, 0x55, 0x60, 0x78, 0xd5, 0x56, 0x49, 0x94, - 0xab, 0x51, 0x8f, 0x9b, 0x59, 0x6a, 0xf6, 0x22, 0xef, 0x5e, 0x93, 0x9e, 0xc4, 0x9f, 0x2e, 0x3d, 0x79, 0x3d, 0x0c, - 0x88, 0xfc, 0x9c, 0xa5, 0xe1, 0x1a, 0x05, 0xc1, 0xa9, 0xd5, 0x0e, 0xa4, 0xf9, 0x08, 0x90, 0xf9, 0x71, 0x1a, 0xbe, - 0xd5, 0xe2, 0x1c, 0xb2, 0x51, 0x1a, 0x27, 0xb6, 0x34, 0xea, 0x21, 0xb8, 0xf3, 0x5e, 0xf3, 0x18, 0x02, 0x1f, 0x7e, - 0xc0, 0xcd, 0xa0, 0xa2, 0xdb, 0x12, 0x13, 0xa5, 0xcd, 0xa3, 0x6e, 0xf9, 0xa8, 0x21, 0x54, 0xb2, 0x32, 0xbc, 0x04, - 0xda, 0xbb, 0x23, 0x30, 0xaa, 0x9c, 0x40, 0x66, 0x58, 0x1c, 0x1e, 0x0d, 0x53, 0x25, 0x28, 0x1a, 0xca, 0xe1, 0x12, - 0xe5, 0x80, 0x98, 0x04, 0x02, 0xa3, 0x62, 0x90, 0xea, 0xca, 0xd4, 0x8b, 0x41, 0xaa, 0x6f, 0x55, 0xa4, 0x3e, 0xcf, - 0xc2, 0x8a, 0xea, 0x16, 0xd1, 0x31, 0x1d, 0x4a, 0xba, 0x34, 0x3b, 0x35, 0xd7, 0xd2, 0x0b, 0xb5, 0x1c, 0x9f, 0xe9, - 0x34, 0x18, 0xc5, 0x33, 0x97, 0xa2, 0xdf, 0xaa, 0xfd, 0xec, 0xbf, 0xc5, 0x94, 0x1a, 0xb1, 0xa9, 0xbd, 0x45, 0x0c, - 0xab, 0xf6, 0x43, 0x56, 0xe5, 0xa0, 0xdd, 0x05, 0x65, 0x63, 0x65, 0x9c, 0xe7, 0x1b, 0xc1, 0xcc, 0x41, 0xdb, 0x58, - 0x35, 0x7d, 0xe8, 0x8d, 0x18, 0xb5, 0x37, 0xa6, 0x1a, 0xf7, 0x04, 0x7e, 0xda, 0xa0, 0xe9, 0x5e, 0xe4, 0x39, 0xea, - 0x91, 0x77, 0xff, 0x33, 0x47, 0x76, 0x26, 0x9f, 0xc4, 0x32, 0xa9, 0xdb, 0xc7, 0x24, 0x58, 0xa8, 0x3a, 0x46, 0x17, - 0x6e, 0x64, 0x4a, 0xfb, 0xb9, 0x33, 0xfd, 0x88, 0x67, 0xf2, 0xb0, 0x1d, 0x1a, 0xf5, 0xa5, 0x61, 0x2d, 0x29, 0xa2, - 0xbe, 0xa0, 0xb7, 0xa6, 0x3a, 0x3a, 0xa2, 0x5e, 0x47, 0x60, 0x75, 0x45, 0x1b, 0xd4, 0x00, 0x4c, 0xc6, 0xb5, 0xad, - 0xcd, 0xe7, 0x60, 0x6a, 0xab, 0x2a, 0x78, 0x4a, 0x77, 0x85, 0xd2, 0xbd, 0x49, 0x5d, 0xb7, 0x86, 0xd8, 0x02, 0x06, - 0x04, 0x6e, 0xf4, 0xd4, 0xf4, 0x07, 0x4d, 0x54, 0x00, 0x1a, 0x34, 0x6e, 0x67, 0x3a, 0x47, 0xa2, 0xdf, 0xa9, 0x4d, - 0xdb, 0x4c, 0xf5, 0xaa, 0xf2, 0x01, 0x54, 0xfc, 0x59, 0x3a, 0xbf, 0x34, 0x23, 0x16, 0xc0, 0xb8, 0x07, 0xce, 0x54, - 0xef, 0x34, 0x03, 0xeb, 0x89, 0x3c, 0xcf, 0x4a, 0x9e, 0x48, 0x01, 0x33, 0x22, 0xaf, 0xaf, 0xa5, 0x80, 0x61, 0x50, - 0x03, 0x80, 0x16, 0xcd, 0x65, 0x34, 0xe1, 0x5f, 0xd5, 0x74, 0x5f, 0x1e, 0xfe, 0x95, 0xce, 0xf5, 0xf5, 0xb8, 0x06, - 0x43, 0xe5, 0x75, 0xc5, 0x77, 0x32, 0x7d, 0xcd, 0x9f, 0x78, 0x99, 0x96, 0x72, 0x5d, 0xec, 0x64, 0xf9, 0xea, 0x6b, - 0xfe, 0x54, 0xe7, 0x39, 0x7a, 0x52, 0xd3, 0x34, 0xbe, 0xdf, 0xc9, 0xf2, 0xf7, 0xaf, 0x9f, 0xd8, 0x3c, 0x5f, 0x8d, - 0x6b, 0x7a, 0xcb, 0xf9, 0x47, 0x97, 0x69, 0xa2, 0xab, 0x1a, 0x3f, 0xf9, 0xbb, 0xcd, 0xf5, 0xa4, 0xa6, 0xd7, 0x52, - 0x54, 0xcb, 0x9d, 0xa2, 0x8e, 0xbe, 0x3e, 0xfa, 0x3b, 0xff, 0xda, 0x74, 0xef, 0xa8, 0xa6, 0x7f, 0xae, 0xe3, 0xa2, - 0xe2, 0xc5, 0x4e, 0x71, 0x7f, 0xfb, 0xfb, 0xdf, 0x9f, 0xd8, 0x8c, 0x4f, 0x6a, 0x7a, 0xcf, 0xe3, 0x8e, 0xb6, 0x4f, - 0x9e, 0x3e, 0xe1, 0x7f, 0xab, 0x6b, 0xfa, 0x33, 0xf3, 0x83, 0xa3, 0x9e, 0x66, 0x9e, 0x1e, 0x3e, 0x91, 0x4d, 0xd4, - 0x80, 0xa1, 0x87, 0x06, 0x90, 0x4b, 0xab, 0xa6, 0xd9, 0xe3, 0x95, 0x0b, 0x6e, 0xdf, 0xe7, 0x71, 0x1a, 0xaf, 0xe0, - 0x20, 0xd8, 0xa0, 0x71, 0x56, 0x01, 0x9c, 0x2a, 0xf0, 0x9e, 0x51, 0x49, 0xb3, 0x52, 0xfe, 0x93, 0xf3, 0x8f, 0x30, - 0x68, 0x08, 0x69, 0xa3, 0x22, 0x03, 0xbd, 0x5d, 0xe9, 0xc8, 0x46, 0xe8, 0xbf, 0xd9, 0x8c, 0x83, 0xe3, 0xc3, 0xe8, - 0xf5, 0xfb, 0x61, 0xc1, 0x44, 0x58, 0x10, 0x42, 0xff, 0x08, 0x0b, 0x70, 0x28, 0x29, 0x98, 0x97, 0xcf, 0xf8, 0x9e, - 0x6b, 0xa3, 0xb0, 0x10, 0x44, 0x77, 0x91, 0x7d, 0x40, 0xd5, 0xa3, 0xef, 0xd0, 0x0d, 0xf1, 0xb2, 0xc2, 0x82, 0xa1, - 0x55, 0x0d, 0xcc, 0x10, 0x14, 0xff, 0x9a, 0x87, 0x12, 0x7c, 0xe2, 0x01, 0x3e, 0x7a, 0x4c, 0x66, 0x5c, 0x5d, 0x6b, - 0xdf, 0x5e, 0x86, 0x05, 0x0d, 0x74, 0xdb, 0x21, 0xe8, 0x40, 0xe4, 0xbf, 0x00, 0x4f, 0x81, 0x81, 0x0f, 0x0b, 0xbb, - 0x94, 0xbb, 0xfe, 0xea, 0x3f, 0x1b, 0xd6, 0xd1, 0x85, 0x1f, 0xfd, 0xd9, 0xba, 0xb0, 0x67, 0x64, 0x2a, 0x8f, 0xcb, - 0xe1, 0x64, 0x3a, 0x18, 0x48, 0x17, 0xc7, 0xed, 0x34, 0x9b, 0xff, 0x3c, 0x97, 0x8b, 0x05, 0xea, 0xbe, 0x71, 0x5e, - 0x67, 0xfa, 0x6f, 0xa4, 0x9d, 0x0f, 0x5e, 0x9f, 0xfe, 0x7a, 0x7e, 0x76, 0xfa, 0x12, 0x9c, 0x0f, 0x3e, 0xbc, 0xf8, - 0xee, 0xc5, 0x7b, 0x15, 0xdc, 0x5d, 0xcd, 0x79, 0xbf, 0xef, 0xa4, 0x3e, 0x21, 0x1f, 0x56, 0xe4, 0x30, 0x8c, 0x1f, - 0x17, 0xca, 0xe8, 0x81, 0x1c, 0x33, 0x0b, 0x85, 0x0c, 0x55, 0xd4, 0xf6, 0x77, 0x39, 0x9c, 0x78, 0x60, 0x16, 0xf7, - 0x0d, 0x11, 0xae, 0xdf, 0x72, 0x1b, 0x64, 0x4d, 0x9e, 0x78, 0xfd, 0xe0, 0x64, 0x2a, 0x1d, 0x5b, 0x58, 0x30, 0x28, - 0x1b, 0xda, 0x74, 0x9a, 0xcd, 0x8b, 0x85, 0x6d, 0x97, 0x5b, 0x20, 0xa3, 0x34, 0xbb, 0xbc, 0x0c, 0x15, 0x74, 0xf5, - 0x09, 0x68, 0x00, 0x4c, 0xa3, 0x0a, 0xd7, 0x22, 0x3e, 0xf3, 0xcb, 0x8f, 0xc6, 0x5e, 0xf3, 0xee, 0x50, 0xf7, 0x64, - 0x9a, 0x55, 0x35, 0x06, 0x74, 0x30, 0xa1, 0xdc, 0x0d, 0xba, 0x09, 0x26, 0xa3, 0xda, 0xf2, 0xf3, 0xbc, 0x5a, 0x98, - 0xe6, 0xb8, 0x61, 0xa8, 0xbc, 0x92, 0xd7, 0xb2, 0x81, 0xc8, 0x40, 0x32, 0x0c, 0x7b, 0x34, 0x46, 0x91, 0xfa, 0xc1, - 0xae, 0x77, 0xfc, 0x26, 0x97, 0x10, 0x4d, 0x31, 0x03, 0xe9, 0xfc, 0xa9, 0x50, 0xce, 0xe5, 0x92, 0xf1, 0xb9, 0x58, - 0x9c, 0x80, 0xdb, 0xf9, 0x5c, 0x2c, 0x22, 0x0c, 0xca, 0x97, 0x41, 0xac, 0x12, 0xb0, 0x7b, 0x71, 0x10, 0xbe, 0x9d, - 0xd0, 0x06, 0x76, 0x03, 0x49, 0x36, 0x28, 0xed, 0x4a, 0x43, 0x94, 0x3b, 0xe5, 0xd1, 0x06, 0x91, 0x87, 0x58, 0x35, - 0xaf, 0xda, 0x9e, 0x6c, 0xe6, 0x62, 0x82, 0xab, 0x2c, 0x66, 0x72, 0x1a, 0x1f, 0xb3, 0x62, 0x1a, 0x43, 0x29, 0x71, - 0x9a, 0x86, 0x31, 0x9d, 0x50, 0x41, 0x48, 0xc2, 0xf8, 0x3c, 0x5e, 0xd0, 0x04, 0xa5, 0x04, 0x21, 0x84, 0xfc, 0x18, - 0xa1, 0x6d, 0x0e, 0x2c, 0x79, 0xbb, 0xfd, 0x3c, 0xfd, 0xdc, 0x8e, 0xe1, 0x32, 0x2a, 0x42, 0x37, 0xe8, 0xac, 0xe1, - 0xdf, 0x88, 0x0a, 0x1a, 0x63, 0xc5, 0x10, 0x04, 0xbc, 0xc0, 0xa8, 0x84, 0x05, 0x89, 0x59, 0x05, 0x51, 0x04, 0xca, - 0x79, 0xbc, 0x60, 0x05, 0x6d, 0xda, 0x9c, 0xc6, 0xda, 0x24, 0xa8, 0xe7, 0xb0, 0xd4, 0x0e, 0xa4, 0x52, 0x21, 0xf6, - 0xf8, 0x4c, 0x44, 0x37, 0xda, 0xd0, 0x00, 0x50, 0xa0, 0x94, 0x5c, 0xfc, 0xf6, 0xf3, 0x3d, 0xdc, 0x14, 0xf4, 0x3f, - 0xdb, 0x98, 0x68, 0x67, 0xb9, 0x3a, 0xf4, 0xe6, 0x0b, 0x1a, 0xe7, 0x39, 0x84, 0x62, 0x33, 0x08, 0xe4, 0x22, 0xab, - 0x20, 0xa2, 0xc5, 0x7d, 0x60, 0x42, 0xc2, 0x41, 0x9b, 0x7e, 0x86, 0xd4, 0x86, 0x98, 0x5c, 0x79, 0x62, 0x60, 0xb7, - 0x55, 0x82, 0x80, 0x23, 0x3d, 0xcf, 0xfe, 0x6a, 0x62, 0xac, 0x69, 0x6a, 0x66, 0xe2, 0x6d, 0x28, 0x44, 0x83, 0x16, - 0x44, 0x33, 0x78, 0xff, 0x5c, 0x73, 0xbc, 0xea, 0xc0, 0x0f, 0x78, 0xe7, 0xe2, 0xcc, 0xab, 0x99, 0x47, 0xe4, 0xd4, - 0x47, 0x39, 0xa2, 0x5f, 0xf2, 0xb0, 0x1a, 0xe9, 0x64, 0x8c, 0x95, 0xc4, 0x41, 0x6f, 0x83, 0x05, 0x73, 0x42, 0x57, - 0x3c, 0xb4, 0x7c, 0xfc, 0x4b, 0x64, 0x32, 0x4a, 0x6a, 0xac, 0xe8, 0x4a, 0x8b, 0x11, 0xe7, 0x35, 0xcc, 0xd2, 0x64, - 0x45, 0x17, 0x0b, 0x4d, 0x9a, 0x85, 0x32, 0x0d, 0xf0, 0x09, 0xb4, 0x18, 0xb9, 0x87, 0x9a, 0x36, 0x10, 0x1a, 0x76, - 0x87, 0x80, 0x8f, 0xdc, 0x43, 0x87, 0xff, 0x9f, 0x67, 0x17, 0x88, 0xb4, 0x37, 0x37, 0x91, 0xf1, 0x48, 0xdd, 0xc0, - 0x41, 0x31, 0x3e, 0xf6, 0xcd, 0xc4, 0xcf, 0x9c, 0xd1, 0x87, 0xa4, 0xf2, 0x1d, 0x3e, 0x58, 0xfe, 0x78, 0x53, 0x33, - 0x2b, 0x23, 0x58, 0x0f, 0xdb, 0x2d, 0x2e, 0x88, 0xb6, 0x0b, 0x20, 0xf5, 0x8c, 0x57, 0x0b, 0xdf, 0x78, 0x35, 0xde, - 0x63, 0xbc, 0xea, 0xce, 0xd4, 0x30, 0x27, 0x1b, 0xd4, 0x67, 0x29, 0x79, 0x7e, 0x8e, 0x32, 0xc1, 0xa6, 0xcb, 0x59, - 0x49, 0x55, 0x2a, 0xa1, 0xbd, 0xd8, 0xcf, 0x18, 0xdf, 0x11, 0x8c, 0xb3, 0xe2, 0x30, 0x12, 0xa8, 0x4a, 0x25, 0x75, - 0xd8, 0x2b, 0x40, 0x3d, 0x06, 0xef, 0x0d, 0x86, 0xa8, 0x91, 0xb1, 0x9b, 0x36, 0x10, 0x1a, 0x1a, 0xeb, 0xd1, 0x9e, - 0xb5, 0x1e, 0xdd, 0x6e, 0x2b, 0xe3, 0x6f, 0x27, 0xd7, 0x45, 0x82, 0xa8, 0xc2, 0x6a, 0x34, 0x01, 0xde, 0x34, 0xb1, - 0xb7, 0x25, 0xa7, 0xb4, 0xc0, 0xf0, 0xd9, 0x7f, 0x84, 0xa5, 0x53, 0x49, 0x94, 0x64, 0x56, 0x46, 0x03, 0x77, 0x0e, - 0x3e, 0x8f, 0x2b, 0x58, 0x03, 0x10, 0xc9, 0x11, 0x3d, 0x5c, 0xff, 0x08, 0xa5, 0xcb, 0x2c, 0xc9, 0x4c, 0x42, 0x66, - 0x2e, 0xd2, 0x76, 0xd6, 0xc1, 0xc4, 0x99, 0xd4, 0x7a, 0x63, 0x21, 0x87, 0x06, 0xf9, 0x01, 0x94, 0x21, 0x0e, 0x9f, - 0x7c, 0x30, 0xa1, 0x52, 0x85, 0x52, 0x6d, 0x74, 0xb3, 0x1b, 0x78, 0xe5, 0x43, 0x76, 0xcd, 0xcb, 0x2a, 0xbe, 0x5e, - 0x19, 0x4b, 0x62, 0xce, 0xf6, 0xb9, 0xed, 0x51, 0x61, 0x5e, 0xbd, 0x79, 0xf1, 0xdd, 0x69, 0xe3, 0xd5, 0x2e, 0xe2, - 0x68, 0x08, 0xb6, 0x15, 0x63, 0x8c, 0xde, 0xe2, 0xd3, 0x60, 0xa2, 0x5c, 0x23, 0xd0, 0xbb, 0x14, 0xf4, 0xdb, 0x9f, - 0xeb, 0x09, 0x78, 0xcd, 0xf5, 0xf2, 0x4b, 0x3e, 0x02, 0x96, 0xa8, 0xd0, 0xb3, 0xc2, 0xdc, 0xac, 0xcc, 0xf6, 0x76, - 0x2b, 0x32, 0xd3, 0xae, 0x34, 0x32, 0x10, 0xaf, 0xb6, 0xc3, 0x58, 0xb8, 0x74, 0x4d, 0xb7, 0x83, 0x5d, 0x2d, 0x3d, - 0x4b, 0xe4, 0xed, 0xb6, 0x84, 0x0e, 0xd9, 0x01, 0xf7, 0x5e, 0xc6, 0x77, 0xf0, 0xb2, 0xf4, 0xba, 0xd9, 0x0c, 0x9e, - 0x00, 0x66, 0xc2, 0x85, 0xb3, 0x2c, 0x8e, 0x19, 0x4f, 0x42, 0x15, 0x9b, 0xab, 0x21, 0xf2, 0x56, 0x84, 0xd6, 0xec, - 0xaf, 0x50, 0x8c, 0xc0, 0xee, 0xe4, 0xec, 0x63, 0xb6, 0x9a, 0x2d, 0x01, 0x35, 0xff, 0x3a, 0x13, 0x40, 0x73, 0xed, - 0x5a, 0xb0, 0x4d, 0xa1, 0xcd, 0x75, 0xfd, 0x2c, 0x5e, 0xc5, 0x09, 0xa8, 0x6e, 0xc0, 0x5b, 0xe4, 0x5e, 0x8b, 0xae, - 0x0c, 0xba, 0x28, 0x7d, 0xa0, 0x1c, 0x4b, 0x0a, 0x1d, 0x7d, 0xef, 0x09, 0x75, 0xee, 0x19, 0xc0, 0x25, 0x8d, 0x9a, - 0xa7, 0x5a, 0xca, 0x58, 0x00, 0x2c, 0x74, 0x30, 0x53, 0x64, 0x2b, 0xba, 0x33, 0x98, 0x14, 0xf0, 0xd6, 0x00, 0x7f, - 0x88, 0xac, 0x52, 0x77, 0xc5, 0x32, 0x2c, 0x3d, 0xfb, 0xeb, 0x7e, 0x3f, 0xf6, 0xec, 0xaf, 0x2f, 0x35, 0xad, 0x8b, - 0xdb, 0x0d, 0x20, 0x35, 0x06, 0x10, 0x39, 0xd5, 0x03, 0x61, 0x22, 0x8a, 0x35, 0x7d, 0xff, 0x4e, 0x4d, 0x16, 0x05, - 0x42, 0xbf, 0x53, 0xaf, 0x27, 0x25, 0x01, 0x9d, 0x5a, 0xc5, 0x4e, 0x06, 0xda, 0xec, 0x03, 0x02, 0xa2, 0xfa, 0x19, - 0xd9, 0x7c, 0xa1, 0x9c, 0x8b, 0x55, 0xf8, 0xf0, 0x31, 0x85, 0x80, 0xc2, 0x1d, 0x35, 0x3a, 0x6f, 0x43, 0x24, 0x50, - 0x56, 0x28, 0x62, 0xcd, 0x8b, 0xb5, 0x24, 0x64, 0x3e, 0x5e, 0xa0, 0xe0, 0xca, 0x01, 0xbb, 0x72, 0x36, 0x19, 0x96, - 0x11, 0x67, 0xe1, 0xfe, 0x6f, 0x26, 0x0b, 0x82, 0x9a, 0x2b, 0x3f, 0x90, 0xe3, 0x4e, 0xa6, 0xc6, 0x9e, 0x6a, 0xd4, - 0x20, 0x98, 0x8c, 0x20, 0x30, 0xdc, 0xf0, 0x33, 0x3e, 0x3e, 0x5a, 0x10, 0x50, 0x91, 0x59, 0xb3, 0x10, 0xf3, 0xe2, - 0xf8, 0x2b, 0x40, 0x8d, 0x19, 0x1d, 0x3d, 0x9d, 0x72, 0x06, 0x87, 0x28, 0x1d, 0x83, 0x8c, 0x56, 0xc0, 0x6f, 0xa1, - 0x7e, 0xb7, 0x4e, 0x7c, 0x1f, 0xfa, 0x55, 0xd0, 0xcb, 0x18, 0x18, 0x8e, 0x68, 0x72, 0x18, 0xf2, 0xc1, 0x64, 0x00, - 0xda, 0x12, 0x6f, 0xf7, 0xb5, 0xb4, 0xe2, 0xe6, 0x74, 0xe9, 0x74, 0xff, 0xa4, 0x4d, 0x90, 0x44, 0x2a, 0x59, 0xa9, - 0x88, 0x01, 0x84, 0xb2, 0x54, 0xdb, 0x64, 0x09, 0x96, 0x15, 0x66, 0x49, 0x73, 0x83, 0x92, 0xb8, 0xbb, 0x19, 0x38, - 0x46, 0xcd, 0x3a, 0x0d, 0xcb, 0x96, 0x1b, 0x35, 0xc0, 0xe7, 0x24, 0xac, 0xb0, 0x37, 0x9c, 0x99, 0xf4, 0xce, 0x74, - 0xb8, 0x3a, 0xe6, 0xec, 0x35, 0x47, 0x30, 0x8e, 0x04, 0x6f, 0x3c, 0x74, 0xc9, 0x34, 0x54, 0x64, 0xca, 0x38, 0x98, - 0xf6, 0x00, 0xf7, 0x9e, 0x83, 0x71, 0x18, 0x1b, 0x54, 0x96, 0xd4, 0xa7, 0xde, 0x5d, 0x08, 0x04, 0x69, 0xad, 0x97, - 0xf9, 0x0c, 0x4f, 0xcf, 0x08, 0x65, 0x7f, 0xc8, 0xe1, 0x0b, 0xb0, 0xa3, 0x20, 0x27, 0x13, 0xfe, 0xf4, 0xf1, 0x6e, - 0xa0, 0x2a, 0x3e, 0x08, 0x0e, 0x62, 0x91, 0x1e, 0x04, 0x03, 0x01, 0xbf, 0x0a, 0x7e, 0x50, 0x49, 0x79, 0x70, 0x19, - 0x17, 0x07, 0xf1, 0x2a, 0x2e, 0xaa, 0x83, 0xdb, 0xac, 0x5a, 0x1e, 0x98, 0x0e, 0x01, 0x34, 0x6f, 0x30, 0x88, 0x07, - 0xc1, 0x41, 0x30, 0x28, 0xcc, 0xd4, 0xae, 0x58, 0xd9, 0x38, 0xce, 0x4c, 0x88, 0xb2, 0xa0, 0x19, 0x20, 0xac, 0x71, - 0x1a, 0x00, 0x9f, 0xba, 0x66, 0x29, 0xbd, 0xc4, 0x70, 0x03, 0x62, 0xba, 0x86, 0x3e, 0x00, 0x8f, 0xbc, 0xa6, 0x31, - 0x2c, 0x81, 0xcb, 0xc1, 0x80, 0xac, 0x21, 0x72, 0xc1, 0x9a, 0xda, 0x20, 0x0e, 0xe1, 0x5a, 0xd9, 0x69, 0xef, 0x02, - 0x33, 0x6d, 0xb7, 0x80, 0xa8, 0x3c, 0x21, 0xfd, 0xbe, 0xfd, 0x86, 0xfa, 0x17, 0xec, 0x25, 0xd8, 0x5f, 0x15, 0x55, - 0x98, 0x48, 0xa5, 0xf9, 0xbe, 0x62, 0x27, 0x03, 0x15, 0x71, 0x78, 0xc7, 0x91, 0xa2, 0x8d, 0xca, 0x65, 0xd9, 0x93, - 0x65, 0xc3, 0x57, 0xe2, 0x9a, 0x3b, 0x3f, 0xae, 0x4a, 0xca, 0xbc, 0xca, 0x56, 0x8a, 0xfd, 0x9b, 0x71, 0xcd, 0xfd, - 0x81, 0xf5, 0x67, 0xf3, 0x15, 0x5c, 0x5b, 0xbd, 0x77, 0x4d, 0xae, 0x11, 0x39, 0x4b, 0x28, 0x97, 0xd4, 0x36, 0x0f, - 0x6f, 0xe9, 0xfb, 0xfc, 0xea, 0xdb, 0x4c, 0xa7, 0xf1, 0x59, 0x85, 0x85, 0x0b, 0xd1, 0x8a, 0xe0, 0xd0, 0x90, 0x8b, - 0xe6, 0x11, 0x60, 0xae, 0x7d, 0xb6, 0x82, 0x82, 0xd4, 0xe7, 0x15, 0x7a, 0xb7, 0x42, 0xc2, 0x4b, 0xcd, 0x2e, 0x3d, - 0x0c, 0xa4, 0x8c, 0xdb, 0x43, 0x4b, 0x98, 0xb4, 0xbc, 0x08, 0xef, 0xbd, 0xe6, 0x26, 0xf7, 0x3c, 0xc4, 0xe8, 0x45, - 0x9e, 0x9d, 0x80, 0xb1, 0xee, 0x92, 0x9d, 0x0d, 0x4f, 0xfc, 0x86, 0xe7, 0xac, 0x45, 0xa3, 0xe9, 0x92, 0x25, 0xfd, - 0x7e, 0x0c, 0x26, 0xde, 0x29, 0xcb, 0xe1, 0x57, 0xbe, 0xa0, 0x6b, 0x06, 0x98, 0x62, 0xf4, 0x12, 0x12, 0x52, 0x44, - 0x22, 0x59, 0xab, 0x93, 0xe4, 0x13, 0xdd, 0x05, 0x60, 0xf4, 0xcb, 0x59, 0x1a, 0x2d, 0xf7, 0x9a, 0x59, 0x20, 0x79, - 0x86, 0xbe, 0xeb, 0x60, 0x7b, 0x63, 0x1f, 0xa4, 0x9c, 0x1f, 0x8b, 0xe9, 0x60, 0xc0, 0x89, 0x86, 0x1b, 0x2f, 0x95, - 0xb8, 0x56, 0xb7, 0xb8, 0x63, 0x18, 0x4b, 0x7d, 0x5b, 0xc4, 0xe0, 0x80, 0x5d, 0xb4, 0xb2, 0xdb, 0x07, 0xd8, 0x57, - 0x8e, 0x77, 0xa9, 0xb2, 0x3b, 0x3d, 0x66, 0x9a, 0xcb, 0x56, 0x93, 0x4e, 0x2a, 0xf6, 0x13, 0xf9, 0x26, 0x77, 0xd0, - 0xe5, 0x72, 0xac, 0x79, 0xcb, 0x01, 0xa8, 0xe8, 0x47, 0x8a, 0xea, 0x7e, 0x86, 0x23, 0xcc, 0x83, 0x75, 0x9b, 0x4f, - 0x0e, 0x4d, 0x81, 0x43, 0xe4, 0x49, 0x1b, 0x4d, 0x01, 0xdd, 0xbb, 0x78, 0xdc, 0xd5, 0x6f, 0x4b, 0x77, 0x81, 0x12, - 0xed, 0x54, 0xdc, 0xf0, 0x63, 0xa2, 0x4e, 0x67, 0xda, 0x10, 0xfa, 0x57, 0x46, 0xdc, 0x5f, 0x1a, 0x57, 0xf1, 0xa6, - 0x77, 0xf9, 0x8c, 0x43, 0x9d, 0xdd, 0x10, 0x0a, 0xc0, 0x55, 0x7b, 0x3a, 0x75, 0x63, 0x48, 0xaf, 0x94, 0xe8, 0x36, - 0x38, 0xd8, 0x5e, 0x9f, 0x71, 0x14, 0xfd, 0x18, 0x35, 0xf2, 0x6d, 0x24, 0x1e, 0xcb, 0x41, 0xfc, 0xb8, 0xa0, 0xcb, - 0x48, 0x3c, 0x2e, 0x06, 0xf1, 0x63, 0x59, 0xd7, 0xbb, 0xe7, 0xca, 0xfe, 0x3e, 0x22, 0xcf, 0xba, 0xb3, 0x97, 0x4a, - 0xd8, 0x18, 0x78, 0x76, 0x2d, 0x20, 0x9c, 0x82, 0x27, 0xb2, 0xb5, 0xf4, 0xa1, 0x73, 0xbb, 0x8f, 0x2d, 0x93, 0x04, - 0x41, 0xcf, 0xdb, 0x6c, 0x12, 0xc5, 0xce, 0x36, 0x8f, 0x3e, 0x9c, 0x02, 0x09, 0xdd, 0x6e, 0x9b, 0x75, 0xb5, 0x06, - 0x14, 0xd3, 0x70, 0xcc, 0x0f, 0x8b, 0xd1, 0xad, 0xef, 0xae, 0x7f, 0x58, 0x8c, 0x96, 0x64, 0x38, 0x31, 0x93, 0x1f, - 0x9f, 0x8c, 0x67, 0x71, 0x34, 0xa9, 0x3b, 0x4e, 0x0b, 0x8d, 0x7f, 0xea, 0xdd, 0x42, 0x11, 0x38, 0x15, 0x23, 0x38, - 0x72, 0x2a, 0x94, 0x93, 0x52, 0x03, 0xc3, 0xff, 0xa0, 0xda, 0xd1, 0xa6, 0xbd, 0x8e, 0xab, 0x64, 0x99, 0x89, 0x2b, - 0x1d, 0x3e, 0x5c, 0x47, 0x17, 0xb7, 0x01, 0xed, 0xbc, 0xcb, 0xb4, 0xe3, 0xd7, 0x49, 0x83, 0x9e, 0xb8, 0x9a, 0x19, - 0x70, 0xeb, 0x7e, 0x84, 0x66, 0x08, 0x8c, 0x96, 0xe7, 0xef, 0x10, 0x73, 0xfb, 0x17, 0x65, 0xf3, 0xab, 0x68, 0x9f, - 0x23, 0x23, 0x65, 0x9b, 0x8c, 0x54, 0x60, 0x84, 0x29, 0x45, 0x12, 0x57, 0x21, 0x04, 0xb2, 0xff, 0x9c, 0xe2, 0x5a, - 0x2c, 0xbd, 0xd7, 0x20, 0x4c, 0xb0, 0x5d, 0xd0, 0x7e, 0x75, 0x3b, 0xb7, 0x95, 0x16, 0x7b, 0xa4, 0xbe, 0xcf, 0x9d, - 0xed, 0x8a, 0x26, 0x7f, 0x9f, 0x37, 0xa0, 0x0d, 0x20, 0xca, 0x7d, 0x7d, 0x54, 0x02, 0x27, 0x23, 0x6e, 0x28, 0x31, - 0x7a, 0x41, 0x57, 0x27, 0x72, 0xcf, 0x4e, 0xcd, 0x9b, 0x8a, 0x99, 0x8a, 0x2b, 0xdf, 0xec, 0x99, 0xff, 0x60, 0x28, - 0xa8, 0x00, 0x03, 0x6f, 0x73, 0xc6, 0xa3, 0x03, 0xdd, 0xad, 0xd1, 0x69, 0xc1, 0x66, 0x41, 0x5d, 0xd6, 0x6d, 0x1b, - 0x0f, 0x1a, 0x71, 0x50, 0x14, 0xab, 0x42, 0x8d, 0x84, 0x27, 0x02, 0x01, 0x53, 0x76, 0xcd, 0x23, 0x23, 0xa8, 0xe9, - 0x4d, 0x28, 0x6c, 0x28, 0xf8, 0xab, 0x44, 0x35, 0xbd, 0x09, 0x6d, 0x32, 0x71, 0x9a, 0x41, 0x04, 0x33, 0x62, 0xbb, - 0xdf, 0x02, 0xda, 0xdc, 0x9a, 0xd1, 0xa6, 0xae, 0xad, 0xb6, 0x0a, 0xb9, 0xa4, 0x48, 0x59, 0xfe, 0x3b, 0x35, 0x15, - 0x94, 0xd4, 0x72, 0xd1, 0x9b, 0x34, 0x5d, 0xf4, 0x78, 0x66, 0x24, 0x81, 0xca, 0x2d, 0x77, 0x8c, 0xfe, 0x10, 0x16, - 0x78, 0xc4, 0xc4, 0x89, 0x05, 0x73, 0xab, 0x13, 0x96, 0xcd, 0xc5, 0x62, 0xb4, 0x92, 0x10, 0x36, 0xf8, 0x98, 0x65, - 0xf3, 0x52, 0x3f, 0x84, 0xbe, 0xb0, 0xf4, 0x2d, 0xd8, 0xc5, 0x06, 0x2b, 0x59, 0x06, 0xe0, 0x7b, 0x41, 0x37, 0x2b, - 0x59, 0x46, 0x52, 0x75, 0x3f, 0xae, 0xb1, 0x04, 0x95, 0x56, 0xa8, 0xb4, 0xa4, 0xc6, 0x82, 0xc0, 0x57, 0x55, 0x97, - 0x0f, 0xc9, 0xae, 0x02, 0xf5, 0xd4, 0x51, 0x03, 0x4e, 0x81, 0xaa, 0x02, 0x0b, 0x92, 0xa0, 0x32, 0x74, 0x55, 0x60, - 0x5a, 0x81, 0x69, 0xa6, 0x0a, 0x17, 0x65, 0x76, 0x28, 0xcd, 0x7a, 0xc9, 0x67, 0xf1, 0x20, 0x4c, 0x86, 0x31, 0x79, - 0x8c, 0x50, 0xfb, 0x87, 0x79, 0x14, 0x6b, 0xb9, 0xe4, 0xca, 0xf9, 0xc5, 0xdf, 0x7e, 0xc2, 0x5e, 0xf7, 0x1c, 0x83, - 0x05, 0x38, 0x4b, 0xdb, 0xeb, 0x4c, 0xbc, 0x93, 0xad, 0xe0, 0x38, 0x98, 0x45, 0x39, 0xac, 0x7a, 0x72, 0x44, 0x73, - 0x91, 0x6b, 0xef, 0x22, 0x44, 0x0e, 0x32, 0x7b, 0x0c, 0xb0, 0x1b, 0xe1, 0xeb, 0xd0, 0xda, 0xdc, 0xea, 0x0a, 0xf1, - 0x37, 0x4a, 0x24, 0x7e, 0x94, 0xf2, 0xe3, 0x7a, 0xa5, 0x72, 0x55, 0x06, 0x8f, 0x55, 0x37, 0x83, 0x67, 0xda, 0xf7, - 0x58, 0xfb, 0xb7, 0xb6, 0x9b, 0xe3, 0xbd, 0x07, 0x0f, 0x5a, 0xff, 0x5b, 0x4f, 0x42, 0x68, 0xaf, 0x9c, 0xa4, 0xee, - 0xa8, 0xd1, 0x33, 0x93, 0x35, 0xa2, 0x12, 0xa6, 0x76, 0xa7, 0x72, 0x0c, 0xd4, 0x74, 0x00, 0xd7, 0x12, 0x35, 0x41, - 0x4f, 0x0a, 0x36, 0x86, 0x23, 0xce, 0xe2, 0xa0, 0x1d, 0xc7, 0x28, 0x5e, 0xce, 0x95, 0x78, 0x39, 0x3f, 0x61, 0x1c, - 0xa0, 0xb5, 0x00, 0xa9, 0x5e, 0xc3, 0x7e, 0xe6, 0x0a, 0x16, 0xd8, 0xdc, 0xf9, 0x8e, 0x2c, 0x90, 0x21, 0x4e, 0x36, - 0xc7, 0xc9, 0x1e, 0xd7, 0x7a, 0xee, 0x05, 0x3e, 0x4e, 0xea, 0x85, 0x57, 0x57, 0xd9, 0xae, 0x6b, 0xc9, 0xca, 0x79, - 0x31, 0x98, 0x40, 0x50, 0x96, 0x72, 0x5e, 0x0c, 0x27, 0x0b, 0x9a, 0xc3, 0x8f, 0x45, 0x03, 0x1d, 0x62, 0x39, 0x48, - 0xe0, 0xd2, 0xd9, 0x63, 0xc0, 0x1b, 0x4a, 0x2d, 0xee, 0xc6, 0x3a, 0x72, 0xac, 0xa3, 0x38, 0x0c, 0x63, 0xc0, 0x95, - 0x75, 0x02, 0xef, 0xfd, 0xd7, 0xc7, 0x26, 0x20, 0xab, 0x76, 0x85, 0x57, 0xa3, 0xdc, 0x75, 0xa5, 0xd1, 0x97, 0x94, - 0x9e, 0xf0, 0x82, 0xa7, 0x92, 0xed, 0xb6, 0x67, 0xe0, 0x6c, 0x89, 0x87, 0xc4, 0x3b, 0x46, 0xf4, 0x62, 0xda, 0xc8, - 0xcc, 0x09, 0x9c, 0xd9, 0xee, 0xb2, 0x8d, 0xf9, 0xb1, 0x03, 0x1c, 0x2c, 0x82, 0x90, 0xb8, 0x21, 0x0c, 0x13, 0x3b, - 0x29, 0x87, 0x5a, 0x08, 0xd7, 0xb5, 0xf0, 0x3a, 0x4e, 0xcb, 0x18, 0x5c, 0xa4, 0xb5, 0x6d, 0xe2, 0x1e, 0xba, 0xee, - 0xf9, 0x31, 0xb7, 0x3a, 0x46, 0x5b, 0x48, 0xbf, 0x1d, 0x9d, 0x3e, 0x70, 0x18, 0x80, 0xa6, 0x07, 0xb3, 0xaa, 0x7d, - 0x26, 0x71, 0x73, 0xda, 0x09, 0x42, 0x22, 0x10, 0x45, 0xe9, 0x8c, 0x30, 0xfd, 0x3b, 0xcd, 0x65, 0x15, 0xad, 0x1e, - 0xe4, 0x99, 0x43, 0x9e, 0x85, 0xde, 0xf6, 0xa0, 0x55, 0x73, 0x37, 0x18, 0x27, 0x6e, 0xb7, 0x77, 0xfe, 0xdf, 0xb2, - 0xae, 0xad, 0xd6, 0x88, 0xc7, 0xed, 0xea, 0x07, 0x8d, 0xbd, 0xda, 0x53, 0x31, 0x60, 0x56, 0xd2, 0x3b, 0xa3, 0x4a, - 0x5e, 0x64, 0xbc, 0xc4, 0x93, 0x6a, 0xd5, 0xf0, 0xf1, 0xbe, 0xcd, 0x46, 0xe6, 0x81, 0x4c, 0x01, 0xf1, 0xfc, 0x36, - 0x35, 0xea, 0xe3, 0x14, 0x25, 0xe0, 0xef, 0x74, 0x7c, 0x23, 0xfa, 0xd1, 0xbe, 0xb8, 0xe2, 0xd5, 0xdb, 0x5b, 0x61, - 0x5e, 0x3c, 0xb7, 0x3a, 0x7f, 0xfa, 0xba, 0xf0, 0xa1, 0xc3, 0x51, 0x7b, 0x07, 0x45, 0x96, 0x4c, 0x9c, 0x4c, 0x8c, - 0xac, 0x4d, 0xcc, 0x3e, 0x2a, 0xb8, 0x98, 0xa8, 0x42, 0xcf, 0x3a, 0x7b, 0xc2, 0x14, 0xa0, 0x6f, 0x1c, 0xa3, 0x92, - 0x31, 0x2c, 0x18, 0xa8, 0xd3, 0x94, 0x10, 0x3d, 0x14, 0x33, 0x8c, 0x57, 0x0c, 0xa0, 0x30, 0x85, 0x02, 0x51, 0x74, - 0xf6, 0xe1, 0x40, 0x13, 0xfa, 0xfd, 0xdb, 0x54, 0x67, 0xa0, 0x65, 0x3d, 0x2d, 0x40, 0x54, 0x07, 0xd1, 0x56, 0x79, - 0x11, 0xfe, 0xb0, 0xa4, 0x65, 0x46, 0x97, 0x82, 0xa6, 0x82, 0x26, 0x19, 0xbd, 0xe4, 0x4a, 0x54, 0x7c, 0x29, 0x98, - 0xa2, 0xed, 0x86, 0xb0, 0xff, 0xd0, 0xa0, 0xeb, 0xad, 0x58, 0x6b, 0x68, 0x77, 0x82, 0x8c, 0xd0, 0x7c, 0xa1, 0x83, - 0x90, 0xa1, 0x72, 0x12, 0xba, 0x56, 0x69, 0xbc, 0x02, 0x97, 0x4c, 0xb3, 0xd1, 0x32, 0x2e, 0xc3, 0xc0, 0x7e, 0x15, - 0x58, 0x4c, 0x0e, 0x4c, 0x3a, 0x5b, 0x5f, 0x3c, 0x93, 0xd7, 0x2b, 0x29, 0xb8, 0xa8, 0x14, 0x44, 0xbf, 0xc1, 0x7d, - 0x37, 0x71, 0xd5, 0x59, 0xb3, 0x56, 0xfa, 0xd0, 0xb7, 0x3e, 0x6b, 0xe3, 0xbe, 0x30, 0x38, 0x06, 0x3b, 0x1f, 0x11, - 0x03, 0x69, 0x50, 0xe9, 0x16, 0x87, 0x26, 0x40, 0x97, 0x0e, 0x29, 0x64, 0xc9, 0x54, 0xa6, 0x4a, 0x50, 0xf1, 0x8d, - 0xdf, 0x4b, 0x59, 0x8d, 0xfe, 0x5c, 0xf3, 0xe2, 0xfe, 0x8c, 0xe7, 0x1c, 0xc7, 0x28, 0x48, 0x62, 0x71, 0x13, 0x97, - 0x01, 0xf1, 0x2d, 0xaf, 0x82, 0xa3, 0xd4, 0x84, 0x8d, 0xd9, 0xa9, 0x1a, 0xb5, 0x5e, 0x05, 0xfa, 0xca, 0x28, 0xdf, - 0x18, 0x0c, 0x4d, 0x44, 0x15, 0xf4, 0xbd, 0x56, 0xf7, 0xb4, 0xba, 0x61, 0x01, 0xf1, 0xe7, 0x4a, 0x2f, 0xd4, 0x7a, - 0xdd, 0x8c, 0xb9, 0x61, 0x22, 0x04, 0x8d, 0xbe, 0xaa, 0x17, 0x0e, 0x3f, 0x7f, 0xa3, 0x2c, 0x89, 0xe0, 0xc5, 0x26, - 0x5d, 0x17, 0x26, 0x96, 0x06, 0xd5, 0x01, 0x73, 0xa3, 0x4d, 0xce, 0xaf, 0x40, 0xf4, 0xe7, 0xac, 0x88, 0x26, 0x75, - 0x4d, 0x15, 0x82, 0x61, 0xb4, 0xb9, 0x6b, 0xa4, 0xd3, 0x7b, 0xf0, 0x72, 0x33, 0xd6, 0x48, 0xda, 0xd3, 0xb1, 0xa6, - 0x05, 0x2f, 0x57, 0x52, 0x94, 0x10, 0xdd, 0xb9, 0x37, 0xa6, 0xd7, 0x71, 0x26, 0xaa, 0x38, 0x13, 0xa7, 0xe5, 0x8a, - 0x27, 0xd5, 0x7b, 0xa8, 0x50, 0x1b, 0xe3, 0x60, 0xeb, 0xd5, 0xa8, 0xab, 0x70, 0xc8, 0xaf, 0x2e, 0x5f, 0xdc, 0xad, - 0x62, 0x91, 0xc2, 0xa8, 0xd7, 0xfb, 0x5e, 0x34, 0xa7, 0x63, 0x15, 0x17, 0x5c, 0x98, 0xa8, 0xc5, 0xb4, 0x62, 0x01, - 0xd7, 0x19, 0x03, 0xca, 0x55, 0xec, 0xce, 0x4c, 0xc5, 0x32, 0x8c, 0xcb, 0xf2, 0xc7, 0xac, 0xc4, 0x3b, 0x00, 0xb4, - 0x06, 0x4e, 0x8b, 0x99, 0x01, 0x01, 0xb9, 0xcf, 0x0d, 0x2e, 0x02, 0x0b, 0x8e, 0x9e, 0x8c, 0x57, 0x77, 0x01, 0xf5, - 0xde, 0x48, 0x75, 0x3d, 0x64, 0xc1, 0x78, 0xf4, 0x34, 0x70, 0xc8, 0x21, 0xfe, 0x47, 0x4f, 0x8e, 0xf6, 0x7f, 0x33, - 0x09, 0x48, 0x3d, 0x05, 0x55, 0x85, 0x51, 0x88, 0xc2, 0xb4, 0xbf, 0x5e, 0xab, 0x5b, 0xee, 0xdb, 0x8b, 0x92, 0x17, - 0x37, 0x10, 0xad, 0x9d, 0x4c, 0x33, 0x20, 0xe7, 0x52, 0x25, 0xc0, 0xa2, 0x88, 0xab, 0xaa, 0xc8, 0x2e, 0xc0, 0x44, - 0x09, 0x0d, 0xc0, 0xcc, 0xd3, 0x4b, 0x74, 0xf8, 0x88, 0xe6, 0x01, 0xf6, 0x29, 0x58, 0xd4, 0xa4, 0x2e, 0xa1, 0xb0, - 0xe4, 0x00, 0x83, 0xd5, 0xa9, 0xb8, 0xd2, 0x8e, 0x53, 0xaf, 0x7e, 0x8f, 0x96, 0x12, 0x63, 0xcd, 0xea, 0x79, 0x8a, - 0x2f, 0x4a, 0x99, 0xaf, 0x2b, 0xd0, 0x9e, 0x5f, 0x56, 0xd1, 0xd1, 0x93, 0xd5, 0xdd, 0x54, 0x75, 0x23, 0x82, 0x5e, - 0x4c, 0x15, 0xce, 0x5b, 0x12, 0xe7, 0x49, 0x38, 0x19, 0x8f, 0xbf, 0x38, 0x18, 0x1e, 0x40, 0x32, 0x99, 0xfe, 0x35, - 0x54, 0x8e, 0x5c, 0xc3, 0xc9, 0x78, 0x5c, 0xff, 0x5e, 0x9b, 0x30, 0xdf, 0xa6, 0x9e, 0x67, 0xbf, 0x1f, 0xab, 0xf5, - 0x7f, 0x72, 0x7c, 0xa8, 0x7f, 0xfc, 0x5e, 0xd7, 0xd3, 0xd7, 0x45, 0x38, 0xff, 0x57, 0xa8, 0xd6, 0xf7, 0x69, 0x51, - 0xc4, 0xf7, 0x35, 0x44, 0x36, 0x15, 0xce, 0xbb, 0x86, 0x7a, 0x64, 0x81, 0x1e, 0x91, 0xe9, 0xa5, 0x60, 0xf0, 0xcd, - 0xfb, 0x2a, 0x0c, 0x78, 0xb9, 0x1a, 0x72, 0x51, 0x65, 0xd5, 0xfd, 0x10, 0xf3, 0x04, 0xf8, 0xa9, 0x19, 0xc7, 0x67, - 0x85, 0x21, 0xbe, 0x97, 0x05, 0xe7, 0x7f, 0xf1, 0x50, 0x19, 0x8b, 0x8f, 0xd1, 0x58, 0x7c, 0x4c, 0x55, 0x37, 0x26, - 0x5f, 0x53, 0xdd, 0xb7, 0xc9, 0xd7, 0x60, 0x92, 0x95, 0xb5, 0xbf, 0x51, 0xc6, 0x9a, 0xd1, 0x98, 0xde, 0xbc, 0xcc, - 0xb3, 0x15, 0x5c, 0x0a, 0x96, 0xfa, 0x47, 0x4d, 0xe8, 0x7b, 0xde, 0xce, 0x3e, 0x1a, 0x8d, 0x9e, 0x15, 0x74, 0x34, - 0x1a, 0x7d, 0xcc, 0x6a, 0x42, 0x57, 0xa2, 0xe3, 0xfd, 0x7b, 0x4e, 0x2f, 0x64, 0x7a, 0x1f, 0x05, 0x01, 0x5d, 0x66, - 0x69, 0xca, 0x85, 0x2a, 0xeb, 0x2c, 0x6d, 0xe7, 0x55, 0x2d, 0x44, 0x20, 0x24, 0xdd, 0x46, 0x84, 0x64, 0x22, 0xf4, - 0xed, 0x4e, 0xcf, 0x46, 0xa3, 0xd1, 0x59, 0x6a, 0xaa, 0x75, 0x17, 0x94, 0xd7, 0x68, 0x4e, 0xe1, 0xfc, 0x14, 0xc0, - 0x1a, 0xc9, 0x44, 0x7f, 0x39, 0xfc, 0xef, 0xe1, 0x6c, 0x3e, 0x1e, 0x7e, 0x33, 0x5a, 0x3c, 0x3e, 0xa4, 0x41, 0xe0, - 0x83, 0x78, 0x87, 0xda, 0xba, 0x65, 0x5a, 0x1e, 0x8f, 0xa7, 0xa4, 0x1c, 0xb0, 0x27, 0xd6, 0xb7, 0xe8, 0x8b, 0x27, - 0x80, 0xcc, 0x8a, 0x22, 0xe5, 0xc0, 0x49, 0x43, 0xf1, 0x6a, 0xf6, 0x4a, 0x00, 0x5e, 0x9c, 0x8d, 0xec, 0x60, 0xb4, - 0xa2, 0xe3, 0x08, 0xca, 0xab, 0xad, 0xa9, 0x48, 0x8f, 0xb1, 0xcc, 0x44, 0x49, 0x1d, 0x4f, 0xcb, 0xdb, 0xac, 0x4a, - 0x96, 0x18, 0xe8, 0x29, 0x2e, 0x79, 0xf0, 0x45, 0x10, 0x95, 0xec, 0xe8, 0xe9, 0x54, 0xc1, 0x1d, 0x63, 0x52, 0xca, - 0xaf, 0x20, 0xf1, 0x9b, 0x31, 0x42, 0xc2, 0x12, 0xed, 0xc1, 0x89, 0x35, 0xbe, 0xcc, 0x65, 0x0c, 0x1e, 0xad, 0xa5, - 0xe6, 0xe1, 0xec, 0xc9, 0x68, 0xed, 0x51, 0x5a, 0xcd, 0x91, 0xd0, 0x9c, 0x50, 0x32, 0x79, 0x58, 0x52, 0xf9, 0xc5, - 0x04, 0xbd, 0xa4, 0xc0, 0xcd, 0x3c, 0x82, 0xe3, 0xdf, 0x5a, 0x7a, 0xe8, 0xe5, 0x93, 0xb2, 0xc3, 0xf9, 0xff, 0x2e, - 0xe9, 0x62, 0x70, 0xe8, 0x86, 0xe6, 0xad, 0x76, 0xe7, 0xad, 0x90, 0x71, 0xac, 0xc2, 0x67, 0x29, 0xb1, 0xc6, 0xb8, - 0x9c, 0x9d, 0x6c, 0x4c, 0x77, 0x46, 0x55, 0x91, 0x5d, 0x87, 0x44, 0xf7, 0xca, 0x81, 0x84, 0x06, 0x51, 0x36, 0xc2, - 0xf5, 0x03, 0xd6, 0x33, 0x5e, 0x27, 0x6f, 0x78, 0x51, 0x65, 0x89, 0x7a, 0x7f, 0xd3, 0x78, 0x5f, 0xd7, 0x26, 0xa0, - 0xea, 0xbb, 0x82, 0xc1, 0x3c, 0xbf, 0x2d, 0x00, 0xc4, 0x14, 0x69, 0x80, 0x4f, 0x30, 0x83, 0xa0, 0x76, 0xcd, 0xbc, - 0x6a, 0x04, 0xdf, 0x80, 0xaf, 0xde, 0x15, 0x80, 0x41, 0x12, 0x82, 0x14, 0x19, 0x42, 0x03, 0x81, 0x40, 0xc3, 0x90, - 0x0b, 0x0c, 0x7e, 0xe2, 0xc5, 0x91, 0x54, 0x4e, 0x89, 0x3c, 0x0c, 0xf0, 0x47, 0x40, 0x55, 0x00, 0x12, 0xe3, 0x71, - 0x08, 0x2f, 0xd4, 0x2f, 0xf7, 0x46, 0xed, 0x11, 0xf6, 0x3a, 0x0d, 0x21, 0xd8, 0x10, 0x3e, 0x04, 0xb0, 0xa4, 0x08, - 0x7d, 0x8b, 0x5c, 0x46, 0x18, 0x5c, 0xe6, 0xd9, 0x4a, 0x27, 0x55, 0xa3, 0x8e, 0xe6, 0x43, 0xa9, 0x1d, 0xc9, 0x01, - 0xf5, 0xd2, 0x63, 0x4c, 0x2f, 0x54, 0xba, 0x2a, 0xca, 0x19, 0xe5, 0xbc, 0xd3, 0x13, 0xe3, 0xc2, 0x16, 0x72, 0x88, - 0x84, 0xf3, 0xae, 0x50, 0xa1, 0x70, 0xf8, 0x02, 0xc0, 0xc0, 0x40, 0xda, 0xb1, 0x1b, 0xef, 0x46, 0x65, 0x3f, 0xe7, - 0xec, 0xf0, 0xbf, 0xe7, 0xf1, 0xf0, 0xaf, 0xf1, 0xf0, 0x9b, 0xc5, 0x20, 0x1c, 0xda, 0x9f, 0xe4, 0xf1, 0xa3, 0x43, - 0xfa, 0x92, 0x5b, 0x2e, 0x0d, 0x16, 0x7e, 0x23, 0xd8, 0x8f, 0x5a, 0x09, 0x41, 0x14, 0xe0, 0x0d, 0xcb, 0xad, 0xc6, - 0x09, 0x00, 0x1e, 0x06, 0xff, 0x15, 0xa0, 0xd1, 0x94, 0xbb, 0x78, 0x81, 0xbe, 0x44, 0xfd, 0x3e, 0xf9, 0xaa, 0x61, - 0x30, 0x08, 0xe2, 0x1a, 0x15, 0x13, 0x86, 0xe8, 0x32, 0x26, 0x0a, 0x06, 0xd9, 0x66, 0xdf, 0x6e, 0x7b, 0x6d, 0x49, - 0x18, 0x7e, 0xe9, 0x67, 0x9a, 0x98, 0x79, 0x87, 0x1b, 0xdb, 0x4a, 0xae, 0x42, 0xc4, 0x0a, 0xd4, 0xbf, 0x72, 0x06, - 0xb1, 0x37, 0x6f, 0x32, 0xf0, 0xe9, 0xb0, 0x5f, 0x8c, 0x67, 0xc0, 0x46, 0xc1, 0x9d, 0xaf, 0xe0, 0x97, 0x19, 0xb8, - 0x79, 0x8b, 0x18, 0x05, 0x0e, 0x76, 0x49, 0xf4, 0xfb, 0xbd, 0x3c, 0x0b, 0x73, 0x8d, 0x3b, 0x9d, 0xd7, 0x46, 0x0d, - 0x81, 0x3a, 0x72, 0x50, 0x3f, 0xe8, 0x21, 0x18, 0xaa, 0x21, 0x28, 0x3a, 0xda, 0xe2, 0xea, 0xb5, 0xf5, 0x14, 0xa6, - 0xb7, 0xaa, 0xbe, 0x62, 0xf4, 0x87, 0xcc, 0x04, 0x16, 0xd2, 0xae, 0x39, 0xd6, 0x35, 0xc7, 0x48, 0x7b, 0xfa, 0x7d, - 0xd1, 0x20, 0x3f, 0x9d, 0x85, 0x07, 0x81, 0x2a, 0x55, 0xee, 0x94, 0x45, 0xb9, 0x2d, 0xcd, 0x1b, 0xc3, 0x9a, 0xe6, - 0x99, 0x8d, 0x73, 0x33, 0xeb, 0xf5, 0xc2, 0x10, 0x1d, 0x3c, 0xb1, 0x54, 0xac, 0x0d, 0xc2, 0x1d, 0x99, 0x84, 0xd1, - 0x35, 0xc8, 0x2e, 0xc3, 0x73, 0x4e, 0x90, 0x4f, 0x05, 0xf6, 0x41, 0x55, 0xeb, 0xe5, 0x84, 0xc7, 0x46, 0xbe, 0x6c, - 0x04, 0x0d, 0xf2, 0x92, 0xa2, 0xde, 0xc4, 0xed, 0xd8, 0x47, 0x2d, 0xe4, 0xca, 0x4d, 0x3d, 0xed, 0x69, 0x52, 0xd1, - 0x63, 0xbd, 0x4a, 0xfd, 0x02, 0x4b, 0x0b, 0x4b, 0x3e, 0x08, 0xed, 0x69, 0x5a, 0x81, 0x19, 0x6e, 0x6c, 0x06, 0x43, - 0x3f, 0x1c, 0x3f, 0x01, 0x9d, 0x51, 0xdb, 0x12, 0xc2, 0xd8, 0x0d, 0xc2, 0xca, 0x7b, 0x22, 0x5f, 0x3c, 0xf1, 0x2e, - 0x06, 0x21, 0x37, 0x9b, 0x59, 0x34, 0x30, 0xdd, 0xaf, 0x65, 0xb3, 0x79, 0xba, 0xb9, 0x5e, 0x94, 0x50, 0x01, 0xdb, - 0x6d, 0x25, 0x08, 0xfe, 0xfd, 0x98, 0xcd, 0xf0, 0x6f, 0xd6, 0xef, 0xf7, 0x42, 0xfc, 0xc5, 0x31, 0x98, 0xd1, 0x5c, - 0x2c, 0xd8, 0x47, 0x90, 0x31, 0x91, 0x08, 0x53, 0x95, 0x31, 0x20, 0xab, 0xc0, 0x22, 0xd0, 0x7c, 0xa0, 0x72, 0x61, - 0x26, 0x7b, 0x99, 0x73, 0x0d, 0x39, 0x6d, 0x8d, 0x53, 0x36, 0xca, 0x12, 0xe5, 0xca, 0x91, 0x8d, 0xe2, 0x3c, 0x8b, - 0x4b, 0x5e, 0x6e, 0xb7, 0xfa, 0x70, 0x4c, 0x0a, 0x0e, 0xec, 0xba, 0xa2, 0x52, 0x25, 0xeb, 0x48, 0x75, 0xe3, 0x2f, - 0xc3, 0x02, 0xf7, 0x29, 0x9f, 0x17, 0x86, 0x46, 0x1c, 0x80, 0x30, 0x83, 0xa9, 0x5b, 0x7a, 0x2f, 0x2c, 0xa0, 0x79, - 0x25, 0x21, 0x1b, 0x4c, 0xf5, 0x2c, 0x7c, 0x63, 0x26, 0xe6, 0xc5, 0x02, 0xc2, 0xea, 0x14, 0x0b, 0xcd, 0x6c, 0xd2, - 0x84, 0xc5, 0x00, 0x9b, 0x17, 0x93, 0x29, 0xc4, 0x77, 0x57, 0xe5, 0xc4, 0x0b, 0x73, 0xdf, 0x4e, 0x1c, 0x72, 0x08, - 0xbc, 0xaa, 0x0d, 0xba, 0x9a, 0x6d, 0x38, 0xea, 0x48, 0x39, 0x31, 0xf9, 0xfd, 0x54, 0x41, 0x88, 0x3b, 0x71, 0x24, - 0x5c, 0xde, 0x6c, 0x17, 0x5e, 0x74, 0x20, 0xe8, 0xa8, 0xc1, 0x29, 0x3f, 0x31, 0x38, 0x1a, 0x93, 0x74, 0xe3, 0x9d, - 0x20, 0x45, 0x18, 0x93, 0x8d, 0x64, 0xd7, 0x32, 0x14, 0xf3, 0x78, 0x01, 0xca, 0xcb, 0x78, 0x01, 0x96, 0x46, 0xc6, - 0x20, 0x15, 0xe4, 0x77, 0xdc, 0x0b, 0x85, 0x45, 0x71, 0x85, 0x48, 0xcf, 0xea, 0xf7, 0x51, 0xd1, 0x0e, 0x05, 0x82, - 0xe2, 0x0e, 0x65, 0x9e, 0x9c, 0xf5, 0x58, 0x20, 0xb1, 0x21, 0x60, 0x7c, 0xa5, 0xd3, 0x54, 0x6b, 0xdd, 0x1b, 0x33, - 0x0f, 0x7c, 0x9a, 0x8d, 0x84, 0xac, 0xce, 0x2f, 0x41, 0xa4, 0xe4, 0xa3, 0xe3, 0x23, 0xbf, 0x88, 0x3b, 0xcb, 0xbc, - 0xb5, 0x2d, 0x2a, 0xd9, 0xc9, 0x06, 0x40, 0x0b, 0x75, 0xf4, 0x2c, 0x25, 0xb7, 0x29, 0x49, 0xed, 0x36, 0x05, 0xac, - 0x24, 0x7f, 0x01, 0x43, 0xf0, 0xb5, 0x03, 0xe1, 0x74, 0xac, 0x10, 0xaf, 0x69, 0x8a, 0x48, 0x93, 0x61, 0x49, 0x71, - 0x6c, 0x4b, 0x44, 0x41, 0xb5, 0x65, 0xd9, 0xc1, 0x30, 0x51, 0x82, 0x9f, 0xa7, 0x1e, 0x25, 0x0a, 0x02, 0xaa, 0x87, - 0x1c, 0x24, 0xd8, 0xb6, 0x81, 0xf0, 0x80, 0x3c, 0xa2, 0x37, 0xd6, 0xdf, 0x65, 0x9d, 0x67, 0x17, 0x9a, 0xe7, 0x72, - 0xbd, 0x2b, 0xcc, 0x18, 0xe1, 0x49, 0x66, 0xc2, 0x06, 0x78, 0xe7, 0x99, 0x51, 0xdb, 0xf4, 0x3c, 0xbc, 0xb6, 0x53, - 0x8c, 0xd0, 0xb7, 0x67, 0xd0, 0x4d, 0x30, 0xaf, 0x0e, 0x9b, 0xf5, 0x4a, 0x41, 0x6a, 0x98, 0x5a, 0x34, 0x31, 0xeb, - 0x59, 0x83, 0xf2, 0xed, 0xb6, 0xa7, 0xe7, 0x6a, 0xff, 0xdc, 0x6d, 0xb7, 0x3d, 0xec, 0xd6, 0xf3, 0xb4, 0xdb, 0x2a, - 0xbe, 0x52, 0x1f, 0xb4, 0xc7, 0x9f, 0xbb, 0xf1, 0xe7, 0x06, 0xd9, 0xa4, 0x74, 0x34, 0xd3, 0xd6, 0x07, 0xe1, 0x81, - 0xd3, 0xfb, 0x46, 0x93, 0xbe, 0xcb, 0x42, 0x49, 0x57, 0xa2, 0x51, 0x5d, 0xed, 0x4c, 0x4c, 0x1f, 0x5c, 0xff, 0x0f, - 0xaf, 0x02, 0x3c, 0xe2, 0xd4, 0xce, 0xde, 0xdb, 0xa0, 0xa2, 0xd1, 0x16, 0x8e, 0x14, 0xa1, 0x07, 0x24, 0x61, 0x5f, - 0xcb, 0x5a, 0xdc, 0xe6, 0x59, 0xf6, 0x30, 0x7d, 0x7a, 0x95, 0xfa, 0x5e, 0x08, 0x6e, 0x99, 0x65, 0xe6, 0xc0, 0xab, - 0x28, 0x0e, 0x68, 0xd4, 0x45, 0xfb, 0xae, 0xb3, 0xb2, 0x04, 0xaf, 0x17, 0xb8, 0x57, 0x9e, 0x71, 0x1f, 0x7e, 0xef, - 0xaa, 0x6a, 0x6e, 0xd2, 0xb3, 0x6c, 0x9e, 0x2d, 0xb6, 0xdb, 0x10, 0xff, 0x76, 0xb5, 0xc8, 0xd1, 0xe4, 0x39, 0xe8, - 0x34, 0x31, 0x92, 0x11, 0xd3, 0x8d, 0xf3, 0x36, 0xff, 0x1b, 0xd1, 0x70, 0x9a, 0x38, 0x05, 0x7a, 0x31, 0x7b, 0x04, - 0x32, 0x29, 0x03, 0x72, 0x20, 0x66, 0x7a, 0xcd, 0x40, 0x34, 0x32, 0x11, 0x01, 0xae, 0x30, 0x36, 0x12, 0x8d, 0x4e, - 0x38, 0xa9, 0x09, 0x58, 0xb0, 0xda, 0xf2, 0x3e, 0x58, 0xda, 0x56, 0x15, 0xf7, 0xde, 0x92, 0xe6, 0xb8, 0x0e, 0x9c, - 0xaf, 0x83, 0x19, 0x62, 0x53, 0x76, 0xb5, 0x40, 0xee, 0x97, 0xd7, 0xb4, 0x37, 0xae, 0x13, 0x98, 0xb5, 0x4d, 0x6d, - 0x19, 0x3f, 0x5b, 0xfa, 0x8f, 0x7a, 0x70, 0x95, 0x31, 0xd8, 0xdc, 0x58, 0x69, 0xd8, 0x7d, 0xe3, 0xf9, 0x52, 0x40, - 0x78, 0x3a, 0x9f, 0x1e, 0x9f, 0x65, 0x1e, 0x3d, 0x06, 0xa2, 0x63, 0x3e, 0x2a, 0xdd, 0x47, 0x76, 0xf7, 0xfa, 0x01, - 0x01, 0xe7, 0x55, 0xbb, 0xa0, 0x79, 0xb9, 0x80, 0xc0, 0xaa, 0x5e, 0x79, 0x85, 0xe5, 0x33, 0x63, 0x76, 0x05, 0x64, - 0xa8, 0x20, 0x10, 0xb8, 0xbb, 0xeb, 0x5c, 0x88, 0x55, 0x87, 0x95, 0x39, 0x4d, 0xc2, 0x4e, 0x42, 0x34, 0x6f, 0x0d, - 0x66, 0xc1, 0x7f, 0x05, 0x83, 0x72, 0x10, 0x44, 0x41, 0x14, 0x04, 0x64, 0x50, 0xc0, 0x2f, 0xc4, 0x5d, 0x23, 0x18, - 0xb3, 0x05, 0x3a, 0xfc, 0x8e, 0x33, 0x9f, 0x11, 0x79, 0xd1, 0x08, 0xeb, 0xe9, 0x06, 0xe0, 0x42, 0xca, 0x9c, 0xc7, - 0xe8, 0x73, 0xf2, 0x8e, 0xb3, 0x8c, 0xd0, 0x77, 0xde, 0xa9, 0xfc, 0x88, 0x37, 0x82, 0xfd, 0xed, 0x0e, 0xdb, 0x4b, - 0x90, 0x57, 0xf4, 0xc6, 0xf4, 0x1d, 0x27, 0x51, 0xd6, 0x70, 0xa6, 0xe6, 0xd0, 0xb3, 0xca, 0xb2, 0x56, 0xd4, 0x90, - 0x1b, 0x14, 0x73, 0x23, 0xcb, 0xe4, 0x64, 0xda, 0x6a, 0x4e, 0x05, 0xae, 0x3b, 0xbb, 0x5e, 0x40, 0x72, 0x28, 0x34, - 0x4b, 0x67, 0xc3, 0x79, 0xdb, 0x96, 0x3d, 0x6f, 0x9d, 0x42, 0x5e, 0x43, 0x54, 0x34, 0x48, 0x47, 0x40, 0x0d, 0xad, - 0xb8, 0xaa, 0xc0, 0x85, 0xd9, 0xb4, 0x87, 0x9b, 0xf6, 0x98, 0x66, 0x7c, 0x80, 0x98, 0x79, 0x1c, 0x5b, 0x06, 0x76, - 0x24, 0x0e, 0xdf, 0xc7, 0xf9, 0x02, 0xed, 0xd2, 0x5b, 0x57, 0x8b, 0x47, 0x58, 0x7b, 0xde, 0x0a, 0x09, 0x01, 0xe2, - 0xd3, 0x54, 0xba, 0xdd, 0x06, 0x01, 0x0c, 0x70, 0xbf, 0xdf, 0x03, 0xae, 0xd5, 0xb0, 0x93, 0xe6, 0xd6, 0x6c, 0x89, - 0xbd, 0xa2, 0xf0, 0x18, 0x98, 0x53, 0xf3, 0x9f, 0x41, 0x40, 0xf1, 0xdc, 0x0d, 0xc1, 0xde, 0x94, 0x9d, 0x6c, 0x8a, - 0x7e, 0xff, 0x79, 0x81, 0x0f, 0x28, 0x17, 0x06, 0x31, 0xb7, 0x8e, 0xe3, 0x61, 0xd8, 0x27, 0xf5, 0x21, 0x8e, 0x45, - 0x9e, 0x85, 0x8e, 0xb0, 0x54, 0x86, 0xb0, 0x70, 0xc5, 0x48, 0x07, 0x71, 0x50, 0x93, 0xce, 0xc1, 0xaa, 0x5c, 0xf0, - 0xe5, 0x5e, 0xef, 0x0d, 0xc0, 0xa4, 0x67, 0xde, 0xb0, 0xbc, 0xf7, 0x00, 0xd1, 0x7a, 0x3d, 0x5c, 0x28, 0xee, 0xe5, - 0xcb, 0x06, 0x1a, 0x27, 0xbe, 0xb4, 0xec, 0xfa, 0x4c, 0xcb, 0x4a, 0x46, 0xa3, 0x51, 0x55, 0x2b, 0xc9, 0x87, 0x23, - 0x2f, 0x2d, 0x14, 0x4f, 0x19, 0xa7, 0x3c, 0x05, 0xcb, 0x77, 0x43, 0xe9, 0xe6, 0x0b, 0xba, 0xe2, 0x22, 0x55, 0x3f, - 0x3d, 0xf4, 0xcd, 0x06, 0x71, 0xcd, 0x9a, 0x3a, 0x1c, 0x3b, 0xfc, 0x10, 0x00, 0xd3, 0x3e, 0xcc, 0x5c, 0xba, 0x86, - 0xe9, 0x05, 0xf1, 0x6c, 0x5c, 0xf0, 0xd0, 0xe5, 0x01, 0xec, 0x43, 0x73, 0x48, 0xe2, 0xa7, 0xf0, 0x73, 0x66, 0xd2, - 0x3a, 0x3e, 0xc3, 0xd9, 0x8c, 0x4a, 0x75, 0x23, 0x68, 0xbf, 0x86, 0x44, 0x62, 0x90, 0x9e, 0x1b, 0x0c, 0x45, 0xeb, - 0x6e, 0x03, 0x57, 0x7e, 0x4b, 0xef, 0x7c, 0x1a, 0x04, 0x58, 0xdf, 0x58, 0x0c, 0x00, 0xa8, 0xe2, 0x0f, 0x54, 0x5d, - 0x99, 0x2b, 0x8a, 0x69, 0x98, 0x4a, 0xb4, 0x77, 0x1c, 0xd7, 0x51, 0xe3, 0x3a, 0x2c, 0x58, 0x69, 0x6d, 0x9b, 0xdd, - 0x5b, 0x5a, 0xd8, 0x12, 0x50, 0x2d, 0x88, 0x3b, 0x01, 0x7c, 0x68, 0xa4, 0x3a, 0x10, 0x64, 0xf7, 0xc1, 0x01, 0x00, - 0x6f, 0x78, 0x1e, 0x86, 0xf0, 0x07, 0x16, 0x0e, 0x2c, 0x4b, 0xd5, 0xcf, 0xe5, 0x34, 0x86, 0x73, 0x37, 0x57, 0x3b, - 0x7c, 0xb6, 0x04, 0xc5, 0xa6, 0x9a, 0x53, 0x73, 0xf9, 0xca, 0x1b, 0xfb, 0x3d, 0x26, 0x98, 0xc7, 0xcc, 0x36, 0xfc, - 0xd6, 0xd3, 0x6d, 0x7d, 0x83, 0xdd, 0xc0, 0x49, 0x7b, 0xe1, 0xb4, 0x17, 0xdb, 0xa5, 0x81, 0xfc, 0xab, 0x1b, 0x42, - 0x84, 0x57, 0x9a, 0x58, 0x64, 0x0d, 0x99, 0x8e, 0xc5, 0x0a, 0x51, 0x6d, 0x2a, 0x9e, 0x69, 0x03, 0x81, 0x72, 0xaa, - 0x2e, 0x4c, 0xad, 0x54, 0x26, 0x0c, 0xe2, 0x4e, 0x09, 0x8b, 0x2a, 0x03, 0x0c, 0x83, 0x0a, 0x29, 0xae, 0xad, 0xe7, - 0x2f, 0x5c, 0xbe, 0x99, 0x69, 0xb3, 0xfd, 0xf4, 0x65, 0x1e, 0x5f, 0x6d, 0xb7, 0x61, 0xf7, 0x0b, 0x30, 0x47, 0x2d, - 0x95, 0x86, 0x11, 0x9c, 0x40, 0x94, 0xe4, 0x7a, 0x4f, 0xce, 0x89, 0xe3, 0xe4, 0xda, 0xcd, 0x9b, 0xed, 0xa4, 0x18, - 0x81, 0x05, 0x9c, 0xb8, 0x48, 0x07, 0x5a, 0x2a, 0x49, 0xed, 0x29, 0xe0, 0x6d, 0x7a, 0x47, 0xa9, 0xf0, 0x6a, 0xa1, - 0x49, 0x48, 0xe5, 0xee, 0x25, 0x76, 0xd4, 0x80, 0x73, 0x52, 0x77, 0x10, 0x70, 0xda, 0xd3, 0x8d, 0xb5, 0x8a, 0x64, - 0x93, 0xe0, 0xbd, 0xd2, 0x43, 0x97, 0x68, 0xa7, 0x76, 0xb7, 0xad, 0xca, 0x16, 0x0a, 0xe6, 0x41, 0xce, 0x12, 0x75, - 0x3c, 0xa0, 0xd0, 0x45, 0x1d, 0x0d, 0xf9, 0x82, 0x14, 0x7a, 0xe5, 0x68, 0x55, 0xf3, 0xae, 0x64, 0xa0, 0x54, 0xab, - 0x20, 0xaf, 0x89, 0x75, 0x5f, 0xcb, 0x1a, 0x8b, 0x2b, 0x27, 0xa4, 0xb0, 0x09, 0x9f, 0x5b, 0x8a, 0x85, 0x59, 0xec, - 0x8d, 0xa9, 0x2f, 0x5c, 0x22, 0xb4, 0xdd, 0x6d, 0x88, 0xd1, 0x06, 0xeb, 0x66, 0xbb, 0x7d, 0x55, 0x84, 0xf3, 0x6c, - 0x41, 0xe5, 0x28, 0x4b, 0x11, 0x52, 0xcd, 0x78, 0x2c, 0xdb, 0x2e, 0x98, 0x89, 0xa1, 0xae, 0x3d, 0x5e, 0x92, 0x29, - 0xd6, 0x26, 0xc9, 0x51, 0x7c, 0x21, 0x0b, 0xb5, 0xd6, 0x08, 0xc1, 0xc3, 0xfd, 0x8f, 0x14, 0x62, 0xda, 0x99, 0x75, - 0xf7, 0xed, 0xce, 0x0d, 0xf1, 0x0f, 0x08, 0xac, 0x50, 0xb2, 0x57, 0xc5, 0xe8, 0x22, 0x13, 0x29, 0xee, 0x54, 0x15, - 0x25, 0x58, 0xad, 0x83, 0x66, 0xcb, 0xed, 0xbd, 0xd8, 0x12, 0x05, 0x88, 0xf3, 0x2c, 0x34, 0xe3, 0x59, 0x39, 0xcb, - 0x99, 0x8c, 0x62, 0x43, 0xa2, 0xd2, 0x8b, 0x12, 0xef, 0xf3, 0x34, 0xa6, 0x87, 0x6e, 0x0d, 0x82, 0xeb, 0xea, 0xce, - 0x46, 0x9a, 0x2f, 0x08, 0x51, 0x13, 0x20, 0x61, 0xa3, 0x9a, 0x53, 0xeb, 0x4a, 0x3c, 0xcc, 0x2a, 0x9f, 0xeb, 0x83, - 0xf8, 0x4a, 0x00, 0x0f, 0xeb, 0x6d, 0xef, 0x6b, 0xe1, 0xb1, 0x36, 0xf8, 0x76, 0xbb, 0xbd, 0x12, 0xf3, 0x20, 0xf0, - 0x18, 0xcd, 0x5f, 0x94, 0xc4, 0xbc, 0x37, 0xa6, 0xb0, 0xe2, 0x7d, 0x17, 0xbf, 0x6e, 0x52, 0x6b, 0x2d, 0x72, 0x77, - 0xb8, 0x3e, 0xe0, 0x79, 0x4a, 0x1c, 0xed, 0xa8, 0x9c, 0x4a, 0x6b, 0x3b, 0x80, 0x5d, 0x11, 0x18, 0x28, 0xfb, 0xfb, - 0x94, 0x6d, 0xc0, 0x3c, 0x11, 0xac, 0x8f, 0xd0, 0x6f, 0x4b, 0xe9, 0x4f, 0xc6, 0x68, 0xdc, 0x23, 0xd7, 0x55, 0x74, - 0xc4, 0x75, 0x34, 0x7b, 0x1e, 0xfd, 0xed, 0xe9, 0x98, 0x16, 0xb1, 0x48, 0xe5, 0x35, 0xa8, 0x20, 0x40, 0x19, 0x82, - 0x8e, 0x10, 0x9a, 0x1a, 0x80, 0x06, 0xc1, 0x0d, 0xc0, 0x3f, 0x3b, 0x9d, 0x28, 0x6d, 0x4d, 0x3e, 0x46, 0xab, 0x2a, - 0x72, 0xd6, 0x86, 0x76, 0x53, 0xc9, 0x21, 0x79, 0x5c, 0x02, 0xbe, 0x25, 0x36, 0x4b, 0xd9, 0xa0, 0xa8, 0xcd, 0xa6, - 0x5e, 0x2b, 0x76, 0xe4, 0xae, 0x51, 0xb4, 0x59, 0x8b, 0xda, 0x6e, 0x64, 0xbe, 0x98, 0xde, 0x59, 0x61, 0xe0, 0xd4, - 0xb4, 0xe6, 0x76, 0x07, 0x4a, 0xce, 0xd6, 0x67, 0x72, 0x13, 0x20, 0x0e, 0x30, 0x5c, 0x77, 0xf3, 0xdb, 0x05, 0xa1, - 0x77, 0xec, 0xce, 0x8a, 0x55, 0x6f, 0xad, 0x5c, 0xc4, 0xa4, 0xdd, 0x0e, 0x26, 0x70, 0x19, 0x67, 0x85, 0x7d, 0xa1, - 0xd5, 0x0d, 0x45, 0x47, 0xdb, 0xa4, 0xfd, 0xbc, 0xa3, 0xdd, 0x70, 0xc1, 0xb7, 0x62, 0x1d, 0xe7, 0x96, 0x35, 0x55, - 0x68, 0xda, 0x81, 0xde, 0x0e, 0x01, 0xcd, 0xd9, 0x98, 0x2e, 0x69, 0x8a, 0x17, 0x68, 0xba, 0x06, 0x33, 0x9d, 0x4b, - 0xe8, 0x6b, 0xb7, 0x8f, 0xf6, 0xa5, 0xea, 0x89, 0xf0, 0x96, 0x28, 0xf8, 0xb6, 0xa4, 0xe0, 0xa5, 0x96, 0xf3, 0xd8, - 0xcc, 0x21, 0xe0, 0xd3, 0xa8, 0x12, 0xbd, 0x93, 0xe2, 0x0a, 0xb4, 0x99, 0x70, 0x04, 0x9a, 0xaa, 0x11, 0x5b, 0x39, - 0xc0, 0xed, 0xc5, 0xd3, 0x80, 0x50, 0x90, 0xea, 0xae, 0xed, 0x8a, 0xbc, 0x63, 0x27, 0x9b, 0x3b, 0x30, 0x13, 0xae, - 0xd6, 0x65, 0xeb, 0x2b, 0x9b, 0xec, 0x3e, 0xae, 0x09, 0xb6, 0xdd, 0xdb, 0x20, 0xe1, 0x1d, 0xbd, 0x25, 0x9b, 0xdb, - 0x7e, 0x3f, 0x84, 0xfe, 0x10, 0xaa, 0x3b, 0x74, 0xd7, 0xd9, 0xa1, 0x3b, 0x9f, 0xf9, 0xb5, 0x7a, 0x3e, 0xe5, 0x1d, - 0xf2, 0x01, 0x4d, 0xd6, 0xe8, 0x2a, 0xbe, 0x87, 0x4d, 0x1d, 0x55, 0x54, 0x55, 0x1e, 0x25, 0x14, 0x54, 0xe2, 0x19, - 0x2f, 0xcf, 0x38, 0xc6, 0x7a, 0xd5, 0x4f, 0xef, 0x34, 0xaf, 0xb6, 0x36, 0x6b, 0xb3, 0x5c, 0x5f, 0x80, 0x85, 0xc4, - 0x05, 0x8f, 0xae, 0x35, 0x2d, 0xb9, 0xf2, 0x98, 0xfa, 0x73, 0x1c, 0x95, 0xe0, 0x32, 0xce, 0x72, 0x50, 0xe3, 0x5e, - 0x36, 0xfb, 0x1f, 0x6a, 0xdb, 0xb1, 0x65, 0xe3, 0xcc, 0xbd, 0x09, 0xc9, 0xe6, 0x7f, 0x6c, 0xa0, 0x5e, 0x87, 0x18, - 0x21, 0xd6, 0x2c, 0xe8, 0xb7, 0x0c, 0x62, 0x85, 0x06, 0xe5, 0x3a, 0x49, 0x78, 0x59, 0x06, 0x46, 0xa9, 0xb5, 0x66, - 0x6b, 0x73, 0x9e, 0x3d, 0x62, 0x27, 0x8f, 0x7a, 0x8c, 0xdd, 0x11, 0x9a, 0x68, 0x9d, 0x90, 0xa9, 0x31, 0xf2, 0xb4, - 0x40, 0xba, 0x43, 0x51, 0x76, 0x19, 0xbe, 0x45, 0x21, 0x4b, 0x7b, 0x9f, 0x9b, 0x13, 0x59, 0x7d, 0xa3, 0x8d, 0x50, - 0x22, 0x95, 0x08, 0xb2, 0xf1, 0x5b, 0x04, 0x30, 0x86, 0x66, 0x07, 0x64, 0xb3, 0x64, 0x67, 0xf4, 0xdc, 0x9a, 0x04, - 0xc1, 0xeb, 0xb7, 0x2a, 0xd1, 0x8c, 0xb2, 0x22, 0xba, 0xca, 0xe8, 0xe7, 0x3e, 0x24, 0xd1, 0x79, 0x48, 0xfc, 0xdc, - 0xb0, 0xb4, 0x6e, 0x42, 0x14, 0x33, 0x9b, 0x0d, 0xaf, 0xba, 0xfb, 0xa8, 0xb1, 0xad, 0x8c, 0x8f, 0xf9, 0x9d, 0x4d, - 0x23, 0x53, 0xe8, 0xeb, 0x70, 0xd2, 0xef, 0xc3, 0x5f, 0x4d, 0x3f, 0xf0, 0x96, 0x82, 0xbf, 0xd8, 0x23, 0x52, 0x27, - 0x2c, 0x00, 0x78, 0xc6, 0x9c, 0x57, 0xcd, 0x09, 0x7c, 0xc4, 0x4e, 0x36, 0x8f, 0xc2, 0xb3, 0xc6, 0xcc, 0xdd, 0x87, - 0x78, 0xa9, 0x4a, 0x7a, 0xde, 0x3c, 0x99, 0x81, 0x58, 0x59, 0xad, 0xf9, 0x1d, 0xb3, 0xfa, 0x04, 0x20, 0x52, 0x77, - 0xd6, 0xc1, 0x16, 0x3f, 0x36, 0x5d, 0x26, 0x9b, 0x94, 0xb5, 0x99, 0x28, 0xa5, 0x22, 0x69, 0x2e, 0x02, 0xe8, 0x37, - 0x0c, 0x47, 0x0d, 0x70, 0xe7, 0x7a, 0xec, 0xcd, 0xd0, 0x78, 0x63, 0x6a, 0xe8, 0xd9, 0x46, 0x2f, 0x6f, 0x47, 0x21, - 0xcc, 0x58, 0x44, 0x77, 0xee, 0x58, 0x0c, 0xcf, 0xe8, 0x5b, 0xa8, 0xf0, 0x75, 0x88, 0xd1, 0x85, 0x49, 0x5d, 0x4f, - 0xd7, 0x6a, 0x2b, 0xdd, 0x12, 0x9a, 0x63, 0x54, 0x23, 0xaf, 0x6d, 0xf7, 0xd4, 0x08, 0xed, 0x09, 0xe5, 0xe1, 0x1d, - 0xad, 0xe8, 0xad, 0x65, 0x11, 0x9c, 0xfc, 0xd8, 0xcb, 0x4f, 0xe8, 0x85, 0x27, 0x30, 0x29, 0xda, 0x1a, 0xc0, 0xef, - 0x51, 0x3f, 0x9c, 0xd5, 0x53, 0x2b, 0xe5, 0xf0, 0x14, 0xbe, 0x64, 0x03, 0x72, 0x05, 0xbd, 0x58, 0x63, 0x76, 0x12, - 0x83, 0x0e, 0x6a, 0x67, 0x77, 0x78, 0x93, 0x52, 0x86, 0x68, 0x8d, 0xe8, 0x20, 0xaf, 0xfe, 0x09, 0x9a, 0x3e, 0x48, - 0x0b, 0x53, 0xba, 0x46, 0x01, 0x0f, 0xe8, 0x9b, 0xfa, 0xfd, 0x1c, 0x9f, 0x6b, 0xcf, 0x32, 0x0d, 0x7b, 0xbc, 0x24, - 0x74, 0xe9, 0xc5, 0xd1, 0x02, 0x69, 0xb3, 0x63, 0x15, 0x80, 0x15, 0x49, 0xa0, 0x11, 0x09, 0x58, 0x2e, 0x79, 0xe2, - 0xb2, 0x0d, 0x1a, 0xd4, 0x44, 0x25, 0x85, 0x2c, 0x91, 0x04, 0x7e, 0x18, 0x41, 0x99, 0xa2, 0x18, 0xc4, 0xbd, 0x7a, - 0x79, 0xc5, 0x35, 0x35, 0x60, 0x4d, 0x11, 0x4c, 0xb0, 0x4e, 0xa7, 0x40, 0x6c, 0xc5, 0x7a, 0x05, 0x9e, 0xa8, 0x8e, - 0x13, 0x47, 0x96, 0x00, 0x0d, 0xf4, 0x7c, 0xe9, 0xb4, 0x5b, 0xde, 0x9e, 0x68, 0xa9, 0x62, 0x73, 0xef, 0xc5, 0xc2, - 0x72, 0x8f, 0x95, 0xbf, 0x1d, 0x68, 0x2f, 0xac, 0x76, 0x44, 0xd4, 0x60, 0x75, 0xd8, 0xb6, 0xf3, 0x43, 0x69, 0xa8, - 0xee, 0x95, 0x63, 0x02, 0x2a, 0xba, 0x8a, 0xab, 0x65, 0x94, 0x8d, 0xe0, 0xcf, 0x76, 0x1b, 0x1c, 0x06, 0x60, 0x11, - 0xfa, 0xf3, 0xfb, 0x1f, 0x23, 0x0c, 0x57, 0xf5, 0xf3, 0xfb, 0x1f, 0xb7, 0xdb, 0xa7, 0xe3, 0xb1, 0xe1, 0x0a, 0x9c, - 0x5a, 0x07, 0xf8, 0x03, 0xc3, 0x36, 0xd8, 0x25, 0xbb, 0xdd, 0x3e, 0x05, 0x0e, 0x42, 0xb1, 0x0d, 0x66, 0x17, 0x2b, - 0xc7, 0x36, 0xc5, 0x6a, 0xe8, 0x1d, 0x09, 0xd8, 0x7d, 0x3b, 0x2c, 0xc5, 0x2e, 0xf5, 0x51, 0x21, 0x29, 0xf5, 0xa2, - 0x7f, 0xd1, 0x29, 0xb0, 0xa4, 0x60, 0xca, 0x1b, 0x2c, 0xab, 0x6a, 0x55, 0x46, 0x87, 0x87, 0xf1, 0x2a, 0x1b, 0x95, - 0x19, 0x6c, 0xf3, 0xf2, 0xe6, 0x0a, 0x00, 0x26, 0x02, 0xda, 0x78, 0xb7, 0x16, 0x99, 0x79, 0xb1, 0xa0, 0xcb, 0x0c, - 0xd7, 0x24, 0x98, 0x1d, 0xe4, 0xdc, 0xea, 0x26, 0xa7, 0xc4, 0x3e, 0x80, 0x0d, 0xe6, 0x76, 0xdb, 0xe0, 0x17, 0x4e, - 0x46, 0x4f, 0x67, 0xcb, 0x4c, 0x1b, 0xb8, 0x72, 0xb3, 0xff, 0x49, 0xe4, 0xa5, 0xa1, 0xe2, 0x93, 0x4c, 0x5f, 0x64, - 0xc0, 0xe7, 0xb1, 0xbf, 0x44, 0xe8, 0xb3, 0x5c, 0x8d, 0xd6, 0x00, 0x1b, 0x9b, 0x5d, 0xde, 0x8f, 0x52, 0x0e, 0x11, - 0x3a, 0x02, 0xab, 0xae, 0x59, 0x66, 0xc4, 0xb7, 0xa9, 0xb8, 0x6f, 0xa9, 0xc2, 0xfe, 0x12, 0x9e, 0xf3, 0x0e, 0x37, - 0x8e, 0x43, 0xbd, 0x49, 0x14, 0xbe, 0x40, 0x21, 0x2a, 0x47, 0xe3, 0x42, 0x27, 0x90, 0xca, 0x3c, 0x26, 0x14, 0x73, - 0xb8, 0x77, 0x3f, 0xa7, 0xce, 0x5c, 0xc6, 0x17, 0xee, 0xbd, 0xf0, 0x65, 0x26, 0x77, 0x12, 0x40, 0xa2, 0x54, 0xed, - 0x3f, 0x7d, 0x42, 0x6a, 0xfc, 0xaf, 0x54, 0x6b, 0x00, 0x7a, 0x3f, 0x41, 0x4d, 0x8e, 0x20, 0x60, 0x2b, 0xa6, 0x7e, - 0x74, 0x01, 0x2b, 0x99, 0xff, 0x80, 0xba, 0x1d, 0xc1, 0x36, 0x2a, 0x9e, 0x50, 0x54, 0xd1, 0x82, 0xa7, 0x6b, 0x91, - 0xc6, 0x22, 0xb9, 0x8f, 0x78, 0x3d, 0xc5, 0x92, 0x98, 0x8d, 0x18, 0xf6, 0x53, 0xb3, 0x0b, 0x3f, 0x16, 0x0d, 0x93, - 0x78, 0x5a, 0xfa, 0xdb, 0xca, 0xdb, 0x4c, 0x96, 0x71, 0x46, 0xa6, 0x5c, 0x21, 0x98, 0x5b, 0x7d, 0x8f, 0x39, 0xc1, - 0x9f, 0x1c, 0x3d, 0x21, 0xf4, 0x4e, 0x4e, 0x4b, 0x04, 0xe9, 0x13, 0xa9, 0x75, 0x5d, 0xc5, 0x7e, 0x4d, 0x21, 0xaa, - 0x85, 0x60, 0x10, 0xca, 0xd4, 0xb4, 0x4f, 0xf1, 0x7d, 0xb6, 0xec, 0xbf, 0x4c, 0xd9, 0x92, 0x6c, 0x04, 0x74, 0x4c, - 0x3a, 0xef, 0x57, 0x6f, 0xcf, 0xce, 0xbc, 0xdf, 0xa0, 0x09, 0x07, 0xd5, 0x0d, 0xb4, 0xab, 0x20, 0xd3, 0x18, 0xc5, - 0x66, 0x31, 0xd6, 0x6e, 0x4d, 0x44, 0x10, 0x84, 0xbb, 0x9c, 0x85, 0xed, 0x76, 0x42, 0xbc, 0x0d, 0x24, 0x50, 0xe0, - 0xda, 0x46, 0x39, 0x09, 0x89, 0xba, 0x90, 0xe9, 0xc9, 0xba, 0x91, 0x2c, 0xd0, 0x6b, 0xec, 0x28, 0xa0, 0xa7, 0xdc, - 0x3e, 0x05, 0xf4, 0x7d, 0xc1, 0x4e, 0xf9, 0x20, 0x18, 0x62, 0xbc, 0xd9, 0x80, 0xde, 0x4a, 0xf5, 0x08, 0x1e, 0xd3, - 0xc0, 0x72, 0xd1, 0x97, 0x05, 0x43, 0x98, 0xa5, 0x3f, 0x53, 0x36, 0xf9, 0xfa, 0xef, 0x6e, 0x7e, 0x2f, 0xb4, 0x98, - 0x1d, 0x84, 0xe2, 0xf6, 0x7a, 0x02, 0xc4, 0xaf, 0xe2, 0xd7, 0x60, 0x6d, 0xae, 0x25, 0xde, 0x6e, 0x7a, 0xfe, 0x10, - 0xbe, 0x1c, 0xdd, 0x7e, 0x52, 0x9a, 0x4f, 0x20, 0x68, 0x8f, 0x93, 0x94, 0xbb, 0xef, 0x3e, 0x4a, 0x57, 0x11, 0x8c, - 0x16, 0x20, 0xf8, 0xed, 0xad, 0xe4, 0xbc, 0x29, 0xfc, 0xc7, 0x3a, 0xdf, 0x63, 0x2c, 0x15, 0x79, 0x86, 0xd3, 0xdf, - 0x00, 0x07, 0xbf, 0xf7, 0x6f, 0x65, 0xd6, 0x90, 0xe8, 0x42, 0x7d, 0x04, 0xf4, 0x7f, 0xac, 0xc7, 0xef, 0x14, 0x25, - 0x7d, 0x49, 0x9c, 0x23, 0x7c, 0x13, 0x2f, 0xd1, 0x74, 0xb1, 0x37, 0xae, 0xe9, 0x9b, 0xc2, 0xbc, 0xd0, 0x0a, 0x0e, - 0xfb, 0xd6, 0x28, 0x3c, 0xf0, 0xcc, 0xfb, 0x56, 0x34, 0x04, 0xdd, 0xbf, 0xe2, 0xde, 0xf8, 0x56, 0xb0, 0x0c, 0x6f, - 0xca, 0x59, 0x66, 0xee, 0x70, 0xb7, 0x99, 0x48, 0xe5, 0x2d, 0x63, 0xc1, 0x5a, 0x28, 0x73, 0xde, 0x34, 0x98, 0x6d, - 0xea, 0x48, 0x25, 0xbb, 0xef, 0xff, 0x6a, 0x9c, 0xb0, 0xd9, 0x20, 0x38, 0xab, 0x64, 0x11, 0x5f, 0xf1, 0x60, 0xaa, - 0x55, 0x14, 0x19, 0xd8, 0x15, 0x02, 0x52, 0x8e, 0xd3, 0xde, 0xc1, 0x93, 0xa5, 0x66, 0x26, 0xe4, 0xb7, 0xd5, 0x59, - 0xc0, 0x5b, 0x33, 0x9a, 0xa7, 0x15, 0xec, 0x32, 0x5f, 0x49, 0xf1, 0x47, 0x4b, 0x92, 0x8d, 0xf5, 0x37, 0x64, 0xd8, - 0x56, 0x3e, 0x73, 0x01, 0x98, 0x3b, 0xb7, 0x52, 0x05, 0xfd, 0xeb, 0x01, 0x23, 0x84, 0x44, 0x40, 0x38, 0x8b, 0x89, - 0x7b, 0x61, 0xc2, 0x61, 0xba, 0x40, 0x41, 0x31, 0x06, 0x0a, 0xfa, 0x28, 0x43, 0x4e, 0x4f, 0xf9, 0x20, 0x69, 0xcc, - 0xd6, 0x1f, 0xaa, 0x44, 0x7a, 0x23, 0x09, 0x3d, 0x87, 0xdf, 0xe3, 0x16, 0x0f, 0xd4, 0x08, 0xd6, 0xe9, 0x6e, 0x4e, - 0x87, 0x2f, 0x0b, 0x32, 0xfc, 0x13, 0xbc, 0xdd, 0x62, 0x7b, 0x59, 0x4e, 0x60, 0x71, 0xc7, 0x5e, 0xf1, 0x34, 0x57, - 0x2d, 0x4e, 0x88, 0x47, 0x2c, 0x72, 0x9f, 0x58, 0xc0, 0x88, 0x1a, 0x46, 0xe3, 0x87, 0xb3, 0xb7, 0x6f, 0x34, 0x86, - 0x55, 0xee, 0x7f, 0x00, 0x23, 0xaa, 0xa5, 0xed, 0x76, 0xc0, 0x97, 0x23, 0x34, 0x60, 0x4f, 0xdd, 0x60, 0xf7, 0xfb, - 0x26, 0xed, 0xa4, 0xf4, 0xb2, 0x39, 0x31, 0xe8, 0x8e, 0xd2, 0x66, 0xa9, 0x0c, 0x8c, 0xbb, 0x0a, 0x47, 0x73, 0x62, - 0x23, 0x56, 0xf5, 0x3e, 0x0c, 0x97, 0x34, 0xb6, 0xb2, 0x72, 0xbb, 0x9b, 0x70, 0x64, 0x13, 0xe0, 0xfa, 0x14, 0xb4, - 0x57, 0x73, 0x0e, 0x5a, 0x50, 0xa2, 0xc0, 0x11, 0x6d, 0xb7, 0x21, 0x44, 0x24, 0x29, 0x86, 0x93, 0x59, 0x58, 0x0c, - 0x87, 0x6a, 0xe0, 0x0b, 0x42, 0xa2, 0x37, 0xc5, 0x3c, 0x5b, 0x28, 0x04, 0x23, 0x7f, 0x27, 0x7d, 0x5b, 0x28, 0x4e, - 0xb9, 0xf7, 0xad, 0x20, 0x9b, 0x5f, 0x53, 0x8c, 0xc1, 0xe8, 0x34, 0x9b, 0x19, 0x48, 0x58, 0x4f, 0x2b, 0xa2, 0xd6, - 0x91, 0x9d, 0x0d, 0x50, 0xc5, 0xa2, 0x69, 0x30, 0xa8, 0x5b, 0x3c, 0xb1, 0x9e, 0xd1, 0x7b, 0x50, 0x09, 0xa2, 0x5a, - 0xb0, 0x1b, 0xc3, 0xb5, 0xf6, 0x46, 0x84, 0x92, 0x72, 0xd2, 0x64, 0x66, 0xac, 0x68, 0xb0, 0x00, 0x21, 0x69, 0x5c, - 0x56, 0xaf, 0x65, 0x9a, 0x5d, 0x66, 0x80, 0x20, 0xe1, 0xfc, 0x09, 0x65, 0xe3, 0xcd, 0x33, 0x35, 0x2f, 0x5d, 0x89, - 0x33, 0x0b, 0x7b, 0xd2, 0xf5, 0x96, 0x16, 0x24, 0x2a, 0x80, 0x46, 0xf9, 0x5a, 0x9e, 0x7f, 0xec, 0x58, 0x85, 0xec, - 0x7e, 0x38, 0x55, 0xb6, 0x43, 0xfc, 0x84, 0x55, 0xc4, 0x3b, 0xad, 0x2b, 0x25, 0xd2, 0xe8, 0x68, 0x1b, 0x10, 0xc3, - 0x96, 0x7d, 0x8b, 0x1a, 0x3e, 0x08, 0xbb, 0xe8, 0x24, 0x3f, 0xe8, 0x29, 0x1e, 0x5b, 0x03, 0x49, 0x5f, 0x8b, 0xe0, - 0x6b, 0x74, 0xa4, 0x13, 0x65, 0x1a, 0x89, 0x29, 0x24, 0xfa, 0xf5, 0x42, 0x6b, 0x2c, 0xa3, 0xec, 0x2b, 0xf2, 0xbf, - 0xd3, 0xdd, 0xfb, 0x56, 0x6c, 0xb7, 0x30, 0xc9, 0x9e, 0x07, 0x1a, 0x6c, 0x6a, 0xd4, 0x0a, 0xe1, 0xec, 0x9c, 0x56, - 0xa8, 0x1d, 0xeb, 0x85, 0x25, 0x90, 0x07, 0xb0, 0x15, 0x69, 0x50, 0x06, 0xc9, 0xde, 0x14, 0x73, 0xb1, 0x70, 0xa2, - 0x1c, 0xa9, 0xf0, 0xcf, 0xe4, 0x28, 0xe5, 0x70, 0x15, 0x0b, 0x0b, 0x86, 0xfc, 0xea, 0xe8, 0xb2, 0x90, 0xd7, 0x20, - 0x29, 0x31, 0x0c, 0x95, 0xe5, 0x75, 0x71, 0xd5, 0x96, 0x84, 0xf6, 0xce, 0x01, 0x94, 0xa6, 0x00, 0xc1, 0x4b, 0xa3, - 0x86, 0x98, 0x6d, 0xd4, 0xee, 0x8a, 0xf6, 0x92, 0x03, 0xea, 0x74, 0xd7, 0x6e, 0xbd, 0x29, 0x5b, 0x75, 0x2b, 0x2e, - 0xfc, 0x03, 0x4a, 0x3f, 0xe5, 0x83, 0xc2, 0xa7, 0x12, 0xb8, 0xf1, 0xd5, 0x26, 0xcb, 0x2e, 0xef, 0x71, 0xe9, 0x57, - 0x8d, 0xf1, 0xeb, 0xf7, 0x7b, 0x6a, 0x21, 0x34, 0x52, 0x81, 0xf9, 0xf6, 0x99, 0xa9, 0xca, 0x68, 0x4a, 0xed, 0x25, - 0xb8, 0x72, 0xf6, 0x23, 0xa8, 0x88, 0xeb, 0x8a, 0xd4, 0xa6, 0x06, 0xe8, 0xc0, 0xcb, 0x0a, 0xb7, 0xb2, 0x00, 0x8f, - 0x9d, 0x80, 0x6c, 0xb7, 0x3c, 0x0c, 0xf4, 0xa1, 0x13, 0xf8, 0x5b, 0xf2, 0x0c, 0x99, 0x35, 0xfb, 0xf8, 0x93, 0x16, - 0xfc, 0x63, 0x0b, 0x7e, 0x44, 0x71, 0xa7, 0x95, 0xf9, 0xb7, 0xd2, 0xba, 0xc5, 0xfd, 0x3b, 0x99, 0x26, 0x14, 0x95, - 0x09, 0xb5, 0x5f, 0xe9, 0x6f, 0x26, 0x78, 0x94, 0xca, 0xfe, 0x41, 0xc2, 0x07, 0xb3, 0xc6, 0x13, 0x6b, 0x3c, 0x19, - 0x4e, 0xb7, 0xd2, 0xb0, 0x0c, 0x28, 0xf4, 0xf3, 0x32, 0x57, 0x54, 0x3f, 0xff, 0xb4, 0xe6, 0x6b, 0xde, 0x6c, 0xb1, - 0x4d, 0x7a, 0xa0, 0xc1, 0x5e, 0x1e, 0x4d, 0x29, 0x9c, 0x44, 0x9d, 0x1b, 0x89, 0xba, 0xa8, 0x59, 0x86, 0xea, 0x04, - 0xaf, 0xe6, 0xa9, 0x1e, 0xf6, 0x66, 0x22, 0x5a, 0x2b, 0x29, 0x4b, 0x0c, 0x58, 0xeb, 0xc8, 0x43, 0x72, 0xb7, 0xd6, - 0x71, 0xa7, 0xa1, 0x2e, 0x4d, 0xa1, 0x26, 0x58, 0xe1, 0x02, 0x1c, 0x41, 0x3f, 0x16, 0x21, 0x87, 0x6b, 0xaa, 0xd2, - 0x2f, 0x68, 0x4a, 0x9e, 0x78, 0x8a, 0x5a, 0xad, 0x48, 0xb7, 0x1f, 0xe5, 0xd8, 0x0d, 0xdf, 0x38, 0x21, 0x27, 0x46, - 0xe8, 0xef, 0x8e, 0xa5, 0x9c, 0xa1, 0xc5, 0x83, 0x3a, 0xc1, 0x7a, 0x79, 0x4b, 0x81, 0x62, 0x8e, 0x2e, 0xab, 0xae, - 0x79, 0x85, 0xb6, 0x2f, 0xcb, 0x7e, 0x3f, 0xb7, 0xf5, 0xa4, 0xec, 0x64, 0xb3, 0x34, 0xfb, 0x10, 0x15, 0x53, 0xb8, - 0xeb, 0x13, 0xcd, 0x5f, 0x85, 0xfa, 0xaa, 0x2d, 0x73, 0x3e, 0xe2, 0x88, 0x13, 0x92, 0x93, 0xfa, 0x27, 0x35, 0xf5, - 0x4a, 0xdc, 0xaf, 0x2a, 0xf9, 0x45, 0x18, 0x2b, 0x46, 0x17, 0xb8, 0x20, 0x55, 0x2a, 0xef, 0x17, 0x05, 0xc0, 0x5f, - 0x09, 0xf6, 0x26, 0x0d, 0xb5, 0xf2, 0x5b, 0xb4, 0x05, 0xfc, 0x1b, 0xc5, 0x0d, 0x58, 0x05, 0x06, 0x18, 0x4d, 0xb6, - 0xe7, 0x34, 0x81, 0x03, 0x4e, 0x68, 0x15, 0x05, 0x15, 0x66, 0x68, 0xa8, 0x2d, 0x8c, 0x9e, 0xa1, 0x8c, 0x5b, 0x65, - 0xf6, 0x6e, 0x8c, 0x9d, 0x16, 0x78, 0x0d, 0xff, 0x46, 0x2f, 0x14, 0xb3, 0x51, 0x07, 0xe9, 0xd1, 0x49, 0x4c, 0x7f, - 0xdc, 0xc2, 0xc9, 0xcd, 0xc2, 0x59, 0xd6, 0x2c, 0x81, 0xee, 0xc0, 0x05, 0x31, 0xee, 0xf7, 0x73, 0x38, 0x32, 0xcd, - 0xc8, 0x17, 0x2c, 0xa7, 0x31, 0x5b, 0x52, 0xed, 0x79, 0x78, 0x55, 0x85, 0x39, 0x5d, 0x5a, 0x19, 0x6f, 0xca, 0x40, - 0x65, 0xb4, 0xdd, 0x86, 0xf0, 0xa7, 0xdb, 0xda, 0x25, 0x9d, 0x2f, 0x21, 0x03, 0xfc, 0x01, 0x89, 0x28, 0x62, 0x81, - 0xff, 0x5b, 0x8d, 0x53, 0x7a, 0xa2, 0xb4, 0x66, 0x09, 0x5d, 0x33, 0x5d, 0x3f, 0xbd, 0x64, 0xeb, 0xc6, 0x52, 0xd8, - 0x6e, 0xc3, 0x66, 0x02, 0xd3, 0x9c, 0x2b, 0x99, 0x5e, 0xa2, 0x4e, 0x0a, 0xa8, 0x58, 0x78, 0x89, 0xcb, 0x2f, 0x25, - 0x14, 0x9a, 0x3b, 0x5f, 0x2e, 0x8c, 0x12, 0x13, 0x5a, 0x25, 0x3f, 0x7f, 0xa8, 0xcc, 0xd7, 0xc6, 0x43, 0xf0, 0xb7, - 0x34, 0x4c, 0x4c, 0x91, 0xa8, 0x10, 0x9d, 0x7d, 0x0b, 0xb2, 0x1c, 0x01, 0xb8, 0x9e, 0x67, 0xb2, 0xa6, 0x3f, 0xa4, - 0x10, 0x17, 0x1e, 0x1a, 0xf4, 0xae, 0x90, 0xd7, 0x59, 0xc9, 0x43, 0xbc, 0x27, 0x78, 0x9a, 0xd1, 0xfd, 0x06, 0x1f, - 0xda, 0xda, 0xa3, 0x27, 0xc8, 0xc6, 0x53, 0xee, 0xd7, 0xbf, 0x88, 0x70, 0x0e, 0xd1, 0x3b, 0x17, 0x54, 0xab, 0xab, - 0x1d, 0x20, 0x97, 0x67, 0x7b, 0xf5, 0x08, 0x4e, 0x37, 0x7d, 0x7d, 0xab, 0x42, 0x67, 0x0e, 0x20, 0xed, 0x21, 0x59, - 0xd7, 0x5c, 0xef, 0x00, 0x77, 0x24, 0x56, 0x6b, 0xa0, 0xb1, 0x6e, 0x6b, 0x76, 0xda, 0xa3, 0x78, 0x4c, 0x64, 0x66, - 0x2c, 0x52, 0x8c, 0xb9, 0x5b, 0xa7, 0x45, 0xd1, 0x06, 0xcd, 0x10, 0x76, 0xef, 0x3a, 0x7c, 0xdd, 0x8a, 0x38, 0xbf, - 0xdf, 0xf6, 0x05, 0x46, 0xc3, 0x98, 0x6b, 0xf7, 0x3c, 0x43, 0x37, 0x6c, 0xb0, 0x8d, 0x24, 0x88, 0x48, 0x90, 0x99, - 0x3a, 0x10, 0x65, 0x6d, 0x0d, 0xd8, 0x1e, 0x71, 0xbd, 0x69, 0x15, 0x3f, 0xaf, 0x62, 0xf0, 0xf6, 0xac, 0x71, 0x4a, - 0xeb, 0x6b, 0x5c, 0x73, 0x5c, 0x15, 0x22, 0x6a, 0x8b, 0x14, 0x00, 0xc3, 0xce, 0x17, 0xb8, 0x33, 0x2b, 0x0c, 0xe6, - 0x84, 0xa5, 0x92, 0x9d, 0xca, 0xf5, 0xe7, 0xb0, 0xc5, 0x41, 0x2a, 0x5f, 0x7a, 0xfd, 0xfd, 0xcd, 0x17, 0x5f, 0xa0, - 0xdb, 0x9e, 0xf3, 0x23, 0x08, 0x32, 0x81, 0x0e, 0x6a, 0x4a, 0xf5, 0xf8, 0x4b, 0x01, 0xd4, 0x1e, 0xe6, 0xe1, 0x97, - 0x82, 0x89, 0xf8, 0x26, 0xbb, 0x8a, 0x2b, 0x59, 0x8c, 0x6e, 0xb8, 0x48, 0x65, 0x61, 0xa5, 0xc6, 0xc1, 0xe9, 0x6a, - 0x95, 0xf3, 0x00, 0x4c, 0xe5, 0x2d, 0xa3, 0x6c, 0x2b, 0xcb, 0xf4, 0xe0, 0x6a, 0x79, 0x7a, 0xa5, 0x45, 0xe7, 0xe5, - 0xcd, 0x55, 0x10, 0xe1, 0xaf, 0x0b, 0xf3, 0xe3, 0x3a, 0x2e, 0x3f, 0x06, 0x91, 0xb5, 0xa9, 0x33, 0x3f, 0x50, 0x2a, - 0x0f, 0xfe, 0x4e, 0x20, 0xd3, 0xfd, 0xa5, 0x00, 0xcb, 0x6c, 0x5b, 0xf1, 0x71, 0x8c, 0xb5, 0x0e, 0x27, 0x64, 0xa6, - 0x4a, 0xf4, 0xde, 0x25, 0xeb, 0x02, 0xac, 0xfd, 0x14, 0xb6, 0xb3, 0xca, 0x35, 0xc3, 0xca, 0x54, 0x45, 0x66, 0x56, - 0xd6, 0xec, 0x30, 0xb4, 0x4e, 0x34, 0x73, 0xf4, 0x16, 0xd0, 0x0f, 0xe4, 0xf0, 0x8a, 0x96, 0x6b, 0xe6, 0xf9, 0xd8, - 0x34, 0x5e, 0x3f, 0x3a, 0xbc, 0x72, 0x0b, 0xf6, 0xce, 0xde, 0xc9, 0x51, 0x98, 0x08, 0x9e, 0xc6, 0x66, 0x7c, 0x91, - 0x67, 0x05, 0xec, 0xa0, 0xc9, 0x78, 0x4c, 0xbd, 0xa5, 0xd5, 0xba, 0x39, 0x3a, 0x64, 0xdb, 0xec, 0x71, 0xf5, 0x98, - 0x93, 0x43, 0xde, 0x32, 0xb5, 0x6d, 0x5b, 0xc7, 0x79, 0x9a, 0x7c, 0x65, 0xba, 0x2f, 0xd6, 0x36, 0x42, 0xbc, 0x72, - 0x76, 0x74, 0x5e, 0xd2, 0xad, 0x6f, 0x4a, 0x43, 0xaf, 0x25, 0x00, 0xf3, 0x69, 0x03, 0xfe, 0x82, 0x95, 0xeb, 0x51, - 0xc5, 0xcb, 0x0a, 0x24, 0x2c, 0x28, 0xc2, 0x9b, 0x62, 0x6f, 0x0a, 0x77, 0xe3, 0xf4, 0x1c, 0x76, 0xe0, 0x62, 0x8a, - 0xee, 0x38, 0x31, 0x99, 0x95, 0x46, 0x2b, 0x1a, 0xe9, 0x5f, 0xae, 0x2f, 0xb1, 0xee, 0x8b, 0x56, 0xe6, 0xd9, 0x9c, - 0x0a, 0x9b, 0xde, 0x55, 0x2e, 0x9d, 0xa8, 0xdf, 0x32, 0xe1, 0xca, 0x95, 0x20, 0x20, 0xd3, 0x82, 0xf5, 0x0a, 0xb3, - 0x8b, 0xe4, 0x1a, 0x08, 0x19, 0x18, 0xbe, 0x06, 0x6b, 0x51, 0x72, 0x63, 0x05, 0xeb, 0xdd, 0xf3, 0x75, 0x82, 0x90, - 0x82, 0x07, 0x6e, 0x82, 0xbe, 0x6f, 0xdd, 0xbc, 0x1d, 0x25, 0xca, 0x20, 0x3e, 0xb9, 0x76, 0xca, 0x41, 0x02, 0x01, - 0x38, 0xb0, 0x2a, 0x24, 0x89, 0x02, 0x9d, 0x07, 0x57, 0x33, 0x8e, 0x60, 0xf3, 0xca, 0x99, 0x8b, 0x1b, 0xc0, 0x79, - 0xe5, 0xcf, 0x65, 0x83, 0x2d, 0xeb, 0x11, 0x55, 0xe6, 0x8c, 0x53, 0x0c, 0xea, 0x64, 0x09, 0xfa, 0xca, 0x52, 0xda, - 0x2b, 0xd0, 0x34, 0x5e, 0xb3, 0x95, 0xf2, 0x01, 0xa0, 0x17, 0x6c, 0xa5, 0x8c, 0xfd, 0xf1, 0xeb, 0x73, 0xb6, 0xd2, - 0xd2, 0xe0, 0xe9, 0xf5, 0xec, 0x62, 0x76, 0x3e, 0x60, 0x47, 0x51, 0xa8, 0x0d, 0x18, 0x02, 0x17, 0x99, 0x20, 0x18, - 0x84, 0x1a, 0xff, 0x65, 0xa0, 0x02, 0x84, 0x11, 0x8f, 0xc7, 0x46, 0x1c, 0xb1, 0x70, 0x3c, 0xc4, 0x60, 0x60, 0xcd, - 0x17, 0x24, 0x20, 0xd4, 0x94, 0x86, 0xbe, 0x9e, 0xe1, 0x70, 0x72, 0x30, 0x81, 0x54, 0xcc, 0xcc, 0x54, 0x61, 0x6c, - 0x4c, 0x22, 0x88, 0xff, 0xda, 0x59, 0x2f, 0x94, 0xdb, 0x5d, 0xa3, 0x81, 0xa0, 0x19, 0x7c, 0x56, 0xc5, 0x93, 0x83, - 0x61, 0x57, 0xc5, 0x38, 0x0a, 0x37, 0x46, 0xf9, 0x76, 0x7e, 0x0c, 0x60, 0xbe, 0xe7, 0x43, 0x5f, 0x2e, 0x71, 0x7e, - 0xf8, 0x84, 0x3c, 0x7e, 0x42, 0xe8, 0x39, 0x3b, 0xff, 0xe2, 0x09, 0x3d, 0x57, 0xe4, 0xe4, 0x60, 0x12, 0xdd, 0x30, - 0x8b, 0x81, 0x73, 0xa4, 0x9a, 0x40, 0xaf, 0x46, 0x6b, 0xa1, 0x16, 0x98, 0x76, 0x68, 0x0a, 0xbf, 0x19, 0x1f, 0x04, - 0x83, 0x9b, 0x76, 0xd3, 0x6f, 0xda, 0x6d, 0xf5, 0xbc, 0xba, 0x0e, 0x8e, 0xa2, 0xdd, 0x62, 0x26, 0x7f, 0x1f, 0x1f, - 0xb8, 0x39, 0xc0, 0xfa, 0x1e, 0x1e, 0x13, 0xd3, 0xa4, 0x9d, 0x51, 0xf1, 0x6b, 0xfa, 0x0a, 0xfb, 0xd0, 0x2c, 0xb2, - 0xa3, 0x0f, 0xc3, 0x7f, 0xab, 0x13, 0xf5, 0xf9, 0x17, 0x47, 0x40, 0x8e, 0x40, 0x06, 0x8a, 0x25, 0x82, 0x19, 0x0e, - 0x34, 0x05, 0x14, 0x64, 0x7a, 0xdc, 0xa9, 0x1e, 0x7e, 0x35, 0x6a, 0x6a, 0x46, 0x6e, 0x60, 0x6a, 0xb0, 0x2d, 0xf8, - 0x81, 0xea, 0x86, 0xfe, 0x46, 0xa3, 0x3d, 0x69, 0x27, 0x33, 0xf3, 0x92, 0xda, 0x38, 0x77, 0x37, 0x10, 0xd0, 0xd9, - 0xc1, 0x2d, 0x4a, 0xf6, 0xe5, 0xf1, 0xd5, 0x01, 0xae, 0x22, 0x40, 0x0d, 0x63, 0xc1, 0x97, 0x83, 0x2b, 0xbd, 0xb9, - 0x0f, 0x02, 0x32, 0xf8, 0x32, 0x38, 0xf9, 0x72, 0x20, 0x07, 0xc1, 0xf1, 0xe1, 0xd5, 0x49, 0xe0, 0x8c, 0xfb, 0x21, - 0xe4, 0xa5, 0xaa, 0x28, 0x66, 0xc2, 0x54, 0x91, 0xd8, 0xda, 0x73, 0x5b, 0xaf, 0x32, 0x3e, 0xa3, 0xe9, 0xd4, 0x22, - 0xa1, 0x87, 0x29, 0x8b, 0xcd, 0xef, 0x60, 0xc2, 0xaf, 0x83, 0xc8, 0x05, 0x85, 0x9d, 0xe5, 0x51, 0x4c, 0x97, 0xec, - 0x4e, 0x84, 0x29, 0x4d, 0x0e, 0x73, 0x42, 0xa2, 0x70, 0xa9, 0xc0, 0x04, 0xd5, 0xeb, 0x04, 0xe2, 0xda, 0xba, 0xcf, - 0xef, 0x44, 0xb8, 0xa4, 0xf9, 0x61, 0x42, 0x5a, 0x45, 0xb8, 0x08, 0x35, 0x9b, 0x9a, 0x5e, 0xb2, 0x70, 0x45, 0xaf, - 0x80, 0x99, 0x92, 0xeb, 0xf0, 0x0a, 0xb8, 0xbc, 0xf5, 0x7c, 0xb5, 0x60, 0x57, 0x0d, 0xe9, 0x9b, 0xe1, 0x8b, 0x2f, - 0xad, 0x4f, 0x1e, 0xf0, 0x90, 0xce, 0x0f, 0x2f, 0x05, 0x1b, 0x80, 0x9b, 0x8c, 0xdf, 0x7e, 0x2b, 0xef, 0xf4, 0xbc, - 0xb4, 0xa7, 0x18, 0x67, 0xa6, 0x9d, 0x98, 0xb4, 0x13, 0x72, 0xff, 0xbe, 0xed, 0xbb, 0x17, 0xaf, 0x95, 0xcb, 0xaa, - 0x65, 0x48, 0x8a, 0xb5, 0x72, 0x9d, 0x46, 0xc9, 0xa9, 0x15, 0x78, 0xb2, 0x4b, 0x5e, 0x25, 0x4b, 0xff, 0xa0, 0xb2, - 0x56, 0x03, 0xf6, 0x18, 0xb1, 0x2c, 0x14, 0x8e, 0xfd, 0xeb, 0x8c, 0x15, 0x6b, 0x5f, 0xa0, 0x11, 0x23, 0xf7, 0xf6, - 0x3a, 0x63, 0x5e, 0x0c, 0xda, 0x64, 0xed, 0x85, 0xee, 0xf3, 0xd2, 0xf3, 0x16, 0xef, 0xe5, 0x94, 0x1a, 0x46, 0x22, - 0x7a, 0x30, 0x56, 0x66, 0x94, 0x2a, 0x51, 0x6b, 0xd0, 0x88, 0x60, 0x63, 0x17, 0x0c, 0x14, 0x9c, 0x50, 0xb9, 0xa7, - 0xce, 0xf6, 0xed, 0x94, 0x4a, 0x0f, 0x68, 0x97, 0x1a, 0x55, 0xb9, 0x5b, 0x66, 0x92, 0x55, 0x83, 0x60, 0xf4, 0x47, - 0x29, 0xc5, 0x0c, 0xef, 0x8c, 0x2c, 0x98, 0x82, 0x95, 0xa0, 0xaa, 0x65, 0x58, 0x0e, 0x39, 0x6a, 0xf1, 0x8c, 0x4f, - 0xaa, 0xd4, 0x3f, 0x3a, 0x82, 0x06, 0x2f, 0xd7, 0xad, 0xa0, 0xc1, 0x4f, 0xc6, 0x4f, 0xf4, 0x40, 0xa7, 0x6b, 0xed, - 0x78, 0xe8, 0xf3, 0xdb, 0x88, 0x37, 0xae, 0x7b, 0x4f, 0xb5, 0x56, 0xa1, 0x0c, 0xb4, 0x58, 0x51, 0xb9, 0x52, 0x4b, - 0xba, 0xdf, 0x45, 0x00, 0x2c, 0x62, 0x63, 0x36, 0xde, 0xb5, 0xcd, 0x0a, 0x41, 0xa3, 0xcb, 0x4e, 0x36, 0xf1, 0x80, - 0x25, 0xba, 0xb5, 0x83, 0x09, 0x8d, 0x4f, 0x58, 0xd9, 0xef, 0xe7, 0x27, 0x40, 0x4f, 0xb5, 0x11, 0x53, 0x01, 0x47, - 0xfe, 0xe7, 0x56, 0x64, 0x8a, 0x02, 0x9b, 0x35, 0x75, 0xb7, 0xc6, 0x32, 0x12, 0x7d, 0x99, 0xd2, 0xe5, 0x09, 0xcf, - 0x80, 0x69, 0xbd, 0x6e, 0x39, 0xae, 0xec, 0x2a, 0x8e, 0x3c, 0x15, 0x96, 0x15, 0xe7, 0x55, 0x38, 0xde, 0x7a, 0x7c, - 0x83, 0x43, 0xc3, 0xa6, 0x5d, 0xfa, 0x43, 0x08, 0x0b, 0xe1, 0x75, 0x06, 0xb7, 0x11, 0x6d, 0x27, 0x81, 0xca, 0x1b, - 0x73, 0x9d, 0x50, 0x36, 0xb7, 0xeb, 0xb5, 0x67, 0x90, 0x4e, 0xcc, 0x81, 0x52, 0x8d, 0xa0, 0x35, 0x9a, 0x05, 0x55, - 0x23, 0x1e, 0x39, 0x1e, 0xde, 0x19, 0xc4, 0x6a, 0xf9, 0x92, 0xa6, 0x52, 0x34, 0x00, 0xe3, 0x02, 0xb8, 0x3c, 0xfd, - 0xfc, 0xfe, 0xc7, 0x33, 0x1e, 0x17, 0xc9, 0xf2, 0x5d, 0x5c, 0xc4, 0xd7, 0x65, 0xb8, 0x51, 0x63, 0x14, 0xd7, 0x64, - 0x2a, 0x06, 0x4c, 0x9a, 0x95, 0xd4, 0xdc, 0x95, 0x9a, 0x10, 0x63, 0x9d, 0xc9, 0xba, 0xac, 0xe4, 0x75, 0xa3, 0xd2, - 0x75, 0x91, 0xe1, 0xc7, 0x2d, 0x9f, 0xd3, 0x43, 0x00, 0x36, 0x35, 0x2e, 0xa4, 0x91, 0xd4, 0x85, 0x18, 0x73, 0x11, - 0xaf, 0xeb, 0xe3, 0x71, 0xa3, 0xeb, 0x25, 0x7b, 0x3a, 0xfe, 0x6a, 0xfa, 0x3a, 0x0b, 0xb3, 0x81, 0x20, 0xa3, 0x6a, - 0xc9, 0x45, 0xcb, 0x94, 0x53, 0x99, 0x04, 0xa0, 0x8f, 0x67, 0x8f, 0xb1, 0xa3, 0xf1, 0x98, 0x6c, 0xda, 0xe2, 0x01, - 0x1e, 0x2e, 0xd7, 0x61, 0x41, 0x66, 0xba, 0x8e, 0x28, 0x10, 0xfc, 0xae, 0x0a, 0x00, 0xd9, 0xd2, 0x56, 0x65, 0xb8, - 0x34, 0xf6, 0x74, 0x3c, 0xa1, 0x12, 0xbb, 0x1d, 0x92, 0xda, 0xab, 0xd0, 0xcd, 0xbc, 0xf4, 0x3d, 0x8a, 0xa4, 0x71, - 0x59, 0xda, 0xa9, 0x54, 0xaa, 0x3d, 0x33, 0x73, 0x5d, 0x83, 0x98, 0x14, 0xa1, 0xae, 0xbb, 0xf4, 0xea, 0xde, 0x6d, - 0xae, 0x35, 0xdb, 0x01, 0xef, 0x35, 0x68, 0x86, 0x92, 0xb7, 0x98, 0xb7, 0xae, 0x88, 0x9a, 0xae, 0xd6, 0x60, 0x56, - 0x8c, 0xb2, 0xa5, 0x28, 0x5d, 0x53, 0x50, 0x0a, 0x46, 0x97, 0x6b, 0x6f, 0xe1, 0xbe, 0x96, 0x8d, 0x0b, 0x4b, 0xa6, - 0x57, 0x8b, 0x92, 0x12, 0xaa, 0x9b, 0x8a, 0x91, 0x12, 0x46, 0x4a, 0xc3, 0x53, 0xf9, 0x5e, 0xe0, 0x71, 0x9e, 0x07, - 0x51, 0xcb, 0x0b, 0xec, 0xb4, 0x22, 0xa7, 0xe0, 0xe8, 0x65, 0x72, 0x1a, 0x0a, 0xfc, 0x43, 0xa6, 0x40, 0x5d, 0x87, - 0xea, 0x7e, 0x83, 0x9b, 0xff, 0x9f, 0x05, 0x0b, 0x3c, 0xbe, 0xf5, 0x0a, 0xb7, 0xd1, 0x3f, 0x0b, 0x9f, 0x96, 0x3e, - 0x93, 0xbe, 0xab, 0x8b, 0x27, 0xed, 0xcd, 0x46, 0xc9, 0x32, 0xcb, 0xd3, 0x37, 0x32, 0xe5, 0x20, 0x32, 0x43, 0x6b, - 0x50, 0x76, 0x22, 0x1a, 0x37, 0x3c, 0x30, 0x62, 0x6c, 0xdc, 0xf8, 0x7e, 0xc8, 0x40, 0x36, 0x0c, 0x56, 0xdf, 0x2c, - 0x95, 0xc9, 0x1a, 0x10, 0x36, 0xb4, 0xfc, 0x44, 0xe3, 0x6d, 0x84, 0xfa, 0xfa, 0x05, 0x6e, 0x73, 0xa5, 0xef, 0x73, - 0xfe, 0x43, 0x46, 0x7f, 0x40, 0xe0, 0x97, 0x78, 0x05, 0x72, 0x8f, 0x67, 0x50, 0x37, 0xc2, 0xf6, 0x72, 0x0c, 0x96, - 0x84, 0xe8, 0x28, 0xa2, 0x62, 0x81, 0x82, 0xa6, 0x30, 0x88, 0x22, 0xea, 0x82, 0x39, 0xbc, 0xc8, 0x65, 0xf2, 0x71, - 0x6a, 0x7c, 0xe6, 0x87, 0x31, 0xc6, 0x90, 0x0e, 0x06, 0x61, 0x35, 0x0b, 0x86, 0xe3, 0xd1, 0xe4, 0xe8, 0x29, 0x9c, - 0xdb, 0xc1, 0x38, 0x20, 0x83, 0xa0, 0x2e, 0x57, 0xb1, 0xa0, 0xe5, 0xcd, 0x95, 0x2d, 0x03, 0x3f, 0xae, 0x83, 0xc1, - 0x3f, 0x0b, 0x4f, 0xf1, 0x0e, 0x9a, 0x93, 0x73, 0x19, 0x82, 0x8d, 0xfd, 0x9a, 0x80, 0xa4, 0xac, 0xa7, 0xf9, 0x49, - 0x7d, 0xb8, 0x31, 0xa5, 0xfd, 0x33, 0x87, 0x17, 0x1c, 0x76, 0x48, 0xa0, 0x40, 0x1a, 0x4f, 0xb3, 0xd1, 0x2b, 0xa5, - 0xc8, 0x7d, 0x57, 0x70, 0xb8, 0x33, 0xf7, 0x9c, 0xe9, 0x91, 0x53, 0x48, 0x34, 0xb3, 0x80, 0x1b, 0xf9, 0x2b, 0x71, - 0x13, 0xe7, 0x59, 0x7a, 0xd0, 0x7c, 0x73, 0x50, 0xde, 0x8b, 0x2a, 0xbe, 0x1b, 0x05, 0xc6, 0x9a, 0x90, 0xfb, 0xaa, - 0x27, 0x40, 0x4f, 0x80, 0x2d, 0x00, 0x06, 0xc4, 0x3b, 0x66, 0x26, 0x33, 0x1e, 0x81, 0x47, 0x60, 0xd3, 0x07, 0xb2, - 0xb8, 0x77, 0x2e, 0x49, 0xfe, 0x66, 0x2a, 0xed, 0x55, 0xaf, 0xdc, 0x29, 0xc8, 0x7a, 0xb5, 0x95, 0xbb, 0x6e, 0x7d, - 0xf6, 0x4d, 0x87, 0x57, 0xe0, 0x85, 0x04, 0xb7, 0xc8, 0x7e, 0xbf, 0x29, 0xa8, 0x14, 0x46, 0x45, 0xbc, 0x93, 0x5c, - 0xa3, 0x7f, 0xbb, 0x37, 0x36, 0x8a, 0xe4, 0x96, 0x0f, 0x0f, 0xa0, 0xce, 0xe4, 0x5d, 0x71, 0x3b, 0x87, 0xa8, 0xad, - 0xbb, 0xf1, 0xc0, 0x7b, 0x83, 0x76, 0x59, 0x73, 0x04, 0x5b, 0x5e, 0x1c, 0x64, 0x30, 0x16, 0x38, 0x2b, 0x23, 0xa5, - 0xc6, 0x35, 0xa4, 0x16, 0x7c, 0x92, 0xa7, 0x7b, 0xc8, 0x52, 0x4f, 0x82, 0x22, 0xc7, 0xb3, 0x18, 0x32, 0x8d, 0xb7, - 0x81, 0xd8, 0xef, 0x65, 0x08, 0xd2, 0xb4, 0xed, 0xb6, 0x39, 0x02, 0x65, 0xf7, 0xc0, 0x94, 0xa4, 0xae, 0x8d, 0xa9, - 0x81, 0x86, 0x1e, 0x44, 0x8d, 0x54, 0xc4, 0xd9, 0xc9, 0x6b, 0xd0, 0x21, 0x82, 0xef, 0x77, 0x9a, 0x95, 0x1d, 0x2f, - 0x26, 0x04, 0x4f, 0xde, 0x17, 0x77, 0x59, 0x59, 0x95, 0xd1, 0xfb, 0x14, 0x0d, 0xa1, 0x12, 0x29, 0xa2, 0x97, 0x10, - 0x5f, 0xb0, 0xc4, 0xdf, 0x65, 0xf4, 0x63, 0x4a, 0xe3, 0x34, 0xc5, 0xf4, 0xe7, 0x05, 0xfc, 0x7c, 0x06, 0x28, 0x97, - 0xb8, 0x13, 0xa2, 0x0b, 0x09, 0xf6, 0x6a, 0x10, 0xdd, 0xab, 0xe2, 0x80, 0x29, 0x1a, 0xdd, 0x09, 0x8a, 0x98, 0x75, - 0x98, 0xfd, 0xfb, 0x02, 0x85, 0x42, 0xaa, 0x98, 0x5f, 0x84, 0x7d, 0x88, 0x7e, 0xc0, 0x22, 0x4f, 0xdf, 0xbd, 0x32, - 0x43, 0x1a, 0xdd, 0x4b, 0xaa, 0xb7, 0x36, 0x1e, 0x5b, 0x88, 0xd2, 0x13, 0x5d, 0xad, 0xe9, 0x79, 0xbc, 0xca, 0xa2, - 0x0d, 0xe0, 0x4f, 0xbc, 0x7b, 0xf5, 0x4c, 0x59, 0x98, 0x3c, 0xcf, 0x40, 0x71, 0x70, 0xfa, 0xee, 0xd5, 0x6b, 0x99, - 0xae, 0x73, 0x1e, 0x9d, 0x4b, 0x24, 0xad, 0xa7, 0xef, 0x5e, 0xfd, 0x84, 0xe6, 0x5e, 0x3f, 0x16, 0xf0, 0xfe, 0x25, - 0xf0, 0x96, 0x51, 0xbc, 0x86, 0x3e, 0xa9, 0xdf, 0xc9, 0x1a, 0x3b, 0xe5, 0xd5, 0x5a, 0x46, 0x3f, 0xa7, 0xb5, 0x27, - 0xad, 0xfa, 0x57, 0xe1, 0x53, 0x3b, 0x4f, 0xc0, 0x73, 0x97, 0x67, 0xe2, 0x63, 0x64, 0x45, 0x3b, 0x41, 0xf4, 0xe5, - 0xc1, 0xdd, 0x75, 0x2e, 0xca, 0x08, 0x5f, 0x30, 0xb4, 0x0b, 0x8a, 0x0e, 0x0f, 0x6f, 0x6f, 0x6f, 0x47, 0xb7, 0x5f, - 0x8d, 0x64, 0x71, 0x75, 0x38, 0xf9, 0xe6, 0x9b, 0x6f, 0x0e, 0xf1, 0x6d, 0xf0, 0x65, 0xdb, 0xed, 0xbd, 0x22, 0x7c, - 0xc0, 0x02, 0x44, 0xec, 0xfe, 0x12, 0xae, 0x28, 0xa0, 0x85, 0x1b, 0x7c, 0x19, 0x7c, 0xa9, 0x0f, 0x9d, 0x2f, 0x8f, - 0xcb, 0x9b, 0x2b, 0x55, 0x7e, 0x57, 0xc9, 0x47, 0xe3, 0xf1, 0xf8, 0x10, 0x24, 0x50, 0x5f, 0x0e, 0xf8, 0x20, 0x38, - 0x09, 0x06, 0x19, 0x5c, 0x68, 0xca, 0x9b, 0xab, 0x93, 0xc0, 0x33, 0xcd, 0x6d, 0xb0, 0x88, 0x0e, 0xc4, 0x25, 0x38, - 0xbc, 0xa2, 0xc1, 0x97, 0x01, 0x71, 0x29, 0x5f, 0x40, 0xca, 0x17, 0x47, 0x4f, 0xfd, 0xb4, 0xff, 0xa5, 0xd2, 0xbe, - 0xf2, 0xd3, 0x8e, 0x31, 0xed, 0xab, 0x67, 0x7e, 0xda, 0x89, 0x4a, 0x7b, 0xe1, 0xa7, 0xfd, 0xef, 0x72, 0x00, 0xa9, - 0x07, 0xbe, 0xf5, 0xdf, 0x85, 0xd7, 0x1a, 0x3c, 0x85, 0xa2, 0xec, 0x3a, 0xbe, 0xe2, 0xd0, 0xe8, 0xc1, 0xdd, 0x75, - 0x4e, 0x83, 0x01, 0xb6, 0xd7, 0x33, 0x09, 0xf1, 0x3e, 0xf8, 0x72, 0x5d, 0xe4, 0x61, 0xf0, 0xe5, 0x00, 0x0b, 0x19, - 0x7c, 0x19, 0x90, 0x2f, 0x8d, 0x81, 0x8c, 0x60, 0x9b, 0xc0, 0x85, 0x66, 0x1d, 0xda, 0x80, 0x69, 0xbe, 0x34, 0xae, - 0xa6, 0x7f, 0x16, 0xdd, 0xd9, 0xf0, 0x96, 0xa8, 0xdc, 0x74, 0x83, 0x9a, 0xbe, 0x05, 0xef, 0x04, 0x68, 0x54, 0x14, - 0xdc, 0xc4, 0x45, 0x38, 0x1c, 0x96, 0x37, 0x57, 0x04, 0xec, 0x32, 0x57, 0x3c, 0xae, 0xa2, 0x40, 0xc8, 0xa1, 0xfa, - 0x19, 0xa8, 0x48, 0x60, 0x01, 0x42, 0x19, 0xc1, 0x7f, 0x41, 0x4d, 0xdf, 0x49, 0xb6, 0x09, 0x86, 0xb7, 0xfc, 0xe2, - 0x63, 0x56, 0x0d, 0x95, 0x68, 0xf1, 0x46, 0x50, 0xf8, 0x01, 0x7f, 0x5d, 0xd5, 0xd1, 0x9f, 0xe0, 0xc6, 0xdd, 0xd4, - 0xb0, 0xbf, 0x93, 0x8e, 0x45, 0x7d, 0x27, 0xe7, 0xd9, 0x62, 0xda, 0x3a, 0xd0, 0xdf, 0x4a, 0x52, 0xcd, 0xb3, 0x41, - 0x30, 0x0c, 0x06, 0x7c, 0xc1, 0xde, 0xca, 0x39, 0xf7, 0xcc, 0xa7, 0x1e, 0x49, 0x7f, 0x9a, 0x67, 0xd9, 0x00, 0x7c, - 0x53, 0x90, 0x1f, 0x39, 0xfc, 0xef, 0xf9, 0x10, 0x85, 0x87, 0x83, 0x47, 0x87, 0x64, 0x16, 0xac, 0xee, 0xd0, 0xa3, - 0x33, 0x0a, 0x32, 0xb1, 0xe4, 0x45, 0x56, 0x79, 0x4b, 0xe5, 0x7e, 0xdd, 0xf6, 0xf2, 0xd8, 0x7b, 0x36, 0xaf, 0x62, - 0x11, 0xa8, 0x73, 0x0e, 0x14, 0x6f, 0x28, 0x7b, 0x2a, 0x9b, 0x12, 0x52, 0x6d, 0xc8, 0x1b, 0x96, 0x03, 0x16, 0x1c, - 0xf7, 0x86, 0xc3, 0x83, 0x60, 0xe0, 0xd4, 0xb9, 0x83, 0xe0, 0x60, 0x38, 0x3c, 0x09, 0xdc, 0x7d, 0x28, 0x1b, 0xb9, - 0x3b, 0x23, 0x2d, 0xd8, 0xbf, 0x8a, 0xb0, 0xa4, 0x20, 0x1e, 0x93, 0x5a, 0xfc, 0xa5, 0xc1, 0x65, 0x06, 0x00, 0x7d, - 0xa4, 0x24, 0x60, 0x06, 0x56, 0x66, 0x00, 0xa1, 0xca, 0x69, 0xcc, 0xce, 0x81, 0x79, 0x04, 0x8e, 0x59, 0xc1, 0x64, - 0x01, 0x62, 0x49, 0x80, 0x73, 0x17, 0x44, 0xb1, 0x2e, 0xe4, 0x11, 0x04, 0x01, 0xc0, 0x9f, 0xc4, 0x94, 0x82, 0x49, - 0x3a, 0x76, 0x23, 0x08, 0xe2, 0xf8, 0xec, 0x46, 0xb4, 0x26, 0x67, 0x89, 0x0e, 0x66, 0x24, 0x01, 0x36, 0xc4, 0xc0, - 0xf0, 0xc1, 0xfd, 0x1c, 0x94, 0x1e, 0x56, 0xef, 0x84, 0x5c, 0xf0, 0x3d, 0xf7, 0x64, 0xb3, 0x70, 0xf5, 0x84, 0x83, - 0xe0, 0x9e, 0x6b, 0x16, 0x60, 0x54, 0x15, 0xeb, 0xb2, 0xe2, 0xe9, 0x87, 0xfb, 0x15, 0xc4, 0x02, 0xc4, 0x01, 0x7d, - 0x27, 0xf3, 0x2c, 0xb9, 0x0f, 0x9d, 0x3d, 0xd7, 0x46, 0xa5, 0x7f, 0xff, 0xe1, 0xf5, 0x8f, 0x11, 0x88, 0x1c, 0x6b, - 0x43, 0xe9, 0xef, 0x39, 0x9e, 0x4d, 0x7e, 0xc4, 0x2b, 0x7f, 0x63, 0xdf, 0x73, 0x7b, 0x7a, 0xf4, 0xfb, 0x50, 0x37, - 0xbd, 0xe7, 0xb3, 0x7b, 0x3e, 0x72, 0xc5, 0xa1, 0xba, 0xc2, 0x7d, 0x7d, 0xbb, 0xf6, 0x8d, 0x90, 0x1e, 0x9e, 0x67, - 0xca, 0x1b, 0xf3, 0xa3, 0x1d, 0x0c, 0x83, 0x60, 0xaa, 0x85, 0x92, 0x10, 0x85, 0x84, 0x29, 0x01, 0x43, 0x74, 0xa0, - 0x97, 0xd5, 0x14, 0x39, 0x37, 0x35, 0xb2, 0xf0, 0x7e, 0xc0, 0xb4, 0xd0, 0xa1, 0x91, 0x43, 0xf9, 0xc1, 0xe1, 0x84, - 0x31, 0x0b, 0xbf, 0x55, 0xc2, 0xf4, 0xab, 0x45, 0xe5, 0x1c, 0x44, 0x0f, 0xc0, 0x18, 0x57, 0xf0, 0x02, 0xba, 0xc2, - 0x6e, 0xd6, 0x2a, 0x4a, 0x08, 0x82, 0xe9, 0x21, 0x07, 0xe8, 0x61, 0x17, 0xb4, 0xac, 0x2c, 0xd5, 0xad, 0xca, 0x59, - 0xaa, 0xa8, 0xcb, 0x50, 0x56, 0xc6, 0x0a, 0x03, 0xbf, 0x64, 0xdf, 0x17, 0xe8, 0x59, 0x3e, 0x15, 0x5d, 0xf0, 0x42, - 0x28, 0xc1, 0x72, 0x5d, 0xef, 0x44, 0x20, 0xea, 0xfc, 0xd0, 0xbb, 0xea, 0x6b, 0x5c, 0x3f, 0x9e, 0xbe, 0x96, 0x29, - 0xd7, 0x26, 0x14, 0x9a, 0xcf, 0x97, 0xbe, 0x62, 0xa2, 0x60, 0xb7, 0xd0, 0xaf, 0xb6, 0x8d, 0x3e, 0xbb, 0x5f, 0xeb, - 0xcd, 0xa0, 0x44, 0xc7, 0xbc, 0x46, 0xc1, 0xb5, 0x52, 0x28, 0x18, 0xed, 0x6d, 0xfc, 0x09, 0x8e, 0xdc, 0xea, 0xf6, - 0xd0, 0xfb, 0xad, 0x8a, 0xaf, 0xde, 0xa0, 0x6f, 0xa7, 0xfd, 0x39, 0xaa, 0xe4, 0xcf, 0xab, 0x15, 0xf8, 0x50, 0x41, - 0xa4, 0x15, 0x8b, 0xd3, 0x0b, 0xf5, 0x9c, 0xbd, 0x3b, 0x7d, 0x03, 0x7e, 0x94, 0xf8, 0xfb, 0x97, 0xef, 0x82, 0x9a, - 0x4c, 0xe3, 0x59, 0x61, 0x3e, 0xb4, 0x39, 0x20, 0x54, 0x8b, 0x4b, 0xb3, 0xef, 0x67, 0x71, 0x93, 0x7d, 0xd7, 0x6c, - 0x3d, 0x2d, 0x9a, 0x48, 0x52, 0x86, 0xdb, 0x07, 0x03, 0x02, 0x7d, 0x80, 0x28, 0xce, 0xbe, 0xa0, 0x31, 0xa4, 0xf9, - 0xcc, 0xbe, 0x1f, 0x21, 0xf0, 0xc5, 0x4e, 0x48, 0x35, 0xae, 0xb0, 0x68, 0xf4, 0x90, 0xcf, 0x78, 0xa4, 0x0c, 0x8b, - 0xde, 0x63, 0x02, 0x71, 0x86, 0xd3, 0xea, 0x3d, 0x62, 0x40, 0xe3, 0xdd, 0x40, 0xcb, 0x1e, 0xa2, 0x8c, 0xba, 0xec, - 0x0d, 0x8b, 0xef, 0x8f, 0xeb, 0x30, 0xb3, 0x96, 0x97, 0x43, 0xf8, 0x1b, 0x68, 0x03, 0x70, 0xca, 0x91, 0xe5, 0xab, - 0xcc, 0x46, 0x57, 0x4b, 0x4c, 0x6f, 0x22, 0x88, 0x4d, 0xa4, 0xd3, 0x61, 0xed, 0xea, 0x54, 0xbd, 0xab, 0x9d, 0xcf, - 0x44, 0xaf, 0x02, 0xad, 0x5c, 0xdb, 0x1e, 0x0f, 0xe1, 0x3f, 0xb5, 0xb4, 0xc2, 0x46, 0xd8, 0x73, 0xf1, 0x85, 0xe7, - 0xd8, 0x9c, 0x80, 0x06, 0xd7, 0x32, 0x05, 0xe0, 0x2c, 0xad, 0x46, 0xa3, 0x46, 0xd8, 0x67, 0xe5, 0x7c, 0x0e, 0x5b, - 0x0b, 0xf1, 0xb4, 0x00, 0x1c, 0xb8, 0x89, 0xc9, 0xc9, 0xbb, 0x31, 0x39, 0xa7, 0x1f, 0x15, 0xdc, 0x77, 0x70, 0x5e, - 0x2e, 0xe3, 0x54, 0xde, 0x02, 0x36, 0x65, 0xe0, 0xa7, 0x62, 0xa9, 0x5e, 0x42, 0xb2, 0xe4, 0xc9, 0x47, 0xb4, 0xda, - 0x48, 0x03, 0xe0, 0x2a, 0xa7, 0xc6, 0x72, 0x4f, 0x81, 0xa6, 0xba, 0x52, 0x54, 0x42, 0x5c, 0x55, 0x71, 0xb2, 0x3c, - 0xc3, 0xd4, 0x70, 0x03, 0xbd, 0x88, 0x02, 0xb9, 0xe2, 0x02, 0x48, 0x7a, 0xce, 0x7e, 0xcb, 0x34, 0xf6, 0xfa, 0x33, - 0x89, 0x02, 0x26, 0x8d, 0xa2, 0x8c, 0x95, 0xb2, 0x17, 0xd2, 0x44, 0xbf, 0x0b, 0x82, 0xda, 0xbd, 0xfc, 0x13, 0xea, - 0x7e, 0x06, 0xad, 0x08, 0x1b, 0xe0, 0x85, 0x1a, 0xfc, 0x30, 0xb5, 0x4b, 0xce, 0x03, 0x32, 0x74, 0xde, 0x67, 0xb5, - 0xdd, 0xea, 0xcf, 0x96, 0x80, 0xf5, 0x9a, 0x1a, 0x9f, 0xc2, 0x30, 0x21, 0x26, 0x56, 0xb2, 0x55, 0x56, 0xda, 0x0d, - 0x65, 0xda, 0x49, 0x97, 0xcc, 0x6b, 0xe1, 0x34, 0xef, 0x31, 0xb6, 0x1c, 0xa9, 0xdc, 0xfd, 0x7e, 0x68, 0x7e, 0xb2, - 0x9c, 0x3e, 0xd3, 0x21, 0xac, 0xbd, 0xf1, 0xa0, 0x39, 0xd1, 0xea, 0xaa, 0x8e, 0x7e, 0x40, 0x07, 0x60, 0xa6, 0x2d, - 0x42, 0xa5, 0x0b, 0xbe, 0xed, 0x2b, 0x51, 0x71, 0x49, 0xc2, 0x52, 0x49, 0x60, 0x67, 0x37, 0x25, 0x3b, 0x9b, 0x80, - 0x78, 0x86, 0xbb, 0x9e, 0x16, 0x3b, 0x21, 0x4d, 0x78, 0x8b, 0x83, 0x04, 0x44, 0x1d, 0xaa, 0xba, 0x84, 0x6c, 0x8c, - 0xa1, 0x8b, 0x7f, 0x51, 0x0a, 0x13, 0xd6, 0x32, 0xa9, 0x4a, 0x4c, 0x50, 0xa8, 0x72, 0xb7, 0x45, 0x60, 0x89, 0x82, - 0x1d, 0xc0, 0xde, 0xbb, 0x51, 0x37, 0xa3, 0xa6, 0xaa, 0x53, 0x2f, 0xc1, 0xc7, 0x69, 0xd6, 0x55, 0x90, 0x59, 0xd8, - 0x55, 0xb1, 0xe6, 0x81, 0x8e, 0xd5, 0xa5, 0x8c, 0x89, 0xbb, 0xb4, 0xc8, 0x10, 0x1f, 0x19, 0x63, 0x0b, 0x6b, 0x38, - 0xd2, 0xf6, 0xb8, 0xe9, 0x09, 0x42, 0x3f, 0x61, 0x43, 0x09, 0xdc, 0x74, 0xb6, 0xa7, 0xa6, 0x99, 0x0f, 0x88, 0x38, - 0x0c, 0x28, 0x90, 0x6c, 0x1c, 0xd2, 0x1c, 0xe9, 0x0b, 0x92, 0x26, 0x0c, 0x94, 0xad, 0x78, 0x4e, 0x90, 0x15, 0x85, - 0x9e, 0xad, 0xab, 0x36, 0xce, 0x95, 0x61, 0x8e, 0x96, 0x9c, 0x0a, 0x4f, 0x13, 0x64, 0x62, 0x7b, 0xda, 0x66, 0x26, - 0xc3, 0x51, 0xb2, 0xc0, 0xfc, 0x0a, 0xa2, 0xc4, 0x9d, 0x69, 0x56, 0xe5, 0x60, 0x5c, 0xc0, 0x02, 0xad, 0x7c, 0x0f, - 0xea, 0xc6, 0x1a, 0xda, 0x68, 0x58, 0x66, 0xb7, 0x3f, 0xc1, 0x7e, 0xad, 0x9d, 0xd6, 0x65, 0x8a, 0xe5, 0x65, 0x0a, - 0xd1, 0x5e, 0xc8, 0xfc, 0x46, 0x91, 0xe8, 0x4e, 0x11, 0x86, 0x84, 0x75, 0x94, 0x3d, 0x69, 0x53, 0x03, 0xe8, 0xa9, - 0x17, 0x00, 0xbe, 0x73, 0x2d, 0xc3, 0x2e, 0xd2, 0xfd, 0x55, 0xc1, 0xb8, 0x74, 0x83, 0x20, 0x45, 0x6f, 0x52, 0x30, - 0xe7, 0xf5, 0x28, 0xa9, 0x37, 0xa7, 0x2d, 0x33, 0xaa, 0x8e, 0x8a, 0x90, 0x72, 0x82, 0xff, 0xe4, 0x95, 0xd4, 0xc4, - 0x26, 0x4c, 0xf0, 0xc0, 0x87, 0x79, 0x86, 0x0d, 0xbc, 0xdd, 0xbe, 0x4b, 0xc3, 0xa4, 0xcd, 0x36, 0xa4, 0x20, 0xad, - 0x30, 0x71, 0x42, 0xa0, 0xb2, 0x57, 0xb8, 0x5f, 0xb0, 0x9d, 0x34, 0x05, 0x0f, 0xc2, 0x46, 0x03, 0x13, 0xb7, 0xba, - 0xf8, 0x3a, 0x4c, 0x68, 0xb8, 0xa4, 0xda, 0xd9, 0x49, 0x4b, 0x9a, 0xdb, 0xeb, 0xf2, 0xd2, 0xf6, 0x41, 0xc7, 0x52, - 0xeb, 0x1a, 0x1e, 0x68, 0x5e, 0xb3, 0x8b, 0x2b, 0xa6, 0x69, 0xa2, 0xb1, 0x1e, 0x52, 0x96, 0x1c, 0xeb, 0x7a, 0xba, - 0xc2, 0xd5, 0x32, 0xd3, 0x40, 0xf7, 0x12, 0x2f, 0xf4, 0x80, 0x0f, 0x1e, 0xae, 0x48, 0x74, 0x89, 0xcd, 0x66, 0xab, - 0x9a, 0x4c, 0xf3, 0x7d, 0xd9, 0x72, 0x13, 0x20, 0xcf, 0x52, 0xdf, 0xdc, 0x27, 0xc7, 0x9a, 0xb6, 0xf9, 0x49, 0x80, - 0x6b, 0xee, 0x15, 0x90, 0x74, 0x2c, 0x41, 0x17, 0xef, 0xd3, 0x1f, 0x44, 0x6a, 0xa6, 0x82, 0xee, 0x9d, 0x2f, 0x52, - 0x37, 0xbf, 0x00, 0xdb, 0xa8, 0x8d, 0x31, 0xcd, 0xca, 0xd6, 0x61, 0xa2, 0x2c, 0xac, 0x91, 0x85, 0x5c, 0x82, 0x0f, - 0xe6, 0x6e, 0x53, 0xa7, 0xa7, 0x1d, 0x44, 0xd8, 0xef, 0xa2, 0xc7, 0x23, 0x8c, 0x15, 0x6b, 0x90, 0x18, 0x56, 0x61, - 0x4d, 0x9b, 0xcb, 0x21, 0xca, 0xa9, 0x59, 0x32, 0xd1, 0x92, 0xfa, 0x94, 0x22, 0x4a, 0xc1, 0xdc, 0x78, 0x5a, 0x36, - 0x4c, 0x09, 0x11, 0xb2, 0x42, 0x3a, 0xa0, 0x5a, 0x0b, 0x2d, 0xd5, 0x04, 0x01, 0x0f, 0xbd, 0x2c, 0x34, 0xa6, 0x20, - 0xfa, 0x88, 0x0c, 0x37, 0xe2, 0xc8, 0xe8, 0xee, 0x18, 0xc5, 0x04, 0x42, 0x77, 0x7b, 0x79, 0x61, 0xf5, 0x69, 0xd9, - 0x56, 0x07, 0x71, 0x8d, 0x69, 0xb2, 0x87, 0xa0, 0xc6, 0x28, 0x68, 0x73, 0xba, 0xd1, 0x9f, 0x8b, 0xd0, 0xb7, 0x0b, - 0xc7, 0x6e, 0x14, 0x44, 0x42, 0x44, 0x5a, 0xaf, 0xa9, 0x18, 0xa0, 0x76, 0x1e, 0xbb, 0x88, 0x55, 0xba, 0x5b, 0x88, - 0xf2, 0x46, 0x65, 0xfd, 0x71, 0x1d, 0x92, 0xed, 0x16, 0xcb, 0x02, 0x5f, 0xf6, 0xb3, 0xf5, 0x1e, 0x08, 0xf4, 0xd7, - 0xeb, 0x4f, 0x42, 0xa0, 0xbf, 0xca, 0x3e, 0x07, 0x02, 0xfd, 0xf5, 0xfa, 0x7f, 0x1a, 0x02, 0xfd, 0x6c, 0xed, 0x41, - 0xa0, 0xab, 0xc1, 0xf8, 0xb5, 0x60, 0xc1, 0xdb, 0x37, 0x01, 0x7d, 0x2e, 0x59, 0xf0, 0xf6, 0xe5, 0x4b, 0xdf, 0x08, - 0x44, 0x68, 0x24, 0x7f, 0x23, 0x0b, 0x46, 0xdc, 0x16, 0x78, 0x85, 0x5a, 0x27, 0x1f, 0xa8, 0x28, 0x03, 0x20, 0xfa, - 0xf2, 0x9f, 0x59, 0xb5, 0x0c, 0x83, 0xc3, 0x80, 0xcc, 0x1c, 0x24, 0xe8, 0x70, 0x02, 0xb7, 0x37, 0x28, 0xe5, 0xbb, - 0xcf, 0x42, 0x53, 0x1f, 0x8d, 0x46, 0x71, 0x71, 0x85, 0x77, 0x3a, 0xb3, 0x8f, 0x10, 0xef, 0x38, 0xe3, 0xa5, 0x8d, - 0x98, 0xb1, 0x8c, 0xcb, 0x73, 0x1d, 0xaa, 0xa6, 0xb4, 0x3b, 0xb1, 0x5c, 0xca, 0xdb, 0x73, 0x80, 0xed, 0xb7, 0x5b, - 0x33, 0xc6, 0x6e, 0x28, 0x86, 0x58, 0xc7, 0xd3, 0x7d, 0xb6, 0xd6, 0xef, 0x2e, 0xe2, 0x92, 0xbf, 0x8b, 0xab, 0x25, - 0x83, 0x4e, 0xea, 0xed, 0x5a, 0xc8, 0xf5, 0xca, 0x55, 0x72, 0xbe, 0x16, 0x1f, 0x85, 0xbc, 0x15, 0x6a, 0x53, 0x9d, - 0xf3, 0x1b, 0x68, 0x11, 0xdb, 0xa0, 0x32, 0x42, 0xf0, 0xa4, 0xf2, 0x58, 0x2c, 0x05, 0xf2, 0x9e, 0x51, 0x03, 0xf3, - 0xde, 0x91, 0x83, 0x86, 0x76, 0x10, 0xb5, 0xc7, 0xb0, 0x91, 0x45, 0x67, 0x60, 0xe2, 0xf8, 0x02, 0x4a, 0x07, 0x28, - 0x6e, 0x88, 0x03, 0x01, 0x77, 0x0a, 0xe4, 0x79, 0x1b, 0x50, 0x2c, 0xb4, 0xf4, 0xfd, 0x40, 0xd4, 0x19, 0x6a, 0x60, - 0x0c, 0x1b, 0xc3, 0x84, 0xf7, 0x26, 0xf4, 0x05, 0x05, 0x8d, 0x6e, 0x01, 0x2e, 0x87, 0x7f, 0xae, 0xf9, 0x79, 0x96, - 0x22, 0xe0, 0x4d, 0x96, 0x2a, 0x6b, 0xa2, 0x1e, 0x0a, 0x39, 0xf0, 0xd9, 0x53, 0x3e, 0xe9, 0x78, 0x61, 0x9e, 0xbd, - 0xd5, 0x46, 0xa9, 0x58, 0xe7, 0x60, 0xeb, 0xe3, 0xd7, 0x32, 0x97, 0x3a, 0xe0, 0xf4, 0xb9, 0x58, 0x5f, 0xf3, 0x22, - 0x4b, 0xce, 0x97, 0x59, 0x59, 0xc9, 0xe2, 0x7e, 0x61, 0x70, 0x0c, 0x74, 0x59, 0xad, 0x49, 0xdc, 0xfb, 0x1d, 0x38, - 0x33, 0xab, 0xc8, 0x14, 0xc3, 0xa7, 0x63, 0x52, 0x6b, 0x33, 0x68, 0x68, 0x20, 0xb5, 0xbf, 0x53, 0x09, 0xc0, 0xe9, - 0xee, 0xd9, 0x76, 0x8d, 0x36, 0x0d, 0xd8, 0xdb, 0x35, 0x52, 0xb3, 0x94, 0x0a, 0xfe, 0xe7, 0x9a, 0x1b, 0x18, 0xfb, - 0xd0, 0x41, 0x34, 0x97, 0x3d, 0xad, 0x63, 0x50, 0xd8, 0x3e, 0x44, 0xf1, 0xf8, 0x69, 0xfa, 0x02, 0xa1, 0xb6, 0xe1, - 0x6e, 0x8b, 0xda, 0x73, 0x1b, 0xa9, 0xa9, 0x6b, 0x6d, 0xcc, 0xa1, 0xad, 0x8b, 0xd9, 0xa7, 0x32, 0x0c, 0x06, 0xd1, - 0xa7, 0xb2, 0xb0, 0xc9, 0x03, 0x4b, 0x50, 0x65, 0x39, 0x36, 0x16, 0x73, 0x5a, 0x05, 0x0e, 0x89, 0x1e, 0x26, 0x2d, - 0x60, 0xcf, 0x00, 0x52, 0x6d, 0x02, 0xa3, 0xaa, 0xb5, 0xa2, 0x0e, 0x6c, 0x76, 0x8a, 0x46, 0x0b, 0xe1, 0xef, 0x8f, - 0x36, 0xcd, 0xcd, 0x50, 0x1f, 0x3e, 0xda, 0xc4, 0xf0, 0x5f, 0x52, 0xcf, 0x52, 0x5e, 0xc5, 0x59, 0xce, 0xe2, 0x3c, - 0xff, 0x9d, 0x6e, 0xae, 0x79, 0xb5, 0x94, 0x69, 0x14, 0x7c, 0xf7, 0xe2, 0x43, 0x60, 0xb4, 0x96, 0xb9, 0xc6, 0xab, - 0xd1, 0x82, 0xfc, 0x5c, 0x5e, 0x85, 0x39, 0xa1, 0xbd, 0x7c, 0x24, 0x3f, 0xee, 0x04, 0x78, 0xfc, 0xfd, 0xfb, 0x0f, - 0x1f, 0xde, 0x1d, 0xa0, 0xac, 0xbf, 0x77, 0x70, 0xa6, 0x1c, 0xc7, 0x0f, 0x1e, 0x6d, 0x72, 0xad, 0x5d, 0xad, 0x7f, - 0x77, 0x17, 0xf7, 0x96, 0x6e, 0x34, 0xd7, 0x5b, 0xc0, 0xab, 0xa2, 0x35, 0x37, 0xb9, 0x53, 0x60, 0xfa, 0x99, 0x95, - 0x62, 0x21, 0x40, 0xb1, 0xb9, 0xaa, 0x39, 0x0a, 0x28, 0xe4, 0x05, 0x90, 0xfd, 0xb0, 0xda, 0xb3, 0x19, 0xab, 0xae, - 0xcd, 0x28, 0x8b, 0x2a, 0x13, 0x57, 0xe7, 0x48, 0x1f, 0x3e, 0x6b, 0x53, 0x9a, 0x65, 0xa2, 0x28, 0x4a, 0x7b, 0x3f, - 0x36, 0x50, 0xaa, 0xb4, 0x3d, 0xa6, 0xde, 0x65, 0x20, 0x2b, 0x29, 0xeb, 0xa9, 0xff, 0xb1, 0x31, 0x16, 0xf0, 0xd3, - 0x14, 0x86, 0x17, 0x1c, 0x7f, 0xec, 0x24, 0x1e, 0x99, 0xf6, 0xdd, 0xe2, 0x95, 0xf9, 0x38, 0x69, 0x25, 0xcc, 0x86, - 0x93, 0x68, 0x42, 0x6c, 0x68, 0x01, 0x4d, 0xe5, 0xbe, 0x1b, 0xbd, 0x78, 0xf3, 0xe1, 0xd5, 0x87, 0x7f, 0x9d, 0x3f, - 0x3b, 0xfd, 0xf0, 0xe2, 0xbb, 0xb7, 0xef, 0x5f, 0xbd, 0x38, 0x43, 0x1c, 0x3d, 0x8d, 0x55, 0x18, 0x6e, 0xb4, 0x41, - 0x6c, 0xb3, 0xac, 0x48, 0xd4, 0xa4, 0xd9, 0x14, 0x05, 0x16, 0x84, 0x99, 0x6d, 0x91, 0x3f, 0xbf, 0x79, 0xfe, 0xe2, - 0xe5, 0xab, 0x37, 0x2f, 0x9e, 0xb7, 0xbf, 0x1e, 0x4e, 0x6a, 0x52, 0xbb, 0x99, 0xd3, 0xc1, 0x31, 0xb8, 0x1d, 0xaf, - 0x0e, 0x0a, 0x86, 0x0a, 0x59, 0x9f, 0x82, 0x65, 0x40, 0xb1, 0x98, 0x12, 0xd1, 0xe2, 0x6f, 0x1d, 0x88, 0x2a, 0x6b, - 0x6d, 0x80, 0x12, 0x07, 0x33, 0xa3, 0x8a, 0x64, 0x44, 0x02, 0x76, 0x83, 0x2d, 0x07, 0x0c, 0x5f, 0x53, 0x0a, 0x48, - 0x3e, 0x1d, 0xbb, 0x83, 0x2a, 0x7c, 0xfd, 0xf3, 0x24, 0xae, 0xf8, 0x95, 0x2c, 0xee, 0xa3, 0x6c, 0xd4, 0x4a, 0xa1, - 0x8d, 0x25, 0x11, 0x85, 0x20, 0x65, 0x6c, 0x24, 0x11, 0x45, 0x4e, 0x66, 0xde, 0xa0, 0xb8, 0x71, 0x9e, 0x3b, 0xe8, - 0xf8, 0x76, 0xc1, 0x64, 0xb1, 0xdd, 0x76, 0x0c, 0x63, 0x27, 0xbd, 0x8c, 0xe6, 0x99, 0x22, 0xa4, 0x0b, 0xe0, 0xd2, - 0xe0, 0x48, 0x54, 0xe7, 0x1d, 0x33, 0x47, 0xe4, 0xa9, 0x0e, 0x01, 0x09, 0xa6, 0x69, 0xee, 0xb5, 0x89, 0x32, 0xd2, - 0x3c, 0x43, 0xc7, 0x2d, 0x2a, 0x6d, 0x00, 0x5f, 0x5b, 0xa9, 0x6a, 0xe1, 0x69, 0xa0, 0x3d, 0x98, 0x3b, 0x88, 0xcd, - 0x64, 0xe4, 0x78, 0x61, 0x0e, 0xe6, 0x12, 0x8d, 0x19, 0x37, 0xe3, 0x90, 0x47, 0xd2, 0x60, 0xa6, 0x81, 0xfd, 0xd8, - 0x9e, 0x5c, 0xcb, 0xa8, 0x68, 0xa0, 0x1f, 0xca, 0xe6, 0xa0, 0x1e, 0x17, 0xcd, 0x67, 0x58, 0xd8, 0xad, 0x2c, 0x28, - 0xbf, 0x6b, 0x66, 0x82, 0x7b, 0x66, 0x32, 0x53, 0xd5, 0x8f, 0x2a, 0xf9, 0xa3, 0xbc, 0x35, 0xb2, 0xc2, 0xe3, 0xa2, - 0x23, 0x11, 0x77, 0x4b, 0x14, 0x1f, 0x27, 0xea, 0xc7, 0xa4, 0xde, 0x73, 0x70, 0xd4, 0x6e, 0x80, 0xad, 0x2c, 0xfb, - 0x77, 0xc5, 0x3f, 0x9f, 0x3f, 0xda, 0x64, 0xfa, 0xa4, 0xaa, 0x7f, 0xcf, 0x6c, 0x24, 0xd0, 0x06, 0x33, 0x52, 0xeb, - 0xa1, 0xf7, 0x81, 0x27, 0x3b, 0xb2, 0xe9, 0xf5, 0xc1, 0xb2, 0x4e, 0x8e, 0x66, 0xa4, 0x1e, 0xb9, 0x0a, 0x8c, 0xd8, - 0x9d, 0x85, 0xdf, 0xf1, 0x24, 0xec, 0x6a, 0x98, 0x92, 0x35, 0x98, 0x2e, 0x20, 0x16, 0xef, 0xfe, 0x43, 0xc1, 0x7e, - 0x86, 0xbf, 0xb3, 0x14, 0xfe, 0x56, 0xb5, 0x77, 0x30, 0xbc, 0x7b, 0x7b, 0xf6, 0x01, 0x14, 0x1c, 0x31, 0x6a, 0x24, - 0x37, 0x81, 0x36, 0x66, 0x18, 0x82, 0xca, 0x20, 0x88, 0x82, 0x78, 0x05, 0x27, 0x3b, 0xb2, 0x8e, 0x87, 0x77, 0xc3, - 0xdb, 0xdb, 0xdb, 0xff, 0xd3, 0xdc, 0xb3, 0x6e, 0xb7, 0x6d, 0x23, 0xfd, 0xbf, 0x4f, 0xc1, 0x30, 0xd9, 0x94, 0x4c, - 0x48, 0x9a, 0x94, 0x2c, 0x5b, 0x91, 0x2c, 0xb9, 0xcd, 0xa5, 0x5b, 0x77, 0xdd, 0xa6, 0x27, 0x71, 0xfb, 0xed, 0xae, - 0xeb, 0x63, 0x51, 0x12, 0x24, 0x71, 0x43, 0x91, 0x3a, 0x24, 0xe5, 0x4b, 0x15, 0xee, 0xb3, 0xec, 0x23, 0x7c, 0xcf, - 0xd0, 0x27, 0xfb, 0xce, 0xcc, 0x00, 0x24, 0x78, 0x93, 0xe4, 0x4d, 0xda, 0x7e, 0xa7, 0x4d, 0x22, 0x82, 0x00, 0x08, - 0x0c, 0x80, 0xb9, 0x61, 0x2e, 0x26, 0x98, 0x36, 0x9a, 0xeb, 0xc8, 0x67, 0xc1, 0x24, 0x84, 0xc4, 0x24, 0xa9, 0x40, - 0xf8, 0xac, 0x84, 0xf0, 0x21, 0x2e, 0x2a, 0x4f, 0xac, 0xf1, 0x7e, 0x11, 0xde, 0x7e, 0xed, 0xfb, 0xb2, 0xfc, 0x2e, - 0x98, 0x3e, 0x2e, 0xd2, 0x16, 0x10, 0x88, 0x06, 0xd7, 0x10, 0x96, 0x17, 0x5f, 0xf3, 0x8b, 0xe3, 0xe9, 0xf5, 0xf8, - 0xfe, 0x9a, 0x2b, 0xa7, 0xb3, 0xc0, 0xb4, 0xaf, 0x46, 0x27, 0x53, 0xef, 0x46, 0x41, 0xce, 0x74, 0xa0, 0x82, 0x57, - 0x8f, 0xcf, 0xc6, 0xeb, 0x24, 0x09, 0x03, 0x33, 0x0a, 0x6f, 0xd5, 0xe1, 0x09, 0x3d, 0x88, 0x0a, 0x2e, 0x3d, 0xaa, - 0xca, 0x57, 0x13, 0xdf, 0x9b, 0x7c, 0x18, 0xa8, 0x4f, 0x36, 0xde, 0x60, 0x58, 0xe2, 0x3f, 0xed, 0x54, 0x1d, 0xc2, - 0x58, 0x95, 0xaf, 0x7d, 0xff, 0xe4, 0x80, 0x5a, 0x0c, 0x4f, 0x0e, 0xa6, 0xde, 0xcd, 0x50, 0xca, 0x11, 0xc2, 0x2f, - 0xd0, 0x06, 0x3c, 0x16, 0x63, 0x66, 0x72, 0x14, 0xa3, 0x73, 0xff, 0x84, 0x69, 0xb9, 0x14, 0x04, 0x41, 0x47, 0x68, - 0xbc, 0xda, 0x04, 0xf5, 0xaa, 0x3e, 0xf0, 0xfc, 0x1f, 0x3f, 0x6a, 0x99, 0x41, 0xe2, 0x42, 0x8a, 0xd6, 0x85, 0xf7, - 0x3d, 0x58, 0xc5, 0xc0, 0x90, 0x23, 0xba, 0x26, 0x62, 0x8a, 0xf9, 0xba, 0x31, 0x49, 0x0d, 0x4c, 0xb5, 0xe2, 0xae, - 0x80, 0x47, 0xe0, 0x3f, 0x25, 0xd1, 0x68, 0x02, 0xe9, 0x95, 0x25, 0x04, 0xaf, 0x4b, 0xca, 0x77, 0x3a, 0x9b, 0x3c, - 0x60, 0x1c, 0x68, 0xcd, 0xf1, 0x3b, 0xa4, 0x10, 0xd7, 0x7c, 0x1d, 0xd2, 0x7b, 0x65, 0x51, 0x5a, 0xdc, 0x54, 0x24, - 0xd4, 0x12, 0x70, 0x39, 0x2d, 0xac, 0x50, 0xaf, 0xbc, 0x5e, 0x22, 0x7c, 0xe0, 0xa3, 0xb8, 0x69, 0xc9, 0xe0, 0x32, - 0x47, 0x4b, 0x8c, 0x12, 0x3d, 0x06, 0xf7, 0x2e, 0xe9, 0x06, 0x81, 0x19, 0xda, 0x65, 0x6c, 0x84, 0x57, 0x39, 0x0d, - 0x8b, 0x09, 0x7d, 0xf6, 0xc2, 0x34, 0x8f, 0xe4, 0x4b, 0xab, 0x3e, 0x7c, 0xb2, 0x09, 0x90, 0xe8, 0xc5, 0x83, 0x61, - 0x71, 0x1f, 0x24, 0xee, 0xd8, 0xa4, 0xcd, 0xac, 0x2a, 0x5f, 0x4d, 0xc7, 0x7e, 0xb6, 0xd8, 0x74, 0x34, 0x16, 0x6e, - 0x30, 0xf5, 0xd9, 0x85, 0x3b, 0xfe, 0x16, 0xeb, 0xbc, 0x1e, 0xfb, 0xaf, 0xa0, 0x42, 0xaa, 0x0e, 0x9f, 0x6c, 0x88, - 0xac, 0xd7, 0xa1, 0xf1, 0x94, 0xb6, 0x40, 0xf9, 0x3b, 0x3c, 0xf7, 0x0e, 0x8b, 0xa8, 0x35, 0x0e, 0x96, 0x48, 0x31, - 0xe1, 0xd9, 0xe2, 0xc8, 0x78, 0xee, 0x17, 0xd8, 0x9b, 0x0a, 0x3f, 0x94, 0x30, 0xae, 0x50, 0x1c, 0x50, 0x79, 0x67, - 0xca, 0x83, 0x25, 0x92, 0xfb, 0x2e, 0xbc, 0x15, 0x23, 0xe5, 0x00, 0xa0, 0x58, 0x85, 0xa7, 0xaf, 0x46, 0x27, 0xf2, - 0xfd, 0x00, 0x2a, 0x51, 0xa9, 0x5f, 0xf8, 0x95, 0xaa, 0x4a, 0x9e, 0x09, 0x68, 0x75, 0xa7, 0x0e, 0x4f, 0x0e, 0xe4, - 0xda, 0xc3, 0x51, 0xef, 0x5c, 0x9a, 0x1c, 0xf6, 0x0a, 0x40, 0x28, 0x96, 0x55, 0xa8, 0x0e, 0x24, 0xc7, 0xcb, 0xe9, - 0x12, 0x6d, 0x0f, 0x81, 0x16, 0x43, 0xbd, 0x97, 0xad, 0x11, 0xd9, 0xe0, 0x89, 0xde, 0x46, 0xfc, 0xdf, 0x7c, 0xce, - 0xa8, 0xd3, 0x64, 0x41, 0x1c, 0x46, 0x2a, 0xcc, 0xa3, 0x9c, 0x21, 0x47, 0x91, 0x32, 0x73, 0xe1, 0x8c, 0x6a, 0xa9, - 0x29, 0x40, 0xe4, 0xa0, 0xdc, 0x54, 0x9a, 0xd8, 0x48, 0xcf, 0x7f, 0x28, 0x7c, 0x32, 0x25, 0xac, 0x94, 0x0d, 0xb0, - 0x39, 0xf3, 0xd0, 0xe5, 0x5b, 0xcf, 0xf8, 0x9f, 0xd0, 0x98, 0xbb, 0xc6, 0xd2, 0x35, 0xde, 0x07, 0x57, 0x69, 0xed, - 0xea, 0x64, 0x59, 0xc3, 0x0c, 0xd6, 0xd7, 0x20, 0xd6, 0x0e, 0xd7, 0x80, 0x70, 0xbd, 0x80, 0x67, 0x71, 0xeb, 0x80, - 0x0b, 0x37, 0x9a, 0x33, 0x91, 0xac, 0x4b, 0xbc, 0x4d, 0x38, 0x54, 0x74, 0x09, 0x2c, 0x10, 0x88, 0x4a, 0x08, 0x38, - 0x9e, 0x35, 0x49, 0x22, 0xff, 0x6f, 0xec, 0x1e, 0x24, 0xcf, 0x38, 0x09, 0x57, 0xa0, 0x9d, 0x70, 0xe7, 0x5c, 0xdb, - 0x6c, 0x00, 0x2f, 0xb3, 0xcf, 0xe7, 0x3e, 0x7e, 0x64, 0x52, 0xfe, 0xa8, 0x24, 0x9c, 0xcf, 0x7d, 0xa6, 0x49, 0x79, - 0xa6, 0xb2, 0xcf, 0x9c, 0x3e, 0xb2, 0x45, 0x8c, 0x62, 0x3d, 0x6d, 0x3a, 0x39, 0x39, 0x2b, 0x28, 0xee, 0x75, 0x49, - 0x58, 0xc7, 0xdb, 0xa8, 0x1b, 0xbc, 0xd0, 0xe5, 0xeb, 0x92, 0x9f, 0x4c, 0x73, 0x1a, 0xae, 0xc7, 0x3e, 0x33, 0x71, - 0xbb, 0xc3, 0x27, 0x37, 0xe3, 0xf5, 0x78, 0xec, 0x53, 0x62, 0x28, 0x88, 0xb4, 0x15, 0xc6, 0xa8, 0x01, 0x4b, 0xf5, - 0x3e, 0x32, 0x68, 0x49, 0x79, 0xf8, 0x60, 0x1d, 0x07, 0x62, 0x03, 0x7d, 0x20, 0x01, 0x6d, 0x57, 0xf5, 0xd0, 0x0e, - 0x54, 0x10, 0x57, 0x58, 0xac, 0xf6, 0x6b, 0x38, 0xb9, 0xc1, 0xa5, 0xfa, 0x1e, 0x21, 0x8c, 0xd9, 0xeb, 0x5f, 0xd1, - 0xde, 0x55, 0x0d, 0x95, 0x8c, 0x7c, 0x78, 0x1e, 0x31, 0xd5, 0x50, 0x5f, 0x7b, 0xee, 0x3c, 0x08, 0xe3, 0xc4, 0x9b, - 0xa8, 0x57, 0xfd, 0x33, 0x4f, 0xbb, 0x5c, 0x26, 0x9a, 0x7e, 0x65, 0xfc, 0x55, 0xce, 0xf8, 0x24, 0x50, 0x21, 0x26, - 0x7c, 0x6a, 0xa8, 0x23, 0x9f, 0x9e, 0x6d, 0xf5, 0x04, 0xca, 0xc5, 0x3a, 0x7f, 0x1d, 0x40, 0xad, 0x52, 0xee, 0x28, - 0x4c, 0x0a, 0x08, 0xb9, 0xa3, 0xfe, 0xaa, 0xf7, 0x49, 0x2b, 0xf3, 0x6a, 0xbd, 0x41, 0x5e, 0x21, 0xc9, 0xa9, 0x2b, - 0x86, 0x3b, 0x17, 0x3e, 0x82, 0xf4, 0xfc, 0x48, 0xb6, 0x6f, 0x2f, 0xd0, 0xe9, 0xd1, 0xd7, 0x45, 0xc6, 0x03, 0x18, - 0x04, 0x30, 0x2e, 0x0b, 0xc2, 0x44, 0x81, 0x18, 0x5e, 0xf0, 0xc1, 0x51, 0xd9, 0x1e, 0x96, 0xf7, 0xaa, 0xe9, 0x29, - 0xc7, 0x02, 0x2f, 0x91, 0x58, 0x8a, 0xec, 0xef, 0x18, 0x8e, 0xb2, 0x10, 0xb1, 0x87, 0x7b, 0x61, 0xc1, 0xf2, 0x15, - 0xd8, 0x36, 0x09, 0xb1, 0x17, 0x09, 0xf6, 0x93, 0x4d, 0x7c, 0x2a, 0xa8, 0xf6, 0x59, 0x8c, 0x6b, 0x09, 0xfc, 0x08, - 0x27, 0xe3, 0xa9, 0xaa, 0x9c, 0x0a, 0x52, 0x83, 0x75, 0x0b, 0xf8, 0x53, 0x13, 0x5c, 0xae, 0x48, 0xea, 0xae, 0xf1, - 0x14, 0xd4, 0x82, 0xef, 0x2a, 0x1d, 0x3d, 0x08, 0xcb, 0x93, 0xb1, 0x54, 0x09, 0xd8, 0xd6, 0x22, 0x45, 0x00, 0xcc, - 0xc5, 0x99, 0x80, 0x51, 0x7a, 0x0d, 0xfc, 0x23, 0xc4, 0xaa, 0x12, 0x73, 0x34, 0x42, 0x39, 0x5d, 0x98, 0x17, 0xac, - 0xd6, 0x09, 0xc6, 0x20, 0x87, 0x01, 0xb0, 0x54, 0x55, 0x50, 0x5a, 0x04, 0x64, 0x9e, 0x4b, 0x41, 0xa9, 0xaa, 0x78, - 0xd3, 0x6a, 0x19, 0x57, 0xdd, 0x00, 0x8e, 0xc3, 0x69, 0xa0, 0x06, 0x1f, 0x1e, 0x23, 0x3e, 0x8d, 0x89, 0x91, 0x27, - 0xf0, 0xd0, 0x26, 0x78, 0xd3, 0x5d, 0x83, 0x40, 0x26, 0xd4, 0x4f, 0x5f, 0xf3, 0x6b, 0x27, 0x0b, 0x71, 0x89, 0x0b, - 0xd3, 0x1c, 0x3d, 0xd9, 0x04, 0xe9, 0x29, 0xc0, 0x6e, 0xf0, 0x64, 0xe3, 0x66, 0x46, 0x54, 0xea, 0x85, 0x4a, 0x16, - 0x54, 0x23, 0x04, 0xc3, 0x28, 0xbd, 0xce, 0x5d, 0x1a, 0xf3, 0xf9, 0xc2, 0x96, 0xa4, 0x72, 0x05, 0x6d, 0x9a, 0x06, - 0xdc, 0x72, 0x69, 0x15, 0x79, 0x4b, 0x37, 0xba, 0x27, 0x43, 0x27, 0x43, 0xb6, 0x86, 0xd2, 0x55, 0x85, 0xe8, 0x01, - 0x01, 0x80, 0x48, 0x83, 0xaa, 0x7c, 0x95, 0x95, 0x31, 0x3e, 0xdb, 0xcc, 0xda, 0x03, 0xbe, 0x75, 0xad, 0x3e, 0x67, - 0x16, 0xa9, 0x34, 0xa8, 0x49, 0x5f, 0x8b, 0x1b, 0xa6, 0x17, 0x17, 0xa7, 0x17, 0x14, 0x37, 0x1a, 0x4e, 0x86, 0x28, - 0x05, 0x8d, 0x1b, 0x67, 0x86, 0xe9, 0x0e, 0xeb, 0x57, 0x94, 0xde, 0xfd, 0xa1, 0xcb, 0xc1, 0x60, 0x39, 0x02, 0x58, - 0x0e, 0xe2, 0xae, 0x7f, 0x7a, 0x77, 0x96, 0xe5, 0x57, 0x04, 0xd5, 0xf8, 0x88, 0x6f, 0xcc, 0x18, 0xd9, 0x8c, 0x08, - 0x59, 0x0c, 0xca, 0x84, 0xa8, 0x64, 0x5b, 0x28, 0x82, 0xa3, 0x41, 0x63, 0xa7, 0xa3, 0x11, 0x0d, 0x06, 0x21, 0xb6, - 0x8a, 0xd2, 0x93, 0x03, 0xaa, 0x4d, 0x44, 0x91, 0x2a, 0x01, 0x18, 0x22, 0x98, 0x61, 0x0e, 0x05, 0x48, 0x05, 0x3d, - 0x70, 0x72, 0xf9, 0xc6, 0x5a, 0xe2, 0x05, 0xa4, 0x73, 0x5a, 0xe4, 0x68, 0xb0, 0x95, 0x3a, 0x3c, 0xc1, 0xe4, 0x8e, - 0x40, 0xd6, 0x21, 0xfc, 0xd1, 0xc9, 0x01, 0x3d, 0x2a, 0xa5, 0x13, 0x91, 0x77, 0x22, 0x14, 0x94, 0x3d, 0xde, 0xc1, - 0x83, 0x8e, 0x4a, 0x9c, 0xb0, 0x15, 0x94, 0xba, 0xa9, 0xaa, 0x2c, 0x39, 0x07, 0xc5, 0xe3, 0xac, 0x41, 0x10, 0x16, - 0x1b, 0x8c, 0xdf, 0x55, 0x65, 0xe9, 0xde, 0xe1, 0xcc, 0xc5, 0x1b, 0xf7, 0x4e, 0x73, 0xf8, 0xab, 0xfc, 0xac, 0xc5, - 0xc5, 0xb3, 0x36, 0xe1, 0x8b, 0x0b, 0x1e, 0x56, 0x82, 0x73, 0xd6, 0x16, 0x68, 0xb9, 0x52, 0xb3, 0xb8, 0x0b, 0xb1, - 0xb8, 0xd3, 0x86, 0xc5, 0x9d, 0x6e, 0x59, 0x5c, 0x9f, 0x2f, 0xa4, 0x92, 0x81, 0x2e, 0x42, 0xaf, 0xd9, 0x0c, 0x78, - 0x9c, 0x1f, 0xe9, 0xf1, 0x73, 0x86, 0x70, 0x32, 0x63, 0x1f, 0xac, 0x46, 0x1b, 0x60, 0x55, 0x07, 0x17, 0x09, 0x10, - 0xd5, 0x89, 0x67, 0xa7, 0x6e, 0x22, 0x29, 0x04, 0x34, 0xbf, 0x3c, 0x5f, 0xd8, 0xa5, 0xd8, 0xd0, 0xd0, 0x16, 0x0d, - 0x33, 0x5d, 0x6c, 0x99, 0xe9, 0xa4, 0x70, 0x74, 0xf9, 0xb4, 0xe9, 0x10, 0xca, 0x93, 0x82, 0x3d, 0x08, 0x96, 0xf4, - 0xb8, 0x65, 0x8a, 0xfb, 0xb0, 0x19, 0xc7, 0x4a, 0x3b, 0x6a, 0xe5, 0xc6, 0xf1, 0x6d, 0x18, 0xc1, 0x55, 0x34, 0x74, - 0xf3, 0xb0, 0x2d, 0xb5, 0xf4, 0x02, 0x1e, 0xe5, 0xaa, 0x71, 0x33, 0xe5, 0xef, 0xe5, 0x2d, 0xd5, 0xea, 0x74, 0xa8, - 0xc6, 0xca, 0x4d, 0x12, 0x16, 0x21, 0xd0, 0x5d, 0x48, 0x87, 0xf0, 0xff, 0x64, 0x9b, 0xd5, 0xe0, 0x10, 0x5f, 0xc2, - 0xea, 0x88, 0xa1, 0x57, 0xc0, 0x82, 0xd1, 0xdd, 0x53, 0xa0, 0x6f, 0xa4, 0x88, 0x99, 0x51, 0x06, 0xf8, 0x1f, 0xf0, - 0xb8, 0x6a, 0x91, 0xe4, 0xd3, 0xe9, 0x1c, 0xe9, 0xd6, 0xca, 0x9d, 0xbe, 0x07, 0x8b, 0x07, 0xad, 0x65, 0x80, 0xf7, - 0x82, 0x1c, 0x1f, 0x33, 0x22, 0x9e, 0x70, 0x92, 0x23, 0x49, 0xc4, 0x92, 0xdc, 0x36, 0x14, 0xdc, 0xca, 0x5d, 0x73, - 0x76, 0xb5, 0x69, 0xa5, 0x07, 0x73, 0x4f, 0xaf, 0x60, 0x4d, 0x40, 0x6d, 0xfe, 0x60, 0x98, 0xe9, 0xda, 0x7c, 0xc3, - 0x39, 0xd2, 0xe1, 0x4a, 0xec, 0x12, 0x12, 0x5f, 0xdb, 0x42, 0x5a, 0x1e, 0x45, 0x40, 0xb5, 0x2e, 0xed, 0xab, 0xf4, - 0xe9, 0x1c, 0x7f, 0x39, 0x57, 0xe9, 0xd3, 0x31, 0xfe, 0x6a, 0x5d, 0x61, 0x4a, 0xcf, 0x1a, 0x35, 0x81, 0x34, 0x67, - 0x75, 0x58, 0xd8, 0x4f, 0x64, 0x98, 0xfb, 0x80, 0x6d, 0xc3, 0x17, 0xf8, 0xf1, 0x93, 0x4d, 0x0c, 0xae, 0xe8, 0xf2, - 0x1c, 0x02, 0x2b, 0xd2, 0xd3, 0xda, 0xf2, 0x79, 0x43, 0xf9, 0x58, 0xff, 0x83, 0x09, 0x3f, 0xee, 0x92, 0x30, 0xa7, - 0x29, 0x45, 0x25, 0xc7, 0xf5, 0xd8, 0x0b, 0xdc, 0xe8, 0xfe, 0x9a, 0xa4, 0x10, 0x4d, 0xd2, 0xf6, 0x3e, 0xca, 0xa5, - 0xff, 0xfb, 0xa2, 0x1d, 0x40, 0x22, 0xdd, 0x65, 0xdd, 0x73, 0x42, 0x3f, 0xf8, 0x7b, 0x24, 0xf1, 0x77, 0x05, 0x39, - 0x95, 0x2f, 0x48, 0xe1, 0x43, 0xd7, 0x4f, 0x36, 0x1a, 0xab, 0x76, 0x53, 0x9a, 0x6d, 0x89, 0x81, 0x84, 0xe5, 0x41, - 0x99, 0x77, 0x39, 0xf5, 0x7a, 0x78, 0xd1, 0x3f, 0x0e, 0xef, 0xcc, 0x27, 0x9b, 0xe4, 0x54, 0x5d, 0xba, 0xd1, 0x07, - 0x36, 0x35, 0x27, 0x5e, 0x34, 0xf1, 0x81, 0x79, 0x1c, 0xfb, 0x6e, 0xf0, 0x81, 0x3f, 0x9a, 0xe1, 0x3a, 0x41, 0xd3, - 0x9d, 0x9d, 0x22, 0xb2, 0x80, 0x09, 0xe9, 0x0f, 0x91, 0xab, 0xad, 0x81, 0x82, 0xf2, 0x2a, 0xd3, 0xbf, 0xe5, 0x8c, - 0x62, 0x5e, 0xcb, 0x00, 0xcb, 0x73, 0xb0, 0x26, 0x02, 0x57, 0x7e, 0x43, 0xc5, 0xf5, 0x52, 0x0d, 0x79, 0xaa, 0x74, - 0xe5, 0x96, 0xe5, 0xa2, 0xbd, 0xc6, 0x1e, 0xfe, 0xfb, 0xcf, 0x41, 0xc9, 0x43, 0x3e, 0x97, 0xf5, 0xf2, 0x69, 0x33, - 0x84, 0x52, 0x93, 0x5c, 0xc8, 0x1e, 0xf0, 0x71, 0xce, 0x60, 0x36, 0x7f, 0x5a, 0x6e, 0xec, 0xc6, 0xf1, 0x7a, 0xc9, - 0xa6, 0x74, 0xb5, 0x76, 0x9a, 0x0f, 0xaa, 0x28, 0x87, 0xc8, 0x03, 0xfb, 0x65, 0xdd, 0x3a, 0x3e, 0x7c, 0x05, 0xa6, - 0x5c, 0xc0, 0x50, 0x86, 0xb3, 0x99, 0x9a, 0xab, 0x02, 0x76, 0x34, 0x73, 0x0e, 0x7f, 0x59, 0x7f, 0xf3, 0xc6, 0xfe, - 0x26, 0x6b, 0x1c, 0x00, 0x63, 0x2c, 0xec, 0x52, 0x38, 0x5f, 0x2c, 0x8d, 0x57, 0xcc, 0x68, 0xe6, 0x06, 0xcd, 0xd3, - 0xb9, 0x2c, 0x6c, 0xf1, 0x15, 0x63, 0x53, 0x60, 0xb8, 0x8d, 0x4a, 0xe9, 0xb5, 0xcf, 0x6e, 0x58, 0x66, 0xf3, 0x52, - 0xfd, 0x58, 0x4d, 0x0b, 0x0c, 0xca, 0xc9, 0x6f, 0x32, 0x39, 0x57, 0x27, 0x4d, 0x69, 0x84, 0x73, 0xe0, 0x33, 0x97, - 0x8f, 0x58, 0xe9, 0x48, 0x8d, 0x0c, 0x55, 0x1a, 0x40, 0xe3, 0xc8, 0x4e, 0x1b, 0xca, 0x7b, 0x80, 0xa8, 0x1b, 0xc6, - 0x66, 0x38, 0x7a, 0x0f, 0x92, 0x18, 0x70, 0x38, 0xf9, 0x70, 0xf2, 0xb4, 0x5c, 0x6b, 0xd2, 0x04, 0xb1, 0x3a, 0x5d, - 0x9a, 0x4a, 0x4a, 0x1a, 0x61, 0x06, 0x8e, 0xfe, 0x10, 0x42, 0x5d, 0x55, 0xbb, 0x36, 0x4a, 0x71, 0xe6, 0x63, 0x4c, - 0xf1, 0x1d, 0xb0, 0x38, 0x6e, 0x04, 0x58, 0xb6, 0xe8, 0x86, 0x9a, 0xd7, 0x2e, 0xc2, 0x23, 0x2f, 0x37, 0x6c, 0x03, - 0x58, 0x02, 0x9c, 0x60, 0xf9, 0x5b, 0x48, 0x5e, 0xae, 0x97, 0xdc, 0x90, 0x2f, 0x9a, 0x8f, 0x55, 0x6e, 0x64, 0xd5, - 0xf4, 0xfe, 0x56, 0xe5, 0x83, 0x2a, 0x90, 0xe9, 0xda, 0xa1, 0x69, 0x05, 0xd4, 0x5b, 0xd1, 0x2a, 0x61, 0x07, 0x62, - 0x4c, 0x25, 0xfc, 0xca, 0x66, 0x33, 0x36, 0x49, 0x62, 0x5d, 0xe8, 0x98, 0xb2, 0xb0, 0xda, 0x70, 0x7b, 0xf7, 0x68, - 0xa0, 0xfe, 0x00, 0xc1, 0x45, 0x44, 0xf4, 0x39, 0x3e, 0x20, 0x21, 0x33, 0xd5, 0x83, 0x89, 0x7a, 0x2c, 0x82, 0x88, - 0x7f, 0x05, 0xd4, 0xcc, 0x35, 0xe5, 0x38, 0x34, 0x4e, 0x7f, 0xf2, 0x7d, 0x11, 0x66, 0xe6, 0x7e, 0xdb, 0x51, 0xd1, - 0xb6, 0xe3, 0xbb, 0x71, 0xbe, 0xe9, 0x38, 0x76, 0xaa, 0x1a, 0xe0, 0xd4, 0xfa, 0xa1, 0xb4, 0x8d, 0x89, 0x40, 0x0d, - 0xd4, 0xf3, 0xb7, 0xaf, 0xfe, 0xf6, 0xe6, 0xf5, 0xbe, 0x18, 0x01, 0xbb, 0x6c, 0x43, 0x97, 0xeb, 0x60, 0x4b, 0xa7, - 0x3f, 0xfd, 0xf0, 0xb0, 0x6e, 0x5b, 0xce, 0x0b, 0x47, 0x35, 0xc8, 0x0e, 0x59, 0xc2, 0x8b, 0x93, 0xf0, 0x86, 0x45, - 0x9f, 0x0c, 0x06, 0xb9, 0xf3, 0xfa, 0xe1, 0xbe, 0xfd, 0xf1, 0xcd, 0x0f, 0x7b, 0x0f, 0xf5, 0xc8, 0xb1, 0x01, 0xb7, - 0x27, 0xe1, 0xea, 0x01, 0xb3, 0x6b, 0xab, 0x86, 0x3a, 0xf1, 0xc3, 0x98, 0x35, 0x8c, 0xe0, 0xd5, 0xf9, 0xdb, 0xf7, - 0x08, 0xae, 0x9c, 0x05, 0xa1, 0xae, 0x3e, 0x6d, 0xf2, 0x3f, 0xbe, 0x7b, 0xf3, 0xfe, 0xbd, 0x6a, 0x60, 0x5a, 0xe6, - 0x58, 0xee, 0x9d, 0x6f, 0xe2, 0x1d, 0x14, 0xa7, 0x76, 0xaf, 0x13, 0x55, 0x23, 0x41, 0xba, 0x38, 0x1b, 0x2a, 0xab, - 0x6c, 0x73, 0x4e, 0xed, 0xf8, 0x97, 0x49, 0xfa, 0xdd, 0x6b, 0x5e, 0x35, 0xf8, 0x68, 0x3b, 0x49, 0x2d, 0x94, 0x2c, - 0xbd, 0xe0, 0xba, 0xa6, 0xd4, 0xbd, 0xab, 0x29, 0x05, 0xf1, 0xb1, 0x82, 0x1f, 0xd7, 0xe1, 0x52, 0x62, 0x47, 0xd8, - 0xdd, 0x6e, 0x70, 0x49, 0x32, 0xdc, 0x27, 0x0c, 0x9a, 0xa7, 0xd5, 0x28, 0x8f, 0xba, 0xa6, 0x98, 0x0b, 0x5e, 0x19, - 0x6c, 0x27, 0x3e, 0x58, 0x5f, 0x33, 0xf9, 0x9e, 0xb1, 0xc8, 0xaa, 0x72, 0xdf, 0x89, 0x41, 0x49, 0x2a, 0xa0, 0x66, - 0x74, 0x37, 0xc3, 0x69, 0xca, 0xca, 0x9d, 0x82, 0x49, 0xb3, 0x39, 0x0e, 0x93, 0x24, 0x5c, 0xf6, 0x1c, 0x7b, 0x75, - 0xa7, 0x2a, 0x7d, 0xa1, 0xec, 0xe0, 0x16, 0xd7, 0xbd, 0xdf, 0xfe, 0x53, 0x42, 0xf3, 0x54, 0x7e, 0x9d, 0xb0, 0xe5, - 0x8a, 0x45, 0x6e, 0xb2, 0x8e, 0x58, 0xaa, 0xfc, 0xf6, 0xbf, 0xaf, 0x4a, 0x82, 0x7d, 0x5f, 0x6e, 0x43, 0x2c, 0xbd, - 0xdc, 0xe4, 0xda, 0x0f, 0x6f, 0x1f, 0xe5, 0xbe, 0x55, 0x3b, 0x2a, 0x2f, 0xbc, 0xf9, 0x22, 0xab, 0x7d, 0x9a, 0x6c, - 0x99, 0x9b, 0x18, 0x3d, 0xdd, 0x07, 0x28, 0xe7, 0xe1, 0x6d, 0xef, 0xb7, 0xff, 0x64, 0x0a, 0x9b, 0x9d, 0xbb, 0xae, - 0x7e, 0xa0, 0xc5, 0x15, 0xad, 0xaf, 0x53, 0x59, 0x62, 0x78, 0x5f, 0x59, 0xe0, 0x4a, 0x21, 0xed, 0xca, 0xea, 0xe5, - 0xdb, 0x96, 0x39, 0x7d, 0xeb, 0xcd, 0x17, 0x9f, 0x3a, 0x29, 0x00, 0xe8, 0xce, 0x59, 0x41, 0xa5, 0xcf, 0x30, 0xad, - 0x51, 0x6f, 0xff, 0x05, 0xfb, 0xc4, 0x79, 0xed, 0x9a, 0xd2, 0xe7, 0x98, 0x0d, 0xd7, 0xdc, 0xbe, 0x1a, 0x8d, 0xb2, - 0xb4, 0xa4, 0x72, 0x7b, 0xf0, 0x0e, 0x3b, 0xad, 0x94, 0x70, 0xf6, 0xa2, 0x67, 0xeb, 0x14, 0xb6, 0x65, 0x0f, 0x80, - 0xa0, 0x9d, 0x73, 0x0d, 0x38, 0x9a, 0xf1, 0x35, 0xb9, 0x2b, 0x55, 0xbe, 0x5d, 0x41, 0xd6, 0x50, 0x8a, 0x29, 0x2d, - 0xb3, 0x5b, 0x43, 0xa3, 0x7e, 0x38, 0xb7, 0x91, 0xbb, 0xa2, 0x4b, 0x02, 0x05, 0x6f, 0x4c, 0x40, 0xe9, 0x52, 0x92, - 0xa2, 0x6f, 0x5c, 0xff, 0x66, 0x3f, 0x81, 0xaa, 0x99, 0x82, 0x21, 0x69, 0xfe, 0xf3, 0x88, 0x37, 0xd2, 0xe5, 0xfd, - 0x69, 0x37, 0xa6, 0x0a, 0x7b, 0xdb, 0x64, 0x5e, 0xfd, 0xe3, 0x6e, 0xf3, 0xea, 0x8b, 0xbd, 0xcc, 0xab, 0x7f, 0xfc, - 0xec, 0xe6, 0xd5, 0x6f, 0x65, 0xf3, 0x6a, 0xd8, 0xc4, 0x6f, 0xd8, 0x5e, 0x46, 0xcf, 0xc2, 0xc8, 0x28, 0xbc, 0x8d, - 0x07, 0x0e, 0x17, 0x7a, 0xe2, 0xc9, 0x82, 0x81, 0x16, 0x89, 0x83, 0xcb, 0x0f, 0xe7, 0x60, 0x9b, 0xdc, 0x6c, 0x7d, - 0xfc, 0xb9, 0x6c, 0x8f, 0xfd, 0x70, 0xae, 0x4a, 0xc1, 0xd2, 0x03, 0x11, 0x2c, 0x1d, 0x1c, 0xc4, 0x7f, 0xb9, 0x73, - 0x5e, 0x5e, 0x3a, 0xfd, 0xb6, 0x03, 0xc1, 0x46, 0x40, 0x31, 0x80, 0x05, 0x76, 0xbf, 0xdd, 0x86, 0x82, 0x5b, 0xa9, - 0xa0, 0x05, 0x05, 0x9e, 0x54, 0xd0, 0x81, 0x82, 0x89, 0x54, 0x70, 0x04, 0x05, 0x53, 0xa9, 0xe0, 0x18, 0x0a, 0x6e, - 0xd4, 0xf4, 0x32, 0xc8, 0x8c, 0xc7, 0x8f, 0xf5, 0xab, 0x42, 0x9e, 0x8c, 0x3c, 0xff, 0x3c, 0xaf, 0x72, 0x6c, 0x88, - 0xa0, 0x8d, 0xe6, 0xa1, 0xce, 0xcd, 0xff, 0x46, 0x5f, 0x8c, 0xc0, 0x9d, 0x1a, 0x94, 0x7a, 0x06, 0xa8, 0x44, 0xa9, - 0x66, 0x5b, 0xbc, 0x56, 0x7b, 0xaa, 0x9e, 0x7d, 0xa0, 0x25, 0xec, 0xfe, 0x7a, 0xe8, 0x4a, 0x23, 0x2a, 0x77, 0x9e, - 0x2f, 0xb2, 0x08, 0x4e, 0xeb, 0x41, 0xee, 0x91, 0xd6, 0x86, 0x38, 0xb6, 0x70, 0x35, 0xfd, 0x1a, 0xf9, 0x03, 0x2b, - 0x09, 0xc1, 0xe1, 0x48, 0x44, 0x2e, 0x12, 0x1f, 0x50, 0x54, 0xfd, 0xd2, 0xbe, 0xea, 0xbb, 0x79, 0x90, 0x29, 0x1e, - 0xef, 0x8c, 0x46, 0xbf, 0xcc, 0xc2, 0x48, 0x91, 0x98, 0xbb, 0x36, 0x12, 0x77, 0xde, 0x5b, 0x18, 0xa4, 0xe3, 0xee, - 0xcd, 0x21, 0x2e, 0xe8, 0xe9, 0xb4, 0xb7, 0x32, 0x6e, 0x17, 0x2c, 0xe8, 0xcd, 0xb8, 0x39, 0x28, 0xac, 0x3f, 0x59, - 0xf1, 0x2c, 0x75, 0x61, 0x94, 0x86, 0x7b, 0x22, 0x7f, 0x4b, 0xa3, 0x34, 0xb3, 0xad, 0x94, 0x5b, 0x4e, 0x69, 0xb2, - 0xfe, 0xfb, 0x73, 0xd8, 0xb9, 0xbc, 0x66, 0xe3, 0xf5, 0x5c, 0x39, 0x0f, 0xe7, 0x3b, 0x6d, 0x5a, 0xe4, 0x57, 0x30, - 0x4a, 0x95, 0x2e, 0xfa, 0x4c, 0xb1, 0xbd, 0xf9, 0xb7, 0xe8, 0x31, 0x2d, 0xd6, 0x4f, 0x60, 0x6c, 0x4a, 0x42, 0x28, - 0x1b, 0xbe, 0x03, 0xd0, 0x96, 0x8c, 0x4a, 0xce, 0x01, 0x7e, 0xd2, 0xf3, 0x85, 0x2b, 0x8d, 0x67, 0xf8, 0x3d, 0x8b, - 0x63, 0x77, 0x2e, 0xea, 0x57, 0xc7, 0x09, 0x3e, 0x36, 0x99, 0xa4, 0x8f, 0x00, 0x04, 0x9d, 0xb1, 0x57, 0xb1, 0x05, - 0x02, 0x53, 0x66, 0x30, 0x7b, 0x83, 0x45, 0xcb, 0x0d, 0x67, 0x3c, 0x0b, 0x96, 0xa7, 0x68, 0xe2, 0x02, 0x48, 0xe4, - 0x86, 0xf9, 0xe5, 0xc2, 0xc4, 0x9d, 0x97, 0x8b, 0x68, 0xad, 0x53, 0x79, 0x6c, 0x99, 0x85, 0x49, 0xa1, 0xf0, 0x53, - 0x4c, 0x26, 0xfc, 0x70, 0xfe, 0xbb, 0xda, 0x4b, 0x6c, 0xb1, 0x73, 0x79, 0x1f, 0x18, 0x41, 0x32, 0xb2, 0x10, 0xc6, - 0x8a, 0x05, 0x20, 0xec, 0x05, 0xc9, 0xc2, 0x44, 0xef, 0x6e, 0xad, 0x15, 0xe8, 0x86, 0x85, 0x6b, 0xbb, 0x29, 0xc7, - 0xb4, 0xe8, 0x45, 0xf3, 0xb1, 0xab, 0x39, 0xad, 0x63, 0x43, 0xfc, 0xb1, 0xec, 0x8e, 0x9e, 0x62, 0x0f, 0xca, 0xd4, - 0xbb, 0xd9, 0xcc, 0xc2, 0x20, 0x31, 0x67, 0xee, 0xd2, 0xf3, 0xef, 0x7b, 0xcb, 0x30, 0x08, 0xe3, 0x95, 0x3b, 0x61, - 0xfd, 0x5c, 0x75, 0xd3, 0xc7, 0x68, 0x49, 0xdc, 0x61, 0xdf, 0xb1, 0x5a, 0x11, 0x5b, 0x52, 0xeb, 0x2c, 0x18, 0xd2, - 0xcc, 0x67, 0x77, 0x29, 0xff, 0x7c, 0xa1, 0x32, 0x55, 0xc5, 0x2d, 0x47, 0x2d, 0x40, 0x0e, 0xe1, 0x91, 0x96, 0x20, - 0xbe, 0x60, 0x9f, 0x33, 0xf3, 0x3d, 0xab, 0xd5, 0x89, 0xd8, 0x52, 0xb1, 0x3a, 0x8d, 0x9d, 0x47, 0xe1, 0xed, 0x10, - 0x46, 0x8b, 0x8d, 0xcd, 0x98, 0xf9, 0x33, 0x7c, 0x63, 0xa2, 0x73, 0xa7, 0xe8, 0xc7, 0x44, 0x95, 0x0f, 0xf4, 0xc6, - 0x96, 0x7d, 0x78, 0xdd, 0x6b, 0x29, 0x76, 0x7f, 0xe9, 0x05, 0x26, 0x4d, 0xe7, 0xd8, 0x5e, 0x49, 0x7d, 0xc9, 0xf0, - 0xd3, 0x37, 0x58, 0xdd, 0x51, 0xec, 0x3e, 0x88, 0xf6, 0x33, 0x3f, 0xbc, 0xed, 0x2d, 0xbc, 0xe9, 0x94, 0x05, 0x7d, - 0x1c, 0x73, 0x56, 0xc8, 0x7c, 0xdf, 0x5b, 0xc5, 0x5e, 0xdc, 0x5f, 0xba, 0x77, 0xbc, 0xd7, 0xc3, 0xa6, 0x5e, 0xdb, - 0xbc, 0xd7, 0xf6, 0xde, 0xbd, 0x4a, 0xdd, 0x80, 0x23, 0x29, 0xf5, 0xc3, 0x87, 0xd6, 0x51, 0xec, 0xd2, 0x3c, 0xf7, - 0xee, 0x75, 0x15, 0xb1, 0xcd, 0xd2, 0x8d, 0xe6, 0x5e, 0xd0, 0xb3, 0x53, 0xeb, 0x66, 0x43, 0x1b, 0xe3, 0x71, 0xb7, - 0xdb, 0x4d, 0xad, 0xa9, 0x78, 0xb2, 0xa7, 0xd3, 0xd4, 0x9a, 0x88, 0xa7, 0xd9, 0xcc, 0xb6, 0x67, 0xb3, 0xd4, 0xf2, - 0x44, 0x41, 0xbb, 0x35, 0x99, 0xb6, 0x5b, 0xa9, 0x75, 0x2b, 0xd5, 0x48, 0x2d, 0xc6, 0x9f, 0x22, 0x36, 0xed, 0xe3, - 0x46, 0xe2, 0x76, 0xe9, 0xc7, 0xb6, 0x9d, 0x22, 0x06, 0xb8, 0x2c, 0xe0, 0x26, 0xd4, 0x2a, 0x5e, 0x6d, 0xf6, 0xae, - 0xa9, 0xe4, 0x9f, 0x9b, 0x4c, 0x6a, 0xeb, 0x4d, 0xdd, 0xe8, 0xc3, 0x95, 0x22, 0xcd, 0xc2, 0x75, 0xa9, 0xda, 0x46, - 0x80, 0xc1, 0xbc, 0xeb, 0x41, 0xd4, 0xcc, 0xfe, 0x38, 0x8c, 0xe0, 0xcc, 0x46, 0xee, 0xd4, 0x5b, 0xc7, 0x3d, 0xa7, - 0xb5, 0xba, 0x13, 0x45, 0x7c, 0xaf, 0xe7, 0x05, 0x78, 0xf6, 0x7a, 0x71, 0xe8, 0x7b, 0x53, 0x51, 0xd4, 0x74, 0x96, - 0x9c, 0x96, 0xde, 0xc7, 0x98, 0x31, 0x1e, 0x46, 0x3e, 0x72, 0x7d, 0x5f, 0xb1, 0xda, 0xb1, 0xc2, 0xdc, 0x18, 0x6f, - 0x32, 0x14, 0x3b, 0x26, 0xb8, 0x60, 0x7c, 0x18, 0xe7, 0x70, 0x75, 0x97, 0xed, 0x79, 0xe7, 0x68, 0x75, 0x97, 0x7e, - 0xb5, 0x64, 0x53, 0xcf, 0x55, 0xb4, 0x7c, 0x37, 0x39, 0x36, 0xdc, 0x76, 0xe8, 0x9b, 0x86, 0x6d, 0x2a, 0x8e, 0x05, - 0x44, 0x17, 0x7e, 0xe4, 0x2d, 0x57, 0x61, 0x94, 0xb8, 0x41, 0x92, 0xa6, 0xa3, 0xab, 0x34, 0xed, 0x5f, 0x78, 0xda, - 0xe5, 0x3f, 0x34, 0xa2, 0x85, 0x74, 0x3b, 0x98, 0xea, 0x57, 0xc6, 0x1b, 0x26, 0x5b, 0x32, 0x01, 0x19, 0x43, 0x2b, - 0x26, 0xb9, 0x32, 0xd1, 0xdb, 0x6a, 0x65, 0x02, 0x72, 0x56, 0x9d, 0x0c, 0xa3, 0x8a, 0x55, 0x90, 0x02, 0x41, 0x85, - 0x37, 0x6c, 0x70, 0x21, 0x99, 0x45, 0x01, 0xd3, 0x83, 0x95, 0xc9, 0xb5, 0xef, 0x49, 0x13, 0xef, 0xf9, 0xf5, 0x6e, - 0xde, 0xf3, 0x9f, 0xc9, 0x3e, 0xbc, 0xe7, 0xd7, 0x9f, 0x9d, 0xf7, 0x7c, 0x52, 0x75, 0xed, 0x3b, 0x0b, 0x07, 0x6a, - 0x76, 0x97, 0x05, 0xa4, 0x29, 0xa2, 0xa0, 0x79, 0x67, 0xc9, 0x7f, 0xeb, 0x8a, 0x27, 0x7a, 0xa3, 0x34, 0xb0, 0x44, - 0xb9, 0x81, 0x81, 0x7f, 0x1b, 0x0c, 0xfe, 0x1e, 0xc9, 0xcf, 0xb3, 0xd9, 0xe0, 0x75, 0x28, 0x15, 0x64, 0x4f, 0xdc, - 0xcc, 0xa7, 0x10, 0xe0, 0x88, 0xde, 0x64, 0x86, 0x58, 0x90, 0x02, 0x0a, 0xe2, 0xa3, 0x90, 0xb1, 0xfd, 0x34, 0x33, - 0x87, 0xec, 0x17, 0x87, 0xa0, 0x65, 0x06, 0xc6, 0xc2, 0x0b, 0xb6, 0xa2, 0xb4, 0x9e, 0xb3, 0x84, 0x87, 0xad, 0x78, - 0x79, 0x7f, 0x36, 0xd5, 0xce, 0x42, 0x3d, 0xf5, 0xe2, 0xb7, 0x65, 0x1f, 0x54, 0x21, 0x82, 0xc8, 0xd3, 0x49, 0xb9, - 0x49, 0xa3, 0x14, 0x6a, 0x06, 0x5f, 0x53, 0xf3, 0xd3, 0xc2, 0x4c, 0x7b, 0x72, 0x43, 0x9e, 0x6b, 0xb2, 0x42, 0x8c, - 0xb9, 0x4b, 0xdf, 0x86, 0x73, 0x79, 0x98, 0x3e, 0x13, 0x43, 0x77, 0x4c, 0xa9, 0xb9, 0x37, 0x4d, 0x53, 0xbd, 0x2f, - 0x00, 0x21, 0x11, 0x5a, 0xb6, 0x8b, 0x89, 0x8b, 0x73, 0x81, 0x96, 0xdf, 0x45, 0xd3, 0x45, 0xf3, 0x19, 0x98, 0x6e, - 0xf0, 0x6b, 0x69, 0x0e, 0x33, 0x55, 0x21, 0xf0, 0x91, 0x49, 0x8f, 0x34, 0x21, 0xb0, 0x35, 0x90, 0x0d, 0xe1, 0x0a, - 0x0b, 0x52, 0x35, 0x2a, 0x26, 0xe0, 0xa0, 0xed, 0x09, 0x04, 0xda, 0x11, 0xda, 0x2e, 0x42, 0x3b, 0xbc, 0x0e, 0x3e, - 0xa4, 0x6a, 0xc6, 0xfb, 0xe1, 0xf6, 0x1b, 0x9e, 0x1c, 0x40, 0x83, 0x61, 0x49, 0x93, 0xb5, 0xc3, 0x64, 0x16, 0x58, - 0x89, 0xf8, 0xd6, 0xb0, 0xe2, 0x5b, 0xe5, 0xd9, 0x46, 0x04, 0xa9, 0x4a, 0xdc, 0x95, 0x09, 0xea, 0x13, 0xc4, 0xbd, - 0x1c, 0xe3, 0x49, 0xf1, 0xb0, 0xfa, 0xeb, 0x18, 0x70, 0x23, 0x4a, 0xf2, 0x88, 0x7f, 0xfa, 0x93, 0x75, 0x14, 0x87, - 0x51, 0x6f, 0x15, 0x7a, 0x41, 0xc2, 0xa2, 0x14, 0x41, 0x75, 0x89, 0xf0, 0x11, 0xe0, 0xb9, 0xda, 0x84, 0x2b, 0x77, - 0xe2, 0x25, 0xf7, 0x3d, 0x9b, 0xb3, 0x14, 0x76, 0x9f, 0x73, 0x07, 0x76, 0x6d, 0xfd, 0x1e, 0x87, 0xe6, 0x73, 0x64, - 0xfc, 0xa2, 0x2a, 0x3b, 0x23, 0x6f, 0xf3, 0xbe, 0xf4, 0x96, 0xc2, 0x74, 0x01, 0xfb, 0xe1, 0x46, 0xe6, 0x1c, 0xb0, - 0x3c, 0x2c, 0xb5, 0x3d, 0x65, 0x73, 0x03, 0xb1, 0x36, 0xdc, 0x00, 0x89, 0x3f, 0x56, 0x47, 0x57, 0xec, 0xfa, 0x62, - 0xe0, 0x78, 0xf4, 0x7d, 0x46, 0xd6, 0x73, 0x21, 0xa9, 0xa5, 0xb1, 0x4f, 0xcd, 0x31, 0x9b, 0x85, 0x11, 0xa3, 0x90, - 0xee, 0x4e, 0x77, 0x75, 0xb7, 0x7f, 0xf7, 0xdb, 0xa7, 0x5f, 0xdf, 0x4f, 0x10, 0x26, 0x9a, 0xe8, 0x4c, 0xdf, 0xd1, - 0x5b, 0x95, 0x9e, 0x01, 0x6b, 0x48, 0x90, 0x9f, 0x90, 0xc3, 0x49, 0x4f, 0x55, 0xfb, 0xb5, 0x91, 0x33, 0x57, 0x21, - 0xa7, 0x79, 0x11, 0xf3, 0xdd, 0xc4, 0xbb, 0x11, 0x3c, 0x63, 0xfb, 0x68, 0x75, 0x27, 0xd6, 0x18, 0x09, 0xde, 0x03, - 0x16, 0xa9, 0x34, 0x14, 0xb1, 0x48, 0xe5, 0x62, 0x5c, 0xa4, 0x7e, 0x65, 0x36, 0x22, 0x98, 0x54, 0x89, 0xd2, 0x77, - 0x56, 0x77, 0x32, 0x89, 0xce, 0x9b, 0x65, 0x94, 0xba, 0x1c, 0x05, 0x74, 0xe9, 0x4d, 0xa7, 0x3e, 0x4b, 0x0b, 0x0b, - 0x5d, 0x5c, 0x4b, 0x09, 0x38, 0x19, 0x1c, 0xdc, 0x71, 0x1c, 0xfa, 0xeb, 0x84, 0xd5, 0x83, 0x8b, 0x80, 0xd3, 0xb2, - 0x73, 0xe0, 0xe0, 0xef, 0xe2, 0x58, 0x3b, 0xc0, 0x6e, 0xc3, 0x36, 0xb1, 0xfb, 0x10, 0xf4, 0xdf, 0x6c, 0x17, 0x87, - 0x0e, 0xaf, 0xb2, 0x41, 0x1b, 0x35, 0x13, 0x31, 0x80, 0x2c, 0x11, 0xf6, 0x56, 0x2c, 0x87, 0x97, 0x65, 0x81, 0xcf, - 0xb3, 0xa2, 0xb4, 0x38, 0x99, 0xdf, 0xe7, 0x8c, 0xbd, 0xa8, 0x3f, 0x63, 0x2f, 0xc4, 0x19, 0xdb, 0xbe, 0x33, 0x1f, - 0xcf, 0x1c, 0xf8, 0xaf, 0x9f, 0x4f, 0xa8, 0x67, 0x2b, 0xed, 0xd5, 0x9d, 0xe2, 0xac, 0xee, 0x14, 0xb3, 0xb5, 0xba, - 0x53, 0xb0, 0x6b, 0xb4, 0x3c, 0x32, 0xac, 0x96, 0x6e, 0xd8, 0x0a, 0x14, 0xc2, 0x1f, 0xbb, 0xf0, 0xca, 0x39, 0x84, - 0x77, 0xd0, 0xaa, 0x53, 0x7d, 0xd7, 0xda, 0x7e, 0xd4, 0xe9, 0x2c, 0x09, 0xa4, 0xad, 0x5b, 0x89, 0x3b, 0x1e, 0xb3, - 0x69, 0x6f, 0x16, 0x4e, 0xd6, 0xf1, 0xbf, 0xf9, 0xf8, 0x39, 0x10, 0xb7, 0x22, 0x82, 0x52, 0x3f, 0xa2, 0x29, 0x68, - 0xf7, 0x6e, 0x98, 0xe8, 0x61, 0x93, 0xad, 0x53, 0x8f, 0x32, 0x14, 0xb4, 0xac, 0xc3, 0x9a, 0x4d, 0x5e, 0x0f, 0xe8, - 0xdf, 0x6d, 0x95, 0x9a, 0x51, 0xcc, 0x27, 0x80, 0x65, 0x2b, 0x38, 0x1e, 0x0e, 0x0d, 0xbe, 0x9a, 0x76, 0xb7, 0x7e, - 0xb8, 0x97, 0xe2, 0x4b, 0x57, 0x82, 0xa8, 0x70, 0xba, 0xc5, 0xdd, 0xa0, 0xb6, 0xf7, 0xda, 0xb4, 0x47, 0x2a, 0xbd, - 0x6e, 0x21, 0x08, 0x79, 0xdd, 0x3d, 0xb1, 0xfc, 0xe3, 0x17, 0x87, 0xf0, 0x1f, 0x71, 0xf5, 0xff, 0x4c, 0xea, 0x18, - 0xf5, 0xb3, 0xa4, 0xc0, 0xa8, 0x13, 0xab, 0x84, 0x8c, 0xf8, 0xfe, 0xf5, 0x67, 0xb3, 0x87, 0x35, 0xd8, 0xbb, 0x36, - 0x19, 0xed, 0x95, 0x6b, 0xbf, 0x0c, 0x43, 0xc8, 0x9e, 0x5d, 0xad, 0x2e, 0xc0, 0x43, 0x1e, 0x18, 0xc9, 0x00, 0x1a, - 0x09, 0x39, 0x82, 0xec, 0x45, 0x54, 0x6c, 0x43, 0xa2, 0xc4, 0x9b, 0x26, 0x51, 0xe2, 0xf5, 0x6e, 0x51, 0xe2, 0xbb, - 0xbd, 0x44, 0x89, 0xd7, 0x9f, 0x5d, 0x94, 0x78, 0x53, 0x15, 0x25, 0x2e, 0x42, 0x61, 0xa9, 0x6d, 0x9c, 0xad, 0xf9, - 0xcf, 0x9f, 0xe9, 0x2a, 0xf6, 0x3c, 0x1c, 0x74, 0x6c, 0xca, 0x3a, 0x70, 0xf1, 0x5f, 0x0b, 0x16, 0xb8, 0x11, 0xdf, - 0xa1, 0xe1, 0x62, 0x2e, 0x5a, 0x70, 0xcc, 0x8e, 0xdf, 0x91, 0x8a, 0xfd, 0x30, 0x98, 0xff, 0x08, 0x57, 0xf1, 0xa0, - 0x0e, 0x8c, 0xa4, 0x17, 0x5e, 0xfc, 0x63, 0xb8, 0x5a, 0xaf, 0xce, 0xa0, 0xaf, 0x9f, 0xbd, 0xd8, 0x1b, 0xfb, 0x2c, - 0x8b, 0x06, 0x42, 0x86, 0x96, 0x5c, 0xb7, 0x0e, 0xb6, 0xcd, 0xe2, 0xa7, 0x7b, 0x27, 0x7e, 0xa2, 0xf5, 0x33, 0xff, - 0x4d, 0x16, 0x9c, 0x6a, 0xbd, 0x20, 0x02, 0x61, 0xf3, 0x4a, 0x83, 0x7e, 0xb8, 0x30, 0x72, 0x11, 0xea, 0x35, 0xb3, - 0x14, 0x96, 0x35, 0x8d, 0xfd, 0xb0, 0x8a, 0x50, 0xb3, 0xd6, 0x8d, 0x2c, 0x0a, 0x66, 0x55, 0x9d, 0xbf, 0x0c, 0xd7, - 0x31, 0x9b, 0x86, 0xb7, 0x81, 0x6a, 0x04, 0xdc, 0x1c, 0x94, 0x12, 0x09, 0x66, 0x6d, 0x30, 0x7f, 0xf3, 0x7b, 0x64, - 0x94, 0x21, 0x62, 0x02, 0xa4, 0x0f, 0x5f, 0xaf, 0x4c, 0x32, 0x30, 0x30, 0x71, 0x8a, 0x6a, 0x96, 0x68, 0xf0, 0x91, - 0xa6, 0x85, 0x83, 0x87, 0xb5, 0x14, 0x46, 0x41, 0xa1, 0xc5, 0xb5, 0xc2, 0xb1, 0x16, 0x08, 0xe5, 0xa2, 0x08, 0x45, - 0x55, 0xb3, 0x70, 0xfc, 0x0d, 0x85, 0xfa, 0xc8, 0xdf, 0x42, 0x64, 0x88, 0x74, 0xcd, 0xd7, 0x83, 0x07, 0x66, 0xa2, - 0xc7, 0x57, 0x12, 0x18, 0xdf, 0xde, 0xb0, 0xc8, 0x77, 0xef, 0x35, 0x3d, 0x0d, 0x83, 0xef, 0x01, 0x00, 0xaf, 0xc3, - 0xdb, 0x40, 0xae, 0x80, 0xf9, 0xd2, 0x6a, 0xf6, 0x52, 0x6d, 0x08, 0x31, 0x70, 0xa7, 0x92, 0x46, 0x00, 0x99, 0xea, - 0xe7, 0xec, 0xef, 0x06, 0xfd, 0xfb, 0x0f, 0x3d, 0x35, 0xce, 0xc3, 0xec, 0x43, 0x3f, 0xad, 0xf6, 0xf8, 0xcc, 0xd3, - 0xa7, 0x8f, 0x9a, 0xa7, 0xad, 0x4d, 0x7c, 0xe6, 0x8a, 0xfc, 0xf3, 0x5a, 0x4d, 0x6b, 0xbd, 0xf1, 0x14, 0xc0, 0x28, - 0x2e, 0xc2, 0xf5, 0x64, 0x81, 0x26, 0xd5, 0x9f, 0x6f, 0xbe, 0x09, 0xf4, 0x89, 0x89, 0xc2, 0xb3, 0xa9, 0x97, 0x8a, - 0x72, 0x28, 0xe0, 0xf7, 0xdf, 0x40, 0x0c, 0xec, 0x3f, 0x11, 0x0c, 0xd5, 0x5d, 0x93, 0xb9, 0x5b, 0x3f, 0x68, 0xf3, - 0xf6, 0x21, 0x9f, 0x35, 0x8f, 0x2e, 0x25, 0x2e, 0xe9, 0xea, 0x91, 0x4c, 0x5a, 0x06, 0x9a, 0x1c, 0xc9, 0xb5, 0x29, - 0x48, 0xad, 0xf8, 0x0a, 0xb3, 0x48, 0x4c, 0xe7, 0x2e, 0x2d, 0x06, 0xe3, 0xd8, 0xaa, 0x84, 0x64, 0xb8, 0xa1, 0x0b, - 0x43, 0xf4, 0x55, 0x7e, 0xb7, 0xf4, 0x02, 0x03, 0x13, 0xb1, 0x54, 0xdf, 0xb8, 0x77, 0x90, 0x8a, 0x00, 0x90, 0x5b, - 0xf9, 0x15, 0x14, 0x1a, 0xb2, 0x23, 0x27, 0x64, 0x5b, 0x54, 0x6b, 0x21, 0x21, 0x6e, 0x03, 0x47, 0x5f, 0x28, 0x8a, - 0xa2, 0x64, 0x62, 0x84, 0x92, 0xc9, 0x11, 0x58, 0x8e, 0xe2, 0x00, 0xdc, 0x96, 0xa4, 0xab, 0x3b, 0x2a, 0x01, 0xc9, - 0x00, 0xaf, 0xb6, 0x45, 0x01, 0x8f, 0xb6, 0xdb, 0xb1, 0x45, 0x81, 0x10, 0xe8, 0x21, 0x52, 0xaa, 0x1b, 0x41, 0x50, - 0xfe, 0x9e, 0x82, 0x02, 0x3b, 0xbe, 0xe5, 0x9a, 0x60, 0xc5, 0xa6, 0xc7, 0x51, 0x9f, 0xd5, 0x87, 0x65, 0x0d, 0x24, - 0x2c, 0x08, 0xb7, 0x0e, 0xa5, 0x2c, 0x0b, 0x06, 0xab, 0xc1, 0x8d, 0x28, 0x17, 0xdd, 0x25, 0x4b, 0x16, 0xac, 0x55, - 0x4c, 0xcb, 0x88, 0x61, 0x72, 0xa1, 0xce, 0x6b, 0x62, 0xb6, 0x00, 0xdb, 0xd4, 0xb7, 0x5c, 0x10, 0x2d, 0x8c, 0x39, - 0x4a, 0x75, 0x8d, 0x09, 0xf7, 0x4d, 0x8c, 0x39, 0x6e, 0x2b, 0x53, 0x08, 0xbe, 0xa4, 0x61, 0x11, 0x9b, 0x73, 0x6f, - 0x64, 0xe4, 0x14, 0x28, 0x42, 0x15, 0x57, 0x17, 0x09, 0xb0, 0x6b, 0x6e, 0x79, 0xd1, 0xb2, 0x1b, 0x19, 0xb7, 0xa4, - 0x28, 0x8a, 0xf4, 0x6a, 0x37, 0x7c, 0x9c, 0x10, 0x1b, 0xb0, 0xb1, 0x9f, 0x49, 0xa5, 0x9f, 0x86, 0x49, 0x7f, 0x60, - 0xf7, 0x44, 0x48, 0x08, 0x54, 0x1f, 0xd8, 0x3d, 0xdc, 0xdb, 0xbf, 0x01, 0x6d, 0x8a, 0xba, 0x05, 0x5d, 0x1b, 0x90, - 0x6d, 0x67, 0x02, 0xf1, 0x22, 0xb7, 0x1c, 0x20, 0x3b, 0xdd, 0x82, 0xc5, 0x11, 0xc4, 0x81, 0x11, 0xf7, 0xc5, 0x21, - 0xe6, 0xce, 0x24, 0x5a, 0x2d, 0x8c, 0xcd, 0x9a, 0xa3, 0xa1, 0x3f, 0x73, 0x6c, 0xfb, 0xa0, 0x52, 0x1f, 0x14, 0xd9, - 0x75, 0xb5, 0x75, 0x23, 0x19, 0x38, 0xb6, 0xe9, 0x3d, 0xb3, 0x5a, 0xfd, 0x0a, 0x8d, 0x96, 0xc2, 0x39, 0x8f, 0x50, - 0xfd, 0x35, 0x7c, 0xb2, 0xd1, 0x2a, 0x07, 0x52, 0x2f, 0x3b, 0x67, 0xe0, 0xd8, 0x52, 0xae, 0xff, 0x1a, 0x55, 0x49, - 0x3f, 0x05, 0x93, 0xa6, 0xd4, 0x62, 0x23, 0x48, 0x48, 0xa0, 0xc1, 0x31, 0xfa, 0x8b, 0xf2, 0x5c, 0xd1, 0xe8, 0xf8, - 0xe8, 0xfa, 0xa8, 0x2f, 0x30, 0x8a, 0xf0, 0x5e, 0x94, 0x3b, 0x28, 0x7d, 0x31, 0x2e, 0x63, 0x38, 0x1e, 0xfa, 0x9c, - 0xe5, 0x37, 0x7a, 0x5b, 0xb9, 0x05, 0xec, 0xbf, 0x81, 0x7c, 0x5a, 0x63, 0x88, 0xaf, 0x01, 0x35, 0x20, 0x7d, 0xc9, - 0xce, 0x0e, 0x21, 0x6c, 0x92, 0xdc, 0x5d, 0x91, 0x48, 0xee, 0xdf, 0x19, 0x12, 0x1d, 0xbc, 0x43, 0xcb, 0xfa, 0xab, - 0x27, 0x77, 0x0f, 0xec, 0x92, 0x05, 0xd3, 0x62, 0x87, 0x25, 0xfa, 0xb5, 0x7f, 0x77, 0x05, 0x8c, 0x02, 0x79, 0x7d, - 0xc2, 0x1a, 0x8c, 0x92, 0x86, 0x01, 0x6e, 0x7e, 0x3a, 0x6e, 0xde, 0x5e, 0x5c, 0x0c, 0x36, 0xa0, 0xa0, 0x9c, 0x59, - 0x33, 0x49, 0x29, 0x0e, 0xc9, 0x23, 0xd0, 0xb9, 0x59, 0x13, 0x8c, 0x68, 0xe3, 0x4e, 0x4c, 0x84, 0x25, 0x69, 0xde, - 0xc6, 0xe3, 0xe1, 0xa0, 0xf7, 0xd5, 0x5a, 0x7b, 0xbb, 0xb5, 0xd6, 0xc9, 0x2e, 0xad, 0x35, 0x39, 0xee, 0x91, 0xf9, - 0x53, 0xe6, 0xc0, 0x28, 0x98, 0x73, 0xd9, 0x05, 0xb4, 0xa0, 0xea, 0x46, 0x3f, 0x3f, 0xd1, 0xaa, 0xd2, 0x1b, 0xd9, - 0x86, 0xa2, 0xfa, 0x5b, 0x12, 0x50, 0xc4, 0x85, 0xba, 0xac, 0x1b, 0xbf, 0xc8, 0x75, 0xe3, 0x24, 0xd5, 0xe4, 0x2e, - 0x5b, 0x82, 0xfb, 0x97, 0xdc, 0x21, 0x33, 0xe9, 0x20, 0x77, 0x8b, 0xcc, 0x47, 0x2a, 0x39, 0xfa, 0xe5, 0x82, 0x86, - 0xe4, 0x3e, 0x2a, 0xa4, 0x8c, 0xa2, 0x17, 0x69, 0xb1, 0x6a, 0xee, 0xe7, 0x97, 0x97, 0x83, 0xd6, 0x1d, 0x87, 0x9c, - 0x15, 0xcb, 0xdb, 0xa6, 0xe8, 0xe8, 0x25, 0xbf, 0x96, 0x36, 0x49, 0xe6, 0x91, 0x45, 0x00, 0x16, 0x6a, 0xfa, 0xd2, - 0xbd, 0x76, 0x66, 0x03, 0x81, 0x83, 0xac, 0x71, 0x20, 0xdd, 0xad, 0x9d, 0xa7, 0x94, 0x45, 0xf9, 0xd5, 0xb5, 0x83, - 0xd4, 0x9d, 0x6e, 0x82, 0x65, 0x7d, 0x04, 0xc2, 0xfa, 0x4a, 0xd2, 0x20, 0xf4, 0x6c, 0xc5, 0xee, 0xd7, 0x30, 0x00, - 0x48, 0xff, 0xcb, 0xcf, 0x9c, 0x15, 0x00, 0x4d, 0xa4, 0x62, 0xcb, 0x77, 0xfe, 0x78, 0x88, 0x4d, 0x32, 0x3f, 0xc3, - 0xaa, 0xd5, 0x6f, 0x92, 0xbe, 0x67, 0xc3, 0xdd, 0xb5, 0x8a, 0xea, 0x7c, 0x5e, 0xa3, 0x27, 0xc6, 0xc1, 0x77, 0x59, - 0xb4, 0x0e, 0x30, 0x13, 0x8d, 0x99, 0x44, 0xee, 0xe4, 0xc3, 0x46, 0xfa, 0x1e, 0x57, 0x89, 0x82, 0xba, 0xb8, 0x78, - 0xa9, 0xd0, 0x77, 0x31, 0x70, 0x33, 0xeb, 0x59, 0xad, 0x58, 0x52, 0xd4, 0xf4, 0x1e, 0xdb, 0x6d, 0xf7, 0xc5, 0xec, - 0xb0, 0xa4, 0x3f, 0x6d, 0x75, 0x8a, 0xda, 0xf5, 0x6c, 0x1c, 0xcb, 0xf0, 0x57, 0xee, 0xd8, 0xfa, 0xc7, 0x7f, 0x3a, - 0xe6, 0xdf, 0x2c, 0xad, 0xd1, 0xa7, 0x0c, 0x01, 0xda, 0x17, 0x2e, 0xa6, 0xe5, 0x6b, 0x9a, 0x4a, 0x49, 0xd3, 0xb0, - 0x66, 0x9e, 0xef, 0x9b, 0x3e, 0xb8, 0x17, 0x6d, 0x3e, 0x69, 0x7a, 0xd8, 0xcf, 0x1a, 0x52, 0x06, 0x7c, 0x42, 0x3f, - 0xc5, 0x9d, 0x92, 0x2c, 0xd6, 0xcb, 0xf1, 0x46, 0x56, 0x94, 0x4b, 0xfa, 0xf3, 0xaa, 0xce, 0x5c, 0xfe, 0xec, 0x6c, - 0x36, 0x2b, 0x6a, 0x8d, 0x6d, 0xe5, 0x10, 0x35, 0xbf, 0x8f, 0x6d, 0xdb, 0x2e, 0xc3, 0xb7, 0xe9, 0xa0, 0xd0, 0xc1, - 0x30, 0x51, 0x09, 0xdf, 0xdd, 0xbd, 0xa7, 0xfe, 0xa0, 0xd1, 0x52, 0x57, 0x4d, 0xe7, 0x91, 0xb6, 0xda, 0xff, 0x8b, - 0xa1, 0x20, 0x6a, 0xd8, 0x75, 0xfc, 0xab, 0x7b, 0x65, 0x4b, 0x4f, 0xe5, 0x03, 0xfc, 0xb0, 0xc6, 0x3b, 0xf6, 0xfa, - 0x1e, 0x4d, 0x9b, 0xb6, 0x77, 0x6a, 0xe5, 0x64, 0xb7, 0x60, 0xb3, 0xd4, 0x27, 0x4b, 0x25, 0x2f, 0x61, 0xcb, 0xb8, - 0x37, 0x61, 0x78, 0x41, 0x6a, 0x49, 0xd4, 0x16, 0xad, 0x7a, 0xcc, 0x39, 0xd8, 0x71, 0x39, 0x02, 0x0f, 0xdb, 0x0a, - 0x5e, 0x56, 0x55, 0x6e, 0xd6, 0xc4, 0x47, 0x90, 0x8a, 0x6d, 0xaa, 0x17, 0x4e, 0xb8, 0x4d, 0x3b, 0xf6, 0x5f, 0x0a, - 0xf5, 0x14, 0xe0, 0x4e, 0x37, 0xc2, 0xda, 0x84, 0x2e, 0x4f, 0xf0, 0xef, 0xec, 0x72, 0xee, 0xc5, 0xea, 0xae, 0x68, - 0xdc, 0xd5, 0x85, 0xeb, 0xa6, 0x9c, 0x94, 0xd1, 0xa8, 0xeb, 0x50, 0x5f, 0x66, 0x02, 0x34, 0x93, 0xad, 0x5b, 0xc0, - 0x82, 0xa6, 0x90, 0x20, 0xaf, 0xe6, 0x6e, 0x0c, 0xc5, 0x59, 0xd8, 0x79, 0xb9, 0x7e, 0x3f, 0x4f, 0xef, 0x0c, 0x73, - 0x30, 0x9e, 0x77, 0xf1, 0x72, 0xaf, 0xb0, 0x55, 0xd1, 0x54, 0x06, 0xf7, 0x80, 0x90, 0x48, 0x95, 0x75, 0xe4, 0x9b, - 0x94, 0x3d, 0x4e, 0xd3, 0x37, 0xd5, 0x79, 0x37, 0x77, 0xef, 0x74, 0xe0, 0x5e, 0xa3, 0x0a, 0xaa, 0xbd, 0xae, 0xf6, - 0xca, 0x77, 0xd8, 0x62, 0x9c, 0xb0, 0x02, 0xe0, 0x8a, 0xa2, 0xa0, 0xd1, 0x90, 0x52, 0xc2, 0x7d, 0x34, 0xe9, 0xec, - 0xad, 0x8c, 0xac, 0xc5, 0x3c, 0xb1, 0xbb, 0xfa, 0x2a, 0xd4, 0xb7, 0xb8, 0x19, 0x04, 0xd8, 0x71, 0xec, 0x84, 0xcf, - 0x26, 0xec, 0x18, 0x19, 0x5d, 0x39, 0xb8, 0x83, 0xf0, 0x94, 0x9a, 0x14, 0xdf, 0x96, 0x4e, 0x29, 0xde, 0x25, 0x7c, - 0x57, 0xab, 0xbc, 0xbf, 0x28, 0x68, 0xe3, 0xb9, 0x3f, 0x50, 0x4b, 0xdf, 0xab, 0xf6, 0xd2, 0x0b, 0xf6, 0xaf, 0xeb, - 0xde, 0xed, 0x5d, 0x17, 0x98, 0xc3, 0xbd, 0x2b, 0x03, 0x77, 0x49, 0x56, 0x4a, 0xc9, 0xe0, 0x3b, 0xe9, 0xf2, 0x40, - 0x8e, 0x65, 0xa1, 0x62, 0x2b, 0x92, 0xe8, 0x2f, 0xd6, 0x83, 0xd1, 0xc9, 0xe9, 0xdd, 0xd2, 0x57, 0x6e, 0x58, 0x04, - 0x99, 0x34, 0x07, 0xaa, 0x63, 0xd9, 0xaa, 0x82, 0x91, 0x19, 0xbc, 0x60, 0x3e, 0x50, 0x7f, 0xba, 0xf8, 0xc6, 0xec, - 0xaa, 0xa7, 0x60, 0x8e, 0x71, 0x33, 0x47, 0x16, 0xf7, 0xdc, 0xbd, 0x67, 0xd1, 0x75, 0x4b, 0x55, 0x30, 0x61, 0x26, - 0x31, 0xb7, 0x58, 0xa6, 0xb4, 0xd4, 0x3d, 0xf2, 0xb2, 0x29, 0x22, 0xb5, 0xb2, 0x0a, 0x88, 0xd5, 0x69, 0x75, 0x15, - 0xa7, 0x75, 0x68, 0x1d, 0x75, 0xd5, 0xe1, 0x17, 0x8a, 0x72, 0x32, 0x65, 0xb3, 0x78, 0x88, 0xea, 0x98, 0x13, 0xe4, - 0x07, 0xe9, 0xb7, 0xa2, 0x58, 0x13, 0x3f, 0x36, 0x1d, 0x65, 0xc3, 0x1f, 0x15, 0x05, 0x90, 0x51, 0x4f, 0x79, 0x3c, - 0x6b, 0xcd, 0x0e, 0x67, 0x2f, 0xfa, 0xbc, 0x38, 0xfd, 0xa2, 0x50, 0xdd, 0xa0, 0x7f, 0x5b, 0x52, 0xb3, 0x38, 0x89, - 0xc2, 0x0f, 0x8c, 0xf3, 0x92, 0x4a, 0xa6, 0x28, 0x2a, 0x37, 0x6d, 0x55, 0xbf, 0xe4, 0x74, 0xc7, 0x93, 0x59, 0x2b, - 0xaf, 0x8e, 0x63, 0x3c, 0xc8, 0x06, 0x79, 0x72, 0x20, 0x86, 0x7e, 0x22, 0x83, 0xc9, 0x31, 0xeb, 0x00, 0xe5, 0xa8, - 0x7c, 0x8e, 0x73, 0x31, 0xbf, 0x13, 0x08, 0x7b, 0x9e, 0x7b, 0x70, 0xc4, 0xd8, 0x6c, 0xa0, 0x7e, 0xef, 0xb4, 0xba, - 0x86, 0xe3, 0x1c, 0x59, 0x47, 0xdd, 0x89, 0x6d, 0x1c, 0x5a, 0x87, 0x66, 0xdb, 0x3a, 0x32, 0xba, 0x66, 0xd7, 0xe8, - 0x7e, 0xdb, 0x9d, 0x98, 0x87, 0xd6, 0xa1, 0x61, 0x9b, 0x5d, 0x28, 0x34, 0xbb, 0x66, 0xf7, 0xc6, 0x3c, 0xec, 0x4e, - 0x6c, 0x2c, 0x6d, 0x59, 0x9d, 0x8e, 0xe9, 0xd8, 0x56, 0xa7, 0x63, 0x74, 0xac, 0xa3, 0x23, 0xd3, 0x69, 0x5b, 0x47, - 0x47, 0xe7, 0x9d, 0xae, 0xd5, 0x86, 0x77, 0xed, 0xf6, 0xa4, 0x6d, 0x39, 0x8e, 0x09, 0x7f, 0x19, 0x5d, 0xab, 0x45, - 0x3f, 0x1c, 0xc7, 0x6a, 0x3b, 0x86, 0xed, 0x77, 0x5a, 0xd6, 0xd1, 0x0b, 0x03, 0xff, 0xc6, 0x6a, 0x06, 0xfe, 0x05, - 0xdd, 0x18, 0x2f, 0xac, 0xd6, 0x11, 0xfd, 0xc2, 0x0e, 0x6f, 0x0e, 0xbb, 0xff, 0x54, 0x0f, 0x1a, 0xe7, 0xe0, 0xd0, - 0x1c, 0xba, 0x1d, 0xab, 0xdd, 0x36, 0x0e, 0x1d, 0xab, 0xdb, 0x5e, 0x98, 0x87, 0x2d, 0xeb, 0xe8, 0x78, 0x62, 0x3a, - 0xd6, 0xf1, 0xb1, 0x61, 0x9b, 0x6d, 0xab, 0x65, 0x38, 0xd6, 0x61, 0x1b, 0x7f, 0xb4, 0xad, 0xd6, 0xcd, 0xf1, 0x0b, - 0xeb, 0xa8, 0xb3, 0x38, 0xb2, 0x0e, 0x7f, 0x3e, 0xec, 0x5a, 0xad, 0xf6, 0xa2, 0x7d, 0x64, 0xb5, 0x8e, 0x6f, 0x8e, - 0xac, 0xc3, 0x85, 0xd9, 0x3a, 0xda, 0xda, 0xd2, 0x69, 0x59, 0x00, 0x23, 0x7c, 0x0d, 0x2f, 0x0c, 0xfe, 0x02, 0xfe, - 0x2c, 0xb0, 0xed, 0x1f, 0xd8, 0x4d, 0x5c, 0x6d, 0xfa, 0xc2, 0xea, 0x1e, 0x4f, 0xa8, 0x3a, 0x14, 0x98, 0xa2, 0x06, - 0x34, 0xb9, 0x31, 0xe9, 0xb3, 0xd8, 0x9d, 0x29, 0x3a, 0x12, 0x7f, 0xf8, 0xc7, 0x6e, 0x4c, 0xf8, 0x30, 0x7d, 0xf7, - 0x4f, 0xed, 0x27, 0x5b, 0x72, 0x48, 0x14, 0xff, 0x05, 0xff, 0x87, 0x72, 0x2c, 0x8e, 0x8c, 0xf3, 0xa6, 0x4b, 0xc9, - 0x77, 0xbb, 0x2f, 0x25, 0xbf, 0x59, 0xef, 0x73, 0x29, 0xf9, 0xee, 0xb3, 0x5f, 0x4a, 0x9e, 0x97, 0x7d, 0x6b, 0xde, - 0x95, 0x53, 0x41, 0x7d, 0xb7, 0x29, 0xab, 0x1c, 0x3c, 0x57, 0xbb, 0xbc, 0x58, 0x5f, 0x41, 0x68, 0xbf, 0x77, 0xe1, - 0xe0, 0x9b, 0x75, 0xc1, 0xe0, 0x33, 0x04, 0x1c, 0xfb, 0x2e, 0x24, 0x1c, 0xfb, 0xc3, 0x7a, 0x00, 0x56, 0x66, 0x9c, - 0xcd, 0xf1, 0xa6, 0xe6, 0xc2, 0xf5, 0x67, 0x19, 0x8b, 0x04, 0x25, 0x7d, 0x2c, 0x06, 0xc7, 0x35, 0x20, 0xcf, 0x20, - 0xc9, 0xac, 0x97, 0x41, 0x0c, 0x16, 0xc1, 0x60, 0xc9, 0x31, 0x8b, 0xd2, 0x52, 0x63, 0x4b, 0x04, 0x43, 0xbc, 0xe6, - 0x5e, 0x50, 0x8d, 0xef, 0xd1, 0x00, 0xb8, 0xbe, 0x77, 0xa7, 0xda, 0xaf, 0x02, 0x96, 0x75, 0xc2, 0x40, 0x1a, 0xb8, - 0xfd, 0xba, 0xf7, 0x45, 0x33, 0xdc, 0x92, 0xe1, 0x75, 0xf3, 0x48, 0x61, 0x24, 0xe5, 0xf6, 0x4e, 0xd1, 0x8c, 0x77, - 0xd7, 0x34, 0x6b, 0x3e, 0x5f, 0x68, 0xbe, 0xc5, 0x86, 0x38, 0xeb, 0xb8, 0x0c, 0xaa, 0x52, 0x22, 0xe3, 0x5a, 0x80, - 0xe4, 0x02, 0x6a, 0x6e, 0x68, 0x9c, 0x73, 0xaa, 0xb6, 0x82, 0xfc, 0x8e, 0x2d, 0xbd, 0x2b, 0xf4, 0x29, 0x1b, 0x27, - 0x3f, 0xdb, 0xa0, 0x5c, 0xe1, 0xfd, 0x0a, 0x9c, 0x28, 0xe7, 0x78, 0xc6, 0xa1, 0x0c, 0xe7, 0x8d, 0xd4, 0x2f, 0x69, - 0x23, 0xd2, 0x85, 0xb3, 0xa9, 0xf2, 0xa2, 0x8d, 0x6e, 0x09, 0x0e, 0x5b, 0x0a, 0x2e, 0x08, 0x3f, 0x4f, 0x4e, 0x00, - 0x29, 0x39, 0x6a, 0xa0, 0x9f, 0xc3, 0xb6, 0xce, 0x44, 0xbd, 0xc7, 0xb0, 0x89, 0x79, 0xd0, 0x65, 0x45, 0x8e, 0x36, - 0xb3, 0x99, 0xf9, 0xa1, 0x9b, 0xf4, 0x90, 0x4d, 0x93, 0x58, 0xde, 0x16, 0x7a, 0x2c, 0xf4, 0xb7, 0x18, 0xd3, 0xc9, - 0x1d, 0xf3, 0x4e, 0xd0, 0xf3, 0x61, 0x9b, 0xfd, 0x5d, 0xe6, 0x70, 0xb6, 0x29, 0x98, 0xa3, 0x38, 0x9d, 0x63, 0xc3, - 0x39, 0x32, 0xac, 0xe3, 0x8e, 0x9e, 0x8a, 0x03, 0x27, 0x77, 0x59, 0x00, 0x08, 0x38, 0x40, 0x64, 0xc3, 0xf4, 0x02, - 0x2f, 0xf1, 0x5c, 0x3f, 0x05, 0x7e, 0xb8, 0x28, 0xa4, 0xfc, 0x6b, 0x1d, 0x27, 0x30, 0x47, 0xc1, 0xf4, 0xa2, 0xf3, - 0x87, 0x39, 0x66, 0xc9, 0x2d, 0x63, 0x41, 0x83, 0x61, 0x4c, 0xd9, 0x97, 0xe4, 0xf7, 0xb3, 0xac, 0x4f, 0xc9, 0x6a, - 0x6d, 0x9c, 0x04, 0x7c, 0x7f, 0x08, 0xc7, 0x87, 0x74, 0x64, 0xfc, 0xda, 0x84, 0x70, 0xff, 0xb5, 0x1b, 0xe1, 0x26, - 0x6c, 0x1f, 0x84, 0xfb, 0xaf, 0xcf, 0x8e, 0x70, 0x7f, 0x95, 0x11, 0x6e, 0xc1, 0x7f, 0x30, 0xbf, 0x61, 0x7a, 0x8f, - 0xcf, 0x1a, 0x24, 0x51, 0x79, 0xae, 0x1e, 0x10, 0x03, 0x0f, 0xf9, 0x25, 0x44, 0x14, 0xaf, 0x97, 0x85, 0x64, 0x9d, - 0xa8, 0x00, 0xc5, 0x04, 0x1d, 0x94, 0x18, 0xd0, 0x03, 0x57, 0xb7, 0x2c, 0x39, 0x20, 0xbb, 0x55, 0xce, 0x82, 0xc4, - 0xb7, 0xde, 0x71, 0x39, 0x12, 0x2e, 0x74, 0xbf, 0x09, 0xa3, 0xa5, 0x8b, 0xd1, 0x5f, 0x55, 0x4c, 0xf2, 0x0d, 0x0f, - 0x36, 0x38, 0xe3, 0x4e, 0xc2, 0x60, 0x9a, 0xdd, 0x4a, 0xb2, 0xc1, 0x25, 0x71, 0xdc, 0xea, 0x3d, 0x73, 0x23, 0xd5, - 0xa0, 0xd7, 0xb0, 0xb8, 0xcf, 0xda, 0xf6, 0xb3, 0xd6, 0xe1, 0xb3, 0x23, 0x1b, 0xfe, 0x77, 0x58, 0x3b, 0x35, 0x78, - 0xc5, 0x65, 0x18, 0x40, 0x9e, 0x41, 0x51, 0xb3, 0xa9, 0xda, 0x2d, 0x63, 0x1f, 0xf2, 0x5a, 0xc7, 0xf5, 0x95, 0xa6, - 0xee, 0x7d, 0x5e, 0xa7, 0xb6, 0xc6, 0x22, 0x5c, 0x4b, 0xc3, 0xaa, 0x19, 0x8d, 0x17, 0xac, 0x41, 0xcf, 0x2e, 0xd5, - 0x90, 0x5f, 0xf3, 0xe9, 0xe6, 0xf3, 0x62, 0xed, 0xf4, 0x2a, 0x4f, 0x66, 0x2a, 0x92, 0x2a, 0xee, 0x84, 0x20, 0xbf, - 0xa2, 0xb4, 0x31, 0xde, 0x37, 0x66, 0x9c, 0x80, 0x68, 0xdf, 0x59, 0x0a, 0x4a, 0x97, 0x16, 0x28, 0x89, 0xd6, 0xc1, - 0x44, 0xc3, 0x9f, 0xee, 0x38, 0xd6, 0xbc, 0x83, 0xc8, 0xe2, 0x1f, 0xd6, 0x71, 0xd5, 0xdc, 0xa1, 0x9d, 0x67, 0x7e, - 0x8b, 0xc5, 0xaa, 0xb8, 0xcf, 0x12, 0x23, 0xc2, 0x7b, 0x6c, 0x5a, 0x5a, 0x73, 0xe0, 0x3e, 0xcb, 0x1a, 0x3e, 0x4b, - 0x8c, 0xe0, 0x39, 0xdc, 0x7d, 0x0e, 0xec, 0xa7, 0x4f, 0xa9, 0x16, 0x64, 0x51, 0xa7, 0x69, 0x9d, 0x4e, 0xf2, 0xa0, - 0xa1, 0x8a, 0x3b, 0x0f, 0x29, 0x6e, 0x68, 0x6f, 0x62, 0x84, 0xcf, 0x9f, 0x0f, 0x07, 0x8e, 0x8e, 0x59, 0x45, 0x45, - 0x76, 0x70, 0x9e, 0xb0, 0xf6, 0x7c, 0x3f, 0x43, 0x23, 0xbd, 0xd6, 0x95, 0x76, 0x05, 0x32, 0x93, 0x2d, 0xdc, 0x11, - 0x38, 0xf6, 0x82, 0x04, 0x72, 0x64, 0x50, 0xe0, 0x0a, 0x83, 0x1f, 0x51, 0x27, 0x93, 0xba, 0xda, 0x96, 0x6d, 0xd9, - 0x6a, 0xd6, 0x70, 0xe6, 0xcd, 0x07, 0x9b, 0x30, 0x71, 0x21, 0x15, 0xa7, 0x1f, 0xce, 0xc1, 0x8f, 0x2e, 0xf1, 0x12, - 0x1f, 0xf2, 0x3a, 0x82, 0x43, 0xdd, 0x92, 0xe4, 0xf2, 0x94, 0x7b, 0x37, 0xb8, 0xd1, 0x07, 0xcc, 0xed, 0x2d, 0x5c, - 0x71, 0x31, 0x8e, 0xdd, 0xf7, 0x40, 0x0c, 0x35, 0x55, 0x03, 0xdd, 0x00, 0x8b, 0x62, 0x53, 0xf6, 0x16, 0xea, 0x29, - 0xd0, 0x46, 0x57, 0xf9, 0x24, 0x66, 0x91, 0xbb, 0x84, 0x1c, 0x48, 0x9b, 0xd4, 0xe0, 0x98, 0x56, 0xe5, 0xa8, 0x56, - 0x71, 0x5e, 0x1c, 0x19, 0x4a, 0xcb, 0x31, 0x14, 0x1b, 0xd0, 0xad, 0x9a, 0x1a, 0x9b, 0xf4, 0xaa, 0xbf, 0xcb, 0xe0, - 0x81, 0xf0, 0xcb, 0x63, 0x9a, 0x07, 0x99, 0x3a, 0xf0, 0xab, 0xa4, 0x84, 0xe2, 0x17, 0x6b, 0x52, 0x66, 0x13, 0x8f, - 0x2e, 0x3d, 0x2f, 0xd8, 0x5d, 0xa2, 0x63, 0xde, 0x43, 0x5e, 0xc5, 0xd3, 0x37, 0xe8, 0x30, 0xec, 0x05, 0x8a, 0xf7, - 0xf1, 0xa3, 0xe6, 0x81, 0x33, 0xd3, 0x40, 0x82, 0x0f, 0x3c, 0xeb, 0x05, 0x80, 0x79, 0xf9, 0x35, 0x3d, 0x02, 0x0b, - 0x3c, 0x0d, 0xe1, 0xdf, 0xbc, 0x58, 0xfc, 0xe0, 0x66, 0x12, 0x96, 0xef, 0x06, 0x73, 0x40, 0x69, 0x6e, 0x30, 0xaf, - 0x98, 0x63, 0x91, 0xcf, 0x73, 0xa9, 0x34, 0xef, 0x2a, 0x37, 0x95, 0x8a, 0x5f, 0xde, 0x5f, 0x50, 0x5e, 0x57, 0x4d, - 0x05, 0x2a, 0x87, 0x2e, 0xba, 0xf9, 0x4d, 0xee, 0xf3, 0xc1, 0x97, 0x27, 0x4b, 0x96, 0xb8, 0x74, 0x0d, 0x04, 0xc2, - 0x2f, 0xb0, 0x03, 0x0a, 0x27, 0x34, 0x3c, 0x36, 0xd4, 0x60, 0xca, 0x6e, 0xbc, 0x09, 0x97, 0x4b, 0x0d, 0x85, 0xd3, - 0x29, 0x13, 0x2d, 0x3e, 0x07, 0x8e, 0x41, 0x0e, 0x07, 0x13, 0x17, 0x43, 0x1d, 0x0f, 0x82, 0x50, 0x1d, 0x7e, 0x99, - 0xf9, 0x66, 0x36, 0x2d, 0x02, 0x24, 0x57, 0xbf, 0x8c, 0x98, 0xff, 0xef, 0xc1, 0x97, 0x40, 0xb8, 0xbf, 0xbc, 0x52, - 0xf5, 0x7e, 0x62, 0x2d, 0x22, 0x36, 0x1b, 0x7c, 0x59, 0x93, 0x64, 0x1c, 0xc5, 0x7b, 0x1a, 0x8b, 0xda, 0x6e, 0xe5, - 0x21, 0xe7, 0xda, 0x7b, 0x09, 0xf5, 0x43, 0x2e, 0xad, 0x83, 0x04, 0xb8, 0x29, 0xc8, 0xd8, 0x4e, 0x1f, 0xe5, 0xe7, - 0xb1, 0xef, 0x4e, 0x3e, 0xf4, 0xe9, 0x4d, 0xe1, 0xc1, 0x04, 0x6a, 0x3d, 0x71, 0x57, 0x3d, 0x24, 0xaf, 0x72, 0x21, - 0x78, 0x4f, 0x53, 0x69, 0xc6, 0xd9, 0xd5, 0xee, 0x65, 0xdc, 0xca, 0x1b, 0xfc, 0x32, 0x7e, 0xea, 0x76, 0xe1, 0x25, - 0x4c, 0x7c, 0x0a, 0x1f, 0xd2, 0x54, 0x08, 0xea, 0x24, 0xa2, 0xa2, 0x60, 0x6d, 0xb5, 0x15, 0xa7, 0xfb, 0x6d, 0xe7, - 0xc6, 0xb1, 0x17, 0x2d, 0xc7, 0xea, 0xfe, 0xec, 0x74, 0x17, 0x6d, 0xeb, 0xd8, 0x37, 0xdb, 0xd6, 0x31, 0xfc, 0xf9, - 0xf9, 0xd8, 0xea, 0x2e, 0xcc, 0x96, 0x75, 0xf8, 0xb3, 0xd3, 0xf2, 0xcd, 0xae, 0x75, 0x0c, 0x7f, 0xce, 0xa9, 0x15, - 0x08, 0x40, 0x24, 0xef, 0x7c, 0x59, 0xc0, 0x02, 0xd2, 0xef, 0xec, 0x4e, 0xd6, 0x28, 0x90, 0xb7, 0x9a, 0x7b, 0x5d, - 0x40, 0x19, 0x94, 0xfa, 0x07, 0x4d, 0x11, 0xfa, 0x5a, 0x30, 0x60, 0x94, 0xec, 0x47, 0x98, 0xb7, 0x09, 0x3f, 0x74, - 0x91, 0x5f, 0xa5, 0xf6, 0x18, 0xf1, 0x36, 0xf5, 0x39, 0x45, 0x44, 0x22, 0x58, 0xba, 0x08, 0xfe, 0x69, 0x85, 0xa1, - 0xf1, 0x44, 0xfa, 0x2c, 0x09, 0x2b, 0xe5, 0xc9, 0xc8, 0xd3, 0xdd, 0x03, 0x47, 0x6f, 0x7e, 0x96, 0x25, 0xc3, 0xfc, - 0xac, 0x7d, 0x4b, 0x59, 0xca, 0x3e, 0xa9, 0x1f, 0xcc, 0xce, 0x94, 0x27, 0x56, 0x82, 0x88, 0xe2, 0x53, 0x2f, 0xca, - 0x86, 0x27, 0xa1, 0x68, 0xa7, 0x3e, 0x19, 0x8b, 0x0e, 0x59, 0x03, 0xcf, 0x80, 0x4b, 0xbe, 0x71, 0x7d, 0xc9, 0x90, - 0x4d, 0x6a, 0xf9, 0x28, 0xc3, 0xfc, 0x4f, 0x9f, 0xe6, 0x83, 0x33, 0x4b, 0xe3, 0x3e, 0x71, 0x3a, 0x40, 0x76, 0x3b, - 0xac, 0xbd, 0xd5, 0xa6, 0x72, 0x77, 0x2c, 0xfa, 0x3c, 0x08, 0xb5, 0xb0, 0x9b, 0x12, 0x16, 0x1b, 0x8d, 0x86, 0x9d, - 0x15, 0x7b, 0x0d, 0x88, 0xe2, 0x5f, 0x12, 0x75, 0x54, 0xbd, 0x1f, 0x08, 0xf3, 0x83, 0x60, 0x4b, 0xfc, 0x7d, 0x2e, - 0x8b, 0xa9, 0x00, 0x9a, 0x2d, 0xf3, 0xd8, 0xe1, 0x20, 0xfe, 0x67, 0x4f, 0x02, 0x9d, 0x35, 0xc1, 0x5e, 0xa2, 0x74, - 0x5a, 0x0b, 0xce, 0x7b, 0x19, 0x5d, 0x25, 0x82, 0xca, 0xe2, 0x53, 0x15, 0x8a, 0x20, 0x9d, 0x2c, 0x66, 0x90, 0xce, - 0x8c, 0x45, 0x33, 0x6a, 0x91, 0x17, 0x18, 0x1e, 0x26, 0x33, 0x11, 0x8e, 0xa3, 0xfa, 0xd3, 0xa7, 0x8d, 0x44, 0x88, - 0x8c, 0x73, 0x62, 0x96, 0x64, 0x39, 0x2e, 0x55, 0x19, 0xbf, 0xa9, 0x32, 0x8a, 0xc9, 0xfa, 0x45, 0xac, 0x21, 0x6c, - 0x5c, 0x69, 0xef, 0xe1, 0xcf, 0x31, 0x73, 0x13, 0x8b, 0x5f, 0x96, 0x6a, 0x12, 0x71, 0x37, 0x1c, 0xd6, 0x06, 0xeb, - 0x56, 0x1e, 0x41, 0x93, 0x47, 0xa8, 0x7d, 0xb2, 0x79, 0xb9, 0xe6, 0x51, 0x1d, 0xa0, 0x8f, 0x8f, 0x76, 0x1e, 0x80, - 0xec, 0x6d, 0xe2, 0x52, 0x60, 0x18, 0x99, 0xe4, 0x86, 0x89, 0x2b, 0xd2, 0x36, 0x02, 0x5f, 0xde, 0xaf, 0x35, 0xbf, - 0x90, 0x22, 0x3f, 0x0c, 0xdf, 0x5e, 0x7c, 0xad, 0xf0, 0xfd, 0x4f, 0xd6, 0x02, 0x28, 0xc8, 0x50, 0x6a, 0x9f, 0x01, - 0xa5, 0xf6, 0x51, 0x78, 0x6e, 0x29, 0xc8, 0x77, 0x93, 0x1e, 0x10, 0x04, 0x51, 0x01, 0x4d, 0x36, 0x14, 0xcb, 0xb5, - 0x9f, 0x78, 0x2b, 0x37, 0x4a, 0x0e, 0x30, 0xaf, 0x0f, 0x20, 0x39, 0xb5, 0x29, 0x1e, 0x04, 0x99, 0x61, 0x88, 0xc0, - 0xad, 0x49, 0x20, 0xec, 0x30, 0x66, 0x9e, 0x9f, 0x99, 0x61, 0x88, 0x0f, 0xb8, 0x93, 0x09, 0x5b, 0x25, 0x83, 0x42, - 0xfe, 0xa0, 0x70, 0x92, 0xb0, 0xc4, 0x8c, 0x93, 0x88, 0xb9, 0x4b, 0x35, 0x0b, 0x10, 0x5e, 0xed, 0x2f, 0x5e, 0x8f, - 0x97, 0x5e, 0x92, 0x45, 0xd8, 0xa5, 0x09, 0x82, 0x41, 0x04, 0x0c, 0x71, 0x38, 0x4a, 0x39, 0x08, 0xcf, 0xc3, 0x79, - 0x69, 0x47, 0xe5, 0x9c, 0xcb, 0x29, 0xc6, 0x6f, 0x27, 0x49, 0x06, 0xb4, 0xc5, 0x93, 0xd0, 0xbf, 0xe6, 0x31, 0x2c, - 0xb2, 0x40, 0xc0, 0xea, 0xf0, 0x84, 0x8b, 0xb7, 0x0a, 0x86, 0x6f, 0x51, 0x3b, 0x36, 0x44, 0xa8, 0x6f, 0x8a, 0x6e, - 0x71, 0xc0, 0x2b, 0x03, 0x69, 0xa2, 0x9e, 0x31, 0xc9, 0x08, 0x8d, 0xe5, 0x02, 0x18, 0xa1, 0x82, 0xc1, 0xcc, 0xc2, - 0x19, 0x66, 0xee, 0x94, 0x38, 0x2a, 0xe4, 0x95, 0x3e, 0x7e, 0x7c, 0x35, 0xfa, 0xed, 0x3f, 0x90, 0x09, 0x65, 0xe1, - 0x88, 0x98, 0x12, 0x97, 0x72, 0x2d, 0xce, 0x7d, 0x1a, 0x23, 0x34, 0x96, 0x62, 0x53, 0x11, 0xa2, 0x47, 0x6c, 0xad, - 0x74, 0x74, 0x25, 0x42, 0x34, 0x42, 0x92, 0x24, 0x5d, 0x44, 0xbe, 0x18, 0xc1, 0xf2, 0x8e, 0x44, 0x4c, 0x14, 0xe5, - 0x97, 0xbb, 0x97, 0xc7, 0x4a, 0x1e, 0xc3, 0xa8, 0xce, 0xa2, 0x87, 0xf6, 0xd0, 0xf0, 0xc4, 0x55, 0x90, 0x69, 0x41, - 0xf6, 0x23, 0xee, 0x1d, 0xc0, 0x34, 0x17, 0xe1, 0x92, 0x59, 0x5e, 0x78, 0x70, 0xcb, 0xc6, 0xa6, 0xbb, 0xf2, 0xc8, - 0x2e, 0x07, 0xf5, 0x6e, 0x0a, 0x71, 0x7e, 0x99, 0xb9, 0x0b, 0xf1, 0xd7, 0x69, 0x0e, 0xca, 0xb0, 0x18, 0x93, 0xb3, - 0xd3, 0xca, 0xef, 0x01, 0x21, 0x7e, 0x81, 0x04, 0xc7, 0x70, 0x78, 0x72, 0xe0, 0x0e, 0x8b, 0x41, 0x81, 0x2d, 0x91, - 0xbd, 0xa6, 0x48, 0x04, 0x4e, 0x29, 0xb6, 0xaf, 0x08, 0xe3, 0x9b, 0x3f, 0x98, 0xe1, 0x6c, 0x26, 0x07, 0xf2, 0xb5, - 0x8a, 0xc3, 0xcb, 0x80, 0x96, 0x6f, 0xe9, 0x70, 0x45, 0x5f, 0xaa, 0x7e, 0x22, 0xfb, 0xa9, 0xf6, 0x30, 0x82, 0x37, - 0xcc, 0x19, 0x8e, 0x7b, 0x25, 0x20, 0x70, 0x06, 0xb1, 0xc7, 0x54, 0x89, 0xe3, 0x91, 0x72, 0xfa, 0x89, 0x06, 0xce, - 0xe5, 0xd1, 0x60, 0x40, 0x68, 0xae, 0x8c, 0xed, 0x00, 0x88, 0x35, 0x99, 0x7c, 0x60, 0xb2, 0x09, 0x34, 0x34, 0xc9, - 0x5d, 0x16, 0x1b, 0x95, 0xa7, 0x53, 0x1d, 0xe3, 0x81, 0x2b, 0xb6, 0x5f, 0x61, 0x83, 0xc2, 0xc6, 0xe3, 0xeb, 0x0e, - 0xf8, 0x5d, 0xf4, 0x53, 0x42, 0xf3, 0xca, 0x57, 0x84, 0xd1, 0x4d, 0xdf, 0xbd, 0x0f, 0x25, 0x33, 0x26, 0x1e, 0xd1, - 0xe4, 0x1c, 0x4b, 0x2f, 0x84, 0x27, 0x71, 0xe5, 0xa0, 0x65, 0x09, 0x51, 0xaa, 0x87, 0x4d, 0x4e, 0x62, 0xb2, 0xeb, - 0xac, 0xc9, 0x75, 0x8b, 0x93, 0x41, 0xe4, 0x99, 0xe6, 0xe7, 0xb0, 0xf0, 0x12, 0xd1, 0x42, 0x7a, 0x72, 0x00, 0xf3, - 0x83, 0x28, 0x2c, 0x05, 0xc6, 0xc9, 0xd3, 0x21, 0xd4, 0x8b, 0x1b, 0x93, 0x29, 0xd6, 0xd9, 0x54, 0xf0, 0x7c, 0x28, - 0x58, 0x4a, 0xd9, 0xfd, 0xa4, 0x2a, 0x55, 0x5e, 0xc6, 0xae, 0x67, 0x02, 0x77, 0x67, 0x0f, 0xfa, 0x61, 0x8d, 0xa9, - 0x83, 0xd2, 0x7e, 0xc2, 0x44, 0x90, 0x83, 0xf3, 0xa4, 0x21, 0x0e, 0x42, 0x53, 0x15, 0xe2, 0x67, 0xb7, 0x54, 0xc8, - 0xf7, 0xf1, 0xb6, 0x5a, 0x39, 0xe7, 0x94, 0x55, 0x5b, 0xb9, 0x9a, 0xfa, 0x18, 0x77, 0x7c, 0xa5, 0x36, 0x96, 0x42, - 0xbd, 0xf3, 0x64, 0x00, 0x55, 0x85, 0x2e, 0xde, 0x5d, 0xad, 0xa8, 0xb2, 0xde, 0x3f, 0x39, 0x20, 0xb1, 0x74, 0x48, - 0x3b, 0x6c, 0x78, 0x02, 0xa6, 0xdc, 0xb4, 0xe8, 0xee, 0x6a, 0xc5, 0x97, 0x94, 0x7e, 0xd1, 0x9b, 0x83, 0x45, 0xb2, - 0xf4, 0x87, 0xff, 0x07, 0x52, 0xaf, 0x09, 0x6c, 0x30, 0x6a, 0x03, 0x00}; + 0x5e, 0x6e, 0x0b, 0x04, 0x92, 0x99, 0x22, 0xb2, 0xd9, 0xee, 0xa9, 0x2b, 0x90, 0xcb, 0x9a, 0x4d, 0xa4, 0x5d, 0xf0, + 0x72, 0xcc, 0x41, 0x26, 0x53, 0xd6, 0x93, 0xf6, 0xc0, 0x16, 0x04, 0xc9, 0x4d, 0x1a, 0x91, 0x6d, 0x7f, 0x29, 0x3e, + 0x89, 0x01, 0x8d, 0x90, 0x15, 0xf8, 0x42, 0x61, 0x91, 0x03, 0xd7, 0x59, 0xf8, 0x8e, 0xbf, 0xd1, 0x52, 0x31, 0x50, + 0xc3, 0x21, 0x2e, 0xcc, 0xf3, 0x18, 0xe5, 0x7c, 0xa8, 0x0a, 0x9e, 0x5b, 0x0a, 0x44, 0x14, 0xbe, 0x5e, 0x1f, 0xc2, + 0x6b, 0x46, 0xae, 0x4d, 0x70, 0xbd, 0x75, 0x3f, 0xab, 0x97, 0x4b, 0x60, 0x1c, 0x8c, 0xb4, 0xcc, 0x45, 0xa1, 0x93, + 0x37, 0xd9, 0xa5, 0xe8, 0x35, 0x1a, 0xcc, 0x04, 0x9a, 0x22, 0x10, 0x55, 0x0e, 0xfc, 0x22, 0xe1, 0x8f, 0x8d, 0x1d, + 0xa5, 0x98, 0x8d, 0xc0, 0x07, 0xa1, 0xc1, 0x6b, 0x09, 0xeb, 0xb5, 0xb2, 0x11, 0x5e, 0x4c, 0x8e, 0x8d, 0xf5, 0x52, + 0xf6, 0x53, 0x86, 0x92, 0xad, 0xcc, 0x38, 0xb8, 0xdb, 0xea, 0x6f, 0xab, 0xfd, 0x7c, 0xc0, 0xed, 0x35, 0x1e, 0x37, + 0x71, 0x13, 0x0c, 0xa0, 0x56, 0x5b, 0x1b, 0xdc, 0xda, 0xf9, 0xc7, 0xd6, 0x28, 0x99, 0x6d, 0x43, 0x50, 0x94, 0x71, + 0x02, 0xec, 0xcd, 0xad, 0x8f, 0x9b, 0xa8, 0xcc, 0x9c, 0x14, 0xd2, 0x03, 0x90, 0xa3, 0x87, 0x04, 0x3a, 0xb7, 0x3f, + 0x2b, 0xba, 0x50, 0xc9, 0xc4, 0xe5, 0x18, 0xff, 0x11, 0xdc, 0xe6, 0x0d, 0xa2, 0x4f, 0x9f, 0xcc, 0x26, 0xff, 0xf4, + 0x29, 0xc2, 0xa1, 0x71, 0x7d, 0x14, 0xf0, 0x82, 0xd1, 0xb0, 0x0c, 0xad, 0x65, 0x36, 0x7e, 0xb3, 0x5d, 0x35, 0xf6, + 0x81, 0x56, 0x78, 0x07, 0xcb, 0x63, 0x1a, 0xdf, 0x71, 0x46, 0x1d, 0x70, 0x80, 0x37, 0x1b, 0xf0, 0x61, 0xef, 0x4d, + 0xac, 0xd0, 0xd1, 0xd1, 0x9b, 0x58, 0xa2, 0xfe, 0x35, 0x33, 0x77, 0x6e, 0xe0, 0x8d, 0x3e, 0xe0, 0x66, 0xf8, 0x32, + 0x40, 0x80, 0x6b, 0xb6, 0x2d, 0xd9, 0xbc, 0x35, 0xb1, 0x3f, 0x52, 0x88, 0x2d, 0x0e, 0x11, 0x8e, 0x1d, 0x48, 0xa0, + 0xd7, 0x37, 0x21, 0xb4, 0x7b, 0x8c, 0x30, 0x60, 0xe1, 0x4b, 0x5f, 0x41, 0x96, 0xcc, 0x59, 0x31, 0x65, 0xc5, 0x7a, + 0xfd, 0x81, 0x5a, 0xff, 0xbf, 0xad, 0x50, 0x95, 0xaa, 0xd7, 0x68, 0x50, 0x33, 0x7e, 0x10, 0x1f, 0xe8, 0x10, 0x1f, + 0xbe, 0x89, 0x0b, 0x84, 0xc0, 0xc2, 0x88, 0x8b, 0xa5, 0xf7, 0x75, 0xcb, 0x6a, 0xeb, 0x52, 0xa0, 0xb2, 0x91, 0x9c, + 0xb4, 0xf0, 0x8c, 0x64, 0xe5, 0x1a, 0x5d, 0xce, 0x7a, 0x8d, 0x46, 0x8e, 0x64, 0x9c, 0x0d, 0xf2, 0x21, 0xe6, 0xb8, + 0x80, 0xcb, 0xd4, 0xdd, 0x75, 0x58, 0xb0, 0x1a, 0xe5, 0x72, 0xf3, 0x5d, 0xd9, 0xb1, 0xa6, 0xcf, 0xe9, 0x26, 0xdc, + 0xdd, 0x34, 0x20, 0x12, 0xfb, 0x80, 0x2c, 0x2c, 0x90, 0x95, 0x07, 0xb2, 0x30, 0x40, 0x56, 0xa8, 0xbf, 0x80, 0xa0, + 0x4d, 0x0a, 0xa5, 0x3b, 0x14, 0xbd, 0x1e, 0x5e, 0xd4, 0xb9, 0xae, 0x60, 0x6e, 0x22, 0x5c, 0xb8, 0xe5, 0x00, 0x37, + 0x16, 0x37, 0x77, 0x45, 0x56, 0x51, 0x64, 0x22, 0xed, 0xe2, 0x5b, 0xf3, 0x27, 0xb9, 0xc5, 0x77, 0xf6, 0xc7, 0x5d, + 0xa0, 0x4c, 0x7a, 0x5f, 0xd3, 0x36, 0x70, 0x17, 0x97, 0x2e, 0x4a, 0x22, 0x40, 0x6b, 0x17, 0x64, 0x51, 0xd4, 0xdf, + 0x9d, 0x53, 0x36, 0x1c, 0x86, 0x68, 0x10, 0x85, 0x45, 0x40, 0x3a, 0xff, 0xf8, 0x23, 0x42, 0x7d, 0x01, 0xd1, 0x8c, + 0xdc, 0xc9, 0xd6, 0x6c, 0xa3, 0x46, 0x94, 0x44, 0x69, 0xec, 0x83, 0x65, 0xc0, 0xce, 0x88, 0xa2, 0xe0, 0xcd, 0x99, + 0x2a, 0xca, 0x5a, 0x6d, 0x18, 0x66, 0x50, 0x55, 0xf8, 0x8f, 0xab, 0xd5, 0x76, 0xb0, 0x25, 0x03, 0x55, 0x61, 0x22, + 0xdd, 0x20, 0xfb, 0x10, 0x1b, 0x23, 0xec, 0xe8, 0x88, 0x0d, 0xc4, 0x30, 0x78, 0x59, 0xad, 0xb2, 0x20, 0xd1, 0xe1, + 0xc2, 0xc5, 0x19, 0x44, 0xbb, 0x5f, 0xaf, 0xed, 0x5f, 0xf2, 0x9b, 0x91, 0x66, 0xe0, 0x89, 0xbc, 0xe0, 0x8c, 0x15, + 0xfb, 0x65, 0xb1, 0x44, 0xcb, 0xf7, 0x60, 0xd9, 0xe7, 0x62, 0x17, 0x72, 0x37, 0xd5, 0x76, 0xe9, 0x82, 0x63, 0x34, + 0x0a, 0x41, 0xe4, 0xe0, 0xea, 0x48, 0xc3, 0x0b, 0x1d, 0xe6, 0xd5, 0x22, 0x00, 0xe7, 0xaa, 0x0c, 0xe4, 0x0a, 0x47, + 0x4a, 0x02, 0x96, 0xde, 0x86, 0x4e, 0xc2, 0x8f, 0x3a, 0x95, 0x74, 0x2c, 0x24, 0x40, 0x81, 0x23, 0x73, 0x39, 0x6f, + 0x02, 0xf5, 0x33, 0xb4, 0x87, 0xc8, 0x05, 0x26, 0x34, 0x75, 0xd9, 0xd2, 0x45, 0xd4, 0x8a, 0xe6, 0x72, 0xa9, 0xd8, + 0x72, 0x01, 0xe7, 0x7b, 0x99, 0x96, 0xe5, 0x3c, 0xfb, 0x52, 0x4f, 0x01, 0x83, 0xc8, 0x5b, 0x3d, 0x67, 0x62, 0x19, + 0xb9, 0x79, 0xbe, 0xb2, 0xe2, 0xfe, 0x9b, 0x17, 0xf8, 0x03, 0xe9, 0x1c, 0xbf, 0xc2, 0x7f, 0x51, 0xf2, 0xa1, 0xf1, + 0x0a, 0x4f, 0x39, 0xb1, 0xbc, 0x41, 0xf2, 0xe6, 0xf5, 0xf5, 0x8b, 0x77, 0x2f, 0x3e, 0x3c, 0xfd, 0xf4, 0xe2, 0xd5, + 0xb3, 0x17, 0xaf, 0x5e, 0xbc, 0xfb, 0x88, 0xff, 0x49, 0xc9, 0xab, 0x93, 0xf6, 0x45, 0x0b, 0xbf, 0x27, 0xaf, 0x4e, + 0x3a, 0xf8, 0x56, 0x93, 0x57, 0x27, 0x67, 0x78, 0xa6, 0xc8, 0xab, 0xe3, 0xce, 0xc9, 0x29, 0x5e, 0x6a, 0xdb, 0x64, + 0x2e, 0xa7, 0xed, 0x16, 0xfe, 0xcb, 0x7d, 0x81, 0x78, 0x1f, 0xb8, 0xe1, 0xb0, 0x2d, 0xe3, 0x07, 0x53, 0x86, 0x8e, + 0x94, 0x31, 0x44, 0xb9, 0x0c, 0xd0, 0x69, 0xac, 0x42, 0x74, 0xb2, 0xa1, 0xa4, 0xc1, 0x86, 0x11, 0xd0, 0x8a, 0x13, + 0xd7, 0x0e, 0x3f, 0x69, 0xb3, 0x53, 0xa0, 0x4f, 0xbc, 0x14, 0x8e, 0x4b, 0x15, 0x4e, 0xdb, 0x69, 0x31, 0x26, 0xb9, + 0x94, 0x45, 0xbc, 0x04, 0x46, 0xc0, 0x68, 0x2d, 0xf8, 0x49, 0x19, 0xb3, 0x4a, 0x5c, 0x92, 0x76, 0xbf, 0x9d, 0x8a, + 0x4b, 0xd2, 0xe9, 0x77, 0xe0, 0x4f, 0xb7, 0xdf, 0x4d, 0xdb, 0x2d, 0x74, 0x1c, 0x8c, 0xe3, 0xe7, 0x1a, 0x5a, 0x0f, + 0x86, 0xd8, 0x75, 0xa1, 0xfe, 0x2a, 0xb4, 0x57, 0xe9, 0x09, 0xa7, 0x8e, 0x6d, 0xf7, 0xc4, 0x25, 0x33, 0x7a, 0x58, + 0xfe, 0x03, 0xa0, 0xb6, 0x71, 0xab, 0x29, 0x37, 0x8e, 0xfb, 0xc5, 0x4f, 0x04, 0xaa, 0x05, 0xc6, 0x89, 0xd9, 0xba, + 0x85, 0x80, 0x69, 0x34, 0xd9, 0x60, 0x0e, 0x94, 0x28, 0x59, 0x68, 0x1f, 0xdc, 0x5f, 0x35, 0x25, 0x4a, 0x16, 0x72, + 0x11, 0xd7, 0x54, 0x0d, 0xbf, 0x04, 0x66, 0x8e, 0x87, 0x5c, 0xbd, 0xa2, 0xaf, 0xe2, 0x1a, 0xcf, 0x13, 0xb2, 0x76, + 0xe1, 0xb6, 0xf8, 0x87, 0xb3, 0xa2, 0xa8, 0x81, 0xab, 0x04, 0xac, 0x1f, 0x55, 0x53, 0x5f, 0xc2, 0x2b, 0x86, 0xac, + 0xa1, 0xaf, 0x48, 0x40, 0x3d, 0x7f, 0x2d, 0xcd, 0xb8, 0x4a, 0x65, 0xb4, 0x57, 0x44, 0x1b, 0xb3, 0x20, 0xaf, 0x88, + 0xbe, 0x54, 0x06, 0x08, 0x92, 0xf0, 0x81, 0x18, 0xc2, 0x81, 0x6f, 0x07, 0x28, 0x0d, 0x9d, 0x03, 0xb5, 0x52, 0x65, + 0x26, 0x64, 0x3e, 0x4d, 0x88, 0x06, 0xd0, 0x3c, 0x55, 0x2a, 0x28, 0xf3, 0x89, 0x25, 0x0a, 0x86, 0xfe, 0x7b, 0xb8, + 0x01, 0x8e, 0x63, 0x83, 0x8a, 0xa1, 0x5d, 0x8d, 0xa8, 0xe7, 0xb7, 0x2f, 0x5a, 0x27, 0xaf, 0x82, 0xfc, 0xa5, 0xf2, + 0xf6, 0x1e, 0x9f, 0x03, 0x4a, 0x6e, 0x83, 0x8a, 0xb5, 0xb1, 0x8f, 0x07, 0xd7, 0x0b, 0x01, 0x72, 0xac, 0xd1, 0x89, + 0x79, 0xd0, 0xb1, 0x87, 0xf4, 0x31, 0x69, 0xb7, 0x20, 0x88, 0xdb, 0x1e, 0xca, 0xf7, 0xeb, 0x16, 0x4c, 0x75, 0x72, + 0xdb, 0x04, 0x5a, 0x0d, 0x6f, 0x3c, 0xdd, 0x35, 0x79, 0x72, 0x87, 0x55, 0x80, 0x33, 0xec, 0x98, 0x35, 0xc4, 0xb1, + 0x40, 0x2e, 0xf8, 0xad, 0xdd, 0x00, 0x9a, 0x8a, 0x8e, 0x7d, 0x6b, 0xd0, 0x1b, 0x47, 0x5d, 0x36, 0x93, 0xee, 0xf1, + 0xab, 0xa3, 0xa3, 0x58, 0x36, 0xc8, 0x07, 0x84, 0x57, 0x14, 0x6c, 0xb6, 0xc1, 0xf7, 0x8e, 0x5b, 0x26, 0x3e, 0x55, + 0x01, 0x75, 0x9c, 0xa8, 0xda, 0xb1, 0x56, 0x75, 0x56, 0xee, 0x06, 0x3f, 0xa6, 0x0e, 0x6a, 0x04, 0x69, 0x76, 0x74, + 0x9d, 0x1a, 0x94, 0x6b, 0x8e, 0x72, 0xb0, 0x2d, 0x1b, 0x7f, 0x51, 0xf4, 0xc3, 0x87, 0xe6, 0xab, 0x60, 0xc2, 0x35, + 0xd3, 0xa4, 0x0f, 0x8d, 0x0f, 0xe8, 0x87, 0x0f, 0x81, 0xab, 0x23, 0xaf, 0xd8, 0x13, 0xcf, 0x8d, 0xfc, 0x6a, 0xb9, + 0xd2, 0x5f, 0x41, 0xb2, 0x2f, 0xc8, 0xaf, 0x80, 0xe5, 0x94, 0xfc, 0x1a, 0xcb, 0x26, 0x84, 0x80, 0x24, 0xbf, 0xc6, + 0x05, 0xfc, 0xc8, 0xc9, 0xaf, 0x31, 0x60, 0x3b, 0x9e, 0x99, 0x1f, 0x45, 0x09, 0x0c, 0x70, 0xaf, 0x93, 0xd6, 0xcb, + 0xae, 0x58, 0xaf, 0xc5, 0xd1, 0x91, 0xb4, 0xbf, 0xe8, 0x55, 0x76, 0x74, 0x94, 0x5f, 0xce, 0xaa, 0xbe, 0xb9, 0xde, + 0x47, 0x5f, 0x0c, 0x42, 0xe1, 0xc0, 0x34, 0x8d, 0x87, 0x33, 0xfe, 0xa9, 0x46, 0x59, 0xa1, 0x81, 0xe6, 0x69, 0xe7, + 0xfe, 0xf9, 0x05, 0x86, 0x7f, 0xef, 0x07, 0x05, 0x75, 0xe6, 0x27, 0x46, 0xda, 0xac, 0x79, 0x5e, 0xd5, 0xb9, 0x0a, + 0xf0, 0x19, 0x33, 0xd4, 0x14, 0x47, 0x47, 0xfc, 0x32, 0xc0, 0x65, 0xcc, 0x50, 0x23, 0xb0, 0xd8, 0x7b, 0x58, 0xda, + 0x93, 0x19, 0xae, 0x09, 0x1e, 0xf7, 0xe5, 0x83, 0x62, 0x78, 0xa9, 0x1d, 0x35, 0x09, 0x43, 0x80, 0x2b, 0xd2, 0x72, + 0x9b, 0xac, 0x27, 0x9a, 0xea, 0xaa, 0xdd, 0x43, 0x92, 0xa8, 0x86, 0xb8, 0xba, 0x6a, 0x63, 0x50, 0xc9, 0xf7, 0x15, + 0x91, 0xa9, 0x20, 0xde, 0x4d, 0x71, 0x95, 0xcb, 0x54, 0xe1, 0x19, 0x4f, 0x85, 0x97, 0xb3, 0x5f, 0x7b, 0xeb, 0x69, + 0xe3, 0x38, 0x6a, 0x7a, 0x66, 0x58, 0xf4, 0x55, 0xe9, 0xf0, 0x08, 0x9b, 0x54, 0x0d, 0xe1, 0xed, 0xc4, 0x12, 0xf3, + 0x98, 0xf5, 0xf2, 0x63, 0x10, 0x9b, 0x5a, 0x35, 0xda, 0x90, 0x09, 0x9f, 0x9b, 0x54, 0xc1, 0x40, 0x4d, 0xe1, 0x4b, + 0x08, 0x7b, 0x98, 0x55, 0x86, 0xd9, 0xbe, 0x61, 0x28, 0x20, 0xa0, 0xc0, 0x15, 0x61, 0x81, 0x04, 0xcf, 0xb3, 0x1a, + 0xe1, 0xa8, 0x93, 0x0b, 0x3b, 0xb9, 0x4b, 0x05, 0xdd, 0x89, 0xe1, 0xa5, 0xee, 0x21, 0xd1, 0x68, 0x38, 0x6e, 0xfb, + 0x4a, 0x98, 0x41, 0x34, 0xdb, 0xc3, 0x2b, 0xd6, 0x43, 0xaa, 0xd9, 0x2c, 0x0d, 0x20, 0xaf, 0x5a, 0xeb, 0xb5, 0xba, + 0xf4, 0x8d, 0xf4, 0xfd, 0x39, 0x6e, 0xf8, 0x2e, 0x2f, 0x78, 0xfe, 0x2e, 0xc9, 0x20, 0x02, 0xaa, 0x0a, 0x7c, 0xb6, + 0x5c, 0x44, 0x38, 0x32, 0xcf, 0xea, 0xc1, 0x5f, 0xf3, 0x1c, 0x5a, 0x84, 0x23, 0xf7, 0xd2, 0x5e, 0x34, 0xac, 0x06, + 0x2b, 0xb2, 0x32, 0x48, 0x3c, 0x4f, 0x3e, 0x01, 0xe3, 0xa0, 0x3f, 0x2b, 0xb4, 0xaa, 0x7e, 0x27, 0xb9, 0x0b, 0x97, + 0xa2, 0xfc, 0xe3, 0x6f, 0x6e, 0x54, 0x9b, 0xfd, 0x0e, 0xaa, 0x1c, 0x47, 0xbe, 0x2a, 0x3c, 0xa2, 0xf0, 0x9d, 0xd7, + 0x27, 0xdb, 0xee, 0xd1, 0xf3, 0x55, 0xd9, 0x03, 0x70, 0xde, 0x9b, 0x0d, 0xc2, 0xbf, 0xcb, 0xbd, 0x2f, 0x20, 0x47, + 0x9f, 0xa4, 0x78, 0x42, 0x35, 0x8d, 0x1a, 0x6f, 0x8c, 0xe1, 0x9b, 0x95, 0xb3, 0x7a, 0xdf, 0x1a, 0x07, 0xfb, 0xb7, + 0xba, 0x87, 0x00, 0x16, 0xb5, 0xc7, 0x9a, 0xac, 0xec, 0x6b, 0xc2, 0x96, 0xc8, 0xc0, 0xf4, 0x6d, 0x0f, 0x3c, 0xfc, + 0x18, 0x29, 0xb8, 0x55, 0x5b, 0x3e, 0x89, 0x42, 0x64, 0xd8, 0x9a, 0x33, 0x37, 0xa4, 0xd8, 0x3e, 0x8c, 0xe3, 0xef, + 0x06, 0x85, 0x5c, 0xf7, 0x42, 0xd5, 0x89, 0x69, 0xd5, 0x8d, 0x91, 0x3a, 0xd8, 0x36, 0x0b, 0xce, 0xaa, 0xde, 0x8d, + 0x84, 0x52, 0xbd, 0x6b, 0x67, 0xde, 0x26, 0x6d, 0xb6, 0xcd, 0x63, 0xcf, 0xf6, 0xf5, 0x3b, 0x05, 0x86, 0xbc, 0x87, + 0x65, 0xd0, 0xae, 0x2b, 0x38, 0x76, 0xe3, 0x00, 0xb2, 0x92, 0x5c, 0xad, 0xdc, 0xcb, 0x74, 0x7c, 0x20, 0x87, 0x9b, + 0xf2, 0x9d, 0xba, 0x00, 0x0f, 0xaa, 0x91, 0xaa, 0x2c, 0xe4, 0x0c, 0xfc, 0x23, 0x8f, 0x35, 0xfd, 0x10, 0xff, 0x1b, + 0x0e, 0xf8, 0x0a, 0x49, 0x53, 0xab, 0x7e, 0x82, 0xf7, 0xa3, 0x40, 0xe1, 0x6d, 0xeb, 0xfe, 0x24, 0x43, 0x47, 0xdd, + 0xba, 0x4e, 0xc5, 0xfa, 0xc2, 0xd6, 0x15, 0x2b, 0x65, 0xe1, 0x80, 0x6a, 0xc5, 0x68, 0x93, 0x3a, 0xbf, 0x59, 0xf7, + 0xe8, 0xd4, 0x43, 0x01, 0xbe, 0x31, 0x5c, 0x8a, 0x67, 0x05, 0x44, 0x11, 0x0b, 0xf5, 0x69, 0xba, 0x08, 0x5f, 0x55, + 0x1e, 0xc0, 0x3d, 0x61, 0xc9, 0x73, 0x96, 0x2f, 0x81, 0xc3, 0x02, 0x29, 0xa0, 0x50, 0x0a, 0x8b, 0xf5, 0x3a, 0x16, + 0x26, 0xb6, 0x84, 0x0b, 0x2d, 0xec, 0xde, 0x10, 0x31, 0xfa, 0x3b, 0xa8, 0x8b, 0xbd, 0x7a, 0xc4, 0x98, 0xb0, 0xa2, + 0xf0, 0xd2, 0x49, 0x66, 0x41, 0x5f, 0xfb, 0xfa, 0x10, 0xd5, 0x94, 0xfb, 0xb1, 0xd1, 0xf7, 0xbe, 0xe3, 0x73, 0x26, + 0x97, 0xf0, 0x78, 0x13, 0x66, 0x44, 0x31, 0xed, 0xbf, 0x81, 0x82, 0xc0, 0x0b, 0x40, 0x3c, 0xc4, 0x47, 0xe0, 0xab, + 0x3c, 0xad, 0x2b, 0x32, 0xff, 0x24, 0x48, 0x64, 0x42, 0x76, 0x46, 0xfd, 0x08, 0xbc, 0x88, 0x40, 0x84, 0x22, 0x24, + 0x62, 0x62, 0x1c, 0xf5, 0x23, 0xe3, 0x92, 0x15, 0x81, 0xd5, 0x18, 0x28, 0xb9, 0x23, 0x3c, 0x55, 0x15, 0x11, 0x0b, + 0x6b, 0xea, 0xa0, 0x12, 0x4b, 0x8d, 0x99, 0xf6, 0x49, 0xa7, 0x02, 0x21, 0xcd, 0xb6, 0x05, 0x65, 0xbd, 0xa5, 0x2e, + 0xc0, 0x92, 0x18, 0xd3, 0x5b, 0x9e, 0x7c, 0x02, 0x6e, 0x8e, 0x8d, 0x5d, 0xd1, 0x15, 0xbf, 0x06, 0xf5, 0x74, 0x5a, + 0xe0, 0x4f, 0x86, 0x61, 0x1b, 0xa7, 0x74, 0x43, 0x38, 0xce, 0x48, 0x91, 0xd0, 0x5b, 0x88, 0xad, 0x31, 0xe7, 0x22, + 0xcd, 0xf1, 0x9c, 0xde, 0xa6, 0x33, 0x3c, 0xe7, 0xe2, 0x89, 0x5d, 0xf6, 0x74, 0x0c, 0x49, 0xfe, 0x63, 0xb9, 0x21, + 0xe6, 0x69, 0xb0, 0xf7, 0x8a, 0x15, 0x8f, 0x80, 0x57, 0x51, 0x31, 0xea, 0x8d, 0x8d, 0x4d, 0x39, 0xd7, 0x95, 0xf1, + 0xfa, 0x6b, 0x1d, 0x53, 0x9c, 0xe1, 0x1c, 0x25, 0xb9, 0xc4, 0xac, 0x2f, 0xd2, 0xd7, 0x10, 0x57, 0x3b, 0xc3, 0xf6, + 0x59, 0x31, 0x7e, 0xcb, 0xf2, 0x67, 0xb2, 0xf8, 0x60, 0xb6, 0x7c, 0x8e, 0xa0, 0x10, 0xb8, 0xa8, 0x88, 0x26, 0xdc, + 0xee, 0x2d, 0xfb, 0xb2, 0x6a, 0x8a, 0xde, 0xda, 0xa6, 0xdc, 0x10, 0x67, 0x10, 0x90, 0x38, 0x99, 0xf1, 0x46, 0x1b, + 0xb3, 0x7e, 0xeb, 0x3b, 0x8d, 0xce, 0x50, 0x59, 0x12, 0x61, 0x58, 0xab, 0xa6, 0x4a, 0x25, 0x11, 0x4d, 0xe5, 0x24, + 0xbc, 0x95, 0x01, 0x76, 0xaa, 0x70, 0x26, 0x97, 0x42, 0xa7, 0x32, 0xc0, 0x9b, 0xac, 0xda, 0x5c, 0xab, 0x5b, 0x0b, + 0x31, 0x8d, 0xef, 0xec, 0x0f, 0x86, 0x3f, 0x19, 0x15, 0xff, 0x5b, 0x30, 0xec, 0x51, 0xa9, 0x00, 0xf8, 0x81, 0xe1, + 0x2c, 0x40, 0xce, 0xf2, 0x93, 0xb7, 0x00, 0x3e, 0xcb, 0x42, 0xde, 0x41, 0x2a, 0x33, 0xa9, 0x77, 0x90, 0xca, 0x20, + 0xd5, 0x78, 0xd4, 0x1f, 0x8a, 0x4a, 0x59, 0x14, 0x36, 0x48, 0x14, 0x2e, 0xd5, 0xc1, 0x92, 0x88, 0x04, 0xda, 0x35, + 0xa2, 0xdc, 0x9c, 0x0b, 0x08, 0xad, 0x08, 0x8d, 0xdb, 0x6f, 0x7a, 0x0b, 0xdf, 0x77, 0x36, 0x9f, 0xf9, 0xfc, 0x3b, + 0x9b, 0x6f, 0x3a, 0xf2, 0x18, 0x5f, 0xbf, 0xed, 0x34, 0x96, 0xf1, 0xd2, 0x61, 0xed, 0xfb, 0xf2, 0x21, 0x9b, 0x96, + 0x79, 0x30, 0x9c, 0xb4, 0xf1, 0x3c, 0x40, 0xca, 0x66, 0xc5, 0xc3, 0x75, 0x70, 0xbb, 0x75, 0x1c, 0xf3, 0x26, 0x69, + 0x23, 0x74, 0xec, 0x84, 0x2b, 0x11, 0x1b, 0xc9, 0xe9, 0xf8, 0xc3, 0x09, 0xdc, 0xbd, 0x8c, 0xd4, 0x96, 0xaf, 0x94, + 0xad, 0xd6, 0x6c, 0xb7, 0x8e, 0xf9, 0xde, 0x2a, 0x8d, 0x36, 0x9e, 0x33, 0xb2, 0x02, 0x0f, 0x34, 0x5a, 0x58, 0x55, + 0x03, 0xb8, 0xac, 0xbe, 0x10, 0xbf, 0x2e, 0xe9, 0xd8, 0x7c, 0x1f, 0xdb, 0x94, 0xd7, 0x4b, 0xed, 0x93, 0x9a, 0x1c, + 0x06, 0xd1, 0x41, 0xae, 0x64, 0x90, 0x13, 0xf3, 0x13, 0x92, 0x74, 0xd1, 0x65, 0xbb, 0x9f, 0x74, 0x8f, 0xf9, 0x31, + 0x4f, 0x81, 0x87, 0x8d, 0x9b, 0xbe, 0x42, 0xb3, 0xed, 0xeb, 0x3c, 0x5e, 0x8e, 0x78, 0xe6, 0x9a, 0xaf, 0x3a, 0x28, + 0x53, 0xed, 0x1c, 0x21, 0x0b, 0x50, 0xcc, 0xf7, 0x12, 0x64, 0xd7, 0xbb, 0x39, 0xe6, 0x29, 0xf4, 0x03, 0xb5, 0x3a, + 0xb6, 0x56, 0x39, 0xb8, 0x5f, 0x97, 0x80, 0x60, 0xbe, 0xa3, 0xda, 0x5c, 0x6c, 0x7a, 0x33, 0xae, 0x3a, 0x3b, 0xe6, + 0xd5, 0x08, 0xc3, 0x32, 0xbb, 0xfd, 0xf9, 0xa9, 0x55, 0x5d, 0x1e, 0x07, 0x10, 0xf9, 0x75, 0xc9, 0x45, 0xd8, 0x69, + 0xd8, 0xad, 0xcb, 0x09, 0x3b, 0xad, 0xcf, 0x32, 0x28, 0xb2, 0xdb, 0xeb, 0xce, 0x4c, 0xeb, 0xb3, 0xbd, 0x06, 0x47, + 0x42, 0x98, 0x94, 0x59, 0xe9, 0x4c, 0xaa, 0x98, 0x1f, 0xbf, 0x47, 0xae, 0xf5, 0xd7, 0x4b, 0xed, 0xf3, 0x4b, 0x44, + 0x80, 0xec, 0xaa, 0xeb, 0xb2, 0x3a, 0xf4, 0x51, 0x36, 0xf1, 0xea, 0x98, 0x07, 0x2b, 0xf7, 0xf4, 0x76, 0x21, 0x53, + 0x8f, 0xaf, 0xfd, 0x56, 0xba, 0x83, 0x9c, 0x40, 0x3c, 0x5c, 0x77, 0x61, 0x59, 0x90, 0xb3, 0x9b, 0x3b, 0x28, 0x19, + 0x4e, 0xdc, 0x97, 0x7e, 0xcf, 0xec, 0x75, 0x03, 0xbf, 0x4c, 0xba, 0x30, 0xf5, 0xed, 0x1e, 0x8e, 0x3b, 0xd0, 0x87, + 0x81, 0xc3, 0x76, 0x83, 0x3e, 0xb3, 0x82, 0xc8, 0x63, 0x5e, 0x58, 0x3c, 0xbb, 0x22, 0xed, 0x3e, 0x4f, 0xdd, 0x66, + 0x32, 0xa2, 0x51, 0xbb, 0xc9, 0x83, 0x99, 0x01, 0x7e, 0xb9, 0xb2, 0x61, 0x11, 0xbf, 0x4e, 0x01, 0x94, 0x7c, 0xb1, + 0x6a, 0x7d, 0x2a, 0x78, 0xd5, 0x1b, 0x4e, 0xb7, 0xd3, 0xfd, 0xba, 0xc1, 0xed, 0xae, 0x87, 0x27, 0x3c, 0x44, 0x63, + 0xd1, 0xda, 0x4f, 0x7c, 0x0e, 0x1c, 0x50, 0xd2, 0xba, 0xdf, 0x05, 0x17, 0xca, 0x12, 0x96, 0xbb, 0xe5, 0x46, 0x3b, + 0xe5, 0x2c, 0x1c, 0x6d, 0xc9, 0x80, 0x3b, 0xd8, 0x86, 0x28, 0x74, 0x70, 0xdc, 0xc1, 0x49, 0xbb, 0xdd, 0xe9, 0xe2, + 0xe4, 0xac, 0x0b, 0x03, 0x6d, 0x24, 0xdd, 0xe3, 0x91, 0xb2, 0x00, 0x0c, 0x72, 0x36, 0xae, 0xdd, 0x47, 0x10, 0xb4, + 0x2a, 0x14, 0xaf, 0xf9, 0x71, 0x1c, 0xb7, 0x93, 0xfb, 0xad, 0x76, 0xf7, 0xa2, 0x01, 0x00, 0x6a, 0xba, 0x0f, 0x57, + 0xe3, 0xf5, 0x52, 0xd7, 0xab, 0x94, 0x08, 0x5f, 0xaf, 0xd6, 0xf0, 0xd5, 0x1a, 0xed, 0x4d, 0x35, 0x05, 0x5f, 0xd5, + 0x09, 0xe7, 0xb6, 0x88, 0x57, 0xda, 0x84, 0xdb, 0x22, 0xb6, 0x03, 0x89, 0x41, 0x3a, 0x4f, 0xba, 0x9d, 0x2e, 0xb2, + 0x63, 0xd1, 0x0e, 0x3f, 0xca, 0x7d, 0xb2, 0x53, 0xa4, 0xa1, 0x01, 0x49, 0xca, 0xd9, 0xc9, 0x25, 0x48, 0xd4, 0x9c, + 0x5c, 0xb5, 0x9b, 0x73, 0x96, 0xf8, 0x09, 0x98, 0x54, 0x58, 0xce, 0x72, 0x15, 0x5c, 0x52, 0x00, 0x88, 0x4b, 0x30, + 0x2e, 0xba, 0xdf, 0xed, 0xdf, 0x4f, 0xba, 0xe7, 0x1d, 0x4b, 0xf4, 0xf8, 0x65, 0xa7, 0x96, 0x66, 0xa6, 0x9e, 0x74, + 0x4d, 0x1a, 0x74, 0x9d, 0xdc, 0xef, 0x42, 0x19, 0x97, 0x12, 0x96, 0x82, 0x60, 0x1b, 0x55, 0x31, 0x88, 0xb0, 0x91, + 0xd6, 0x72, 0xcf, 0x6b, 0xd9, 0x17, 0x67, 0xa7, 0xf7, 0xbb, 0x21, 0xd4, 0xca, 0x59, 0x98, 0x85, 0x76, 0x13, 0xf1, + 0xb3, 0x83, 0xa5, 0x45, 0xc7, 0x49, 0x37, 0xdd, 0x99, 0xa0, 0xdd, 0x34, 0xc7, 0x06, 0x07, 0x02, 0x85, 0xe3, 0x53, + 0xe1, 0xf4, 0x25, 0xc1, 0xfd, 0x58, 0x65, 0x68, 0x12, 0x2a, 0x9c, 0xfd, 0x3d, 0x65, 0xf0, 0x9e, 0x66, 0x78, 0x55, + 0xf9, 0x98, 0x8a, 0xaf, 0x54, 0xbd, 0xa1, 0x10, 0x41, 0x44, 0x0c, 0x23, 0x17, 0xdf, 0xbc, 0x9e, 0xfb, 0x0f, 0x70, + 0x11, 0x66, 0x02, 0x2e, 0x34, 0xbd, 0x12, 0xb4, 0xe2, 0x05, 0x3e, 0x85, 0x0e, 0xb5, 0x66, 0x58, 0x7d, 0x9e, 0x3a, + 0x93, 0x82, 0x50, 0xb7, 0xf5, 0x9c, 0x7f, 0xaf, 0x5c, 0x52, 0x5e, 0x65, 0x27, 0x5d, 0x94, 0xb8, 0xcb, 0xf2, 0xa4, + 0x8d, 0x92, 0xc0, 0x84, 0xc4, 0x1d, 0xc9, 0x79, 0x46, 0x06, 0xd1, 0x6d, 0x84, 0xa3, 0xbb, 0x08, 0x47, 0xd6, 0x87, + 0xf9, 0x37, 0xf0, 0xe3, 0x8e, 0x70, 0x64, 0x5d, 0x99, 0x23, 0x1c, 0x69, 0x26, 0x20, 0xb0, 0x58, 0x34, 0xc4, 0x33, + 0x28, 0x6d, 0x3c, 0xab, 0xcb, 0xd2, 0x8f, 0xfd, 0x57, 0xe9, 0x7a, 0x6d, 0x53, 0x02, 0x29, 0x73, 0x6c, 0x76, 0xa8, + 0x7d, 0x18, 0x3b, 0xa2, 0x9e, 0x59, 0x8f, 0x30, 0x08, 0x20, 0xf4, 0xce, 0x3f, 0xac, 0x57, 0xc5, 0x24, 0x61, 0xa7, + 0xb0, 0xd2, 0xe0, 0x8a, 0x1e, 0x85, 0x67, 0x58, 0x84, 0x27, 0xc2, 0x17, 0x06, 0xb1, 0xc2, 0xff, 0xce, 0xa5, 0x5c, + 0xf8, 0xdf, 0x5a, 0x96, 0xbf, 0xe0, 0x39, 0x16, 0x67, 0xd1, 0x02, 0x96, 0x5b, 0x36, 0x04, 0xd2, 0x88, 0xd5, 0x47, + 0xf0, 0x69, 0xe2, 0xc2, 0xd4, 0x81, 0x44, 0xf8, 0xc9, 0x08, 0x54, 0x5e, 0x3e, 0xfc, 0x64, 0x43, 0x26, 0x99, 0x4f, + 0x88, 0x99, 0x06, 0x61, 0x91, 0x25, 0x5c, 0x68, 0x4c, 0x0b, 0xa6, 0x54, 0x64, 0x63, 0x09, 0x46, 0x52, 0xf8, 0xc7, + 0x21, 0x7d, 0xca, 0x44, 0x44, 0xa6, 0xc3, 0xfa, 0x6c, 0xad, 0x38, 0x9c, 0xcb, 0x42, 0xa5, 0xf6, 0xa5, 0x18, 0x0f, + 0xc6, 0x45, 0xf9, 0x0c, 0x63, 0x3a, 0xcb, 0x36, 0xd8, 0xde, 0x61, 0x97, 0x85, 0xdc, 0x95, 0x76, 0x58, 0x2a, 0xcf, + 0x36, 0xdf, 0x9a, 0x90, 0xaa, 0xcd, 0x28, 0x98, 0x68, 0x35, 0xa0, 0x2a, 0x70, 0x07, 0x14, 0xb6, 0x41, 0x69, 0xd2, + 0x55, 0x59, 0x32, 0x5d, 0x95, 0xcb, 0x70, 0xd6, 0x6a, 0x6d, 0x36, 0xb8, 0x60, 0x26, 0x90, 0xcb, 0xde, 0x12, 0x90, + 0xaf, 0x66, 0xf2, 0x26, 0xc8, 0x55, 0x69, 0x39, 0x4b, 0xb3, 0x44, 0x51, 0x60, 0x04, 0x1b, 0x6d, 0xf0, 0x57, 0xae, + 0x38, 0xc0, 0xd3, 0xcd, 0x6e, 0x24, 0x65, 0xce, 0x28, 0xc4, 0x50, 0x0b, 0x9a, 0xdc, 0xe0, 0x19, 0x1f, 0xb3, 0xfd, + 0x6d, 0x82, 0x19, 0xf3, 0xbf, 0xd7, 0xa2, 0x47, 0x20, 0xcb, 0xee, 0x19, 0xd4, 0x81, 0x45, 0x5c, 0x43, 0x07, 0xa1, + 0x0c, 0xbe, 0x0c, 0x71, 0x33, 0xa7, 0x77, 0x72, 0xa9, 0x01, 0x2e, 0x4b, 0x2d, 0xdf, 0xb8, 0x70, 0x08, 0x87, 0x2d, + 0xec, 0x23, 0x23, 0xac, 0x20, 0x64, 0x40, 0x0b, 0xdb, 0x88, 0x18, 0x2d, 0xec, 0x02, 0x15, 0xb4, 0xb0, 0x09, 0x4f, + 0xd1, 0xda, 0x94, 0xb1, 0xcd, 0xee, 0xca, 0x27, 0x35, 0xab, 0x4d, 0x30, 0x71, 0xd2, 0xa1, 0x26, 0x3a, 0xb8, 0x3d, + 0x64, 0x84, 0x37, 0x7e, 0xba, 0x7e, 0xfd, 0xca, 0x45, 0xae, 0xe6, 0x13, 0x70, 0xd9, 0x74, 0xaa, 0xb1, 0x3b, 0x1b, + 0x62, 0xbe, 0x52, 0x94, 0x5a, 0xe1, 0xd4, 0x04, 0xfb, 0x14, 0x3a, 0x4f, 0xec, 0xe5, 0xc5, 0x33, 0x59, 0xcc, 0xa9, + 0xbd, 0x31, 0xc2, 0x77, 0xca, 0x3d, 0x3e, 0x6f, 0xde, 0xb7, 0xa9, 0x26, 0xf9, 0x6e, 0xfb, 0x2a, 0x62, 0x92, 0x19, + 0xf9, 0x15, 0xb4, 0x01, 0xa6, 0xb2, 0x1f, 0x38, 0x2b, 0x88, 0x8b, 0xff, 0x1f, 0x90, 0x97, 0xb7, 0x96, 0xba, 0x44, + 0x51, 0x83, 0x1b, 0xfc, 0x64, 0x45, 0xa5, 0xe3, 0xe2, 0xe6, 0xfd, 0x48, 0xd2, 0x72, 0xe2, 0x45, 0xd4, 0x8a, 0xea, + 0x6f, 0xef, 0x1a, 0x55, 0x82, 0x8f, 0x1d, 0x9b, 0xe4, 0x12, 0x44, 0x8f, 0xf2, 0x99, 0x3f, 0x0e, 0xa2, 0x89, 0xbf, + 0x7b, 0xbe, 0x6a, 0x7b, 0x3a, 0x9b, 0x57, 0xea, 0xc4, 0xf2, 0xca, 0x04, 0x3c, 0x1c, 0xed, 0x43, 0x3a, 0x08, 0x07, + 0x89, 0xac, 0xd4, 0x1e, 0xfa, 0x5c, 0xd4, 0x8b, 0xf3, 0xcb, 0x36, 0x6b, 0x9e, 0xad, 0xd7, 0xf9, 0x55, 0x9b, 0xb5, + 0xbb, 0xf6, 0xd9, 0xbd, 0x48, 0x65, 0x40, 0x73, 0xf9, 0x84, 0x67, 0x11, 0x68, 0x67, 0x17, 0x99, 0x09, 0xa7, 0xe0, + 0xa5, 0x69, 0xb2, 0xd4, 0x55, 0x5f, 0x12, 0x8c, 0x4b, 0x89, 0xd5, 0xe3, 0x17, 0xa8, 0xdf, 0x4e, 0x77, 0x5d, 0xa5, + 0x9b, 0xed, 0xe3, 0xe0, 0xc2, 0xa5, 0x40, 0xb8, 0x03, 0x21, 0x0f, 0x40, 0xbf, 0xbb, 0x12, 0x60, 0x1a, 0x04, 0xa8, + 0xac, 0x40, 0xa4, 0xe5, 0xf3, 0xe5, 0xfc, 0x59, 0x41, 0xcd, 0x32, 0x3c, 0xe1, 0x53, 0xae, 0x55, 0x4a, 0x41, 0xba, + 0xdd, 0x97, 0xbe, 0xd9, 0x2f, 0x41, 0x65, 0xb5, 0xf8, 0xbb, 0x89, 0xe6, 0xd9, 0x17, 0xe5, 0x16, 0x0e, 0x61, 0xb3, + 0xb2, 0x02, 0x67, 0x68, 0x83, 0x73, 0x39, 0xa5, 0x05, 0xd7, 0xb3, 0xf9, 0xbf, 0xb5, 0x3a, 0x6c, 0xa0, 0x87, 0xe6, + 0xc2, 0x0a, 0x40, 0x42, 0xc5, 0x78, 0xbd, 0xe6, 0x27, 0xdf, 0xbf, 0x4f, 0xf2, 0x3e, 0xe1, 0x6d, 0xdc, 0xc1, 0xa7, + 0xb8, 0x8b, 0xdb, 0x2d, 0xdc, 0xee, 0xc2, 0xd5, 0x7d, 0x96, 0x2f, 0xc7, 0x4c, 0xc5, 0xf0, 0xfe, 0x9a, 0xbe, 0x4a, + 0x2e, 0x8e, 0xab, 0x57, 0x07, 0x8a, 0xc4, 0xa1, 0x4b, 0x10, 0xfc, 0xde, 0x45, 0x0d, 0x8c, 0xa2, 0x30, 0x64, 0xdd, + 0x22, 0x54, 0x9d, 0x94, 0xfa, 0x85, 0xab, 0xd3, 0x3e, 0xd8, 0x73, 0xdb, 0x95, 0x6d, 0x82, 0xd9, 0xb7, 0xfd, 0x99, + 0x56, 0x3f, 0x9b, 0xba, 0x44, 0x0c, 0x0f, 0xbd, 0x0a, 0x3d, 0xd0, 0x15, 0x69, 0x1f, 0x1d, 0x81, 0xd5, 0x51, 0x30, + 0x1b, 0x6e, 0xa3, 0x1f, 0xf0, 0x66, 0x2d, 0x0d, 0x82, 0x15, 0x80, 0x71, 0xe7, 0x1b, 0x4e, 0x56, 0x16, 0xb6, 0x1a, + 0xa8, 0x30, 0x2b, 0xc2, 0xb8, 0x7a, 0x21, 0xa9, 0x30, 0x42, 0x34, 0x1c, 0x61, 0x2e, 0x18, 0xca, 0x61, 0x0b, 0xcb, + 0xc9, 0x44, 0x31, 0x0d, 0x47, 0x47, 0xc1, 0xbe, 0xb2, 0x42, 0x99, 0x53, 0x64, 0xc4, 0xa6, 0x5c, 0x3c, 0xd4, 0xbf, + 0xb3, 0x42, 0x9a, 0x4f, 0xa3, 0xc1, 0x48, 0x23, 0xb3, 0x8a, 0x11, 0xce, 0x72, 0xbe, 0x80, 0xaa, 0xd3, 0x02, 0x9c, + 0x7e, 0xe0, 0x2f, 0x1f, 0xa7, 0x61, 0x9b, 0x40, 0xbe, 0x7e, 0xb3, 0x31, 0x5d, 0xf0, 0xb8, 0xa0, 0x37, 0xaf, 0xc5, + 0x63, 0xd8, 0x51, 0x0f, 0x0b, 0x46, 0x21, 0x1b, 0x92, 0xde, 0x41, 0x53, 0xf0, 0x01, 0x6d, 0xbe, 0x34, 0x80, 0x4b, + 0x2f, 0xcc, 0x87, 0xad, 0xe8, 0x63, 0x37, 0x26, 0x65, 0x5b, 0x26, 0xd3, 0x9c, 0xd2, 0x55, 0xa6, 0x8d, 0x42, 0x55, + 0x4e, 0x61, 0x83, 0x5d, 0xd4, 0x93, 0x70, 0x30, 0x63, 0xaa, 0x66, 0xe9, 0x60, 0x68, 0xfe, 0xbe, 0xb6, 0x25, 0x5b, + 0xd8, 0x45, 0x9c, 0xd9, 0x60, 0xf3, 0x70, 0x6a, 0x50, 0xbe, 0x8d, 0xe1, 0x1e, 0x16, 0x5e, 0xef, 0xac, 0x91, 0xcf, + 0x33, 0x4f, 0x36, 0xcf, 0x36, 0x1b, 0x33, 0x10, 0x95, 0x82, 0x1e, 0xe8, 0xad, 0xdf, 0x36, 0x2d, 0xd8, 0x1e, 0xe5, + 0x57, 0xb7, 0x85, 0xe7, 0x1c, 0x1e, 0x23, 0xf5, 0xed, 0x5d, 0xeb, 0x42, 0x7e, 0x71, 0x20, 0x69, 0x05, 0x29, 0x76, + 0x3a, 0x41, 0x67, 0xa7, 0x38, 0x18, 0x39, 0xd0, 0xf3, 0xeb, 0x2f, 0x16, 0xd6, 0xfe, 0xf7, 0x9b, 0xb2, 0xa0, 0x89, + 0xa7, 0x53, 0x4e, 0x28, 0xf3, 0xe7, 0xe7, 0x1b, 0x9e, 0x54, 0xa8, 0xe0, 0x5e, 0xf1, 0x82, 0x3d, 0x6d, 0x03, 0x7d, + 0xce, 0xe9, 0x67, 0xfb, 0xc3, 0xc6, 0xf0, 0x29, 0xb5, 0x6c, 0x59, 0x21, 0x95, 0x7a, 0x68, 0xd3, 0xec, 0xd1, 0x03, + 0x47, 0xe4, 0x4b, 0xe8, 0x02, 0x78, 0xfd, 0x71, 0x21, 0x17, 0x06, 0x11, 0xdc, 0x6f, 0x37, 0x6e, 0xe3, 0x2b, 0x00, + 0xde, 0x0e, 0x07, 0xd5, 0x3f, 0x2d, 0x60, 0x7f, 0xa3, 0xb2, 0xa4, 0x1f, 0x6f, 0xc7, 0x1e, 0xff, 0x85, 0x84, 0xa8, + 0xf1, 0x16, 0x0f, 0x13, 0x87, 0x4e, 0x25, 0x6b, 0x56, 0xfe, 0xdc, 0x29, 0x09, 0x18, 0x56, 0x2f, 0x18, 0xb2, 0x71, + 0x3b, 0xc5, 0x6d, 0xe6, 0x7f, 0x50, 0xc1, 0x60, 0xc1, 0xb7, 0x46, 0x52, 0xb1, 0x2c, 0x7e, 0xfb, 0xd4, 0xf9, 0xaf, + 0x3a, 0xc7, 0x75, 0xa8, 0x6b, 0x2f, 0x85, 0x8e, 0x4c, 0x94, 0xe6, 0x08, 0x1d, 0x1d, 0x6d, 0x65, 0xd0, 0x09, 0x00, + 0x1e, 0x39, 0xf6, 0xcb, 0x2f, 0x9f, 0x67, 0xc7, 0x8c, 0xe6, 0xb1, 0x88, 0x42, 0xe6, 0xce, 0x73, 0x73, 0x76, 0x22, + 0x4f, 0xa8, 0x9a, 0xf9, 0xc2, 0x00, 0xc7, 0x47, 0x3b, 0xa9, 0x80, 0xef, 0xd1, 0x66, 0xcf, 0x04, 0xb6, 0xf8, 0x2d, + 0x3b, 0xa9, 0x7d, 0x05, 0xfd, 0x02, 0xad, 0xf6, 0x31, 0x95, 0x5b, 0x0b, 0x1c, 0x6d, 0x4f, 0x64, 0xef, 0xd0, 0xb7, + 0xea, 0x94, 0xac, 0xc7, 0x8b, 0xfd, 0x46, 0x5f, 0x52, 0xec, 0x4b, 0xae, 0x68, 0xdb, 0x88, 0x55, 0xaf, 0x05, 0xeb, + 0xca, 0xd4, 0xa9, 0xba, 0xe6, 0xad, 0x2c, 0x6d, 0x4a, 0xbb, 0x24, 0x7b, 0xb7, 0xc5, 0xc2, 0xab, 0xf0, 0x46, 0xa3, + 0xbc, 0x08, 0x05, 0x7b, 0x2c, 0x31, 0xec, 0x71, 0x02, 0xd7, 0x0b, 0xeb, 0x75, 0x0c, 0x7f, 0xf6, 0x8d, 0x61, 0x9f, + 0xe9, 0xd2, 0x7b, 0xbe, 0xc5, 0xaf, 0x04, 0x01, 0x8b, 0x9d, 0x1d, 0x24, 0x58, 0x77, 0xb9, 0x41, 0xc3, 0x71, 0xe2, + 0xbf, 0xe0, 0xb9, 0x6c, 0xed, 0x5d, 0x0e, 0xe6, 0xd9, 0x37, 0x9e, 0xd8, 0x2b, 0x59, 0xcb, 0x5a, 0xb4, 0xfb, 0x2d, + 0x09, 0x86, 0xd8, 0x4d, 0xe9, 0x1c, 0xb7, 0x92, 0x36, 0x8a, 0x5c, 0xb1, 0x0a, 0xfd, 0xbf, 0x55, 0x24, 0xb3, 0x99, + 0xff, 0x75, 0x7e, 0x7e, 0xee, 0x52, 0x9c, 0xcd, 0x9f, 0x32, 0x1e, 0x70, 0x26, 0x81, 0x7d, 0xe5, 0x19, 0x33, 0x3a, + 0xe4, 0xb7, 0x30, 0x14, 0x22, 0xc8, 0x95, 0x70, 0xec, 0x12, 0xbc, 0xf6, 0x08, 0x94, 0x07, 0xd8, 0xbf, 0x27, 0x5b, + 0xe5, 0xfc, 0x73, 0x51, 0x3e, 0x9c, 0x72, 0xd9, 0x20, 0xfb, 0x6a, 0x3e, 0x07, 0xd6, 0x4c, 0x06, 0x5e, 0x48, 0x88, + 0xb0, 0xfd, 0x6d, 0x58, 0x5a, 0x67, 0x29, 0x83, 0x23, 0x2d, 0x97, 0xd9, 0xcc, 0x6a, 0xfe, 0xdd, 0x87, 0x29, 0xeb, + 0x9e, 0x1a, 0x82, 0xc8, 0x5d, 0x64, 0xe5, 0xa2, 0x82, 0x46, 0x3f, 0x96, 0x01, 0x40, 0x0f, 0x5e, 0xb1, 0x25, 0xfb, + 0x11, 0x1f, 0x54, 0x29, 0xf0, 0xf1, 0xb0, 0xe0, 0x34, 0xff, 0x11, 0x1f, 0x54, 0x81, 0x40, 0xc1, 0x15, 0xd2, 0xc4, + 0xd2, 0xc4, 0xe6, 0x59, 0xed, 0x34, 0x12, 0x40, 0x41, 0xf3, 0xc8, 0x1c, 0x64, 0xcf, 0x5d, 0x8c, 0xc6, 0xa4, 0x83, + 0x5d, 0x70, 0x30, 0x1b, 0x11, 0xd6, 0x06, 0x52, 0x87, 0xb8, 0x75, 0xe5, 0x6c, 0xcc, 0xd7, 0xa3, 0xad, 0x05, 0x31, + 0xca, 0x64, 0x72, 0xf5, 0x9c, 0xc7, 0x3b, 0x8b, 0x85, 0xc2, 0x6a, 0xc1, 0x02, 0xd5, 0xaa, 0x54, 0xe9, 0x61, 0xf1, + 0xdd, 0x82, 0x59, 0x50, 0xc4, 0x6c, 0xbd, 0x87, 0xb7, 0x5c, 0x11, 0x90, 0x92, 0x5d, 0x12, 0xbc, 0x8c, 0x6e, 0x30, + 0x95, 0xac, 0xe6, 0x72, 0xcc, 0x2c, 0xa1, 0x67, 0x4a, 0x47, 0xd8, 0xe4, 0x29, 0x88, 0x24, 0x76, 0xd8, 0xc2, 0x8e, + 0x35, 0x7a, 0x21, 0xbc, 0x90, 0x02, 0xe7, 0xaa, 0x69, 0x62, 0x4e, 0xb9, 0x89, 0x2e, 0xf6, 0x50, 0x2d, 0x58, 0xa6, + 0x2d, 0x02, 0x1c, 0x3a, 0x34, 0x94, 0xe2, 0xb9, 0x01, 0x85, 0x79, 0xd2, 0xdb, 0xa5, 0x3c, 0x86, 0xc5, 0x0b, 0x52, + 0x80, 0xa8, 0x71, 0x31, 0x2d, 0xeb, 0x2c, 0xf2, 0xe5, 0x94, 0x8b, 0x0a, 0x19, 0x0a, 0xa6, 0x16, 0x52, 0xc0, 0x8b, + 0x1a, 0x65, 0x11, 0x43, 0x87, 0x6a, 0xf8, 0x6e, 0x49, 0x58, 0x59, 0xc7, 0x1c, 0x53, 0x5c, 0x54, 0x35, 0x80, 0xb9, + 0x78, 0x68, 0x04, 0x44, 0x1f, 0x5e, 0xf6, 0xb5, 0x78, 0x27, 0x17, 0x55, 0xbe, 0xa7, 0x71, 0x3e, 0x70, 0xbd, 0xb3, + 0x1b, 0x46, 0x1b, 0xf3, 0xe8, 0x55, 0xb0, 0x7d, 0xdf, 0xf3, 0xea, 0x21, 0xb8, 0x8d, 0x79, 0x36, 0xab, 0xcc, 0x1a, + 0xb1, 0xf2, 0x8d, 0x88, 0xaa, 0xbd, 0x7a, 0x55, 0x29, 0x6c, 0x45, 0x80, 0x4a, 0xc1, 0xc7, 0x3b, 0xf9, 0x2f, 0xb4, + 0xcd, 0xb7, 0xe7, 0x50, 0x19, 0x1e, 0xc8, 0x93, 0xa1, 0xaa, 0x07, 0x5c, 0x94, 0x1f, 0x02, 0x58, 0xfc, 0xc8, 0xc4, + 0x0f, 0xde, 0x77, 0x81, 0xcc, 0x99, 0x8a, 0x25, 0x5e, 0x0d, 0xe8, 0x30, 0xb5, 0xf2, 0x50, 0x2a, 0xc1, 0xb6, 0xe7, + 0xa6, 0xe0, 0xda, 0x07, 0x2a, 0xc6, 0x03, 0x36, 0x4c, 0x57, 0xf5, 0x60, 0xc6, 0x36, 0x9c, 0xb2, 0x37, 0xe7, 0x34, + 0xd1, 0x7f, 0xe9, 0x10, 0xe7, 0x04, 0x6c, 0x8f, 0x3d, 0x7b, 0xfa, 0x26, 0xce, 0x50, 0xbf, 0xce, 0xe1, 0xaf, 0x36, + 0x38, 0xc7, 0x19, 0x4a, 0x1f, 0xc6, 0x70, 0x81, 0xb5, 0xc1, 0x00, 0xbe, 0xcc, 0x92, 0x2a, 0xf0, 0x48, 0xcd, 0x8c, + 0xc4, 0xea, 0x2e, 0x02, 0xd1, 0x4a, 0x87, 0xb7, 0xe3, 0xcc, 0x87, 0x03, 0x37, 0xdc, 0xeb, 0x33, 0x23, 0x1c, 0xce, + 0xb3, 0xb8, 0x76, 0xce, 0x70, 0x72, 0x75, 0xc8, 0x6b, 0x27, 0x26, 0x58, 0x7b, 0x87, 0xa7, 0x0a, 0xe8, 0xd1, 0xe0, + 0x54, 0xb1, 0x34, 0x04, 0x62, 0x26, 0x80, 0x37, 0x73, 0x78, 0xb4, 0x05, 0x38, 0x1f, 0x6d, 0x70, 0xf0, 0x95, 0xd6, + 0xba, 0xda, 0x56, 0xa2, 0x6c, 0x36, 0x78, 0x30, 0xce, 0xf0, 0x32, 0xc3, 0xd3, 0x6c, 0x18, 0x1e, 0x37, 0x59, 0x68, + 0xd2, 0xb5, 0x5e, 0x3f, 0x75, 0x66, 0x84, 0xc8, 0xfe, 0xb4, 0xf4, 0x07, 0xf5, 0x01, 0xe1, 0x53, 0xc8, 0x02, 0x5a, + 0xd2, 0x77, 0x7f, 0x1b, 0xf6, 0xb5, 0x70, 0xd4, 0x88, 0x79, 0x62, 0xc9, 0x48, 0xdf, 0xff, 0x28, 0xb3, 0x6c, 0x6b, + 0x8d, 0x68, 0x71, 0x7b, 0x10, 0x35, 0x7c, 0x7b, 0xd5, 0xf9, 0x32, 0x2a, 0xcd, 0x76, 0x00, 0x51, 0xac, 0x71, 0x92, + 0x0e, 0xd6, 0x48, 0xae, 0xd7, 0xb1, 0x4d, 0x21, 0x3c, 0x99, 0x33, 0xaa, 0x96, 0x85, 0x79, 0x40, 0x2f, 0x56, 0x28, + 0x31, 0xfc, 0x2e, 0x76, 0x36, 0xa2, 0xf0, 0x5e, 0x9d, 0x04, 0xc3, 0x8d, 0x58, 0x10, 0x59, 0x13, 0xb9, 0x3f, 0x65, + 0x95, 0x65, 0x90, 0x20, 0xc2, 0x88, 0xfc, 0xf6, 0xba, 0x54, 0xd8, 0x27, 0xfa, 0xec, 0x1f, 0xe3, 0x0b, 0x08, 0x37, + 0x6f, 0x53, 0x5a, 0x8c, 0xe8, 0x14, 0xd8, 0x58, 0x88, 0x43, 0xb8, 0x93, 0xb0, 0x5e, 0x0f, 0x86, 0x3d, 0x61, 0xc8, + 0xb3, 0x7b, 0x40, 0xb0, 0x6c, 0x68, 0x7f, 0x03, 0x70, 0xd5, 0x6d, 0xa9, 0xb9, 0x36, 0xba, 0x1f, 0x6a, 0xde, 0x38, + 0xe3, 0x2e, 0xc9, 0x3d, 0x53, 0x52, 0xbd, 0x44, 0x5e, 0xb3, 0x00, 0x37, 0xa1, 0xab, 0xf0, 0x18, 0x2f, 0xad, 0x0d, + 0xa7, 0x79, 0xd0, 0x8a, 0x9a, 0x77, 0xac, 0xe0, 0xf9, 0x6c, 0xc2, 0x06, 0xd9, 0x10, 0x8f, 0x7d, 0xb8, 0xf3, 0xc3, + 0xb7, 0xf1, 0x18, 0xa1, 0x82, 0x18, 0x98, 0x5a, 0x97, 0xed, 0x71, 0x65, 0xb7, 0x6f, 0x32, 0x0d, 0xc3, 0x60, 0x8c, + 0x98, 0xc7, 0xa1, 0x11, 0x73, 0xde, 0x68, 0xa0, 0x25, 0x19, 0x83, 0x11, 0xf3, 0x32, 0x68, 0x6d, 0x69, 0x1f, 0x3b, + 0x0d, 0xda, 0x5b, 0x22, 0xd4, 0xe3, 0x40, 0xd3, 0x34, 0x3c, 0x6b, 0x52, 0x3d, 0x2b, 0xef, 0x1f, 0xd9, 0x3a, 0xe9, + 0x80, 0x22, 0x61, 0x72, 0xe5, 0x27, 0x61, 0x5d, 0xc3, 0xed, 0xb8, 0x27, 0x66, 0xdc, 0xce, 0xb6, 0x41, 0x0d, 0xe4, + 0x20, 0x1b, 0x0e, 0x7b, 0xd2, 0x5b, 0x49, 0xb4, 0xf0, 0xa4, 0x7a, 0x08, 0xa5, 0x5a, 0xbc, 0xaf, 0x7a, 0xfb, 0xca, + 0x9b, 0xfb, 0xf7, 0x55, 0xb7, 0xcf, 0x63, 0xe0, 0x80, 0x0e, 0xe1, 0x7e, 0xa8, 0x8a, 0x0f, 0x76, 0xd2, 0x81, 0x28, + 0x68, 0x69, 0xab, 0x26, 0x90, 0x5a, 0x33, 0xbb, 0x58, 0x37, 0x15, 0x3a, 0x16, 0x10, 0x86, 0x4c, 0x55, 0xdd, 0xdd, + 0xaa, 0x40, 0x35, 0xc4, 0xe1, 0xd4, 0x7f, 0x6c, 0x8d, 0x58, 0xe3, 0xa8, 0x33, 0x8e, 0x8c, 0x91, 0xa4, 0x5d, 0x3e, + 0x78, 0xfb, 0x08, 0xac, 0x04, 0x7c, 0x0c, 0x6a, 0x93, 0x64, 0x0c, 0x09, 0xde, 0xb2, 0x4c, 0x1b, 0x3e, 0x84, 0x3b, + 0x04, 0xe5, 0x89, 0x0d, 0x4a, 0xeb, 0x2a, 0x59, 0xc8, 0x55, 0x5d, 0xde, 0x05, 0xe8, 0x79, 0x5b, 0xfe, 0xc6, 0x86, + 0x23, 0x0b, 0x06, 0x96, 0xed, 0xec, 0x13, 0xf0, 0xc8, 0xc7, 0x15, 0x82, 0xf8, 0xa5, 0xd0, 0x89, 0x89, 0xd7, 0x7d, + 0x0d, 0x1b, 0x14, 0x2f, 0xc0, 0x41, 0xd0, 0x49, 0x70, 0x18, 0xbc, 0xcb, 0xac, 0x26, 0xd9, 0xe0, 0xd6, 0x9c, 0xc4, + 0x8b, 0xf5, 0xba, 0x85, 0x8e, 0xff, 0x69, 0x9e, 0xa4, 0x9e, 0x94, 0x0a, 0xf7, 0x49, 0xa5, 0x70, 0x07, 0x4b, 0x40, + 0x32, 0x09, 0x74, 0xed, 0x58, 0x86, 0x6a, 0x74, 0x88, 0x96, 0xfe, 0x02, 0x62, 0x67, 0xbb, 0x63, 0x09, 0xf4, 0xec, + 0x3b, 0x05, 0xac, 0xae, 0xbd, 0x2c, 0x81, 0x8c, 0xe0, 0xee, 0x37, 0x81, 0x51, 0x21, 0x1a, 0x9f, 0x3f, 0xf3, 0xaa, + 0x05, 0x4f, 0x9c, 0x3f, 0xd7, 0xdc, 0xb0, 0xee, 0x05, 0xbd, 0x31, 0xcd, 0xc7, 0x13, 0xdc, 0x9c, 0x58, 0x70, 0x9e, + 0x74, 0xe0, 0xa7, 0x85, 0xe8, 0x49, 0x07, 0xbb, 0x54, 0x3c, 0x29, 0x81, 0x1c, 0xa2, 0xa7, 0x33, 0x90, 0x02, 0x56, + 0x3a, 0xb6, 0x5a, 0xa4, 0x29, 0x5a, 0xaf, 0xa7, 0x97, 0xa4, 0x85, 0xd0, 0x4a, 0xdd, 0x70, 0x9d, 0xcd, 0xc0, 0x47, + 0x1a, 0x14, 0x03, 0x6f, 0xa8, 0x9e, 0xc5, 0x08, 0x4f, 0xd0, 0x6a, 0xcc, 0x26, 0x74, 0x99, 0xeb, 0x54, 0xf5, 0x79, + 0x62, 0x03, 0xf7, 0x32, 0x1b, 0x09, 0xee, 0xa4, 0x83, 0xa7, 0x86, 0xbf, 0xfc, 0x60, 0xcc, 0x41, 0x8a, 0xcc, 0x24, + 0x4f, 0x4d, 0x02, 0xe6, 0x49, 0x96, 0x4b, 0xc5, 0x6c, 0x33, 0x3d, 0x6b, 0x5b, 0x0e, 0x21, 0xc9, 0x23, 0x5d, 0x70, + 0x63, 0x45, 0x19, 0xa5, 0x33, 0xa2, 0xfa, 0xea, 0xa4, 0x93, 0x4e, 0x31, 0x4f, 0x80, 0xd3, 0x7b, 0x27, 0x63, 0xd6, + 0x28, 0x6f, 0x45, 0xe7, 0xe8, 0x78, 0x86, 0x45, 0x75, 0x89, 0x3a, 0x47, 0xc7, 0x53, 0x84, 0xe7, 0x0d, 0x32, 0x53, + 0xe0, 0x31, 0xcc, 0xc5, 0xff, 0x91, 0xf2, 0xdf, 0x1c, 0x36, 0x84, 0x98, 0x7e, 0x0b, 0x3b, 0x85, 0x8d, 0xa3, 0x34, + 0x27, 0xe0, 0xb5, 0xd8, 0x3e, 0xc7, 0x19, 0x99, 0x36, 0x73, 0x1f, 0x70, 0xcf, 0xb4, 0xd2, 0xb8, 0xd5, 0xe8, 0x38, + 0xc3, 0xe3, 0xed, 0xa4, 0xd8, 0xcc, 0xb5, 0x99, 0xa7, 0x19, 0x9c, 0xef, 0xd5, 0x28, 0x5c, 0xf9, 0xe5, 0x76, 0x52, + 0x58, 0xde, 0x01, 0xb7, 0x39, 0xc6, 0xa2, 0x49, 0x71, 0x8e, 0xe7, 0xcd, 0x57, 0x78, 0xde, 0x7c, 0x5f, 0x66, 0x34, + 0x96, 0x58, 0x40, 0xf0, 0x3e, 0x48, 0xc4, 0xf3, 0x2a, 0x79, 0x8c, 0x45, 0xc3, 0x94, 0xc7, 0xf3, 0x46, 0x55, 0xba, + 0xb9, 0xc4, 0xa2, 0x61, 0x4a, 0x37, 0xde, 0xe3, 0x79, 0xe3, 0xd5, 0xbf, 0x98, 0x74, 0x94, 0x02, 0xba, 0x2c, 0xd0, + 0x2a, 0xb3, 0x43, 0xbc, 0xfe, 0xf5, 0xed, 0xbb, 0xf6, 0xa7, 0xce, 0xf1, 0x14, 0xfb, 0xf5, 0xcb, 0x0c, 0x8e, 0x65, + 0x3a, 0x66, 0x4d, 0x80, 0x68, 0x86, 0x3b, 0xc7, 0x33, 0xdc, 0x39, 0xce, 0x5c, 0x53, 0x9b, 0x79, 0x83, 0xdc, 0xea, + 0x10, 0x8a, 0x3a, 0x4a, 0x43, 0xf8, 0xf8, 0xc9, 0xa6, 0x53, 0x54, 0x03, 0x25, 0x3a, 0x9e, 0xd6, 0x40, 0x05, 0xdf, + 0xcb, 0xda, 0x77, 0x55, 0xaf, 0xc2, 0x20, 0x0b, 0x25, 0x14, 0xae, 0xb9, 0x01, 0x4f, 0x2d, 0xc5, 0x40, 0x26, 0x4c, + 0xb1, 0x40, 0xf9, 0x0e, 0x28, 0x8c, 0xf2, 0xc4, 0x0c, 0x3d, 0x98, 0x8e, 0x49, 0xfc, 0xff, 0x79, 0x32, 0xe5, 0xd0, + 0xcb, 0x2d, 0xb3, 0x33, 0x3d, 0x37, 0x99, 0x70, 0xf8, 0xc0, 0x63, 0xfd, 0x5f, 0x3b, 0x50, 0x6c, 0x40, 0x8a, 0xff, + 0x2f, 0x1d, 0x5d, 0x08, 0x46, 0xc8, 0x8a, 0xd2, 0xc2, 0x21, 0xfe, 0xf7, 0x87, 0x15, 0x74, 0x5f, 0xec, 0x74, 0x5f, + 0x98, 0xee, 0xc3, 0xa6, 0x8d, 0x2a, 0x27, 0xad, 0x2a, 0x59, 0xf2, 0x5f, 0xa7, 0x5b, 0x3b, 0xa0, 0x11, 0x35, 0x7a, + 0x36, 0x0d, 0x1b, 0x3c, 0x6c, 0xa7, 0x7b, 0x90, 0x79, 0xc3, 0xed, 0x0b, 0xa9, 0x70, 0xf8, 0x06, 0x77, 0xaa, 0x57, + 0x2d, 0xf0, 0xde, 0x54, 0x46, 0x5f, 0x19, 0x87, 0x96, 0x83, 0x74, 0xdb, 0x94, 0xdb, 0x18, 0x4b, 0x27, 0x5d, 0x6c, + 0x5c, 0x11, 0xa1, 0xd2, 0xed, 0x15, 0x28, 0xc5, 0x27, 0xba, 0xc9, 0xcc, 0xd7, 0xa5, 0x4e, 0xcc, 0x25, 0x54, 0xc3, + 0x7c, 0xde, 0x5d, 0xe9, 0x44, 0xcb, 0x85, 0xcd, 0xbb, 0xbb, 0x84, 0x3e, 0x41, 0xc3, 0xda, 0x08, 0xec, 0xf6, 0xb9, + 0xb3, 0x83, 0x0c, 0x0e, 0xc1, 0xf0, 0x00, 0x72, 0xa4, 0xc5, 0xf6, 0x81, 0x4d, 0x6b, 0xd8, 0x75, 0xd1, 0x2c, 0x13, + 0x6d, 0xab, 0x4d, 0x93, 0x6b, 0xf7, 0x30, 0x5f, 0x84, 0x3c, 0x85, 0x28, 0xac, 0x7e, 0x7c, 0x0f, 0xbb, 0xf1, 0xb5, + 0xc6, 0x48, 0xd4, 0x95, 0x4c, 0x25, 0xf4, 0x93, 0x5b, 0xcc, 0x92, 0x3b, 0xe3, 0xc5, 0xa8, 0x8c, 0xbf, 0x8f, 0x89, + 0xcb, 0x1f, 0x55, 0x92, 0x1c, 0x58, 0xf6, 0x37, 0x58, 0x72, 0x0b, 0xe6, 0x89, 0x65, 0x35, 0x89, 0x75, 0x72, 0x17, + 0x2c, 0xa2, 0x34, 0x8d, 0x6c, 0x0c, 0x03, 0x6a, 0x9a, 0xb1, 0xea, 0xc1, 0x43, 0x08, 0xf4, 0xd0, 0x2f, 0x4b, 0x69, + 0xd7, 0x59, 0x5a, 0xeb, 0x5e, 0x9b, 0xee, 0xb7, 0x07, 0x54, 0x4d, 0xe3, 0x26, 0xe0, 0x9a, 0xfe, 0xd5, 0x24, 0x92, + 0x11, 0xfb, 0x9b, 0xb3, 0xe2, 0xf1, 0xb2, 0x30, 0x98, 0x26, 0xfa, 0x3a, 0xc9, 0x16, 0x6d, 0x30, 0xd5, 0xcb, 0x16, + 0x9d, 0x5b, 0xec, 0xbe, 0xef, 0xec, 0xf7, 0x1d, 0x16, 0x7d, 0x66, 0x32, 0x52, 0x66, 0x8a, 0xf9, 0xef, 0x3b, 0xfb, + 0x7d, 0x87, 0x77, 0x07, 0xf3, 0xc5, 0x5f, 0x28, 0x96, 0xec, 0x0c, 0x97, 0x60, 0x42, 0x1e, 0x70, 0x37, 0xb5, 0x2c, + 0x13, 0x04, 0xb6, 0x96, 0x00, 0x71, 0x3e, 0x9f, 0xc6, 0x15, 0xaf, 0x86, 0x80, 0xfb, 0xf4, 0xae, 0xed, 0x55, 0x2a, + 0xf0, 0x98, 0xa0, 0x11, 0x31, 0xb1, 0x6d, 0xcc, 0xeb, 0x66, 0xc0, 0xe5, 0x11, 0x5d, 0xea, 0x49, 0x12, 0xe0, 0x55, + 0x8d, 0xca, 0xdb, 0x14, 0x29, 0xbf, 0x48, 0x90, 0xe3, 0x8b, 0x3d, 0xa2, 0x8a, 0x01, 0xac, 0xca, 0x92, 0x3e, 0x81, + 0xd4, 0xf3, 0x43, 0x4f, 0xcd, 0x6d, 0xe4, 0xb1, 0xef, 0xfc, 0x7e, 0x61, 0x7a, 0x56, 0xc8, 0xe5, 0x74, 0x06, 0x3e, + 0xb4, 0xc0, 0x32, 0x14, 0xa6, 0x5e, 0x65, 0xeb, 0x5f, 0x93, 0xdc, 0x04, 0x50, 0x38, 0xdd, 0x94, 0x09, 0xcd, 0xf4, + 0x92, 0xe6, 0xc6, 0x92, 0x94, 0x8b, 0xe9, 0x23, 0x79, 0xfb, 0x12, 0xb0, 0x9b, 0x12, 0xdd, 0xd8, 0x93, 0xf7, 0x16, + 0x76, 0x00, 0xce, 0x08, 0xdb, 0x57, 0xf1, 0xa1, 0x02, 0x9d, 0x3f, 0xce, 0x09, 0xdb, 0x57, 0xf5, 0x09, 0xb3, 0xd9, + 0x33, 0xb2, 0x35, 0xdc, 0x7e, 0x9c, 0x35, 0x72, 0x74, 0xd2, 0x49, 0xf3, 0x9e, 0x27, 0x06, 0x16, 0xa0, 0x01, 0x70, + 0x77, 0xb6, 0x67, 0x79, 0x77, 0x43, 0x40, 0xef, 0x92, 0x49, 0x7b, 0x5d, 0x6e, 0x52, 0xd6, 0xeb, 0x4e, 0x45, 0x05, + 0x0b, 0x3c, 0x0b, 0xf6, 0x02, 0xb5, 0x5f, 0x7b, 0x28, 0xce, 0x2f, 0xd9, 0xb6, 0xe9, 0x79, 0xd9, 0x77, 0x6f, 0xcf, + 0x22, 0x63, 0x9b, 0xf6, 0x76, 0x0f, 0x91, 0xb0, 0x9c, 0xb0, 0x0e, 0x38, 0xe1, 0xaa, 0x76, 0x40, 0x80, 0x3e, 0x05, + 0x22, 0x37, 0x96, 0x64, 0xb5, 0xa9, 0x8c, 0xee, 0x03, 0xbf, 0x5b, 0x4a, 0xa4, 0x1b, 0x6d, 0x49, 0x30, 0x7d, 0x82, + 0x51, 0xd3, 0x99, 0xa7, 0xa9, 0x6b, 0xaf, 0x2e, 0x6f, 0x8b, 0xb6, 0xfe, 0x0d, 0x68, 0x6c, 0xb6, 0x87, 0x89, 0xa1, + 0x0c, 0x62, 0xa0, 0xf7, 0x11, 0xef, 0x35, 0x1a, 0x19, 0x02, 0x85, 0x4c, 0x36, 0xc4, 0x32, 0xf1, 0x5a, 0xf4, 0xa3, + 0x23, 0x03, 0x8f, 0x2a, 0x01, 0x61, 0x0a, 0x42, 0x48, 0xd8, 0xb5, 0x41, 0xd8, 0x70, 0xb9, 0x6a, 0xb9, 0xb0, 0x91, + 0x6a, 0x43, 0x07, 0xff, 0xaf, 0x70, 0xd9, 0xea, 0x99, 0xe5, 0xa2, 0x18, 0xdc, 0xcc, 0x0d, 0x58, 0x24, 0x48, 0x8f, + 0x36, 0xdb, 0x43, 0x71, 0x7f, 0x2e, 0x36, 0x1b, 0x02, 0x12, 0x73, 0x98, 0xa0, 0x68, 0x38, 0x37, 0xc6, 0x58, 0x25, + 0x95, 0x96, 0xb5, 0x26, 0x31, 0x07, 0x01, 0xa3, 0xc3, 0x75, 0x5f, 0xdd, 0xa6, 0x0c, 0xdf, 0xa5, 0x02, 0xdf, 0x80, + 0x27, 0x4d, 0x2a, 0xb1, 0x7b, 0xbc, 0xa0, 0xd8, 0x10, 0xdd, 0xf3, 0xec, 0x6d, 0x01, 0xeb, 0x6c, 0xf6, 0x88, 0x08, + 0x7e, 0x57, 0xbf, 0xda, 0xe0, 0xbb, 0x85, 0x5f, 0x81, 0xf5, 0x73, 0x70, 0x92, 0x62, 0xd1, 0x90, 0xcd, 0xc2, 0x1d, + 0x19, 0x50, 0xae, 0xe2, 0x97, 0xc3, 0xd4, 0x9d, 0x62, 0xb8, 0xf6, 0xf1, 0x0a, 0xbf, 0xdf, 0x6a, 0xb7, 0xa1, 0xca, + 0xe2, 0x76, 0x6f, 0x8a, 0x86, 0xac, 0x9a, 0xde, 0x93, 0xb9, 0x95, 0x52, 0xff, 0x7a, 0x8f, 0x5b, 0x3b, 0xed, 0xfb, + 0x69, 0xbe, 0xf5, 0xe8, 0x5c, 0x35, 0xed, 0x53, 0x6b, 0x45, 0x70, 0xf0, 0xb3, 0x85, 0x9b, 0x3b, 0x03, 0x0e, 0xe0, + 0xe7, 0xef, 0x68, 0x5e, 0x67, 0x10, 0x9d, 0xde, 0x6a, 0xc6, 0xd7, 0xf1, 0x1f, 0xe3, 0x46, 0xdc, 0x4f, 0xff, 0x48, + 0xfe, 0x18, 0x37, 0x50, 0x1f, 0xc5, 0x8b, 0xdb, 0x35, 0x9b, 0xaf, 0x21, 0xd8, 0xda, 0xbd, 0x13, 0xfc, 0x26, 0x2c, + 0xc9, 0x35, 0xcd, 0x79, 0xb6, 0x76, 0x0f, 0x02, 0xae, 0xdd, 0xab, 0x44, 0x6b, 0xf3, 0xc6, 0xd5, 0x3a, 0x96, 0xa3, + 0x1c, 0x02, 0x0b, 0xc7, 0x07, 0xcd, 0xfe, 0xa0, 0xd5, 0x7c, 0x30, 0xb4, 0xff, 0x9a, 0x08, 0xf7, 0xa8, 0x16, 0xb1, + 0xed, 0xde, 0xd6, 0xd6, 0x8f, 0xc1, 0xb0, 0x03, 0x42, 0x81, 0x83, 0x5c, 0xfa, 0x3a, 0x43, 0xd6, 0xf7, 0x64, 0xbd, + 0x66, 0x2e, 0x9a, 0xb5, 0xd3, 0xe0, 0x97, 0xb1, 0x99, 0x8e, 0xdb, 0x49, 0xa7, 0xe7, 0xc5, 0x58, 0xd2, 0x80, 0x48, + 0xd3, 0x98, 0x41, 0x20, 0xa9, 0x95, 0xe1, 0xb0, 0x16, 0xb7, 0x51, 0x5a, 0xdd, 0x1f, 0x41, 0xca, 0x0f, 0x51, 0xca, + 0x4f, 0x08, 0x04, 0xd0, 0xb6, 0xcc, 0x51, 0xd9, 0x90, 0xf7, 0x5d, 0x7a, 0x68, 0x9c, 0x19, 0x1a, 0x7c, 0xbd, 0x6e, + 0x55, 0xc3, 0x54, 0x45, 0x7d, 0x98, 0xab, 0x0d, 0x16, 0xe4, 0x0d, 0xe8, 0x9a, 0x15, 0x11, 0xfd, 0xd0, 0x55, 0x1e, + 0xde, 0x43, 0xc6, 0x92, 0x80, 0x93, 0x7e, 0x5f, 0xf4, 0x0b, 0x72, 0xf5, 0x30, 0x06, 0x1f, 0x33, 0xcc, 0x07, 0x7a, + 0x50, 0x0c, 0x87, 0x28, 0x75, 0x4e, 0x67, 0xa9, 0x89, 0xb8, 0x12, 0xf8, 0x25, 0x17, 0xe0, 0x97, 0xac, 0x10, 0x1b, + 0x14, 0x43, 0xf2, 0x30, 0x8b, 0x25, 0x38, 0xe5, 0xef, 0xf1, 0x79, 0x7c, 0x1a, 0x1a, 0x98, 0x9a, 0x61, 0x99, 0x8b, + 0x6c, 0xb0, 0x98, 0xb3, 0x96, 0x40, 0x70, 0x33, 0xe0, 0x2e, 0xb5, 0x21, 0xd1, 0x58, 0x03, 0x45, 0xb7, 0x51, 0x68, + 0x66, 0xf4, 0x62, 0xa7, 0x8d, 0x41, 0xe4, 0xf0, 0xc2, 0x5c, 0xc3, 0x58, 0x04, 0x32, 0x97, 0xab, 0x1e, 0xfb, 0xcb, + 0x0f, 0x9b, 0x15, 0x06, 0xaf, 0xc8, 0x74, 0xe8, 0x8e, 0x63, 0xc6, 0x57, 0x79, 0xe2, 0x18, 0x82, 0x4c, 0x2c, 0x95, + 0x6e, 0x38, 0x26, 0xae, 0xa4, 0xcf, 0xc4, 0x90, 0xed, 0x86, 0x67, 0xe6, 0x42, 0x37, 0xdb, 0x7f, 0x3a, 0xb7, 0x73, + 0x4e, 0xb8, 0xd1, 0x4a, 0x1a, 0x6d, 0xd4, 0x33, 0x43, 0x55, 0x5d, 0x30, 0xbf, 0x87, 0x4e, 0x4b, 0x8b, 0x9d, 0xab, + 0x77, 0x2f, 0x7c, 0x9d, 0xaf, 0x8c, 0xbf, 0xc5, 0xaa, 0xd0, 0x8a, 0x0c, 0xb7, 0x5b, 0xc8, 0x9b, 0x33, 0x3d, 0xf4, + 0x8a, 0x5c, 0xa8, 0x0e, 0x7f, 0x51, 0x4f, 0x98, 0x07, 0x3b, 0xa3, 0x86, 0xf0, 0xe8, 0xf7, 0x26, 0x03, 0xe5, 0x1f, + 0x4c, 0x4c, 0xe6, 0x2c, 0xb9, 0xa1, 0x85, 0x88, 0x7f, 0x7c, 0x21, 0x4c, 0xac, 0xaa, 0x03, 0x18, 0xc8, 0x81, 0xa9, + 0x78, 0x00, 0xb7, 0x26, 0x7c, 0xc2, 0xd9, 0x38, 0x3d, 0x88, 0x7e, 0x6c, 0x88, 0xc6, 0x8f, 0xd1, 0x8f, 0xe0, 0xee, + 0xec, 0x5e, 0x87, 0x2c, 0xe3, 0x42, 0xf8, 0x7b, 0xac, 0x87, 0xa5, 0x4a, 0x19, 0x6b, 0xaf, 0x5b, 0x0e, 0x2f, 0xa4, + 0xee, 0x65, 0xf1, 0x43, 0x47, 0xac, 0x6d, 0x0a, 0xd6, 0x21, 0x25, 0x85, 0x67, 0x57, 0xcc, 0xad, 0x16, 0x73, 0x97, + 0x5a, 0xc2, 0x5f, 0x5f, 0x3d, 0x2c, 0x55, 0xd0, 0x70, 0x10, 0xba, 0xd2, 0x16, 0x12, 0x60, 0xe0, 0x52, 0xfa, 0x74, + 0xba, 0x33, 0x89, 0x8c, 0xb2, 0x18, 0xde, 0x3d, 0x08, 0x02, 0x09, 0xb0, 0xad, 0xb0, 0x2a, 0x70, 0xb9, 0x52, 0x45, + 0xbd, 0x94, 0x04, 0x02, 0xd0, 0x97, 0xde, 0x83, 0xf2, 0xb2, 0xe8, 0x35, 0x1a, 0x12, 0xb4, 0xb0, 0xd4, 0x5c, 0xab, + 0x62, 0x7a, 0x18, 0xbe, 0x6a, 0x18, 0x7c, 0x78, 0x87, 0xb4, 0xad, 0xa7, 0x45, 0x29, 0xa1, 0x76, 0x07, 0x1d, 0x82, + 0x55, 0x76, 0x50, 0xfe, 0x6d, 0x4c, 0x91, 0xcd, 0x1f, 0xb0, 0x1f, 0xa8, 0xeb, 0x70, 0xe8, 0x0a, 0x56, 0xbd, 0x94, + 0x51, 0x30, 0x60, 0xe5, 0x14, 0xa8, 0xbd, 0x93, 0x8c, 0x66, 0x33, 0x06, 0xea, 0x7e, 0x5b, 0xb4, 0x9a, 0xdb, 0x93, + 0xba, 0xdf, 0x90, 0x71, 0xf6, 0x11, 0xc6, 0xd9, 0x47, 0x81, 0x17, 0x8b, 0x24, 0x3f, 0xcb, 0x58, 0xe3, 0x58, 0x35, + 0x05, 0x3a, 0xe9, 0x00, 0x77, 0x06, 0x0e, 0x3c, 0x60, 0x8b, 0x72, 0x74, 0x44, 0x9d, 0xc5, 0x3d, 0x6d, 0x64, 0xde, + 0xdb, 0x13, 0x6a, 0x17, 0xb1, 0xc0, 0xcd, 0x9a, 0x99, 0x16, 0xb4, 0x56, 0x18, 0xe7, 0xf1, 0x30, 0x22, 0x63, 0x2d, + 0x7e, 0xc2, 0x96, 0x35, 0x55, 0xfd, 0x06, 0x9a, 0xa3, 0x5a, 0x90, 0x9b, 0x17, 0xc6, 0x5b, 0x95, 0x0c, 0xa2, 0x68, + 0x68, 0x39, 0x15, 0x62, 0x48, 0xc6, 0xa0, 0x35, 0x0c, 0x6e, 0xb5, 0xd7, 0x6b, 0xee, 0x11, 0x5f, 0xd4, 0xbc, 0xd5, + 0xcc, 0x2d, 0x40, 0x56, 0xc4, 0x51, 0x79, 0x6f, 0x12, 0x81, 0xf7, 0x6d, 0x19, 0x21, 0x6d, 0x35, 0xb0, 0x4f, 0x57, + 0x96, 0x8a, 0xcd, 0x77, 0x74, 0x3a, 0x4c, 0x23, 0x3b, 0xa2, 0x08, 0x7f, 0x2a, 0x21, 0x09, 0x57, 0x49, 0x9f, 0x54, + 0x26, 0x17, 0x4c, 0xa5, 0x1c, 0x7f, 0x2a, 0xa4, 0xd4, 0xd7, 0xf6, 0x4b, 0xe2, 0xea, 0x4e, 0x46, 0xe0, 0x4f, 0x53, + 0xa6, 0xdf, 0xd1, 0x62, 0xca, 0xc0, 0xaf, 0xc8, 0xdf, 0x8e, 0xa5, 0x94, 0x5c, 0xbd, 0x10, 0xf1, 0x80, 0x62, 0x78, + 0x77, 0x75, 0x88, 0xb5, 0x09, 0x81, 0x52, 0xe2, 0x22, 0x5c, 0x10, 0xbd, 0x29, 0xe4, 0xed, 0x5d, 0x5c, 0x60, 0xe7, + 0x00, 0x58, 0x3a, 0x4d, 0x02, 0xfc, 0xcb, 0xc7, 0x7c, 0xac, 0xc6, 0x9c, 0x1a, 0x5d, 0xbf, 0xfb, 0x9d, 0x7c, 0x02, + 0x7a, 0x5b, 0x3a, 0x0a, 0x0e, 0x5a, 0x43, 0xc8, 0x85, 0xbb, 0x30, 0xb8, 0xf8, 0x0a, 0x6b, 0x17, 0x85, 0xf1, 0xc6, + 0x02, 0xe8, 0x3d, 0xca, 0xc0, 0x82, 0x0d, 0x73, 0x4c, 0xe1, 0xd1, 0xda, 0x29, 0xd3, 0x41, 0x54, 0x90, 0x27, 0xe5, + 0xb3, 0xa4, 0xb5, 0xda, 0x6f, 0xd9, 0x04, 0xee, 0x30, 0x92, 0x6f, 0x17, 0x4e, 0x1c, 0x78, 0x40, 0xa6, 0xc9, 0x6c, + 0xb3, 0x6f, 0x7c, 0xe4, 0x91, 0xd7, 0x93, 0x78, 0x5f, 0x4b, 0x61, 0xbe, 0x59, 0xd1, 0x0d, 0x86, 0x50, 0x14, 0x61, + 0xbf, 0x37, 0x2a, 0xa6, 0xa8, 0x32, 0x68, 0x83, 0x86, 0xe5, 0x8d, 0xf8, 0x19, 0xce, 0x18, 0x5a, 0x2f, 0x64, 0xef, + 0xe8, 0xac, 0xc3, 0x99, 0xc3, 0x8c, 0x19, 0x81, 0x51, 0x69, 0x59, 0xd0, 0x29, 0x38, 0x3a, 0x57, 0x1f, 0x44, 0xc5, + 0xd5, 0xb1, 0x02, 0xf0, 0x24, 0x33, 0xf8, 0x27, 0xdf, 0x06, 0xeb, 0x61, 0xab, 0x66, 0x98, 0xfa, 0xb3, 0xde, 0x75, + 0x2d, 0x5f, 0x85, 0x38, 0xd2, 0xc6, 0x10, 0x5a, 0xe7, 0xf6, 0x0e, 0x50, 0xc4, 0x05, 0xbd, 0x48, 0x35, 0xfe, 0xa4, + 0x96, 0x23, 0xb3, 0xbe, 0xc6, 0x75, 0x4c, 0x1b, 0x44, 0xb1, 0xee, 0x9a, 0xf8, 0x53, 0xf5, 0x0a, 0xac, 0x4a, 0x81, + 0x75, 0x06, 0xe5, 0x87, 0x2a, 0x2f, 0x1b, 0x52, 0x49, 0xae, 0x4c, 0xa7, 0xd2, 0x74, 0x5a, 0x21, 0x94, 0x4b, 0x4f, + 0xca, 0xfb, 0x57, 0x08, 0x61, 0x60, 0xca, 0xec, 0xc1, 0x2a, 0xb5, 0x83, 0x55, 0xf0, 0xea, 0xc5, 0x16, 0x56, 0x49, + 0x38, 0x9e, 0x4b, 0x34, 0x2a, 0x2a, 0x1c, 0x32, 0xa4, 0x2f, 0xc4, 0x22, 0x48, 0x00, 0x2c, 0x7a, 0x99, 0xb9, 0xbc, + 0xef, 0xe1, 0x50, 0xd8, 0x93, 0x4c, 0xc2, 0xe9, 0x26, 0x34, 0x87, 0xe7, 0x81, 0x55, 0xdf, 0x23, 0xc4, 0xcc, 0xc4, + 0x7f, 0x82, 0x67, 0xa1, 0xbf, 0xff, 0x1c, 0xad, 0xb3, 0x20, 0x4f, 0xff, 0x25, 0x4a, 0x42, 0x63, 0xff, 0x39, 0x1e, + 0x3a, 0x24, 0x0c, 0x07, 0xbe, 0x3d, 0xc2, 0x0a, 0x07, 0x77, 0x8a, 0xf8, 0x0c, 0xee, 0xf0, 0xb1, 0x0e, 0x3d, 0x00, + 0x2c, 0xa1, 0x38, 0x04, 0xf9, 0x16, 0x8a, 0x99, 0x61, 0x6b, 0xb2, 0x0a, 0x2f, 0x70, 0xc1, 0x6a, 0xa1, 0xbc, 0xbf, + 0x6d, 0x79, 0x29, 0xad, 0x76, 0xc9, 0x6b, 0xcc, 0x81, 0xca, 0xcf, 0xf0, 0xc2, 0x57, 0x98, 0xf7, 0xaa, 0xdd, 0x17, + 0xfe, 0xe4, 0x80, 0x9e, 0x42, 0xc0, 0x48, 0xf7, 0x7b, 0x43, 0xb8, 0xa7, 0xe8, 0x65, 0x2e, 0x0e, 0xdb, 0x0e, 0xba, + 0x17, 0x98, 0xab, 0xeb, 0x2a, 0x6b, 0x01, 0xa6, 0xd0, 0xe0, 0xa0, 0x0a, 0x67, 0x04, 0xe6, 0xea, 0x45, 0x59, 0x70, + 0x01, 0xe2, 0x7d, 0x5f, 0x98, 0x9c, 0x32, 0x1a, 0xc0, 0xbb, 0xac, 0x7c, 0x74, 0xaa, 0xcf, 0xc1, 0x65, 0xdc, 0xb0, + 0x89, 0x4f, 0x84, 0x4f, 0x05, 0x56, 0xd2, 0x1a, 0x87, 0x46, 0x74, 0x4c, 0x17, 0x60, 0xb6, 0x01, 0x14, 0xdc, 0x9d, + 0x0f, 0x5b, 0x0b, 0x15, 0x3c, 0xc9, 0x5b, 0x7b, 0x41, 0x9b, 0x10, 0x67, 0xd2, 0x14, 0xdc, 0x6d, 0x17, 0x45, 0x60, + 0x7e, 0xfb, 0x6f, 0x85, 0x45, 0x82, 0x01, 0x95, 0x9a, 0x24, 0x08, 0x4f, 0x50, 0x1a, 0xe9, 0x56, 0x6e, 0x26, 0x90, + 0x4e, 0x44, 0x78, 0xc3, 0xfc, 0x72, 0xeb, 0x7c, 0x75, 0xd4, 0x40, 0x54, 0xd4, 0x40, 0x05, 0xd4, 0x40, 0xd6, 0xb7, + 0x7f, 0x01, 0x0b, 0x61, 0x23, 0x54, 0x89, 0x20, 0x20, 0xc2, 0x42, 0x1b, 0x3e, 0xa0, 0x48, 0x42, 0xc8, 0x1b, 0x40, + 0xc5, 0x94, 0xbc, 0x05, 0xa3, 0x71, 0x78, 0xbd, 0x07, 0xdc, 0x2f, 0x2d, 0xc3, 0xe0, 0x39, 0x05, 0x93, 0xff, 0xcc, + 0xe7, 0x43, 0xf5, 0x72, 0x75, 0x10, 0xc2, 0x4f, 0x20, 0x56, 0x84, 0xe3, 0x2f, 0x7e, 0x06, 0xb2, 0xa9, 0xb0, 0x3c, + 0x3a, 0x92, 0x20, 0xf0, 0x43, 0x14, 0xe1, 0x80, 0x67, 0x78, 0x9b, 0x6d, 0x11, 0x3d, 0x3f, 0x2b, 0x55, 0xcd, 0x4a, + 0x06, 0xb3, 0x2a, 0x3c, 0x8d, 0xa3, 0x1b, 0xc2, 0x40, 0x70, 0xa1, 0x76, 0xdf, 0x20, 0x04, 0xca, 0x96, 0x1b, 0x43, + 0x97, 0x9e, 0x82, 0xf9, 0x68, 0x1c, 0xbd, 0x65, 0xf0, 0xb0, 0xb0, 0x71, 0x47, 0x61, 0x9a, 0x65, 0xda, 0x30, 0x8f, + 0x8d, 0xc0, 0x49, 0x9d, 0xa2, 0xe4, 0xb3, 0xe4, 0x22, 0x8e, 0x9a, 0x57, 0x11, 0x6a, 0xc0, 0xbf, 0x0d, 0x8e, 0x7a, + 0x34, 0xa1, 0xe3, 0xb1, 0x0f, 0x7e, 0x93, 0x11, 0xb3, 0xc9, 0xd6, 0x6b, 0x51, 0x11, 0xf4, 0xc4, 0x6e, 0x30, 0x60, + 0x25, 0x9e, 0x00, 0xfb, 0x60, 0x39, 0x58, 0xf2, 0x4e, 0xc4, 0xca, 0x9f, 0x52, 0x18, 0xac, 0x9e, 0x33, 0x84, 0x70, + 0x16, 0x30, 0x29, 0xff, 0xf9, 0x4c, 0xc3, 0xf5, 0xf3, 0xf3, 0x75, 0x8c, 0x88, 0xf4, 0x41, 0xe4, 0x6a, 0xec, 0x88, + 0x08, 0xc2, 0x96, 0xe9, 0x81, 0x2b, 0xf3, 0x83, 0xb7, 0xae, 0x1e, 0xda, 0x70, 0x71, 0x60, 0x40, 0x8d, 0x02, 0xa3, + 0x15, 0x9c, 0x93, 0x72, 0xe0, 0xa0, 0x84, 0xd0, 0xac, 0x88, 0x67, 0xe4, 0x0a, 0x22, 0xe1, 0x65, 0xa8, 0x07, 0x86, + 0x05, 0x81, 0x04, 0x35, 0x03, 0x09, 0x2a, 0xf3, 0xb5, 0xc7, 0x30, 0xeb, 0xdc, 0xcc, 0x76, 0x86, 0x7a, 0x2e, 0xc8, + 0xcf, 0xcf, 0x3a, 0x1e, 0x03, 0x4b, 0x7b, 0x74, 0x54, 0x40, 0x04, 0x31, 0xa0, 0xe0, 0xa5, 0x04, 0x18, 0x68, 0xc0, + 0x8b, 0x2d, 0x0d, 0xf8, 0x42, 0x1b, 0xaf, 0x03, 0x63, 0xeb, 0x53, 0x06, 0xb9, 0x78, 0x55, 0xed, 0x69, 0x42, 0xc8, + 0x61, 0xab, 0xaf, 0xd3, 0xdd, 0x08, 0x89, 0xfd, 0x8f, 0xda, 0x04, 0x1a, 0x73, 0xa4, 0xbb, 0xda, 0x98, 0x7f, 0xd7, + 0xf4, 0x88, 0xd5, 0x24, 0xa4, 0x0b, 0xd2, 0xe5, 0xf9, 0xb4, 0x57, 0x70, 0xc5, 0x2a, 0x8d, 0x1c, 0x5c, 0x80, 0x3e, + 0x1b, 0x10, 0xa0, 0x40, 0xa5, 0xa9, 0x04, 0x2d, 0xe2, 0x22, 0x29, 0xd9, 0x30, 0xcc, 0x20, 0x4c, 0x61, 0xb5, 0x12, + 0x74, 0x6b, 0x0d, 0x80, 0x77, 0x66, 0xf6, 0x4f, 0xe9, 0x83, 0x4d, 0x37, 0xde, 0x3c, 0x02, 0x08, 0xc8, 0x61, 0xbb, + 0x64, 0xd7, 0xc5, 0x56, 0x65, 0x16, 0xd6, 0x32, 0xb6, 0x72, 0xbb, 0x1e, 0x63, 0xef, 0xc4, 0x2e, 0x9f, 0x00, 0x21, + 0x6a, 0x4b, 0xa6, 0x11, 0x4b, 0x18, 0xb2, 0xae, 0x0d, 0xd9, 0x68, 0x43, 0xe1, 0xa9, 0x44, 0x0e, 0x5c, 0xa2, 0x09, + 0x92, 0xef, 0xb8, 0x04, 0x87, 0xf0, 0xc2, 0x23, 0xfc, 0x57, 0x60, 0x91, 0x0a, 0xcc, 0xb0, 0x5c, 0xaf, 0xa1, 0x9e, + 0xc7, 0xfb, 0x6c, 0x3b, 0x38, 0xa9, 0xdc, 0x1a, 0xbb, 0xb4, 0x13, 0x8f, 0xcb, 0x26, 0x24, 0xce, 0xa0, 0x5f, 0x5f, + 0x11, 0xf5, 0x0f, 0xdb, 0xe9, 0x0b, 0xff, 0x5e, 0x99, 0xdb, 0x81, 0xd8, 0xb0, 0xde, 0x60, 0xf5, 0x01, 0xb4, 0xfc, + 0x73, 0xe6, 0x1f, 0x2a, 0x0b, 0x6e, 0x12, 0xd4, 0xf6, 0x22, 0xf6, 0x58, 0x0f, 0x31, 0x52, 0x5b, 0xdc, 0x3d, 0x42, + 0xfc, 0xe7, 0x9d, 0x28, 0x06, 0x3c, 0xa9, 0xf8, 0xe7, 0x18, 0xf5, 0x20, 0x14, 0xb5, 0xf5, 0xb0, 0x01, 0x4a, 0xbb, + 0xda, 0x54, 0x62, 0x64, 0x48, 0x20, 0xdf, 0xba, 0xf0, 0x82, 0xe6, 0x24, 0x52, 0x20, 0x27, 0x57, 0x5d, 0x3c, 0xca, + 0xb6, 0x84, 0xb9, 0xde, 0x0e, 0x8e, 0x99, 0xab, 0x8d, 0xac, 0x88, 0xdf, 0x01, 0x3b, 0xc3, 0x8d, 0x64, 0xe9, 0xc0, + 0xa7, 0x6a, 0xe0, 0xf3, 0x6b, 0x6e, 0x28, 0x8a, 0x42, 0xfd, 0x77, 0xf6, 0x91, 0x39, 0xf8, 0x9d, 0x06, 0xe2, 0x63, + 0xe6, 0x74, 0x24, 0x5b, 0xa1, 0xd6, 0x9c, 0x1d, 0x2f, 0xdb, 0x8e, 0x30, 0x28, 0x6c, 0xf4, 0xbe, 0x0a, 0x59, 0xc5, + 0xde, 0x4e, 0x45, 0x30, 0xa7, 0x1b, 0x55, 0x39, 0xa7, 0x72, 0xcb, 0xa8, 0x96, 0x9a, 0x06, 0x88, 0x70, 0xe5, 0x13, + 0xc9, 0x87, 0xcc, 0x84, 0x7f, 0x30, 0x18, 0x57, 0x8f, 0x14, 0xfe, 0x61, 0x5f, 0xec, 0x90, 0xdd, 0xe8, 0x70, 0x5b, + 0x41, 0xf3, 0x42, 0x05, 0x0f, 0x38, 0x2a, 0x59, 0x42, 0xa4, 0xc8, 0xd5, 0xa1, 0xaa, 0x99, 0xb2, 0x7d, 0x8a, 0x10, + 0x42, 0xda, 0xe3, 0xac, 0x1b, 0x5a, 0x3d, 0xf4, 0x48, 0xe5, 0x34, 0xb9, 0x43, 0x73, 0x5d, 0x80, 0x0a, 0x23, 0x90, + 0xae, 0xbe, 0xb0, 0xbb, 0x54, 0x42, 0xf4, 0xf2, 0x8d, 0x0b, 0x61, 0xec, 0xac, 0x2c, 0x71, 0x61, 0x46, 0x6d, 0xc3, + 0xe8, 0xba, 0x8d, 0xe1, 0x6c, 0x60, 0xcc, 0x34, 0x28, 0x69, 0x41, 0xa8, 0xeb, 0x1e, 0xbd, 0xcc, 0x4c, 0xa0, 0xc7, + 0x9c, 0xd0, 0x06, 0xc3, 0x33, 0xa2, 0xc1, 0xb2, 0xa9, 0x00, 0x0b, 0xbe, 0x55, 0x91, 0x5a, 0x9b, 0x4d, 0x16, 0x7f, + 0xd4, 0xb1, 0x79, 0xda, 0x2f, 0xaf, 0x98, 0xe7, 0xc2, 0x47, 0x47, 0xc8, 0x7c, 0x3c, 0xba, 0xa7, 0x6f, 0xae, 0x5f, + 0xbc, 0x7c, 0xfd, 0x6a, 0xbd, 0x6e, 0xb3, 0x66, 0xfb, 0x0c, 0xff, 0x43, 0x97, 0xf1, 0x60, 0xcb, 0x28, 0x40, 0x47, + 0x47, 0x87, 0xdc, 0xb8, 0xf0, 0x7c, 0xe1, 0x0b, 0x88, 0x1b, 0xa4, 0x87, 0x38, 0x2f, 0xca, 0x98, 0x20, 0xb7, 0x51, + 0x3f, 0xba, 0x8b, 0x40, 0x09, 0x55, 0x91, 0xbf, 0xdf, 0xb6, 0x67, 0x7f, 0x00, 0x81, 0x89, 0xa0, 0x3e, 0x44, 0x00, + 0x81, 0x78, 0xa5, 0xb8, 0x20, 0xcc, 0x27, 0x40, 0x14, 0xef, 0x09, 0x70, 0xa6, 0x26, 0x6a, 0xd5, 0x44, 0xc5, 0x05, + 0x90, 0x44, 0x1b, 0x8e, 0x92, 0x9e, 0x98, 0x00, 0xde, 0x10, 0x94, 0xd2, 0xfe, 0xea, 0xe5, 0xce, 0x5d, 0x2a, 0x47, + 0xfd, 0x56, 0x9a, 0xe3, 0x99, 0xfb, 0x9c, 0xc1, 0xe7, 0xac, 0xe7, 0x4f, 0x07, 0x71, 0x9c, 0xe3, 0x25, 0x11, 0xc7, + 0xfe, 0x59, 0xc4, 0xd5, 0xa2, 0x60, 0x5f, 0xb9, 0x5c, 0xaa, 0x74, 0x75, 0x9b, 0xca, 0xe4, 0xb6, 0x39, 0x3e, 0x8e, + 0x8b, 0xe4, 0xb6, 0xa9, 0x92, 0x5b, 0x84, 0xef, 0x52, 0x99, 0xdc, 0xd9, 0x94, 0xbb, 0xa6, 0x82, 0x9b, 0x2f, 0x2c, + 0xe0, 0x50, 0xb4, 0x45, 0x1b, 0xcb, 0xed, 0xa2, 0x36, 0xc5, 0x15, 0x0d, 0xa3, 0x29, 0xee, 0xd9, 0xf8, 0x61, 0xf8, + 0x12, 0x5c, 0x9a, 0x34, 0x91, 0x7f, 0x80, 0xf4, 0xd3, 0xaa, 0x0c, 0xdc, 0x67, 0xa4, 0xd5, 0x9b, 0x5d, 0x8a, 0x66, + 0xbb, 0xd7, 0x68, 0xcc, 0x60, 0xef, 0x66, 0x24, 0xf7, 0xc5, 0x66, 0x0d, 0x13, 0x5f, 0xe7, 0x30, 0x5b, 0xaf, 0x0f, + 0x73, 0x64, 0x36, 0xdc, 0x94, 0xc5, 0x7a, 0x30, 0x1b, 0xe2, 0x16, 0x7e, 0x9f, 0x21, 0xb4, 0x62, 0x83, 0xd9, 0x90, + 0xb0, 0xc1, 0xac, 0xd1, 0x1e, 0x5a, 0x43, 0x3b, 0xb3, 0x15, 0x37, 0x10, 0x42, 0x73, 0x36, 0x3c, 0x31, 0x25, 0xa5, + 0xcb, 0xb7, 0x5f, 0xb4, 0x0a, 0xe8, 0xa7, 0x6a, 0xc1, 0xcb, 0x24, 0xee, 0x40, 0x5f, 0xf4, 0xd2, 0x3e, 0xdd, 0x5a, + 0x90, 0xd3, 0x93, 0xca, 0xd5, 0x9e, 0x22, 0x6c, 0x7a, 0x52, 0xc7, 0xc5, 0xb1, 0x69, 0xc6, 0x75, 0x29, 0xdd, 0x77, + 0xa8, 0x19, 0xf9, 0xcb, 0xc1, 0x02, 0x10, 0xa4, 0x82, 0x47, 0x5e, 0xb8, 0x70, 0x4a, 0x21, 0x5c, 0x1c, 0x54, 0x76, + 0x60, 0x92, 0x93, 0x56, 0x2f, 0x37, 0x96, 0xfe, 0xb9, 0x8b, 0x68, 0x4a, 0x31, 0x25, 0x99, 0x2f, 0x99, 0x1b, 0xb0, + 0xd0, 0x6d, 0xca, 0x33, 0x03, 0xbd, 0xd2, 0x10, 0x8f, 0x09, 0xc4, 0x43, 0xea, 0x15, 0xc6, 0xc0, 0x2b, 0x9e, 0x35, + 0x8b, 0x01, 0x1b, 0xa2, 0x93, 0x53, 0x4c, 0x07, 0x7f, 0x66, 0x8b, 0x36, 0x3c, 0x16, 0xf8, 0xe7, 0x90, 0xcc, 0x9a, + 0xb2, 0x4c, 0x10, 0x90, 0x30, 0x6e, 0xca, 0x63, 0xd8, 0x4b, 0x08, 0x67, 0xb6, 0x62, 0x36, 0x60, 0xc3, 0xe6, 0xac, + 0xac, 0xd8, 0xf1, 0x15, 0x1b, 0xb2, 0x4c, 0xb0, 0x15, 0x1b, 0xae, 0x62, 0xf8, 0x3a, 0x83, 0x01, 0x41, 0x08, 0x00, + 0x06, 0x00, 0xd0, 0x28, 0x88, 0xe6, 0x8b, 0x15, 0xf1, 0x9b, 0xdd, 0xde, 0xe3, 0xb7, 0xc0, 0x02, 0xad, 0xb6, 0xff, + 0xf7, 0xa1, 0x0c, 0xd8, 0x53, 0x16, 0x26, 0x66, 0x6e, 0x61, 0x55, 0x74, 0x00, 0x95, 0x12, 0x61, 0x0a, 0x03, 0x99, + 0xc3, 0xcc, 0x40, 0x2d, 0xd0, 0x1a, 0xe4, 0x03, 0x3d, 0x6c, 0x66, 0x70, 0xc4, 0xc0, 0x3b, 0x34, 0x64, 0x66, 0x8c, + 0x09, 0xe3, 0x1c, 0xa6, 0x98, 0x19, 0xf0, 0xcc, 0xd2, 0xd6, 0x46, 0x1a, 0x59, 0xae, 0x9f, 0xf7, 0xff, 0xd2, 0xb1, + 0x1a, 0x14, 0xcd, 0xf6, 0x10, 0x1d, 0x12, 0x62, 0x3f, 0x86, 0xb0, 0xc9, 0x5c, 0x6a, 0xc3, 0x7c, 0x9f, 0x74, 0x52, + 0xfb, 0x09, 0x7f, 0x86, 0x1b, 0xb3, 0x03, 0x40, 0x47, 0x86, 0xcd, 0xfa, 0xcb, 0x9a, 0xca, 0xeb, 0xe3, 0xde, 0x28, + 0x95, 0xfb, 0xde, 0x9d, 0x0e, 0x54, 0x13, 0xa1, 0xb7, 0x1e, 0x2e, 0x1f, 0xea, 0x21, 0x60, 0xc6, 0x60, 0x6e, 0x99, + 0xd1, 0xf7, 0x42, 0x24, 0x17, 0x44, 0x02, 0x4b, 0x82, 0x29, 0x61, 0xb0, 0xb7, 0x8e, 0x8e, 0x4c, 0x35, 0xd6, 0x80, + 0xe7, 0x49, 0x11, 0x08, 0x06, 0x3e, 0x82, 0x32, 0xa0, 0x89, 0x32, 0xb7, 0xe1, 0xe4, 0x23, 0x73, 0xbf, 0x70, 0x79, + 0xfb, 0x58, 0x38, 0x6d, 0xab, 0xb9, 0x1e, 0x2f, 0x0b, 0xdc, 0x95, 0xf7, 0x92, 0x56, 0xc1, 0x8d, 0xec, 0x4d, 0x9e, + 0x32, 0x77, 0xeb, 0xbe, 0x54, 0x67, 0x7f, 0x33, 0x9d, 0xb2, 0x99, 0xce, 0x6e, 0x33, 0x61, 0x5c, 0xc9, 0x6f, 0x59, + 0x45, 0x9a, 0x93, 0x35, 0x51, 0x0b, 0x2a, 0xfe, 0x41, 0x17, 0xa0, 0x1d, 0xe5, 0xf6, 0x5e, 0x15, 0x4e, 0xae, 0x9c, + 0x5c, 0x1d, 0xe6, 0x86, 0xb8, 0x22, 0x73, 0xa1, 0x0e, 0x01, 0x5e, 0x5e, 0x94, 0x8f, 0x0f, 0x70, 0x29, 0x7e, 0x91, + 0x63, 0x17, 0xe5, 0x54, 0x48, 0x2d, 0x05, 0x8b, 0x90, 0x41, 0x55, 0x17, 0x03, 0x7b, 0x65, 0xf7, 0x9e, 0xe8, 0xf3, + 0x41, 0x15, 0x31, 0x6f, 0x68, 0x9e, 0xfb, 0xf8, 0x9e, 0xa6, 0xd8, 0xa9, 0x89, 0x33, 0xf2, 0x5b, 0x16, 0xe7, 0x20, + 0x9b, 0x0d, 0xaa, 0xd7, 0x7e, 0x1b, 0x6d, 0x5c, 0x34, 0x63, 0xd1, 0x37, 0x4f, 0x9c, 0xfc, 0x50, 0x18, 0xe3, 0x00, + 0xeb, 0xe8, 0x8f, 0x30, 0xb5, 0x60, 0xcf, 0x12, 0x4f, 0xa1, 0x93, 0x5b, 0x9b, 0x76, 0x17, 0xa6, 0xdd, 0x99, 0xb4, + 0x0e, 0x94, 0x03, 0xd2, 0xec, 0xca, 0x74, 0xee, 0xfc, 0xf7, 0x1d, 0xbc, 0x74, 0xbb, 0x81, 0x48, 0xdc, 0x8b, 0x47, + 0xc6, 0x18, 0xe2, 0x0d, 0xd8, 0x88, 0xaa, 0xa3, 0xa3, 0x9f, 0x9d, 0xf7, 0x6d, 0x25, 0xcb, 0x7e, 0x2b, 0x1c, 0xd8, + 0x16, 0x53, 0xe9, 0xf2, 0xc6, 0x32, 0x5b, 0x82, 0x5d, 0xe7, 0xe1, 0x37, 0xe2, 0xe1, 0x8b, 0x90, 0x69, 0xb1, 0xae, + 0xe2, 0xaf, 0xe4, 0xb8, 0xf4, 0x10, 0xd5, 0x10, 0x81, 0xb4, 0xb2, 0x2e, 0x0d, 0x4d, 0x47, 0xaf, 0x67, 0x74, 0x2c, + 0x6f, 0xde, 0x4a, 0xa9, 0x87, 0xf6, 0x45, 0x6e, 0x9d, 0xc0, 0xa3, 0x85, 0x35, 0x86, 0xe6, 0xae, 0xf4, 0x4e, 0xb2, + 0x01, 0x51, 0xeb, 0xe3, 0x0e, 0x25, 0x91, 0x58, 0x54, 0x77, 0x21, 0x1c, 0xee, 0x42, 0x30, 0x2f, 0x83, 0xb6, 0x41, + 0xec, 0x76, 0x17, 0xb4, 0x0d, 0x9c, 0xba, 0x6d, 0xe0, 0xf6, 0x60, 0xb0, 0xb0, 0xf7, 0xe1, 0xe5, 0x58, 0x8e, 0x85, + 0xe3, 0x0f, 0xee, 0xd9, 0x07, 0x80, 0x40, 0xed, 0xc3, 0x8a, 0x27, 0x0e, 0x04, 0x89, 0x33, 0x1c, 0xfd, 0xc0, 0xd9, + 0x8d, 0xb5, 0x1c, 0x9e, 0x2f, 0x96, 0x9a, 0x8d, 0xcd, 0x1d, 0x35, 0xa8, 0xf8, 0xea, 0x7e, 0x5e, 0xbf, 0x66, 0x35, + 0xdd, 0xf8, 0x3d, 0x08, 0x23, 0xe1, 0x94, 0x1d, 0x46, 0x21, 0x61, 0x83, 0x59, 0x95, 0xf1, 0xda, 0x7e, 0x87, 0x78, + 0x0f, 0xda, 0x84, 0x13, 0x2c, 0x6a, 0x17, 0x54, 0x11, 0xb6, 0xf1, 0xc6, 0x82, 0x28, 0x0f, 0x6f, 0x76, 0x8c, 0xa6, + 0x57, 0x1b, 0x08, 0x74, 0xdc, 0x8f, 0x9a, 0x51, 0x83, 0xa5, 0x2e, 0x28, 0xb3, 0x8f, 0x30, 0xae, 0x2e, 0xcf, 0x4c, + 0x9c, 0xf6, 0x52, 0xaf, 0xfe, 0x7b, 0x06, 0x06, 0xf8, 0x02, 0xbc, 0xc4, 0xc2, 0xe8, 0xae, 0x03, 0xdd, 0x80, 0xfa, + 0xb2, 0xc1, 0x86, 0x68, 0xbd, 0x6e, 0x95, 0xcf, 0x40, 0xb9, 0x6b, 0x2e, 0x61, 0xaf, 0xb9, 0x84, 0xbb, 0xe6, 0x12, + 0xfe, 0x9a, 0x4b, 0x98, 0x6b, 0x2e, 0xe1, 0xaf, 0xb9, 0x3c, 0x08, 0x7f, 0x0a, 0xe2, 0x38, 0xc6, 0x1c, 0xe2, 0x2a, + 0x6a, 0x1b, 0x19, 0x0f, 0x2e, 0x3c, 0x0f, 0x59, 0xa2, 0xca, 0xe5, 0x0f, 0x63, 0xc8, 0xe5, 0xdb, 0xb6, 0x12, 0xc6, + 0x6d, 0x8a, 0x29, 0x88, 0x9c, 0x7e, 0x74, 0x54, 0xb9, 0x3b, 0x0f, 0x5a, 0xc3, 0x94, 0xe3, 0x95, 0x75, 0xa2, 0xfd, + 0x27, 0xe8, 0xe4, 0xcd, 0xaf, 0x8f, 0xa9, 0xdc, 0x10, 0xe1, 0x4c, 0xee, 0x0f, 0xdb, 0x9e, 0x52, 0xfc, 0x94, 0x99, + 0xf0, 0xe4, 0x3c, 0xd1, 0x46, 0x04, 0x41, 0x88, 0x12, 0xf5, 0xff, 0xb2, 0xf7, 0xae, 0xcb, 0x6d, 0x23, 0x59, 0xba, + 0xe8, 0xab, 0x48, 0x0c, 0x9b, 0x05, 0x98, 0x49, 0x8a, 0xf2, 0xde, 0x33, 0x11, 0x07, 0x54, 0x9a, 0xe1, 0x4b, 0xb9, + 0xcb, 0x5d, 0xe5, 0x4b, 0x5b, 0xae, 0x6a, 0x57, 0x33, 0x78, 0x54, 0x10, 0x90, 0x24, 0xe0, 0x02, 0x01, 0x16, 0x00, + 0x4a, 0xa4, 0x49, 0xbc, 0xfb, 0x8e, 0xb5, 0x56, 0x5e, 0x41, 0x50, 0x76, 0xcf, 0xec, 0xf9, 0x75, 0xce, 0x1f, 0x5b, + 0x4c, 0x24, 0x12, 0x79, 0xcf, 0x95, 0xeb, 0xf2, 0x7d, 0x2c, 0xe2, 0x05, 0xad, 0x77, 0x15, 0x0a, 0x8f, 0xaa, 0x28, + 0xe5, 0x56, 0xf2, 0x32, 0x83, 0x20, 0x76, 0xf4, 0xc2, 0xf0, 0x27, 0x10, 0x42, 0x10, 0x61, 0xc2, 0xe7, 0x61, 0x46, + 0xdb, 0x59, 0xa4, 0x93, 0x7e, 0x1f, 0x66, 0xb8, 0x81, 0x95, 0xfc, 0x5c, 0xf5, 0xd9, 0x7e, 0x1b, 0x84, 0x6c, 0x17, + 0x44, 0xec, 0xb6, 0xd8, 0x06, 0xa5, 0x75, 0x24, 0x5e, 0x2b, 0xc3, 0xdf, 0xc2, 0xeb, 0xe5, 0x21, 0xc4, 0xfb, 0xf4, + 0xd2, 0xfc, 0x2c, 0x6d, 0x45, 0x01, 0xee, 0x23, 0xf4, 0xa8, 0x0e, 0x04, 0x3b, 0xe1, 0x09, 0x0f, 0xe0, 0x64, 0x35, + 0xab, 0xf8, 0xa3, 0x14, 0xc4, 0x89, 0x82, 0x43, 0xc0, 0xd5, 0xf6, 0x3a, 0xfd, 0x0a, 0x86, 0x2f, 0x1d, 0x6c, 0x39, + 0xbc, 0x2d, 0xb6, 0x3d, 0x56, 0xf2, 0x0f, 0xc0, 0xbe, 0xd5, 0x93, 0xb1, 0xba, 0x3d, 0x70, 0xd6, 0xa5, 0x14, 0x1d, + 0x6f, 0x8a, 0xc3, 0xdb, 0xf3, 0xd9, 0x7e, 0x1b, 0x44, 0x6c, 0x17, 0x64, 0x58, 0xeb, 0xa4, 0xe1, 0x38, 0x18, 0xc2, + 0x67, 0x31, 0xc2, 0xfe, 0x2f, 0xea, 0x81, 0x97, 0x90, 0x1a, 0x0a, 0x5c, 0x0c, 0x36, 0x1c, 0xad, 0xed, 0x32, 0x0d, + 0xdc, 0xd4, 0xa0, 0xd7, 0xf7, 0x14, 0xa2, 0xbc, 0x60, 0x34, 0x37, 0x82, 0x75, 0x63, 0xc8, 0xc5, 0xe1, 0xb8, 0x59, + 0x0c, 0x79, 0x49, 0xd3, 0x69, 0x10, 0x4a, 0x77, 0x96, 0x35, 0x24, 0x51, 0xf6, 0x41, 0xa8, 0x5d, 0x5b, 0xf6, 0xdb, + 0xc0, 0xf6, 0xe5, 0x8f, 0x86, 0xb1, 0x7f, 0xb1, 0x78, 0x22, 0xa4, 0x8b, 0x78, 0x0e, 0x82, 0xa8, 0xfd, 0x3c, 0x1b, + 0x6e, 0xfc, 0x8b, 0xf5, 0x13, 0xa1, 0xfc, 0xc6, 0x73, 0x5b, 0x0e, 0x11, 0x59, 0x0b, 0x5f, 0x18, 0x0f, 0x0f, 0xae, + 0x0c, 0x6d, 0x87, 0x83, 0xd0, 0x7f, 0x9b, 0x35, 0x82, 0x1b, 0x1b, 0xda, 0xe7, 0x0b, 0x1f, 0xb6, 0x36, 0x1a, 0x6b, + 0x8a, 0xe9, 0x16, 0xfa, 0x37, 0x99, 0x2d, 0xed, 0x69, 0x54, 0xf2, 0xe2, 0xd4, 0x34, 0x62, 0x21, 0x0c, 0x18, 0xfa, + 0xc9, 0x7c, 0x00, 0xd5, 0xdc, 0xf1, 0x08, 0x64, 0xf2, 0x81, 0x1e, 0xac, 0x49, 0xad, 0xfa, 0x6b, 0x98, 0xc9, 0xff, + 0x23, 0x15, 0x16, 0xa3, 0xbb, 0x6d, 0x98, 0xa9, 0x3f, 0x22, 0xf9, 0x07, 0xcb, 0xf9, 0x2e, 0xf5, 0x42, 0xed, 0xc7, + 0xc2, 0x0a, 0x0c, 0x4a, 0x54, 0x0d, 0xe8, 0x81, 0x08, 0xaa, 0x32, 0x48, 0x33, 0xac, 0xce, 0x41, 0xbf, 0x7b, 0x5a, + 0x75, 0x24, 0x87, 0xb4, 0x56, 0x43, 0x2a, 0x98, 0x2a, 0x35, 0xc8, 0x0f, 0x87, 0x65, 0xca, 0x74, 0x19, 0x70, 0x49, + 0x5f, 0xa6, 0x4a, 0x29, 0xfc, 0x17, 0x02, 0xd0, 0x39, 0xb8, 0xc7, 0x97, 0x63, 0x20, 0xcd, 0xb0, 0xf0, 0x5b, 0xb3, + 0xe3, 0x6b, 0x12, 0x6e, 0x93, 0xe0, 0x62, 0x80, 0x73, 0x74, 0x15, 0x96, 0xcb, 0x14, 0x22, 0xa8, 0x4a, 0xa8, 0x6f, + 0x65, 0x1a, 0x94, 0xb6, 0x1a, 0x84, 0x35, 0x09, 0x75, 0x26, 0xd9, 0xa8, 0xb4, 0xdd, 0x28, 0xcc, 0x16, 0x71, 0x3d, + 0x23, 0xac, 0x39, 0x9b, 0xa9, 0x06, 0x26, 0x0d, 0xc7, 0x4d, 0xa3, 0xb5, 0xa8, 0x50, 0x53, 0x98, 0xd7, 0xb8, 0xaa, + 0x54, 0x75, 0x37, 0xa7, 0x96, 0xd2, 0xa2, 0xbd, 0xea, 0x26, 0xd9, 0x90, 0xcb, 0x50, 0x86, 0xc1, 0x46, 0x8e, 0x60, + 0x02, 0x49, 0x72, 0xe6, 0x6f, 0xe4, 0x1f, 0x6a, 0xd3, 0xb5, 0x80, 0x39, 0xc6, 0x2c, 0x1b, 0x16, 0xf4, 0x0a, 0xdc, + 0x03, 0xad, 0xf4, 0x7c, 0x9a, 0x5d, 0xe4, 0x41, 0x32, 0x2c, 0xf4, 0xb2, 0xc9, 0xf8, 0x5f, 0xc2, 0x48, 0x93, 0x19, + 0x2b, 0x59, 0x64, 0xbb, 0x3a, 0x25, 0xce, 0xe3, 0x04, 0xb6, 0x47, 0xd3, 0x5b, 0xbe, 0xcf, 0x20, 0x2a, 0x08, 0x14, + 0xcc, 0x98, 0x2f, 0xbb, 0x78, 0xea, 0xfb, 0xcc, 0x32, 0x75, 0x1f, 0x0e, 0xc6, 0x8c, 0xed, 0xf7, 0xfb, 0x79, 0xbf, + 0xaf, 0xe6, 0x5b, 0xbf, 0x9f, 0x3c, 0x33, 0x7f, 0x7b, 0xc0, 0xa0, 0x20, 0x27, 0xa2, 0xa9, 0x10, 0xc1, 0x3f, 0x24, + 0x4f, 0x90, 0x8c, 0xee, 0xb8, 0xcf, 0x2d, 0x67, 0xcb, 0xea, 0x08, 0x04, 0xf3, 0x70, 0xb8, 0x54, 0x60, 0xd7, 0x12, + 0x45, 0x42, 0x96, 0xff, 0x04, 0x8c, 0x67, 0xee, 0x03, 0x2c, 0x19, 0x80, 0xb0, 0x55, 0x9e, 0xae, 0xf7, 0x7c, 0x15, + 0xbc, 0xd3, 0xf1, 0xae, 0xb1, 0x22, 0x03, 0x71, 0x0b, 0x6c, 0xc4, 0x5a, 0x7b, 0x40, 0xce, 0x14, 0xe0, 0x78, 0x71, + 0x38, 0x9c, 0xcb, 0x5f, 0xba, 0xd9, 0x3a, 0x81, 0x4a, 0x81, 0xdb, 0xa3, 0x93, 0x83, 0xff, 0x01, 0x34, 0x83, 0x72, + 0x98, 0xd7, 0xdb, 0x3f, 0x98, 0x93, 0x9f, 0x9e, 0xe2, 0x9f, 0xf0, 0x10, 0x9d, 0x7e, 0xbb, 0x37, 0x7f, 0x50, 0x54, + 0x1e, 0x0e, 0x6a, 0xf1, 0x9f, 0x73, 0x5e, 0xc1, 0x2f, 0x7c, 0x13, 0x98, 0x4d, 0xa6, 0xde, 0xc9, 0x37, 0x79, 0xce, + 0xd4, 0x6b, 0xbc, 0x62, 0xf2, 0x1d, 0x0e, 0xe7, 0x62, 0x54, 0x6f, 0x47, 0x4e, 0xb4, 0x53, 0x8e, 0x71, 0x30, 0xf8, + 0x2f, 0xa2, 0x6d, 0x42, 0x80, 0xa1, 0x1c, 0x8e, 0xcc, 0xc6, 0x95, 0x25, 0x9e, 0xa5, 0xf3, 0xcb, 0x49, 0x5d, 0xee, + 0xb4, 0xe2, 0x69, 0x0f, 0x2c, 0x6e, 0x6b, 0xf0, 0x02, 0xb8, 0xb3, 0xd8, 0xba, 0x52, 0x70, 0xb8, 0x80, 0x38, 0xc5, + 0x09, 0x88, 0xa0, 0xfd, 0xbe, 0xc4, 0x7b, 0x05, 0x7d, 0xd2, 0x8f, 0x10, 0x0c, 0xf9, 0x8b, 0x04, 0xdc, 0xf5, 0x7a, + 0x35, 0xc6, 0xf7, 0x52, 0x08, 0xae, 0xcf, 0x34, 0x00, 0x2d, 0xf8, 0x5d, 0x3e, 0x94, 0xd3, 0x6f, 0x22, 0xf0, 0x6c, + 0xd9, 0x9b, 0x28, 0x77, 0x1b, 0x9e, 0xf6, 0xba, 0x85, 0x00, 0x2c, 0xc5, 0x33, 0x25, 0x58, 0x90, 0x53, 0xcc, 0xc5, + 0xff, 0x0b, 0x3e, 0x62, 0xbe, 0x27, 0x5d, 0xc4, 0xd6, 0xdb, 0x47, 0x17, 0x06, 0x12, 0x68, 0x3a, 0x00, 0x3f, 0x5e, + 0x05, 0x74, 0x65, 0xfc, 0x3b, 0x2d, 0xeb, 0xb1, 0x3e, 0xfe, 0x53, 0x70, 0x9f, 0x7e, 0xa2, 0xf0, 0xd1, 0xe1, 0xb8, + 0x4a, 0x47, 0x3b, 0x4a, 0x41, 0x74, 0x74, 0xfb, 0x7c, 0xaa, 0xb2, 0xef, 0x2a, 0x20, 0xb7, 0x1c, 0xb5, 0xa7, 0x02, + 0xb0, 0xd8, 0xd2, 0x11, 0xf8, 0x34, 0xcb, 0x27, 0xe4, 0x7b, 0x3d, 0x15, 0x57, 0x97, 0x3a, 0x5d, 0x3c, 0x1b, 0x4f, + 0xe1, 0x7f, 0x20, 0xf6, 0xb0, 0x4c, 0x91, 0x1d, 0xbb, 0x2e, 0x7e, 0x10, 0x6f, 0x6b, 0x3b, 0xfa, 0x63, 0x07, 0x91, + 0x8e, 0x7b, 0x72, 0xa1, 0xbe, 0x84, 0x54, 0x72, 0xa1, 0x6e, 0x20, 0x76, 0xa1, 0xc6, 0x3b, 0x2e, 0x62, 0xad, 0xbf, + 0xad, 0x51, 0xb0, 0x12, 0x70, 0xa6, 0xbd, 0x05, 0x83, 0x0d, 0xac, 0x5b, 0x96, 0xc1, 0xdf, 0x70, 0x4d, 0x13, 0xb8, + 0x61, 0x91, 0xf5, 0xde, 0x60, 0x2b, 0xbd, 0x05, 0x47, 0xcb, 0xc4, 0xb9, 0x94, 0x24, 0x65, 0x8b, 0x8c, 0xab, 0x47, + 0x21, 0x55, 0xd3, 0xfd, 0xad, 0xa8, 0xef, 0x85, 0xc8, 0x83, 0x55, 0xca, 0xa2, 0x62, 0x05, 0x32, 0x7b, 0xf0, 0xaf, + 0x90, 0x91, 0xa3, 0x1c, 0x38, 0x0a, 0xfd, 0xa3, 0x09, 0x74, 0x9e, 0x3a, 0xd2, 0x79, 0x24, 0xd8, 0x4a, 0x3d, 0x14, + 0x56, 0x5e, 0x40, 0x74, 0xb0, 0x1d, 0x73, 0x2b, 0x4f, 0x42, 0xc5, 0xa6, 0x4c, 0xe4, 0x71, 0x50, 0x4b, 0xc0, 0x58, + 0x41, 0x30, 0x67, 0xb9, 0x74, 0x41, 0xaa, 0x1a, 0x3d, 0x2c, 0x32, 0xf7, 0x63, 0x41, 0xf9, 0x1f, 0xab, 0x9c, 0x70, + 0x7d, 0x19, 0x02, 0x1c, 0xed, 0x63, 0x10, 0x25, 0xc6, 0xfa, 0x45, 0x8b, 0x77, 0x32, 0x73, 0x36, 0xb5, 0xbd, 0x04, + 0x19, 0xdb, 0xe1, 0x57, 0x08, 0xad, 0x16, 0x8a, 0x2c, 0x1a, 0x2e, 0x98, 0x6e, 0x4f, 0x69, 0xd5, 0x3d, 0x6c, 0x78, + 0x52, 0x7a, 0xa8, 0xd4, 0xb7, 0x31, 0x81, 0x65, 0x95, 0x32, 0x7c, 0x3b, 0xa1, 0xea, 0xc4, 0xa0, 0x62, 0xdd, 0xb0, + 0x05, 0x1c, 0x62, 0x31, 0x69, 0xac, 0xb3, 0x01, 0x8f, 0x58, 0x02, 0xff, 0x6c, 0xf8, 0x98, 0x2d, 0x78, 0x34, 0xd9, + 0x5c, 0x2d, 0xfa, 0xfd, 0xd2, 0x0b, 0xbd, 0x7a, 0x96, 0x3d, 0x8e, 0xe6, 0xb3, 0x7c, 0xee, 0xa3, 0xe2, 0x62, 0x32, + 0x18, 0x6c, 0xfc, 0x6c, 0x38, 0x64, 0xc9, 0x70, 0x38, 0xc9, 0x1e, 0xc3, 0x6b, 0x8f, 0x79, 0xa4, 0x96, 0x54, 0x72, + 0x95, 0xc1, 0xfe, 0x3e, 0xe0, 0x91, 0xcf, 0x3a, 0x3f, 0x2d, 0x9b, 0x2e, 0xdd, 0xcf, 0xec, 0xb8, 0x0b, 0xdd, 0x01, + 0x36, 0xde, 0x36, 0xe8, 0xc8, 0xbf, 0xdd, 0x21, 0xa5, 0x6e, 0x32, 0x00, 0xbb, 0xd1, 0x00, 0x87, 0x4c, 0xf5, 0x52, + 0x64, 0xf5, 0x52, 0xa6, 0x7a, 0x49, 0x56, 0x2e, 0xc1, 0x42, 0x62, 0xaa, 0xdc, 0x46, 0x56, 0x6e, 0xd1, 0x70, 0x3d, + 0x1c, 0x6c, 0xad, 0xb8, 0x6c, 0x96, 0x70, 0x5f, 0x58, 0x51, 0xe0, 0xff, 0x2d, 0xbb, 0x61, 0x77, 0xf2, 0x18, 0x78, + 0x8b, 0x8e, 0x49, 0x70, 0x81, 0xb8, 0x63, 0xb7, 0x60, 0x87, 0x85, 0xbf, 0xe0, 0x3a, 0x39, 0x66, 0x3b, 0x7c, 0x14, + 0x7a, 0x05, 0xbb, 0xf5, 0x09, 0x68, 0x17, 0x6c, 0x0d, 0x90, 0x8d, 0x6d, 0xf1, 0xd1, 0xf2, 0x70, 0x78, 0xeb, 0xf9, + 0xec, 0x1e, 0x7f, 0x9c, 0x2f, 0x0f, 0x87, 0x9d, 0x67, 0xd4, 0x7b, 0xd7, 0x3c, 0x61, 0xef, 0x79, 0x32, 0xb9, 0xbe, + 0xe2, 0xf1, 0x64, 0x30, 0xb8, 0xf6, 0x6f, 0x78, 0x3d, 0xbb, 0x06, 0xed, 0xc0, 0xf9, 0x8d, 0xd4, 0x35, 0x7b, 0xb7, + 0x3c, 0xf3, 0x6e, 0x70, 0x6c, 0x6e, 0xe1, 0xe8, 0xed, 0xf7, 0xbd, 0x25, 0x8f, 0xbc, 0x5b, 0x52, 0x31, 0xad, 0xb8, + 0xe2, 0x78, 0xdb, 0xe2, 0x7e, 0xba, 0xe2, 0x21, 0x3c, 0xc2, 0xaa, 0x4c, 0xaf, 0x83, 0xf7, 0x3e, 0x5b, 0x69, 0x16, + 0xb8, 0x7b, 0xcc, 0xb1, 0x26, 0x3b, 0xa1, 0x99, 0xf8, 0x2b, 0xec, 0x9f, 0x6b, 0xd5, 0x3f, 0x34, 0xff, 0x4b, 0xdd, + 0x4f, 0xe0, 0xf6, 0x45, 0x16, 0x24, 0xf6, 0x9e, 0x5f, 0xb3, 0x3b, 0x6e, 0xd8, 0x66, 0xcf, 0x4c, 0xd9, 0x27, 0x4a, + 0x8d, 0x1f, 0x28, 0x75, 0x6d, 0x19, 0x56, 0x5a, 0x57, 0x3e, 0x04, 0x0e, 0x07, 0xe4, 0xa7, 0x25, 0xe2, 0x20, 0xb4, + 0x6e, 0xb2, 0x9a, 0x2b, 0xca, 0xb9, 0xd0, 0x86, 0x99, 0x97, 0x03, 0x8b, 0x59, 0x4a, 0xa1, 0xb1, 0x00, 0x40, 0x30, + 0x29, 0xb4, 0xf6, 0x5e, 0x06, 0x90, 0x13, 0x34, 0xfc, 0xb1, 0xb9, 0x2a, 0xcb, 0x5a, 0xb6, 0x24, 0x44, 0xd9, 0xae, + 0x87, 0x97, 0x08, 0x99, 0xd6, 0xef, 0x9f, 0x13, 0xc9, 0xda, 0xa4, 0xba, 0xaa, 0xd1, 0x12, 0x50, 0x91, 0x25, 0x60, + 0xe2, 0x57, 0x9a, 0x4f, 0x00, 0x9e, 0x74, 0x3c, 0xa8, 0x1e, 0xf3, 0x9a, 0x09, 0x22, 0xdb, 0xa8, 0xfc, 0x49, 0xf1, + 0x0c, 0xc9, 0x08, 0x8a, 0xc7, 0xb5, 0xca, 0x58, 0x18, 0xe6, 0x81, 0x02, 0xf2, 0xee, 0xdd, 0xa9, 0x6f, 0xed, 0x8f, + 0x1d, 0x7b, 0xb6, 0x56, 0xa1, 0x16, 0x6a, 0x0a, 0x97, 0x1c, 0xa2, 0x2b, 0xd0, 0x40, 0x11, 0xc9, 0x78, 0xf2, 0x7a, + 0x70, 0x39, 0x89, 0xae, 0xb8, 0x40, 0x67, 0x7c, 0x7d, 0xd3, 0x4d, 0x67, 0xd1, 0xe3, 0x6a, 0x3e, 0x21, 0x25, 0xd9, + 0xe1, 0x90, 0x8d, 0xaa, 0xba, 0x58, 0x4f, 0x43, 0xf9, 0xd3, 0x43, 0xf0, 0xf5, 0x82, 0x7a, 0x4d, 0x56, 0xa9, 0x7e, + 0x4c, 0x95, 0xf2, 0xa2, 0xe1, 0xa5, 0xff, 0xb8, 0x92, 0xfb, 0x1e, 0x90, 0xd6, 0xf2, 0x92, 0xcb, 0xf7, 0x23, 0xc4, + 0x18, 0xf1, 0x03, 0xaf, 0xe4, 0x11, 0x0b, 0xd5, 0x14, 0xae, 0x79, 0x84, 0x20, 0x6f, 0x99, 0x0e, 0xfe, 0xd6, 0x13, + 0xa7, 0xfb, 0x13, 0xa5, 0x5d, 0x7c, 0x61, 0x51, 0xf7, 0x1c, 0xe9, 0x06, 0xe4, 0x60, 0xc3, 0x74, 0x51, 0x90, 0x6d, + 0x4a, 0x23, 0x68, 0xa3, 0xe5, 0xc0, 0x86, 0x53, 0xa9, 0x0d, 0x67, 0xae, 0x21, 0xb8, 0xcf, 0xcf, 0xd3, 0xd1, 0x0d, + 0x7c, 0x48, 0x75, 0x7b, 0x89, 0x9f, 0x0f, 0x1b, 0x8e, 0x64, 0x76, 0xc4, 0x67, 0x36, 0x91, 0x74, 0x52, 0xe7, 0x0a, + 0xd8, 0xed, 0xec, 0x25, 0xc8, 0x11, 0x33, 0xf7, 0x15, 0xaa, 0x6f, 0xd1, 0x80, 0x2b, 0x63, 0xed, 0x6b, 0x92, 0xb1, + 0xf0, 0xaa, 0x9c, 0x86, 0x03, 0x80, 0xa1, 0xcb, 0xe8, 0x6b, 0x8b, 0x4d, 0x96, 0xfd, 0x52, 0x40, 0x10, 0x44, 0x49, + 0x3c, 0x3e, 0xe0, 0x7d, 0x59, 0x0d, 0x35, 0x4a, 0x3e, 0x96, 0x9d, 0xc0, 0xd7, 0x4b, 0xf4, 0x77, 0x63, 0x2e, 0x31, + 0xe0, 0xcb, 0xaa, 0x2d, 0x28, 0x9c, 0xe7, 0x87, 0xc3, 0x79, 0x3e, 0x32, 0x9e, 0x65, 0xa0, 0x5a, 0x99, 0xd6, 0xc1, + 0xc6, 0xcc, 0x17, 0x0b, 0x7f, 0xb1, 0x73, 0x12, 0x11, 0x05, 0x81, 0x1d, 0x09, 0x0f, 0x22, 0xf5, 0xfb, 0xca, 0xd3, + 0x9d, 0xea, 0xb3, 0xfd, 0x8d, 0x4d, 0xa4, 0x17, 0x94, 0x4c, 0x3e, 0x09, 0xf6, 0xaa, 0xbf, 0x83, 0xb0, 0x21, 0xbc, + 0x79, 0xd5, 0xeb, 0x2c, 0x53, 0xb3, 0x12, 0x24, 0xcc, 0x98, 0x23, 0x78, 0x1c, 0x76, 0x1a, 0xdb, 0xf0, 0xd8, 0xc2, + 0x6a, 0xf4, 0xd6, 0x6c, 0xc9, 0x56, 0xec, 0x56, 0xd5, 0xe9, 0x86, 0x87, 0xd3, 0xe1, 0x65, 0x80, 0xab, 0x6f, 0x7d, + 0xce, 0xf9, 0x92, 0x4e, 0xb0, 0xf5, 0x80, 0x47, 0x13, 0x31, 0x5b, 0x3f, 0x8e, 0xd4, 0xe2, 0x59, 0x0f, 0xf9, 0x0d, + 0xad, 0x3f, 0x31, 0x5b, 0x9a, 0xe4, 0xe5, 0x80, 0xdf, 0x4c, 0xd6, 0x8f, 0x23, 0x78, 0xf5, 0x31, 0x58, 0x31, 0x32, + 0x67, 0x96, 0xad, 0x1f, 0x47, 0x38, 0x66, 0xcb, 0xc7, 0x11, 0x8d, 0xda, 0x4a, 0xee, 0x4b, 0xb7, 0x0d, 0x08, 0x2b, + 0xb7, 0x2c, 0x86, 0xd7, 0x40, 0x3c, 0xd3, 0x46, 0xd2, 0xb5, 0x34, 0xf4, 0xc6, 0x3c, 0x9c, 0xc6, 0xc1, 0x9a, 0x5a, + 0x21, 0xcf, 0x0c, 0x31, 0x8b, 0x1f, 0x47, 0x73, 0xb6, 0xc2, 0x8a, 0x6c, 0x78, 0x3c, 0xb8, 0x9c, 0x6c, 0xae, 0xf8, + 0x1a, 0xc8, 0xcf, 0x26, 0x1b, 0xb3, 0x45, 0xdd, 0x72, 0x31, 0xdb, 0x3c, 0x8e, 0xe6, 0x93, 0x15, 0xf4, 0xac, 0x3d, + 0x60, 0xde, 0x6b, 0x10, 0xa1, 0x24, 0xa4, 0xa6, 0xdc, 0xf4, 0x7a, 0x6c, 0x3d, 0x0e, 0x96, 0x6c, 0x7d, 0x19, 0xdc, + 0xb2, 0xf5, 0x18, 0x88, 0x38, 0xa8, 0xdf, 0xbd, 0x0d, 0x2c, 0xbe, 0x88, 0xad, 0x2f, 0x4d, 0xda, 0xe6, 0x71, 0xc4, + 0xdc, 0xc1, 0x69, 0xe0, 0x82, 0xb5, 0xc8, 0xbc, 0x15, 0x83, 0x4b, 0xc8, 0xc2, 0x8b, 0xd9, 0x66, 0x78, 0xc9, 0xd6, + 0x23, 0x9c, 0xea, 0x89, 0xcf, 0x96, 0xfc, 0x96, 0x25, 0x7c, 0xd5, 0xc4, 0x57, 0x1b, 0xd0, 0x88, 0x1e, 0x65, 0xd0, + 0x57, 0x50, 0x33, 0x73, 0xde, 0x5b, 0x18, 0x95, 0xfb, 0x16, 0x1c, 0x50, 0x90, 0xb6, 0x01, 0x82, 0x24, 0x9e, 0xdd, + 0xcb, 0x70, 0x7d, 0x2d, 0x85, 0x01, 0x37, 0x81, 0x19, 0x30, 0x30, 0xfd, 0x0c, 0x7e, 0x58, 0xe9, 0x12, 0x21, 0xce, + 0x7e, 0x4a, 0x49, 0x32, 0xcf, 0xdf, 0x8b, 0x34, 0x77, 0x0b, 0xd7, 0x29, 0xcc, 0x8a, 0x02, 0xd5, 0x4f, 0x49, 0x69, + 0x60, 0xa1, 0x12, 0x99, 0x4a, 0xc1, 0x2f, 0x9b, 0xf3, 0x28, 0x3b, 0x46, 0xe7, 0x3a, 0xbf, 0x9c, 0x38, 0xa7, 0x93, + 0xbe, 0xff, 0xc0, 0x31, 0x6c, 0x21, 0x03, 0x17, 0xfe, 0xd4, 0x13, 0xc6, 0xa9, 0x15, 0x88, 0xa9, 0xe4, 0xd9, 0x53, + 0xf8, 0x4c, 0x68, 0x75, 0x74, 0xe1, 0xfb, 0x41, 0xa1, 0x4d, 0xd2, 0x2d, 0x48, 0x52, 0xf0, 0x14, 0x3d, 0xe7, 0xbc, + 0x0d, 0x54, 0x8a, 0x11, 0x2d, 0x88, 0xb4, 0xb5, 0xce, 0x1c, 0xa4, 0x2d, 0xcd, 0x77, 0x4d, 0xfc, 0x1c, 0x16, 0x70, + 0x11, 0x2d, 0x6c, 0x0d, 0x8f, 0xaa, 0x58, 0xb9, 0x37, 0x79, 0x8e, 0x70, 0x46, 0x97, 0x32, 0x01, 0x70, 0xbd, 0x5f, + 0x85, 0xb5, 0xc2, 0x2b, 0x6a, 0x6e, 0xf2, 0xa2, 0xa6, 0x4f, 0xb6, 0xc0, 0x7d, 0x2c, 0x4a, 0x14, 0x38, 0x6b, 0xc1, + 0x80, 0xad, 0xb0, 0x64, 0x27, 0x85, 0x4d, 0xd1, 0x12, 0x7a, 0x7b, 0xfc, 0x74, 0x50, 0x33, 0x19, 0x40, 0x13, 0x40, + 0xe3, 0xf1, 0x2f, 0x00, 0x35, 0xbd, 0xae, 0xc5, 0xba, 0x0a, 0x4a, 0xa5, 0xdc, 0x84, 0x9f, 0x81, 0x61, 0x86, 0x1f, + 0x0a, 0xb9, 0x4d, 0x94, 0xc8, 0xf9, 0x71, 0x53, 0x8a, 0x45, 0x29, 0xaa, 0xa4, 0xdd, 0x50, 0xf0, 0x88, 0x70, 0x1b, + 0x34, 0x66, 0x6e, 0x4f, 0x74, 0xd1, 0x8a, 0x50, 0x8e, 0xcd, 0x3a, 0x46, 0x1a, 0x65, 0x76, 0xb2, 0xeb, 0x64, 0xa1, + 0xfd, 0xbe, 0xca, 0x21, 0xeb, 0x80, 0x35, 0x92, 0xaf, 0xd7, 0x1c, 0xba, 0x6d, 0x94, 0x17, 0xf7, 0x9e, 0xaf, 0xe0, + 0x34, 0xc7, 0x13, 0xbb, 0xeb, 0x75, 0xa7, 0x48, 0xc4, 0x2b, 0x9c, 0x54, 0xf9, 0x48, 0x16, 0x8e, 0x3b, 0x77, 0x5a, + 0x8b, 0x55, 0xe5, 0xb2, 0x9e, 0x5a, 0x1c, 0x11, 0xf8, 0x54, 0x1e, 0xed, 0x85, 0xb6, 0x45, 0xb1, 0x10, 0x46, 0x8f, + 0x4e, 0xf8, 0x49, 0x09, 0xac, 0xaf, 0xc3, 0x61, 0xe9, 0x47, 0x1c, 0xfd, 0x4e, 0xa3, 0xd1, 0x0d, 0x21, 0x0d, 0x4f, + 0xbd, 0x68, 0x74, 0x53, 0x17, 0x75, 0x98, 0x3d, 0xcb, 0xf5, 0x40, 0x61, 0x18, 0x81, 0xfa, 0xc1, 0x55, 0x06, 0x9f, + 0x45, 0x88, 0x9a, 0x07, 0xa6, 0xd9, 0x10, 0x8e, 0xba, 0xc0, 0x43, 0x2b, 0x68, 0x31, 0x33, 0x1f, 0x85, 0x18, 0x3e, + 0xa4, 0x8b, 0xf3, 0x27, 0x64, 0xe5, 0x03, 0xec, 0x0e, 0xdd, 0x85, 0x72, 0xce, 0x54, 0x0c, 0xf0, 0xa3, 0x80, 0x7c, + 0x94, 0x80, 0x9b, 0x01, 0xb2, 0x47, 0x96, 0x00, 0x62, 0xc5, 0xe8, 0x68, 0xf2, 0xb9, 0xef, 0x45, 0x0a, 0xde, 0xd9, + 0x67, 0xb9, 0x9a, 0x30, 0x14, 0x3e, 0x31, 0xd0, 0xcd, 0x6f, 0xfc, 0xf6, 0xbc, 0x05, 0x23, 0xbb, 0x24, 0xc5, 0x6b, + 0xcd, 0x70, 0xbf, 0x01, 0xb7, 0x23, 0xa0, 0xac, 0xa9, 0x8e, 0x49, 0xb6, 0x69, 0x88, 0x64, 0xc0, 0x8c, 0x18, 0x11, + 0x54, 0x96, 0x0b, 0xff, 0xbb, 0x97, 0x45, 0x81, 0x03, 0xb8, 0x9a, 0xc9, 0xe0, 0xb5, 0x0b, 0xa3, 0x02, 0xe0, 0x9c, + 0x86, 0x4e, 0x69, 0xaf, 0xaa, 0x0e, 0xc9, 0xaa, 0xf9, 0xc1, 0x6c, 0xde, 0x34, 0x4c, 0x8c, 0x08, 0xa2, 0x8b, 0x70, + 0x82, 0xe9, 0x15, 0xe9, 0x6b, 0x25, 0xa7, 0xa3, 0x55, 0x47, 0x6b, 0x89, 0x89, 0xb9, 0xa2, 0xf8, 0x6b, 0xc0, 0xe3, + 0x06, 0xaf, 0x4e, 0xd2, 0x74, 0xa2, 0x7a, 0xf4, 0xf8, 0x75, 0x9a, 0x4e, 0x4a, 0xdc, 0x15, 0x7e, 0x03, 0x2e, 0x9a, + 0x6d, 0x3e, 0xf4, 0xe3, 0x17, 0x14, 0x71, 0x51, 0x83, 0x2b, 0xef, 0x54, 0x5f, 0xa9, 0x3e, 0x82, 0x5a, 0x78, 0x62, + 0x64, 0x2d, 0x3c, 0xb9, 0x64, 0xad, 0x05, 0xc1, 0xcc, 0xe6, 0xc0, 0x85, 0xfc, 0x4a, 0x29, 0xe2, 0x4d, 0x24, 0xd4, + 0x62, 0xd0, 0x7a, 0xcc, 0x9c, 0x55, 0xa3, 0x1b, 0x95, 0x19, 0xa1, 0x7d, 0x5b, 0x8b, 0xce, 0x6f, 0xe4, 0xa7, 0x3c, + 0xb5, 0x2f, 0xdb, 0xe3, 0x7c, 0xbc, 0x47, 0x77, 0xd5, 0x59, 0x66, 0x52, 0xc6, 0x27, 0xb3, 0x04, 0x85, 0xbb, 0x04, + 0x1b, 0x90, 0x64, 0xbf, 0xd5, 0x01, 0x32, 0x6a, 0xaf, 0xfd, 0xae, 0xb3, 0x7c, 0x75, 0xb3, 0x35, 0x14, 0x95, 0x5a, + 0x49, 0x8a, 0x83, 0x0c, 0xd7, 0x6d, 0xe5, 0xc3, 0xc5, 0x05, 0xf4, 0x8c, 0x91, 0xc8, 0x3c, 0x7f, 0x22, 0x5f, 0x82, + 0x73, 0xc6, 0x59, 0x21, 0x30, 0x61, 0xac, 0xde, 0xb5, 0x96, 0x4a, 0x43, 0x8a, 0xb1, 0xa3, 0x51, 0x96, 0x55, 0x96, + 0x2e, 0xb3, 0xb5, 0x84, 0x2d, 0xab, 0xc8, 0x2d, 0x6c, 0x9d, 0xc9, 0x6a, 0x7e, 0xa8, 0xb8, 0x83, 0xf2, 0xcd, 0x96, + 0x19, 0xdf, 0x4b, 0x64, 0xef, 0x36, 0x50, 0xc2, 0xb3, 0xd1, 0x7f, 0x20, 0xfd, 0x36, 0xc3, 0x38, 0xe5, 0xb6, 0x92, + 0x16, 0xe0, 0xf4, 0x0f, 0x87, 0x0f, 0x15, 0x06, 0x0d, 0x8e, 0x30, 0x8e, 0xac, 0xdf, 0xbf, 0xa9, 0xbc, 0x1a, 0x13, + 0x75, 0x7c, 0x56, 0xbf, 0x5f, 0xd1, 0xc3, 0x69, 0x35, 0x5a, 0xa5, 0x5b, 0x64, 0x27, 0xb4, 0xb1, 0xf2, 0x83, 0x5a, + 0x01, 0xb3, 0xb7, 0x3e, 0x9f, 0x0e, 0x40, 0xc7, 0x02, 0x24, 0x9a, 0xcd, 0x44, 0x62, 0x4e, 0xba, 0x27, 0xe1, 0xf1, + 0x81, 0x05, 0x0e, 0x30, 0x15, 0xff, 0xa7, 0xf0, 0x66, 0x60, 0x83, 0x46, 0x89, 0xbe, 0x46, 0x57, 0xb5, 0xb9, 0xd1, + 0xf1, 0xd2, 0x53, 0x48, 0x64, 0x05, 0xab, 0xe6, 0xbe, 0xdc, 0xc0, 0x69, 0x0f, 0x35, 0x87, 0xca, 0x02, 0xfc, 0xed, + 0x17, 0x60, 0xf0, 0xc8, 0xa0, 0xb0, 0xdd, 0x5a, 0x68, 0x6f, 0xcc, 0x52, 0x0d, 0x15, 0xe1, 0xa0, 0xf3, 0x95, 0x98, + 0xd5, 0x23, 0xfa, 0x7b, 0x7e, 0x38, 0xac, 0x08, 0x0c, 0x38, 0x2c, 0x65, 0x26, 0x5a, 0x28, 0x96, 0xd6, 0xd9, 0x8c, + 0xea, 0xc0, 0x03, 0x13, 0x73, 0x16, 0xee, 0x00, 0xb4, 0x49, 0xad, 0x02, 0xbd, 0x8a, 0xe8, 0x27, 0xee, 0xd7, 0xf6, + 0xeb, 0xf5, 0xc8, 0x2c, 0x1d, 0xb9, 0x31, 0x16, 0x00, 0x1c, 0x78, 0x5e, 0x93, 0x3c, 0x27, 0x5f, 0x43, 0xbb, 0x27, + 0x17, 0xf2, 0x27, 0x28, 0x5b, 0x78, 0xae, 0x9a, 0x56, 0x16, 0x2b, 0xae, 0xaa, 0x57, 0x17, 0xbc, 0x32, 0x99, 0x56, + 0x69, 0x25, 0x2a, 0x25, 0x18, 0x50, 0x97, 0x78, 0xad, 0x69, 0x46, 0xa9, 0x8d, 0x3a, 0x13, 0x35, 0x60, 0x83, 0xfd, + 0x54, 0x6d, 0x74, 0x72, 0x2e, 0x9f, 0x5f, 0x1a, 0x87, 0x4f, 0xbb, 0x7a, 0x33, 0x53, 0x39, 0xf0, 0xd7, 0xca, 0x87, + 0x56, 0x8f, 0x81, 0x0e, 0xc8, 0xe9, 0x8f, 0x61, 0x31, 0xb1, 0x3b, 0x34, 0x6f, 0x77, 0x97, 0xd5, 0x45, 0x7a, 0xa7, + 0x29, 0x99, 0xd5, 0x5b, 0x3e, 0xb3, 0x7a, 0x74, 0xc0, 0x8b, 0x87, 0x7a, 0xaf, 0x30, 0x93, 0x08, 0x2e, 0x86, 0x6a, + 0x12, 0xd9, 0x1d, 0x68, 0xcd, 0xa3, 0x8a, 0x09, 0xf0, 0x83, 0x52, 0x6b, 0x7a, 0x6f, 0x77, 0x85, 0x3a, 0xa5, 0xf0, + 0xb8, 0xb5, 0xe4, 0x07, 0xe6, 0x4e, 0xbb, 0xd6, 0xf9, 0x78, 0x7e, 0xe9, 0xfb, 0x8d, 0x3c, 0xa1, 0xcd, 0xce, 0xe4, + 0xf4, 0x4f, 0xde, 0xea, 0x1f, 0xa6, 0xfa, 0x16, 0xba, 0x13, 0xf4, 0x19, 0xba, 0xaa, 0xba, 0x2b, 0xb1, 0x85, 0xa1, + 0x9e, 0x58, 0xe4, 0x85, 0x3c, 0x69, 0x8d, 0x1d, 0x07, 0x7b, 0x03, 0x9c, 0xf8, 0xe5, 0xe1, 0x20, 0xae, 0x72, 0x9f, + 0x9d, 0x77, 0x8d, 0xac, 0x1c, 0xc0, 0x0a, 0xa2, 0x60, 0xdc, 0x9a, 0x8f, 0x6d, 0x90, 0x2e, 0x71, 0x35, 0x3e, 0x7e, + 0x43, 0xb1, 0x4c, 0x36, 0x11, 0x17, 0x17, 0xf9, 0xe3, 0xa7, 0x40, 0x5a, 0xd6, 0xef, 0x47, 0xcf, 0x2e, 0xa7, 0x4f, + 0x87, 0x51, 0x00, 0x8e, 0x5d, 0xf6, 0xf2, 0x32, 0xe6, 0xab, 0x4b, 0x66, 0x99, 0xc2, 0x22, 0xdf, 0x0c, 0xa8, 0x2e, + 0x59, 0x2d, 0x5d, 0xaf, 0x00, 0x4b, 0x97, 0xdf, 0xdc, 0x87, 0xa9, 0x01, 0x8d, 0xac, 0xb9, 0x3b, 0xcd, 0xb5, 0x40, + 0xa9, 0xe7, 0xfd, 0xcc, 0x90, 0xaf, 0xcb, 0xa0, 0x2b, 0x48, 0xf7, 0x3c, 0x22, 0xbd, 0xdc, 0x4b, 0xa7, 0xfb, 0x7d, + 0x29, 0xc0, 0x52, 0x5f, 0x8a, 0x2f, 0xa0, 0xb0, 0x68, 0x7c, 0x23, 0x40, 0x5b, 0x43, 0x35, 0xed, 0x95, 0xa2, 0xea, + 0x05, 0xbd, 0x52, 0x7c, 0xe9, 0xe9, 0xa1, 0x32, 0x5f, 0x96, 0x8e, 0xfe, 0x27, 0xd4, 0x5c, 0x70, 0x42, 0xcc, 0xc4, + 0x1c, 0x40, 0x25, 0x68, 0xe3, 0xbb, 0x3d, 0xda, 0xf8, 0x54, 0xaf, 0xe2, 0xa6, 0xcf, 0x6b, 0x6b, 0x99, 0x13, 0xc2, + 0xa6, 0x7b, 0x09, 0x50, 0x91, 0x57, 0xc2, 0x23, 0x58, 0x7e, 0xf9, 0x43, 0x9e, 0xae, 0x10, 0xad, 0xe3, 0x9e, 0x65, + 0x2e, 0x8d, 0xfd, 0x6b, 0x83, 0xe9, 0xeb, 0xdb, 0x6d, 0x91, 0x9f, 0x9a, 0x98, 0xb0, 0x1e, 0x2b, 0xfa, 0xe6, 0x5d, + 0xb8, 0x12, 0x28, 0x70, 0x28, 0x91, 0xd8, 0xa6, 0x0a, 0x45, 0x3c, 0x48, 0xfa, 0x74, 0xd1, 0xfa, 0x34, 0xc0, 0xd4, + 0x5a, 0x0e, 0xcc, 0x21, 0x5c, 0xc5, 0x85, 0x8f, 0x9e, 0xbe, 0xc5, 0x2c, 0x9c, 0x4f, 0xbc, 0x8f, 0x5e, 0x31, 0x32, + 0x1f, 0xf7, 0x51, 0xa9, 0xa4, 0x7f, 0x1e, 0x0e, 0xb3, 0x6a, 0xee, 0x3b, 0xf4, 0x91, 0x1e, 0xaa, 0x5c, 0x50, 0xf6, + 0xc6, 0x98, 0x44, 0xa0, 0x34, 0xc6, 0xfb, 0x38, 0x38, 0xce, 0xfb, 0x34, 0x80, 0xd4, 0x3e, 0xf1, 0x9e, 0x94, 0x1c, + 0x9e, 0x73, 0xcc, 0x09, 0xa5, 0x15, 0x01, 0x13, 0x7a, 0x86, 0x72, 0xdd, 0x29, 0x05, 0x93, 0x1c, 0x12, 0x0c, 0x7f, + 0xd5, 0xbc, 0x89, 0x15, 0x08, 0xbb, 0x66, 0x5e, 0x8d, 0x1e, 0x55, 0x49, 0x58, 0x0a, 0x38, 0x2a, 0x33, 0xcf, 0xb0, + 0x37, 0x3c, 0x32, 0x8c, 0x1c, 0x2c, 0xf7, 0x47, 0x75, 0x22, 0x72, 0x8f, 0x2e, 0x30, 0x2a, 0x0b, 0xcf, 0x1b, 0xba, + 0xd2, 0xa0, 0x92, 0xec, 0xf8, 0x2b, 0xae, 0x01, 0xb5, 0x35, 0x46, 0x0c, 0x05, 0x8c, 0x82, 0xd7, 0xf6, 0x87, 0x90, + 0x45, 0xd9, 0xfa, 0x0d, 0x8e, 0xf9, 0xac, 0xe4, 0xae, 0x77, 0x38, 0x0b, 0x2d, 0x21, 0x4f, 0xee, 0x18, 0xa4, 0x69, + 0x2c, 0x8d, 0x80, 0x13, 0x91, 0x6c, 0x63, 0x29, 0x1c, 0x01, 0x04, 0x04, 0xba, 0x29, 0x33, 0x8c, 0xe9, 0x60, 0xe4, + 0x79, 0xd4, 0x33, 0xde, 0xab, 0xf0, 0x14, 0xd2, 0x64, 0xfb, 0x7a, 0xfe, 0xde, 0x08, 0xb2, 0x72, 0xcb, 0x39, 0x1e, + 0x16, 0xdf, 0x38, 0xfb, 0x2a, 0x27, 0x4f, 0x31, 0xcb, 0x48, 0xef, 0x14, 0xf3, 0x02, 0xfe, 0x54, 0x96, 0xfa, 0x1c, + 0xa5, 0xb7, 0xcc, 0x27, 0xab, 0x48, 0xba, 0xf0, 0x36, 0xfd, 0x7e, 0x3c, 0x52, 0x87, 0x9a, 0xbf, 0x8f, 0x47, 0xf2, + 0x0c, 0xdb, 0xb0, 0x84, 0x85, 0x56, 0xc1, 0x18, 0x40, 0x12, 0x1b, 0x11, 0x0d, 0x46, 0x7b, 0x73, 0x38, 0x9c, 0x6f, + 0xcc, 0x59, 0xb2, 0x07, 0xd7, 0x57, 0x9e, 0x98, 0x77, 0xe0, 0xcb, 0x3c, 0x26, 0x88, 0xd8, 0xcc, 0xdb, 0xb0, 0x1a, + 0x3c, 0xd8, 0xc1, 0xf5, 0x11, 0x5b, 0x14, 0x6b, 0x1d, 0x4b, 0x65, 0x1d, 0x9c, 0xd6, 0xb1, 0x69, 0x46, 0x4a, 0x91, + 0x7d, 0x8e, 0xfd, 0xbd, 0x1b, 0x5c, 0x5d, 0x1b, 0x83, 0x5a, 0xe3, 0x0e, 0x73, 0xe7, 0x54, 0x40, 0x3d, 0xa6, 0x2b, + 0xa8, 0x9e, 0x55, 0xe4, 0xcb, 0x6f, 0xed, 0x1c, 0x10, 0x34, 0x02, 0x81, 0x8b, 0x06, 0x4a, 0xa6, 0x4b, 0x39, 0xef, + 0x02, 0x42, 0x7c, 0x97, 0x82, 0x3e, 0x9d, 0xc1, 0x26, 0x36, 0x9f, 0x40, 0x2c, 0x9a, 0xee, 0x73, 0xad, 0x99, 0x2f, + 0x46, 0xb4, 0x33, 0xeb, 0x6e, 0x91, 0x5b, 0x2d, 0x44, 0x32, 0x7a, 0xb6, 0x99, 0x70, 0xd7, 0xa1, 0x9c, 0x91, 0x80, + 0x09, 0x5a, 0x5b, 0x29, 0xf9, 0x5c, 0xf7, 0x3a, 0x41, 0x7b, 0x20, 0x69, 0xdd, 0xbf, 0x59, 0x74, 0x46, 0xc9, 0xc9, + 0xf5, 0x26, 0x67, 0x90, 0x82, 0x05, 0xdb, 0xcb, 0x9c, 0x70, 0x03, 0x7c, 0x64, 0xb3, 0xe4, 0x34, 0x0d, 0xf2, 0x58, + 0x18, 0xa4, 0x8f, 0x36, 0xbf, 0x2c, 0xa0, 0x43, 0xc9, 0xa2, 0x11, 0xe2, 0x01, 0x76, 0x0e, 0xc9, 0x55, 0x81, 0xba, + 0x69, 0xa0, 0x2b, 0x57, 0xce, 0x14, 0x53, 0xe0, 0x42, 0x28, 0x88, 0xda, 0xd1, 0x49, 0x54, 0xce, 0xfb, 0xa4, 0xba, + 0xcc, 0xa7, 0x85, 0x34, 0x0d, 0xe4, 0xd3, 0xca, 0x31, 0x0f, 0x6c, 0x6d, 0xe3, 0x9a, 0xc0, 0x40, 0xa7, 0xf6, 0xb5, + 0x28, 0xe7, 0x58, 0x45, 0xf4, 0x3e, 0x7f, 0x54, 0xd9, 0xd3, 0x07, 0x11, 0x36, 0x2a, 0xd0, 0x58, 0x4a, 0x8c, 0x8d, + 0x1c, 0xff, 0x96, 0x28, 0x1b, 0x32, 0x04, 0x84, 0x90, 0x36, 0x72, 0xfa, 0x61, 0x7d, 0xf9, 0x2e, 0xd3, 0xfe, 0x9f, + 0x24, 0x7e, 0x1b, 0xec, 0xe5, 0xd4, 0x9f, 0x7a, 0xc4, 0xe3, 0xb5, 0x46, 0x8f, 0x29, 0xe9, 0x36, 0xc8, 0x53, 0xe5, + 0x29, 0x48, 0x26, 0x8c, 0x05, 0x04, 0x8b, 0x72, 0xc1, 0x73, 0x5e, 0x71, 0x09, 0xf7, 0x51, 0xcb, 0x8a, 0x08, 0x55, + 0x89, 0x9c, 0x3e, 0x5f, 0x01, 0xcf, 0x04, 0x04, 0x3a, 0xc6, 0x48, 0xa3, 0x0a, 0xbe, 0x04, 0xc6, 0x3a, 0x50, 0x76, + 0x9a, 0x91, 0xe0, 0xb2, 0x7b, 0x8d, 0x44, 0xa9, 0xaf, 0x48, 0x49, 0xfa, 0x56, 0xd4, 0x78, 0x25, 0x56, 0x11, 0x09, + 0x64, 0xa8, 0x21, 0x62, 0x55, 0x3d, 0x75, 0xaf, 0x8a, 0xc9, 0x60, 0x50, 0xf9, 0x72, 0x7a, 0xe2, 0x0d, 0x0d, 0x95, + 0x77, 0x5d, 0xd1, 0x4e, 0xcf, 0xb5, 0x52, 0xde, 0x42, 0x5a, 0x82, 0xa6, 0x61, 0xa4, 0x39, 0x94, 0xba, 0x92, 0xee, + 0xc6, 0x20, 0xbe, 0x64, 0xa2, 0x67, 0x3b, 0xb5, 0xa3, 0xb4, 0x25, 0xed, 0x21, 0xa4, 0xe7, 0x2e, 0xf9, 0x98, 0x85, + 0x5c, 0xdd, 0x29, 0x27, 0xe5, 0x55, 0x88, 0x4e, 0xee, 0x7b, 0x0c, 0x89, 0x40, 0x9f, 0x73, 0x0c, 0xeb, 0xa2, 0xa1, + 0xce, 0x61, 0x85, 0x98, 0x2d, 0x94, 0x30, 0x5f, 0x32, 0x9e, 0x4a, 0x06, 0x0d, 0x80, 0x0c, 0xf8, 0xe2, 0x65, 0x60, + 0xf9, 0x2b, 0x88, 0x1f, 0x6d, 0x7c, 0x38, 0xfc, 0x55, 0x53, 0x88, 0xed, 0x5f, 0xb0, 0x19, 0xc2, 0xa3, 0x7a, 0xc0, + 0x33, 0xdf, 0xc4, 0x09, 0x5a, 0x01, 0x49, 0x99, 0x1d, 0x4d, 0x64, 0xaf, 0x7a, 0x08, 0xa7, 0xb2, 0x02, 0x75, 0x94, + 0x75, 0x56, 0xc2, 0x8f, 0x30, 0xd5, 0xad, 0xc4, 0x5a, 0xa0, 0xcd, 0xd5, 0x8a, 0xb5, 0x00, 0x0e, 0xfc, 0x1c, 0x82, + 0x27, 0xf2, 0x39, 0xb8, 0x18, 0x14, 0xe0, 0x73, 0x00, 0xbc, 0xc8, 0x5d, 0x78, 0x30, 0x0f, 0x2c, 0xab, 0x11, 0x86, + 0xa3, 0x8a, 0x58, 0xbf, 0x66, 0x3b, 0xf2, 0x81, 0xdb, 0x31, 0x3e, 0xd7, 0x1e, 0x4b, 0x96, 0x83, 0x51, 0xe6, 0x5e, + 0x2d, 0xd1, 0xf3, 0x26, 0x8d, 0x9b, 0xd1, 0xa3, 0x7d, 0x2d, 0xff, 0x17, 0xf4, 0x32, 0xe8, 0x6f, 0xe1, 0x96, 0xd7, + 0xfc, 0x61, 0xb9, 0x70, 0x9a, 0x5e, 0x41, 0xa4, 0x8c, 0x1a, 0x91, 0x31, 0x84, 0x4d, 0xaa, 0x9b, 0xdb, 0xa4, 0xba, + 0x10, 0xf0, 0x74, 0x44, 0xaa, 0x6b, 0x21, 0x6d, 0xe4, 0xd3, 0x3a, 0x90, 0xb1, 0x48, 0xef, 0x7e, 0xfc, 0xdb, 0xf3, + 0x4f, 0x6f, 0x7e, 0xfb, 0xf1, 0xe6, 0xcd, 0xbb, 0xd7, 0x6f, 0xde, 0xbd, 0xf9, 0xf4, 0x3b, 0x41, 0x78, 0x4c, 0x85, + 0xca, 0xf0, 0xe1, 0xfd, 0xf5, 0x1b, 0x27, 0x83, 0xed, 0xcd, 0x90, 0xb5, 0x6f, 0xe4, 0x60, 0x08, 0x44, 0x36, 0x08, + 0x19, 0x64, 0xa7, 0x64, 0x8e, 0x99, 0x98, 0x63, 0xec, 0x9d, 0xc0, 0x64, 0x0b, 0x92, 0xc3, 0x32, 0x2f, 0x19, 0x91, + 0xab, 0x42, 0xeb, 0x07, 0xb4, 0xe0, 0x2d, 0xb8, 0xc8, 0xa4, 0xf9, 0xf2, 0x37, 0x82, 0xd8, 0xa7, 0x95, 0x94, 0xfb, + 0x6a, 0x5b, 0xf3, 0x7c, 0x7b, 0xbf, 0x97, 0x70, 0xfe, 0x73, 0x69, 0x44, 0x2d, 0xc0, 0x01, 0xf8, 0x1c, 0xfe, 0xb8, + 0xd2, 0x96, 0x34, 0x99, 0x45, 0xfb, 0x19, 0x43, 0xd0, 0xa5, 0x81, 0x34, 0xb1, 0x47, 0x5e, 0xea, 0x93, 0x85, 0x04, + 0xee, 0x88, 0xe1, 0xd3, 0x8a, 0xa0, 0x57, 0x8c, 0x28, 0x2e, 0xb9, 0x42, 0xa5, 0x94, 0xfc, 0x1b, 0x65, 0x17, 0x15, + 0x72, 0x56, 0xb0, 0x3b, 0x45, 0x8e, 0x8c, 0x1f, 0x04, 0x13, 0x5f, 0x0e, 0xee, 0xbf, 0xc4, 0x3b, 0x9c, 0x29, 0x8e, + 0xe4, 0x84, 0xff, 0x99, 0x61, 0x60, 0x7f, 0x0e, 0x3e, 0xaf, 0x0e, 0xf3, 0xf2, 0x46, 0x9f, 0x72, 0x0b, 0x3e, 0x9e, + 0x2c, 0xae, 0xc0, 0x60, 0xbf, 0x50, 0xcd, 0x5d, 0xf3, 0x7a, 0xb6, 0x98, 0xb3, 0xfd, 0x2c, 0x9a, 0x07, 0x4b, 0x36, + 0xcb, 0xe6, 0xc1, 0xaa, 0xe1, 0x6b, 0x76, 0xcb, 0xd7, 0x56, 0xd5, 0xd6, 0x76, 0xd5, 0x26, 0x1b, 0x7e, 0x0b, 0x12, + 0xc2, 0xdb, 0xcc, 0x03, 0xde, 0xe3, 0xa5, 0xcf, 0x36, 0x20, 0xd1, 0xae, 0xd8, 0x06, 0x2e, 0x62, 0x6b, 0xfe, 0xa6, + 0xf2, 0x36, 0xac, 0x64, 0xe7, 0x63, 0x96, 0xe3, 0xfc, 0xf3, 0xe1, 0x01, 0xed, 0x85, 0xfa, 0xd9, 0xa5, 0x7a, 0x36, + 0x51, 0x76, 0xb3, 0xcd, 0xe8, 0xe6, 0x2e, 0xad, 0x36, 0x61, 0x86, 0x9e, 0xe5, 0xf0, 0xd1, 0x56, 0x0a, 0x7e, 0xfa, + 0x06, 0xbf, 0x64, 0x4d, 0x9c, 0x7f, 0xa6, 0x6d, 0xbb, 0x2a, 0xb1, 0x15, 0xb4, 0x28, 0xb2, 0x5a, 0xe1, 0x81, 0x39, + 0x7f, 0x06, 0x0b, 0x18, 0x7b, 0x8e, 0x73, 0x5e, 0xfb, 0x23, 0x64, 0xbc, 0x77, 0x00, 0xd0, 0x32, 0xc7, 0x01, 0x1e, + 0xb1, 0x62, 0x14, 0x0d, 0xde, 0xf9, 0xa5, 0xb2, 0x5a, 0x69, 0x4e, 0x42, 0xdb, 0x88, 0x55, 0xcb, 0x91, 0xaa, 0x19, + 0x91, 0x3e, 0x48, 0xcf, 0xfb, 0x1e, 0x51, 0x0d, 0xf6, 0x64, 0x5e, 0x07, 0xf6, 0xe9, 0x7d, 0x6b, 0x55, 0x77, 0x7e, + 0x4f, 0x95, 0x2e, 0x39, 0xb2, 0xe5, 0xa7, 0xcb, 0xf0, 0x5e, 0xfd, 0x29, 0xb9, 0x3e, 0x14, 0x38, 0xc2, 0x43, 0x15, + 0x70, 0xbe, 0x5e, 0x89, 0x76, 0x27, 0xc2, 0xae, 0x5c, 0x02, 0x42, 0x7c, 0x49, 0xd3, 0x1c, 0x8f, 0x23, 0x9a, 0x88, + 0xb0, 0x89, 0xd1, 0x5f, 0xd8, 0x7d, 0x28, 0xb1, 0x9c, 0xe7, 0x1a, 0x94, 0x5c, 0x32, 0x78, 0x4f, 0xda, 0x6b, 0xd0, + 0x2c, 0xaf, 0x4a, 0x4d, 0x26, 0x72, 0x50, 0x3e, 0x1c, 0x0a, 0xd8, 0x4b, 0x8d, 0x9f, 0x26, 0xfc, 0x84, 0xe5, 0xad, + 0xbd, 0x35, 0xa5, 0xa8, 0xa4, 0x01, 0x2a, 0xf0, 0x31, 0x83, 0xff, 0xdd, 0x19, 0x62, 0xc1, 0x14, 0x1d, 0x3f, 0x9c, + 0x89, 0xb9, 0xf5, 0xdc, 0x2a, 0xeb, 0x28, 0x5b, 0xa3, 0x9c, 0x80, 0x7f, 0x4f, 0x75, 0x9c, 0x24, 0xc2, 0xa9, 0xf7, + 0x88, 0x8b, 0xba, 0x97, 0x43, 0xd4, 0x0d, 0xfb, 0x54, 0xe9, 0x60, 0xcb, 0x69, 0x1a, 0x1c, 0x89, 0x5f, 0xa9, 0xcf, + 0x3e, 0x64, 0x16, 0x8f, 0x3a, 0xb2, 0x11, 0x25, 0x69, 0x1c, 0x8b, 0x1c, 0xb6, 0xf7, 0x1b, 0xb9, 0xff, 0xf7, 0xfb, + 0x10, 0x4e, 0x5a, 0x05, 0x71, 0xe9, 0x09, 0x44, 0x84, 0xa3, 0xc3, 0x8f, 0x08, 0x4f, 0xa4, 0xaa, 0xf0, 0x51, 0x7d, + 0xe2, 0xc6, 0xec, 0x5e, 0x98, 0xa3, 0x7a, 0x0b, 0x30, 0x8c, 0xf5, 0xd6, 0x22, 0x24, 0xd1, 0x4a, 0x33, 0xda, 0x7a, + 0x40, 0x8c, 0x78, 0xbf, 0xb6, 0xc8, 0x60, 0xac, 0x2d, 0x89, 0x04, 0xf0, 0x25, 0x09, 0x19, 0xda, 0x36, 0x02, 0x33, + 0x86, 0xb7, 0xb3, 0xe2, 0xd2, 0x75, 0xd8, 0xe6, 0x1c, 0xbe, 0x90, 0x1b, 0xcd, 0x3a, 0xa2, 0x34, 0x41, 0xc8, 0x3f, + 0xe0, 0x64, 0xa1, 0x30, 0x9a, 0x57, 0x47, 0xe9, 0x24, 0xb1, 0xbe, 0xef, 0x2a, 0x15, 0x6c, 0x36, 0xd7, 0xa8, 0x2f, + 0x3b, 0x4a, 0x7e, 0x09, 0x4e, 0x3a, 0x4e, 0xb2, 0xc8, 0x41, 0xd4, 0xa2, 0x72, 0xae, 0x93, 0xb0, 0xb4, 0xab, 0x53, + 0x6d, 0xd6, 0xeb, 0xa2, 0xac, 0xab, 0x57, 0x22, 0x52, 0xf4, 0x3e, 0xea, 0xd1, 0x23, 0x09, 0xa9, 0xd0, 0xaa, 0xd4, + 0x2e, 0x8f, 0xc0, 0x6d, 0x53, 0x2b, 0xb6, 0xe5, 0x12, 0x96, 0xa8, 0xf1, 0x9f, 0xa0, 0x8f, 0x72, 0x71, 0x2f, 0x03, + 0x34, 0x3a, 0x9e, 0x9a, 0xb7, 0x1e, 0x78, 0xe5, 0x28, 0xbf, 0xb4, 0xda, 0xa4, 0x5f, 0x01, 0x99, 0xd1, 0xfe, 0xd1, + 0x52, 0x02, 0x99, 0x81, 0x99, 0xb4, 0x34, 0x24, 0x72, 0x14, 0xb3, 0x34, 0xff, 0x13, 0x57, 0x6c, 0x85, 0x48, 0xc3, + 0x6a, 0xee, 0xf1, 0x1f, 0x2b, 0xaf, 0x96, 0x6b, 0x99, 0x69, 0x6e, 0x96, 0x38, 0x56, 0x2c, 0x2e, 0xea, 0x75, 0x25, + 0xb2, 0x40, 0x88, 0x23, 0x4c, 0x63, 0x3d, 0xf5, 0x46, 0x69, 0xf5, 0x01, 0x09, 0x65, 0x7e, 0xc4, 0xde, 0x8e, 0xbd, + 0x1e, 0x64, 0x21, 0x8e, 0x2d, 0x07, 0x9b, 0xad, 0xf7, 0xa9, 0x4c, 0x45, 0x7c, 0x56, 0x17, 0x67, 0x9b, 0x4a, 0x9c, + 0xd5, 0x89, 0x38, 0xfb, 0x01, 0x72, 0xfe, 0x70, 0x46, 0x45, 0x9f, 0xdd, 0xa7, 0x75, 0x52, 0x6c, 0x6a, 0x7a, 0xf2, + 0x1a, 0xcb, 0xf8, 0xe1, 0x8c, 0xb8, 0x6a, 0xce, 0x68, 0x24, 0xe3, 0xd1, 0xd9, 0x87, 0x0c, 0x48, 0x5e, 0xcf, 0xd2, + 0x15, 0x0c, 0xde, 0x59, 0x98, 0xc7, 0x67, 0xa5, 0x58, 0x82, 0xc5, 0xa9, 0xec, 0x7c, 0x0f, 0x32, 0xac, 0xc2, 0x3f, + 0xc5, 0x19, 0x40, 0xbb, 0x9e, 0xa5, 0xf5, 0x59, 0x5a, 0x9d, 0xe5, 0x45, 0x7d, 0xa6, 0xa4, 0x70, 0x08, 0xe3, 0x87, + 0xf7, 0xf4, 0x95, 0x5d, 0xde, 0x66, 0x71, 0x97, 0x45, 0xfe, 0x14, 0xbd, 0x8a, 0x88, 0x49, 0xa3, 0x12, 0x5e, 0xbb, + 0xbf, 0x6d, 0xee, 0x1f, 0x5e, 0x37, 0x76, 0x3f, 0xbb, 0x63, 0x44, 0x17, 0xd4, 0xe3, 0x95, 0xa4, 0x54, 0x50, 0x40, + 0xe0, 0x44, 0xb3, 0xc6, 0x83, 0x3b, 0x0e, 0x78, 0x35, 0xb0, 0x05, 0x5b, 0xfb, 0xfc, 0x59, 0x2c, 0xc3, 0xb4, 0x37, + 0x01, 0xfe, 0x55, 0xf6, 0xa6, 0xeb, 0x60, 0x81, 0xf7, 0x2d, 0x64, 0x1b, 0x7a, 0xf3, 0x8a, 0x3f, 0xf7, 0x72, 0xf5, + 0x37, 0xfb, 0x27, 0x00, 0x61, 0x40, 0xcc, 0xaa, 0x8f, 0x26, 0xee, 0x9d, 0x95, 0x65, 0xe7, 0x64, 0xd9, 0xf5, 0xd0, + 0xaf, 0x49, 0x8c, 0x4a, 0x2b, 0x4b, 0xe9, 0x64, 0x29, 0x21, 0x0b, 0xf8, 0xc4, 0x68, 0x6a, 0x23, 0x80, 0xb0, 0x1d, + 0xa5, 0xf2, 0x85, 0xca, 0x8b, 0x28, 0x9c, 0x13, 0x3c, 0x4f, 0xc4, 0xe8, 0xce, 0x4a, 0x06, 0x0c, 0x87, 0x10, 0xcc, + 0x41, 0x5b, 0xec, 0x0d, 0xdd, 0x44, 0xfc, 0xf5, 0xba, 0x28, 0xdf, 0xc4, 0xe4, 0x53, 0xb0, 0x3b, 0xf9, 0xb8, 0x84, + 0xc7, 0xe5, 0xc9, 0xc7, 0x21, 0x7a, 0x24, 0x9c, 0x7c, 0x0c, 0xbe, 0x47, 0x72, 0x5e, 0x77, 0x3d, 0x4e, 0x90, 0x5b, + 0x48, 0xf7, 0xb7, 0x63, 0x12, 0xa0, 0x79, 0x0d, 0xcb, 0x51, 0x53, 0x71, 0xcd, 0xcc, 0x18, 0xcf, 0x1b, 0xbd, 0x3f, + 0x76, 0xbc, 0x65, 0x0a, 0xc5, 0x2c, 0xe6, 0x35, 0xfc, 0x9e, 0x55, 0x81, 0xba, 0xeb, 0x6d, 0x92, 0x5b, 0x66, 0xf5, + 0x1c, 0xed, 0xbe, 0xef, 0xeb, 0x44, 0x50, 0xfb, 0x3b, 0xec, 0x79, 0x66, 0xbd, 0xab, 0x62, 0xe0, 0x52, 0x25, 0x3b, + 0x64, 0xaa, 0x9a, 0x1e, 0xa8, 0x94, 0x06, 0x4f, 0x2f, 0xad, 0xcb, 0x97, 0x4a, 0x1b, 0x79, 0xa6, 0xf9, 0x0d, 0xe0, + 0xc5, 0xd4, 0x65, 0xb1, 0xfb, 0xe6, 0xbe, 0x82, 0xdb, 0x78, 0xbf, 0xbf, 0xae, 0x3c, 0xf3, 0x13, 0x17, 0x80, 0xbd, + 0xa9, 0xd0, 0x3a, 0x81, 0x52, 0xc3, 0x3a, 0x7c, 0x99, 0x88, 0xe8, 0xcf, 0x76, 0xb9, 0xce, 0x5c, 0x07, 0x8c, 0x28, + 0xe2, 0xb7, 0xf1, 0xe8, 0x0f, 0x50, 0x5c, 0x1b, 0x7b, 0x40, 0x58, 0x87, 0x84, 0x3e, 0x23, 0x00, 0xa9, 0x47, 0x1f, + 0x25, 0xf7, 0xa0, 0x59, 0xd1, 0xdc, 0x31, 0xf9, 0xb9, 0xbe, 0x52, 0xfa, 0xfb, 0x75, 0xe5, 0x91, 0x39, 0xa5, 0x6d, + 0xa6, 0xb1, 0x5a, 0x53, 0x09, 0x84, 0x57, 0x54, 0xb2, 0x0a, 0x9f, 0xcd, 0x1b, 0xd1, 0xef, 0xcb, 0x23, 0x3c, 0xad, + 0x7e, 0xdc, 0x62, 0x7c, 0x2b, 0x20, 0x1a, 0x09, 0x50, 0xb0, 0x02, 0xcc, 0x8b, 0x6c, 0x66, 0xf7, 0x71, 0x40, 0x95, + 0x12, 0x4d, 0xe3, 0x6c, 0x9e, 0xdf, 0xd3, 0x9b, 0xb2, 0x83, 0x4e, 0x9d, 0x2a, 0x70, 0xc1, 0x55, 0xc9, 0x78, 0x65, + 0x3d, 0x91, 0xcf, 0x6f, 0x6e, 0x37, 0x69, 0x16, 0xbf, 0x2f, 0x7f, 0xc5, 0xb1, 0xd5, 0x75, 0x78, 0x60, 0xea, 0x74, + 0xed, 0x3c, 0xd2, 0xda, 0x0b, 0x01, 0x11, 0xed, 0x1a, 0x6a, 0xbd, 0xb0, 0xd0, 0x23, 0x3d, 0x11, 0xce, 0x49, 0xa2, + 0xa6, 0x1d, 0x68, 0x69, 0x84, 0xbe, 0xbe, 0xe6, 0xf4, 0x17, 0x06, 0x6b, 0x9f, 0x8f, 0x19, 0x90, 0x95, 0xe8, 0xc7, + 0xea, 0xa1, 0xb1, 0x99, 0x43, 0xcf, 0x5a, 0x95, 0x67, 0x5e, 0x75, 0x38, 0x20, 0x3e, 0x8c, 0xfe, 0x92, 0xdf, 0xef, + 0xbf, 0xa2, 0xf9, 0xc7, 0x84, 0x1a, 0x3f, 0xdb, 0x0c, 0xd0, 0xb5, 0xef, 0xca, 0x03, 0x51, 0xcf, 0xb5, 0x4a, 0x10, + 0xe2, 0x0d, 0x62, 0xa2, 0x19, 0x31, 0x07, 0xa7, 0x1d, 0x6a, 0xfe, 0x49, 0x6a, 0x40, 0x88, 0x12, 0xaf, 0x63, 0xca, + 0x82, 0x9c, 0x36, 0x71, 0xa4, 0x1f, 0x85, 0x13, 0xf9, 0x51, 0x54, 0x45, 0x76, 0x07, 0x17, 0x0c, 0xa6, 0xde, 0xd3, + 0x7e, 0x89, 0x7e, 0x4b, 0x38, 0x72, 0x8e, 0x56, 0x85, 0x20, 0x72, 0x42, 0x58, 0x6b, 0x08, 0x13, 0xc4, 0x06, 0xf1, + 0xb2, 0xef, 0x92, 0x0c, 0x47, 0x0a, 0x2e, 0xeb, 0xd8, 0x31, 0xe6, 0xea, 0xa8, 0x7a, 0x0d, 0x60, 0xbc, 0x72, 0x04, + 0xcd, 0x46, 0x91, 0x5d, 0x42, 0x54, 0x91, 0xe3, 0x09, 0xa8, 0x1d, 0x94, 0xc6, 0x66, 0x7a, 0x3e, 0x0e, 0xf2, 0xd1, + 0x4d, 0x85, 0x3a, 0x27, 0x96, 0xf1, 0x1a, 0x80, 0xb5, 0x73, 0xd5, 0xcf, 0xb3, 0x1a, 0x3c, 0x69, 0x88, 0xcf, 0xc7, + 0x68, 0x7b, 0x65, 0x73, 0x50, 0x6d, 0xa7, 0xb3, 0xf2, 0x8a, 0xe9, 0x72, 0x60, 0xdc, 0x37, 0xbc, 0xa2, 0x38, 0xc3, + 0x8f, 0x1e, 0x6c, 0x71, 0xfe, 0x74, 0x43, 0xed, 0xc7, 0xdc, 0xa8, 0x87, 0x81, 0xd6, 0x82, 0x37, 0x05, 0xb1, 0xfe, + 0x7e, 0xe8, 0xc8, 0xf6, 0x5e, 0x8b, 0x8c, 0x26, 0x9f, 0xfd, 0xfc, 0x43, 0x99, 0xae, 0x52, 0xb8, 0x2f, 0x39, 0x59, + 0x34, 0xf3, 0x10, 0xd8, 0x1b, 0x62, 0xb8, 0x3e, 0x2a, 0x3c, 0xa2, 0xac, 0xdf, 0x87, 0xdf, 0x57, 0x19, 0x98, 0x62, + 0xe0, 0xba, 0x42, 0x30, 0x1e, 0x02, 0x41, 0x3c, 0x4c, 0xa3, 0x93, 0x41, 0x0d, 0xda, 0xf0, 0x0d, 0x40, 0x66, 0x80, + 0x47, 0xe6, 0xc2, 0x23, 0xe0, 0x2e, 0x70, 0xed, 0xc9, 0x78, 0xec, 0x4f, 0x4c, 0x43, 0xa3, 0xa6, 0x34, 0xd3, 0x73, + 0xe3, 0x37, 0x1d, 0xd5, 0x72, 0xed, 0xfc, 0xc7, 0x97, 0xfc, 0x06, 0xbd, 0xa0, 0xe5, 0xe5, 0x3e, 0x52, 0x97, 0xfb, + 0x8c, 0xe2, 0x32, 0x91, 0x1c, 0x16, 0xc4, 0xb2, 0x84, 0x03, 0x8f, 0x51, 0xc9, 0x62, 0x4b, 0x8f, 0x55, 0xd1, 0xf2, + 0x45, 0xb9, 0x41, 0x3a, 0x74, 0x42, 0xb0, 0x44, 0x05, 0xc1, 0x12, 0x18, 0x17, 0xb1, 0xe6, 0x9b, 0x41, 0xce, 0xe2, + 0xd9, 0x66, 0xce, 0x91, 0xb0, 0x2e, 0x39, 0x1c, 0x0a, 0x09, 0x36, 0x93, 0xcd, 0xd6, 0x73, 0xb6, 0xf6, 0x19, 0x28, + 0x01, 0x4a, 0x99, 0x26, 0x28, 0x4d, 0x2b, 0xb6, 0xe2, 0xa6, 0x35, 0x58, 0xad, 0xa6, 0x6c, 0x55, 0x53, 0x76, 0x4e, + 0x53, 0x8e, 0x2a, 0x28, 0x39, 0xa1, 0x14, 0x65, 0x18, 0xc0, 0x88, 0x4d, 0xa2, 0xab, 0x0c, 0x7d, 0xbc, 0x13, 0x1e, + 0x41, 0x15, 0x11, 0xf9, 0x84, 0x21, 0x04, 0x26, 0xa2, 0xb8, 0x50, 0x85, 0x62, 0x80, 0x8c, 0x48, 0x20, 0x98, 0xa8, + 0xd4, 0x29, 0x30, 0x1f, 0x4d, 0x15, 0xc3, 0xa6, 0x3d, 0x51, 0xbe, 0xa7, 0x8e, 0x7b, 0x94, 0x6d, 0x7e, 0x16, 0xbb, + 0x20, 0x44, 0xee, 0xc6, 0x9d, 0xfa, 0x19, 0xf1, 0xde, 0xee, 0x08, 0xe3, 0x27, 0x3b, 0x6e, 0x11, 0xae, 0x08, 0xb6, + 0x50, 0x73, 0x88, 0xc5, 0xbc, 0x9a, 0x24, 0xa8, 0x65, 0x49, 0xfc, 0x0d, 0x4f, 0x06, 0x39, 0x5b, 0x80, 0x07, 0xed, + 0x9c, 0x65, 0x80, 0xbf, 0x62, 0xb5, 0xe8, 0xf7, 0xda, 0x5b, 0x80, 0xfc, 0xb4, 0xb1, 0x1b, 0x85, 0x89, 0x11, 0x24, + 0xea, 0x76, 0x65, 0x20, 0x3f, 0x7c, 0xc0, 0xe9, 0x78, 0xec, 0x29, 0x63, 0x6e, 0x65, 0x7a, 0x99, 0xce, 0x95, 0x7c, + 0x23, 0xf7, 0xd2, 0x87, 0x5e, 0x82, 0x9d, 0x03, 0xde, 0x40, 0xda, 0xc0, 0x6b, 0xd8, 0x2e, 0xbc, 0x36, 0x48, 0x98, + 0x11, 0x60, 0x8b, 0xe3, 0x63, 0xa4, 0x04, 0x86, 0x70, 0x9c, 0xa5, 0x00, 0x4c, 0xa3, 0x2f, 0xb3, 0x95, 0x7d, 0x99, + 0xd5, 0x9a, 0x2d, 0x95, 0xd3, 0xbd, 0x73, 0xeb, 0x76, 0x3e, 0x97, 0x00, 0x60, 0x52, 0xe7, 0x40, 0x9c, 0x99, 0x60, + 0x97, 0x26, 0x91, 0xe5, 0x63, 0x98, 0x2f, 0xc5, 0xeb, 0xb2, 0x58, 0xa9, 0xae, 0x68, 0xfb, 0xcc, 0xe4, 0x33, 0xd2, + 0x49, 0xa8, 0x80, 0x82, 0x42, 0xae, 0xf5, 0xe9, 0xbb, 0xf0, 0x5d, 0x50, 0x68, 0x60, 0xb6, 0x0a, 0xf7, 0x34, 0x59, + 0x23, 0xf5, 0x46, 0xd5, 0xef, 0x93, 0x6b, 0x20, 0xd5, 0x99, 0x43, 0xcb, 0x9e, 0x57, 0x18, 0x20, 0x76, 0xd4, 0x67, + 0x24, 0xd4, 0x81, 0xd4, 0x03, 0x86, 0x10, 0x6d, 0xd3, 0xc7, 0x9f, 0x0c, 0x89, 0x2e, 0xc0, 0x16, 0xa2, 0x0d, 0xfc, + 0xf8, 0x13, 0xec, 0xb3, 0x20, 0x3c, 0xa6, 0xf9, 0x5b, 0x48, 0x3a, 0x36, 0x70, 0x5a, 0x7d, 0x0a, 0x3e, 0x48, 0x72, + 0x30, 0x51, 0x07, 0x2f, 0xf7, 0x97, 0x7e, 0x1f, 0xb6, 0xec, 0x5c, 0x4a, 0x75, 0xac, 0xd4, 0xdb, 0xb6, 0xf6, 0x83, + 0x68, 0x0b, 0x8e, 0x10, 0xac, 0x9d, 0x21, 0x22, 0x98, 0x19, 0x44, 0xd8, 0xb5, 0x50, 0x77, 0x7b, 0x4a, 0x2d, 0x8b, + 0x7a, 0xdb, 0x53, 0x4a, 0xdd, 0x86, 0xe1, 0xbb, 0x09, 0x66, 0x8a, 0x1b, 0x7e, 0x9d, 0x79, 0xa1, 0xde, 0x78, 0x2c, + 0x9e, 0x76, 0xcf, 0xdf, 0x2f, 0x78, 0x35, 0xdb, 0x28, 0x13, 0xe6, 0x92, 0x2f, 0x66, 0xa1, 0xec, 0x6a, 0x69, 0xdc, + 0xf9, 0xe2, 0x2d, 0xd4, 0x7c, 0xf0, 0x0f, 0x87, 0x04, 0xe2, 0x8d, 0xe2, 0xab, 0x65, 0x23, 0xb7, 0xae, 0xc9, 0xe6, + 0xaa, 0x04, 0xd4, 0xef, 0xf3, 0x35, 0xee, 0xb7, 0x58, 0xff, 0xee, 0x69, 0x90, 0xb1, 0x9a, 0xe1, 0x8a, 0x29, 0x7c, + 0x0a, 0x00, 0x83, 0xc3, 0xa9, 0x20, 0x2d, 0xf0, 0x86, 0x97, 0xc3, 0xcb, 0xc9, 0x86, 0x4c, 0xba, 0x1b, 0x1f, 0xb9, + 0xb3, 0x40, 0xd5, 0xfb, 0x1d, 0xc5, 0x49, 0x83, 0x44, 0x63, 0xaf, 0xc1, 0xe7, 0x59, 0x46, 0xb9, 0x68, 0xe2, 0x3e, + 0x24, 0x5f, 0xe9, 0x01, 0xcc, 0x55, 0x28, 0x01, 0xa2, 0xdf, 0x58, 0x16, 0x1b, 0xd1, 0xb6, 0xd8, 0xc0, 0x52, 0xaa, + 0xe6, 0x7a, 0x35, 0x7d, 0xf1, 0x4a, 0x34, 0xef, 0xa3, 0x19, 0xa7, 0x34, 0x1a, 0x70, 0x9c, 0x46, 0xe1, 0xf6, 0xfd, + 0x9d, 0x28, 0x17, 0x19, 0x58, 0xb2, 0x55, 0x38, 0xc5, 0x65, 0xa3, 0xce, 0x88, 0xe7, 0x79, 0xac, 0x00, 0x3a, 0x1e, + 0x12, 0x00, 0xd5, 0x05, 0x01, 0x15, 0xd1, 0x52, 0x7a, 0x2b, 0xb4, 0x58, 0xa8, 0x37, 0x1c, 0xa5, 0xf0, 0x47, 0xfa, + 0xf3, 0x20, 0x9f, 0x02, 0x10, 0xbb, 0x3e, 0x8e, 0x5e, 0x17, 0x25, 0x7d, 0xaa, 0x98, 0xe5, 0x72, 0x30, 0x81, 0x5d, + 0x9d, 0xc8, 0x50, 0x2b, 0xc8, 0x5b, 0x75, 0xe5, 0xad, 0x4c, 0xde, 0xc6, 0x38, 0x25, 0x3f, 0x70, 0xd3, 0xb1, 0x46, + 0x0c, 0xbc, 0xf2, 0xb4, 0x4e, 0x13, 0xa4, 0xc9, 0x1b, 0x60, 0x18, 0xe2, 0x77, 0x99, 0xf7, 0xdc, 0x73, 0xa4, 0x2a, + 0x48, 0x66, 0xdb, 0xcc, 0x53, 0x17, 0x51, 0x7d, 0xe5, 0xd4, 0xd2, 0x99, 0xd3, 0x8f, 0x00, 0xde, 0x63, 0x6a, 0xd2, + 0x90, 0x8f, 0x70, 0x5b, 0x8a, 0xaf, 0xb7, 0xea, 0x1a, 0x2f, 0x8d, 0xce, 0xdd, 0xcb, 0x97, 0xee, 0x34, 0xe8, 0xa7, + 0x20, 0x28, 0xe7, 0xf3, 0x52, 0xc0, 0x9e, 0x32, 0x9b, 0xeb, 0xd5, 0xaa, 0x15, 0x5a, 0x87, 0xc3, 0x58, 0x3b, 0x0a, + 0x69, 0x75, 0x16, 0xb0, 0xd5, 0x48, 0xa7, 0x04, 0x08, 0xc1, 0x71, 0x1a, 0x76, 0x82, 0x71, 0x97, 0x4e, 0x23, 0xb2, + 0x5e, 0x29, 0x49, 0x17, 0x66, 0x90, 0xfc, 0x93, 0xbc, 0x9e, 0x01, 0x2d, 0x01, 0x1c, 0x8a, 0x58, 0xc2, 0xc3, 0x49, + 0x72, 0x05, 0xd0, 0xe9, 0x70, 0x50, 0x69, 0x68, 0xce, 0x6a, 0x96, 0xcc, 0x27, 0xb1, 0x54, 0x55, 0x1e, 0x0e, 0x9e, + 0x72, 0x33, 0xe8, 0xf7, 0xb3, 0x69, 0xa9, 0x5c, 0x00, 0x82, 0x58, 0x17, 0x06, 0x88, 0x47, 0x5a, 0x78, 0xb2, 0xe8, + 0x53, 0x12, 0xbf, 0x9c, 0x25, 0x73, 0x93, 0x0d, 0xef, 0xc0, 0x08, 0x36, 0xe3, 0xba, 0xa4, 0x4c, 0x7b, 0x54, 0x7e, + 0xcf, 0xe8, 0xa9, 0xed, 0x6b, 0xad, 0xb6, 0x88, 0x75, 0x1d, 0x5c, 0x95, 0xa8, 0xa7, 0xf8, 0xa0, 0x24, 0xc1, 0xfb, + 0x95, 0x73, 0x33, 0x52, 0xbe, 0x16, 0xb9, 0x1f, 0xb4, 0x33, 0xb5, 0x72, 0xe0, 0x08, 0xe4, 0x58, 0x45, 0x25, 0xaf, + 0x77, 0x1d, 0x82, 0x47, 0x77, 0xa5, 0x02, 0xe5, 0xe0, 0x67, 0x20, 0x46, 0xd7, 0x57, 0x9d, 0x35, 0xd4, 0x4c, 0xa3, + 0xca, 0x23, 0xe8, 0xd4, 0x01, 0x3c, 0x29, 0x78, 0xa9, 0xd5, 0x8f, 0x87, 0x83, 0x67, 0x7e, 0xf0, 0xf7, 0x99, 0xbe, + 0x85, 0x98, 0x28, 0xa7, 0x1a, 0x21, 0x71, 0xa5, 0x24, 0x11, 0x1f, 0x2f, 0x5a, 0x56, 0x8c, 0xca, 0xf0, 0x9e, 0x57, + 0xaa, 0x7c, 0x75, 0xaa, 0xf2, 0x62, 0xa4, 0x6d, 0x09, 0xbc, 0x26, 0xff, 0x10, 0xb9, 0xe6, 0xad, 0xaf, 0xbb, 0xca, + 0xd0, 0x97, 0xb2, 0x02, 0x1d, 0xc1, 0x56, 0x96, 0x92, 0x03, 0x3e, 0xa9, 0xee, 0xaa, 0x55, 0xeb, 0x73, 0xca, 0x36, + 0xc2, 0x4d, 0x7e, 0x1d, 0x3b, 0x38, 0x52, 0x7e, 0x83, 0xe7, 0x02, 0xd8, 0x6b, 0xc0, 0xde, 0x9c, 0xb3, 0xa2, 0x79, + 0x70, 0x48, 0xdb, 0x02, 0x8d, 0xcc, 0xdc, 0xce, 0xd5, 0x7d, 0x5b, 0x1e, 0xa5, 0x31, 0x44, 0xa6, 0x3d, 0x30, 0x1d, + 0x6c, 0x46, 0xf9, 0xef, 0x29, 0xbf, 0x55, 0x38, 0x06, 0xbe, 0x9d, 0x7a, 0x07, 0x50, 0xf5, 0xb4, 0x41, 0xc6, 0x9a, + 0x61, 0x68, 0x65, 0x97, 0x4b, 0xa1, 0x25, 0x68, 0xa9, 0x9b, 0x20, 0x38, 0x3f, 0x22, 0xca, 0x11, 0x80, 0x2e, 0x52, + 0xc0, 0x04, 0x3f, 0xa5, 0xed, 0xee, 0xf7, 0xd7, 0xa9, 0x47, 0xee, 0x5d, 0xa1, 0xb2, 0x59, 0x7e, 0x22, 0x18, 0xfb, + 0x89, 0xc6, 0x0c, 0x3a, 0xba, 0x22, 0x27, 0x3c, 0x6b, 0x75, 0x58, 0xd7, 0x4d, 0x19, 0x94, 0xc5, 0x31, 0xaf, 0xa6, + 0xb3, 0x3f, 0x1e, 0xed, 0xeb, 0x06, 0x59, 0xc8, 0xff, 0x60, 0x3d, 0x24, 0x83, 0xee, 0x41, 0x28, 0x44, 0x6f, 0x1e, + 0xcc, 0xf0, 0x3f, 0xb6, 0xe1, 0xd9, 0x77, 0xdc, 0xa8, 0x13, 0xc0, 0x1c, 0x71, 0xbd, 0xf4, 0x14, 0x6d, 0x3d, 0xdc, + 0x02, 0xd9, 0x1a, 0x2f, 0x6f, 0xed, 0x35, 0x90, 0x53, 0x1c, 0xff, 0x92, 0x67, 0x6a, 0x65, 0x83, 0x9f, 0x9e, 0xb2, + 0x1d, 0x78, 0x78, 0x11, 0x02, 0x8a, 0x61, 0xd9, 0xf8, 0xa5, 0xe5, 0x38, 0xa3, 0xff, 0xe6, 0x11, 0xc3, 0x60, 0x11, + 0xf9, 0xf1, 0x45, 0x29, 0xc4, 0x57, 0xe1, 0x7d, 0xaa, 0xbc, 0x25, 0x39, 0x65, 0x2e, 0xf5, 0x30, 0xba, 0x2e, 0x49, + 0xdf, 0x25, 0x1f, 0x5b, 0xc3, 0xf6, 0x87, 0x76, 0xbf, 0x19, 0x22, 0x08, 0xa1, 0x1c, 0x3f, 0x67, 0x74, 0x42, 0xe3, + 0xc3, 0x6a, 0x76, 0x7a, 0xfd, 0xde, 0x39, 0x5e, 0xb0, 0x35, 0x1a, 0xe0, 0xf1, 0xd0, 0xc5, 0x3c, 0x51, 0x43, 0xa7, + 0xeb, 0xda, 0x39, 0x78, 0x60, 0x90, 0xe5, 0xc9, 0x77, 0x0c, 0x4b, 0xec, 0x4f, 0x22, 0x9e, 0xb4, 0x55, 0x1b, 0x9b, + 0x23, 0xd5, 0x46, 0xcd, 0xc0, 0x0f, 0x5e, 0x41, 0x81, 0xd1, 0x05, 0xe9, 0x16, 0x8c, 0xc3, 0x11, 0x80, 0xac, 0x18, + 0xc7, 0x23, 0x83, 0x09, 0x0c, 0xe9, 0x86, 0xa2, 0x00, 0x3c, 0x3c, 0x8e, 0x07, 0x21, 0x03, 0x48, 0x17, 0x3c, 0x34, + 0x6c, 0x93, 0x90, 0xf2, 0xf3, 0x3c, 0xaf, 0xd5, 0x10, 0xfa, 0xce, 0x42, 0x75, 0xec, 0x47, 0xda, 0x2b, 0xd6, 0xb5, + 0x2a, 0x1d, 0xd9, 0xea, 0x00, 0x7d, 0x43, 0x06, 0xbe, 0x75, 0x6c, 0x01, 0x10, 0x2d, 0xf1, 0x7b, 0xea, 0xd5, 0xbe, + 0x8c, 0x59, 0xa1, 0x5e, 0xbf, 0x31, 0xed, 0x7a, 0x25, 0x2d, 0x0a, 0xa8, 0xb8, 0x6d, 0xd5, 0xf6, 0x48, 0xce, 0x7f, + 0x78, 0xd7, 0xd1, 0x8e, 0xcf, 0x4e, 0x8d, 0x2d, 0xa1, 0xcc, 0x2d, 0x9e, 0xc8, 0xea, 0x68, 0x4b, 0x75, 0xaa, 0x0f, + 0xb8, 0xd4, 0xa4, 0x3a, 0x33, 0x30, 0xbc, 0x46, 0x80, 0x72, 0x0b, 0x91, 0x34, 0x0e, 0x7b, 0xe7, 0x93, 0x41, 0xc1, + 0xdc, 0x22, 0x01, 0x09, 0x6c, 0x63, 0x6b, 0x17, 0xcd, 0xf5, 0xeb, 0xf7, 0xd4, 0xab, 0xda, 0x54, 0xf5, 0xe0, 0x8d, + 0x17, 0x38, 0x7b, 0xa7, 0xb5, 0x80, 0x00, 0x0a, 0x5b, 0xcb, 0x72, 0x70, 0xee, 0x76, 0x55, 0x4b, 0x45, 0x19, 0xf5, + 0xfb, 0xe7, 0xbf, 0xa7, 0xa8, 0x88, 0x3d, 0x55, 0x9c, 0xb2, 0x7e, 0xbb, 0x65, 0xde, 0x54, 0x96, 0xbc, 0x41, 0x15, + 0xad, 0xd5, 0x51, 0x53, 0xb9, 0x6e, 0xae, 0x5a, 0x32, 0x41, 0x8c, 0xee, 0xd3, 0xb5, 0xce, 0x9d, 0x7a, 0xef, 0x55, + 0x1c, 0x31, 0x10, 0xdc, 0x74, 0x8f, 0x0f, 0x0e, 0x42, 0xa3, 0xa2, 0x5c, 0x70, 0xa3, 0xb4, 0xaa, 0xa4, 0x14, 0xf2, + 0x56, 0x45, 0x73, 0xa6, 0x8f, 0x00, 0x88, 0x00, 0xab, 0x44, 0xfd, 0x6f, 0xbe, 0x34, 0xc6, 0x83, 0x07, 0xbe, 0x26, + 0xd7, 0xb1, 0xf5, 0xfe, 0x69, 0x8d, 0xb4, 0xda, 0x38, 0x26, 0xb5, 0xea, 0x65, 0xab, 0x78, 0xd9, 0xbd, 0x4e, 0xc5, + 0xe0, 0xf9, 0xff, 0xdc, 0x07, 0xa8, 0x11, 0x2d, 0x65, 0x70, 0xeb, 0x6a, 0x80, 0xc6, 0x87, 0x63, 0xe1, 0x1b, 0x3f, + 0x64, 0x9c, 0x0f, 0x66, 0xe8, 0xa8, 0x36, 0x07, 0x07, 0x04, 0x47, 0x75, 0x8f, 0xc6, 0x84, 0x59, 0x38, 0xf7, 0x20, + 0x50, 0x7d, 0xe2, 0x3e, 0xe3, 0xda, 0x0b, 0xda, 0x04, 0x3e, 0x59, 0xd7, 0x35, 0x45, 0x80, 0x8b, 0xd8, 0x98, 0x88, + 0x21, 0x2e, 0x9b, 0x44, 0xea, 0x9b, 0x31, 0x28, 0x00, 0x8a, 0x67, 0x15, 0xc9, 0xa5, 0x37, 0x69, 0x5e, 0x89, 0xb2, + 0xd6, 0xcd, 0xa8, 0x58, 0x31, 0x04, 0x80, 0x87, 0xa0, 0xb8, 0xaa, 0xcc, 0x84, 0x46, 0x6c, 0x20, 0x95, 0xa5, 0x60, + 0xd5, 0xb0, 0xf0, 0x9b, 0xf6, 0x9b, 0xe4, 0xa4, 0x77, 0x3e, 0x6e, 0x9d, 0x3b, 0xf6, 0xbd, 0xa3, 0x90, 0xd2, 0x1e, + 0x8a, 0x09, 0x82, 0xe0, 0xa7, 0x75, 0x38, 0x7f, 0xc6, 0x9f, 0x11, 0x98, 0x8a, 0x6c, 0xc6, 0x80, 0x83, 0x10, 0x91, + 0x19, 0xbf, 0xe7, 0xf0, 0x19, 0x2f, 0x27, 0xe1, 0x70, 0xe8, 0x83, 0x3e, 0x94, 0x67, 0xb3, 0x70, 0x28, 0xe6, 0xd2, + 0x7b, 0x1d, 0xac, 0x75, 0x21, 0xaf, 0x27, 0x21, 0xa2, 0x85, 0x86, 0x3e, 0x38, 0xaf, 0xbb, 0xe6, 0x08, 0x4b, 0x00, + 0x9a, 0x38, 0xfa, 0xb2, 0x7e, 0x3f, 0xf2, 0xb4, 0xa1, 0x45, 0x8a, 0x8b, 0x46, 0x99, 0xcd, 0x72, 0xd9, 0x09, 0x1b, + 0xd7, 0x6e, 0x81, 0x50, 0x3c, 0x4c, 0x5b, 0xa8, 0x5a, 0x4f, 0xf5, 0x7a, 0x6e, 0xda, 0x7d, 0xf7, 0xa0, 0x5a, 0xe5, + 0x48, 0x67, 0x6d, 0xba, 0x52, 0xab, 0x5b, 0x46, 0xd5, 0x3a, 0x4b, 0x23, 0xaa, 0xdc, 0x24, 0x77, 0x8d, 0x5a, 0xf0, + 0xc9, 0x86, 0x2e, 0x53, 0x76, 0xb6, 0x06, 0x27, 0x8e, 0x3c, 0x97, 0xdc, 0xf2, 0xdd, 0x79, 0x45, 0x77, 0xa7, 0xda, + 0xb7, 0x00, 0xf7, 0x66, 0xd8, 0x90, 0x39, 0xaf, 0xb1, 0xd3, 0x20, 0x4c, 0x02, 0x3f, 0x62, 0x1f, 0x33, 0x64, 0x83, + 0x01, 0x1d, 0x85, 0xf4, 0xbf, 0xb6, 0xcc, 0x91, 0x80, 0xc9, 0x5f, 0xcf, 0xfd, 0xe6, 0xa6, 0xc8, 0x61, 0x31, 0x7e, + 0xd8, 0x60, 0xa4, 0xb1, 0x5a, 0x83, 0x61, 0xb9, 0x44, 0xe4, 0x4f, 0xed, 0x8e, 0x69, 0xaa, 0xe3, 0xcd, 0x7a, 0xad, + 0xf9, 0xd5, 0xd3, 0xa7, 0xba, 0x3e, 0xff, 0xed, 0xfb, 0xcb, 0xb0, 0x66, 0xf6, 0x87, 0x20, 0x94, 0x76, 0xef, 0x16, + 0xe7, 0x8e, 0x44, 0xef, 0x58, 0x69, 0x66, 0x97, 0x76, 0xc9, 0x2e, 0x4d, 0x69, 0xd7, 0xe4, 0x7a, 0xf5, 0x8d, 0xf2, + 0xc6, 0xce, 0x2b, 0xa6, 0xfb, 0xf7, 0x42, 0xef, 0x28, 0xa7, 0x6a, 0x02, 0x11, 0x4d, 0xda, 0x91, 0xb8, 0xdd, 0x2b, + 0xc3, 0xa7, 0x93, 0xbc, 0x5d, 0xc2, 0x51, 0xd7, 0xb0, 0xdc, 0x7c, 0xfb, 0xd7, 0xbc, 0xea, 0xac, 0x70, 0xfb, 0xa5, + 0x31, 0x6b, 0x7f, 0x0a, 0xe2, 0xaa, 0xfe, 0xf4, 0x1e, 0xd5, 0x4c, 0xc9, 0xff, 0x55, 0x8f, 0x81, 0xab, 0x9f, 0x4c, + 0x3b, 0xba, 0xa7, 0x10, 0x36, 0x98, 0xfd, 0xfc, 0xf8, 0xa1, 0x45, 0xd7, 0xe8, 0x02, 0x45, 0x72, 0x00, 0x9d, 0xbb, + 0x64, 0x84, 0xf7, 0x3b, 0xc6, 0xb9, 0x7f, 0xf5, 0x9b, 0x9a, 0x1c, 0x21, 0xa2, 0x5d, 0x84, 0x03, 0x80, 0xb8, 0xd3, + 0x54, 0xd6, 0xa1, 0x06, 0xe8, 0x03, 0x02, 0xeb, 0xd0, 0xb7, 0x19, 0xc0, 0x41, 0x1f, 0x6d, 0x9e, 0x45, 0x20, 0xaf, + 0x7b, 0x77, 0xec, 0x2d, 0xdb, 0xf9, 0xfc, 0xd9, 0x2a, 0xf5, 0xee, 0xd0, 0x21, 0xf8, 0x7c, 0xec, 0x4f, 0x2f, 0x03, + 0x83, 0x0b, 0xcd, 0xde, 0x3e, 0x11, 0x6c, 0xc7, 0x76, 0x4f, 0x10, 0xa9, 0xa8, 0x3b, 0xff, 0xf0, 0xd2, 0x44, 0xcf, + 0x3b, 0x2f, 0x2c, 0xf9, 0x02, 0xc0, 0x03, 0x59, 0x0c, 0x28, 0x3e, 0x0b, 0xef, 0x57, 0x96, 0x80, 0x9a, 0xfc, 0x96, + 0xaf, 0xbd, 0x77, 0x94, 0x7a, 0x03, 0x7f, 0x0e, 0x28, 0x7d, 0x92, 0x73, 0x6f, 0x39, 0xbc, 0xf5, 0x2f, 0x9e, 0x82, + 0xf3, 0xc4, 0x6a, 0x78, 0x03, 0x7f, 0x15, 0x7c, 0xe8, 0x2d, 0x07, 0x98, 0x58, 0xf2, 0xa1, 0xb7, 0x1a, 0x40, 0xaa, + 0xc2, 0x85, 0xc4, 0xd8, 0x87, 0xcf, 0x41, 0xce, 0xf0, 0x8f, 0xdf, 0x35, 0x06, 0xeb, 0xe7, 0xa0, 0xd0, 0x68, 0xac, + 0xa5, 0x0a, 0x59, 0x8a, 0xc5, 0x99, 0x00, 0x9b, 0x70, 0xdc, 0xed, 0x8b, 0x55, 0x6d, 0xd6, 0x82, 0xfe, 0x7c, 0xc0, + 0xf7, 0x68, 0xac, 0xae, 0xca, 0xb9, 0x28, 0x3f, 0x22, 0x7d, 0xaa, 0xe3, 0x63, 0x54, 0x6c, 0xea, 0xee, 0x74, 0xaa, + 0x55, 0x47, 0xda, 0xef, 0xca, 0x35, 0xd8, 0xf1, 0x3a, 0x39, 0xb2, 0x14, 0x9e, 0x75, 0xd8, 0x79, 0xe9, 0x94, 0xe8, + 0x30, 0x8c, 0x77, 0x5b, 0xf5, 0x8c, 0xa1, 0x3c, 0x37, 0x18, 0xd3, 0x05, 0x8f, 0xf8, 0xb3, 0x41, 0x2e, 0x43, 0x63, + 0x3e, 0x20, 0x1b, 0x86, 0xf2, 0xa1, 0x45, 0x86, 0x84, 0x88, 0xf7, 0x50, 0x09, 0xd8, 0xb6, 0xa0, 0x4c, 0x0a, 0x38, + 0x8b, 0x06, 0xbf, 0xd7, 0x5e, 0x0e, 0xbc, 0x07, 0x91, 0xdf, 0x48, 0x97, 0x72, 0x89, 0x8d, 0x4e, 0x1c, 0xcb, 0x42, + 0x3b, 0x8f, 0xeb, 0xaf, 0x63, 0x50, 0xbf, 0x57, 0xfa, 0x0d, 0xca, 0xd9, 0x1f, 0x25, 0xeb, 0xb4, 0xf1, 0xc4, 0xf8, + 0x97, 0xab, 0xfc, 0x53, 0xb4, 0xd4, 0xc3, 0xff, 0x67, 0x4c, 0xa1, 0xf4, 0x2f, 0xd3, 0x32, 0xda, 0xac, 0x16, 0xa2, + 0x14, 0x79, 0x24, 0x4e, 0xbe, 0x16, 0xd9, 0xb9, 0x7c, 0xe7, 0x53, 0xe8, 0x17, 0x80, 0x96, 0x7d, 0x82, 0x8c, 0xfe, + 0x8d, 0x09, 0x3e, 0xfc, 0x4d, 0x3b, 0xd7, 0xe6, 0x7c, 0x3c, 0xc9, 0xaf, 0xac, 0xbd, 0xdb, 0xf1, 0x22, 0x31, 0x8a, + 0xb1, 0xdc, 0x57, 0xdd, 0xac, 0x9c, 0xa8, 0xe4, 0xc0, 0x48, 0xd7, 0x64, 0x2f, 0x57, 0xb2, 0x6e, 0xa7, 0x5b, 0x09, + 0x44, 0x54, 0x81, 0xf7, 0x18, 0x57, 0xb1, 0x8f, 0x60, 0xba, 0xee, 0xb8, 0x8c, 0x76, 0xbc, 0x67, 0xbc, 0x3a, 0x51, + 0x56, 0x70, 0xbb, 0x11, 0xed, 0x09, 0x1d, 0xfd, 0x34, 0xa9, 0x2d, 0x0b, 0x07, 0x20, 0x77, 0x09, 0x63, 0xd9, 0x10, + 0xac, 0x18, 0x94, 0xbe, 0x5e, 0x53, 0xb2, 0x2c, 0xc0, 0xa2, 0xb3, 0xcb, 0x08, 0xc4, 0xb0, 0x6e, 0x9a, 0x13, 0x3a, + 0x5e, 0xba, 0x38, 0xef, 0xb5, 0x8a, 0x14, 0x3c, 0xa3, 0x45, 0xc7, 0xdc, 0x74, 0xa4, 0x1b, 0xa3, 0xbd, 0x7d, 0x61, + 0x10, 0x52, 0x3c, 0x7f, 0x60, 0xab, 0x75, 0x71, 0x91, 0x78, 0x85, 0x4c, 0xb4, 0x20, 0x96, 0x22, 0x30, 0xe3, 0x85, + 0xa6, 0x11, 0x26, 0x28, 0x53, 0x82, 0x45, 0x6b, 0x74, 0x68, 0x7f, 0x58, 0xc2, 0xee, 0x31, 0x46, 0x80, 0x40, 0x95, + 0xe9, 0x45, 0xd8, 0x9a, 0x30, 0x9b, 0xba, 0xd8, 0x00, 0x6d, 0x15, 0x43, 0x83, 0xb0, 0x36, 0xc4, 0x7c, 0x4c, 0xf3, + 0xe5, 0x3f, 0xb1, 0x18, 0xdb, 0x13, 0x88, 0xed, 0xdd, 0xae, 0x49, 0x98, 0xee, 0xb5, 0xb8, 0xb1, 0x5e, 0x6e, 0x4f, + 0x39, 0xa6, 0x76, 0xac, 0x8d, 0xda, 0xb1, 0x16, 0x7a, 0xc7, 0x5a, 0xeb, 0x1d, 0x6b, 0xd9, 0xf0, 0x47, 0x99, 0x17, + 0xb3, 0x04, 0xf4, 0xbb, 0x2b, 0xae, 0x1a, 0x04, 0xcd, 0xd8, 0xb0, 0x5b, 0xf8, 0x2d, 0xb1, 0x76, 0x4b, 0xff, 0x62, + 0xc1, 0x6e, 0x4c, 0x1f, 0xe8, 0xd6, 0x01, 0x96, 0x11, 0x35, 0xf9, 0x0e, 0x79, 0x37, 0x9d, 0x15, 0x85, 0xdb, 0x13, + 0xbb, 0xf1, 0xd9, 0x5b, 0xf3, 0xe6, 0xdd, 0x93, 0x08, 0x72, 0xef, 0xb8, 0x77, 0x37, 0x7c, 0xeb, 0x5f, 0xe8, 0x16, + 0xc8, 0xc9, 0x2c, 0x67, 0x20, 0x75, 0xc4, 0x27, 0x88, 0x56, 0xf6, 0x94, 0xef, 0x84, 0xdc, 0xd9, 0xd6, 0x4f, 0xee, + 0xdc, 0x6d, 0x6d, 0xf9, 0xe4, 0x8e, 0x55, 0x23, 0x8a, 0x15, 0xa7, 0x29, 0x12, 0x66, 0xd1, 0x06, 0x78, 0xea, 0xe5, + 0xfb, 0x1d, 0x3b, 0xe6, 0x70, 0xf7, 0xa4, 0xa3, 0xe3, 0xe5, 0x1c, 0xb0, 0xbb, 0xff, 0x68, 0x13, 0x36, 0x56, 0xba, + 0x56, 0xa1, 0xc3, 0xdd, 0x93, 0x4c, 0xe3, 0x39, 0x1c, 0xc9, 0xa7, 0x63, 0x8d, 0x0d, 0x82, 0xba, 0x3e, 0x67, 0x50, + 0x3b, 0x76, 0x5f, 0x13, 0x76, 0xd9, 0x31, 0xaf, 0x75, 0xcd, 0xdb, 0x2b, 0x4f, 0xc5, 0x86, 0x80, 0x0e, 0x5f, 0xab, + 0x1b, 0xe4, 0x5f, 0x02, 0xa7, 0x08, 0x00, 0x39, 0x1c, 0x2f, 0x79, 0xec, 0xfb, 0x34, 0x4b, 0xeb, 0x1d, 0x6a, 0x2d, + 0x2a, 0xcb, 0x32, 0xac, 0xbd, 0x1f, 0xb4, 0x62, 0x58, 0x6a, 0xfa, 0xa7, 0xe3, 0xc0, 0xed, 0x6c, 0xb7, 0x32, 0x76, + 0x19, 0x4f, 0x8a, 0x8b, 0xdf, 0x4e, 0x0b, 0xe5, 0xda, 0xcd, 0xdb, 0xf8, 0x4d, 0xab, 0x25, 0x4b, 0x6b, 0x3d, 0xe4, + 0xa5, 0x65, 0x11, 0x81, 0x00, 0x86, 0x23, 0x65, 0x17, 0x4b, 0xb8, 0x47, 0x58, 0xdd, 0x83, 0x50, 0x32, 0x2f, 0x5c, + 0x3c, 0x65, 0x31, 0x24, 0x02, 0x6c, 0x77, 0xa8, 0xd8, 0x16, 0x2e, 0x9e, 0xb2, 0x0d, 0x2f, 0xfa, 0xfd, 0x4c, 0x75, + 0x0a, 0x59, 0x77, 0x16, 0x7c, 0xa3, 0x9a, 0x63, 0x0d, 0x35, 0x5b, 0x9b, 0x64, 0x6b, 0x9c, 0xdb, 0x8a, 0x8f, 0x65, + 0x5b, 0xf1, 0xb1, 0xb2, 0xd6, 0xa5, 0x7b, 0xbd, 0x47, 0x75, 0x01, 0x6c, 0xfd, 0xb7, 0xc7, 0x2b, 0xd7, 0xf3, 0x19, + 0x01, 0x7c, 0xdd, 0xf0, 0xf1, 0xe4, 0x06, 0xbd, 0x4a, 0x6e, 0xfc, 0xdb, 0x81, 0x1a, 0x7f, 0xa7, 0x73, 0x6f, 0x00, + 0xba, 0x92, 0xf2, 0x0a, 0xc8, 0x3b, 0xc8, 0x31, 0xb7, 0xec, 0xca, 0xbb, 0x93, 0xef, 0xb0, 0xb7, 0xbc, 0x9e, 0xdd, + 0xcc, 0xd9, 0x0e, 0x9c, 0x0a, 0x92, 0x81, 0xbd, 0xac, 0xd8, 0x2e, 0x88, 0xed, 0x84, 0xdf, 0x09, 0x98, 0xf2, 0x39, + 0x04, 0x71, 0x05, 0xb7, 0x10, 0x87, 0x27, 0xff, 0x1c, 0xdc, 0xb5, 0x36, 0xeb, 0x3b, 0x66, 0x75, 0x4e, 0xb0, 0x66, + 0x56, 0x0f, 0x06, 0x8b, 0x66, 0xb2, 0xea, 0xf7, 0xbd, 0x9d, 0x76, 0x7c, 0x5a, 0x4a, 0x9d, 0xd8, 0x69, 0xad, 0xd6, + 0x0d, 0x7b, 0x2b, 0xb5, 0x2e, 0xc6, 0xd0, 0x03, 0xc4, 0x4f, 0xb7, 0x03, 0x7e, 0xd7, 0xb1, 0xb6, 0xbc, 0xb7, 0xec, + 0x86, 0xed, 0xe0, 0x12, 0xd4, 0xb4, 0x97, 0xfd, 0x49, 0xe5, 0x82, 0x76, 0xec, 0x92, 0x78, 0x38, 0x63, 0x56, 0x29, + 0x33, 0xeb, 0xa4, 0xba, 0x12, 0x9d, 0x31, 0x9d, 0xb5, 0x9e, 0xcf, 0xd5, 0x7c, 0x52, 0x68, 0x50, 0xbf, 0x73, 0xe2, + 0x23, 0x2a, 0x3a, 0x4f, 0x60, 0x6b, 0x59, 0x41, 0xac, 0xf6, 0x39, 0x58, 0x6b, 0xb5, 0x4b, 0xbf, 0x97, 0x0f, 0xb8, + 0x4d, 0x39, 0xac, 0x03, 0x83, 0x9a, 0x13, 0x2b, 0xea, 0x21, 0xdb, 0x31, 0x6e, 0x7e, 0x7a, 0xf9, 0x83, 0x13, 0x96, + 0xac, 0x58, 0xed, 0x4f, 0x7f, 0x7b, 0xe2, 0xe9, 0xef, 0xd4, 0xfe, 0x85, 0xf0, 0x83, 0xf1, 0xbf, 0x6b, 0xf7, 0xb5, + 0x16, 0xa3, 0xb2, 0x55, 0x8e, 0xd0, 0xb8, 0x5b, 0x49, 0x93, 0xe5, 0x27, 0xe1, 0x09, 0x6b, 0xc1, 0xb3, 0x5c, 0x2f, + 0xd1, 0xac, 0x80, 0x15, 0xd6, 0x32, 0x09, 0x57, 0x18, 0xab, 0xa5, 0xad, 0xbe, 0x45, 0xd3, 0x1c, 0x1f, 0xce, 0xb5, + 0x41, 0x99, 0x72, 0x76, 0x46, 0xac, 0x86, 0xcb, 0xb0, 0x34, 0xa1, 0x08, 0xd9, 0xbd, 0x1d, 0xdc, 0xd8, 0x29, 0x4b, + 0x29, 0xc3, 0x39, 0x06, 0x13, 0x1e, 0x89, 0x51, 0x95, 0xef, 0xef, 0x4b, 0x8a, 0x9c, 0xb6, 0xe5, 0xa0, 0x0a, 0x61, + 0x1f, 0x49, 0x94, 0xc0, 0xad, 0x48, 0x0b, 0x45, 0xca, 0xe2, 0x6f, 0x07, 0xe8, 0x02, 0x2f, 0xa0, 0xae, 0x46, 0xdd, + 0xfe, 0x70, 0xc4, 0xc3, 0x07, 0xa6, 0x3e, 0x30, 0x62, 0x49, 0xa0, 0xb6, 0xe7, 0x59, 0xba, 0x04, 0x15, 0x7e, 0x0f, + 0x57, 0x13, 0xb1, 0x9f, 0x5b, 0x52, 0x54, 0x64, 0x23, 0xbd, 0xa1, 0x35, 0x78, 0x84, 0xd6, 0x94, 0x17, 0x4e, 0xaa, + 0x4d, 0x3a, 0xef, 0x08, 0x39, 0x56, 0xdf, 0x5a, 0xc2, 0x68, 0x57, 0xf4, 0xe2, 0xde, 0xd1, 0x7b, 0x9e, 0xae, 0x7a, + 0xee, 0x4f, 0x5c, 0x31, 0x4f, 0x6e, 0x23, 0x50, 0xb7, 0x82, 0xea, 0xf6, 0x5e, 0x25, 0x58, 0xb0, 0xa4, 0xdd, 0xc7, + 0x6f, 0x67, 0xed, 0x40, 0x54, 0xc6, 0x2a, 0x7d, 0x4b, 0x12, 0xf6, 0xc4, 0xa0, 0x53, 0xa8, 0xca, 0xed, 0xee, 0x68, + 0x0b, 0x5c, 0xc7, 0x2c, 0x45, 0xcf, 0x6d, 0x91, 0xbb, 0xe5, 0xdf, 0x3d, 0x57, 0xe4, 0xec, 0x97, 0x80, 0xe0, 0xd4, + 0x7c, 0x43, 0x7c, 0x39, 0xc2, 0xa3, 0xea, 0x16, 0x38, 0x4e, 0xdf, 0x01, 0xfc, 0xc3, 0xe1, 0x12, 0x34, 0x01, 0xb1, + 0x60, 0xbd, 0x34, 0xee, 0xb1, 0x5e, 0x5c, 0x6c, 0x96, 0x49, 0xbe, 0x01, 0x67, 0x06, 0x4a, 0xb5, 0xf4, 0x03, 0xc7, + 0x6a, 0x01, 0x15, 0x0e, 0x66, 0x27, 0xf5, 0xc2, 0x32, 0xea, 0x31, 0x7d, 0x7e, 0x06, 0x7b, 0x47, 0x48, 0x00, 0xdc, + 0x2f, 0xfb, 0x80, 0x04, 0x3c, 0x74, 0x66, 0x07, 0x84, 0x13, 0x66, 0x51, 0x15, 0x48, 0x24, 0x47, 0xfa, 0xd9, 0x63, + 0x26, 0x92, 0x3f, 0x98, 0xf5, 0x9c, 0x53, 0xa2, 0xc7, 0x7a, 0xea, 0x08, 0xe9, 0xb1, 0x9e, 0x75, 0x44, 0xf4, 0x58, + 0xcf, 0x3a, 0x3e, 0x7a, 0xac, 0x67, 0x8e, 0x9d, 0x1e, 0x04, 0x26, 0x40, 0xe4, 0x01, 0xeb, 0xd1, 0x64, 0xea, 0x29, + 0xee, 0x01, 0xa2, 0x41, 0x60, 0x3d, 0x29, 0x9c, 0xf7, 0x00, 0x79, 0x8c, 0xc4, 0xea, 0xa0, 0xf7, 0x1f, 0xe3, 0xc7, + 0x3d, 0x23, 0x23, 0x8f, 0x5b, 0x87, 0xd5, 0xff, 0xfa, 0x4f, 0x08, 0x80, 0xc3, 0xb3, 0xa9, 0x77, 0x39, 0x86, 0xac, + 0xb2, 0x8c, 0x40, 0xf2, 0x13, 0x83, 0x2f, 0x5f, 0x00, 0x54, 0x7d, 0xa6, 0x6b, 0x35, 0x39, 0x6a, 0x8f, 0x39, 0x74, + 0xc5, 0x00, 0xb0, 0x0d, 0x4b, 0x54, 0xd5, 0xc2, 0x26, 0x2c, 0x6e, 0x3f, 0xc3, 0x68, 0x2e, 0x9b, 0x5e, 0xd0, 0x40, + 0x3d, 0x42, 0xf0, 0x4b, 0xeb, 0xa1, 0xb5, 0x96, 0x29, 0x87, 0xae, 0x8d, 0xa2, 0xca, 0x86, 0xba, 0x84, 0xd5, 0x5a, + 0x44, 0x35, 0x51, 0xa4, 0x5c, 0x32, 0x8a, 0x62, 0xa9, 0x82, 0x7d, 0x26, 0x96, 0x10, 0x35, 0x4f, 0x5b, 0x6d, 0x15, + 0xec, 0x97, 0x80, 0xb0, 0x16, 0xd6, 0x42, 0x3a, 0x83, 0xda, 0x3b, 0xfd, 0x48, 0xf9, 0xcb, 0x0b, 0xb9, 0x9d, 0x5b, + 0x28, 0xc2, 0xed, 0x39, 0x28, 0x6f, 0xea, 0xaa, 0x54, 0x44, 0xa3, 0x25, 0x50, 0xca, 0x9c, 0x20, 0xb2, 0x00, 0x01, + 0x1c, 0x37, 0x10, 0xf8, 0xbc, 0xc6, 0x27, 0xd0, 0x28, 0x04, 0xf2, 0x03, 0xab, 0x70, 0xed, 0x21, 0x2d, 0xb5, 0x46, + 0x44, 0x89, 0xf8, 0xd1, 0xd5, 0x73, 0x6c, 0x5f, 0x3d, 0x8d, 0xb5, 0xa5, 0x34, 0x41, 0xfc, 0xc4, 0x62, 0x0b, 0x31, + 0x41, 0x54, 0x87, 0xe8, 0x08, 0x96, 0x13, 0x42, 0x14, 0xfe, 0x14, 0xfa, 0xa9, 0x81, 0xbf, 0x64, 0x8b, 0x22, 0xaf, + 0x09, 0x16, 0xb3, 0x62, 0x80, 0x56, 0x45, 0xe0, 0x99, 0xce, 0x96, 0xca, 0x9c, 0xe6, 0xd1, 0x91, 0x1d, 0x9c, 0x77, + 0x1d, 0xec, 0xa5, 0x2f, 0x63, 0x27, 0xcb, 0xa6, 0x51, 0x1b, 0x1b, 0x22, 0xe1, 0x15, 0xf9, 0xcb, 0x2c, 0x35, 0xce, + 0x91, 0xb9, 0x5c, 0xdf, 0x75, 0xb1, 0x5c, 0xd2, 0x36, 0x61, 0x15, 0x22, 0xd4, 0x6d, 0x43, 0xe5, 0x52, 0x98, 0x8d, + 0x4d, 0xd3, 0x00, 0x5f, 0x28, 0x2a, 0x95, 0xaa, 0xd4, 0x56, 0x2a, 0x39, 0xe1, 0x5d, 0xdf, 0xd4, 0x22, 0x75, 0x45, + 0xb0, 0x8d, 0x19, 0xea, 0xa1, 0xdc, 0xa8, 0xb1, 0x6f, 0x3b, 0x56, 0xe9, 0x1d, 0x26, 0xc8, 0x19, 0x79, 0x91, 0x83, + 0x8b, 0x92, 0x82, 0xcc, 0xd5, 0x10, 0xe6, 0x0f, 0x1a, 0x3e, 0x2d, 0x2c, 0xf7, 0x50, 0x02, 0x66, 0x47, 0x0d, 0x0f, + 0x23, 0x04, 0x22, 0x2e, 0x95, 0x7d, 0xc5, 0xc4, 0xef, 0x29, 0x98, 0x25, 0x13, 0xba, 0x17, 0xb1, 0x28, 0x42, 0x1b, + 0x9f, 0x24, 0xc9, 0xd4, 0xd3, 0x14, 0xdc, 0xc8, 0x65, 0x98, 0xa3, 0x11, 0x5a, 0xf2, 0x91, 0x03, 0xe9, 0x6b, 0x39, + 0x95, 0xe0, 0x23, 0xea, 0x14, 0x70, 0x3c, 0x3f, 0x2f, 0xac, 0x9f, 0x2c, 0x97, 0x98, 0xcb, 0xda, 0xfc, 0x97, 0x1d, + 0x1d, 0x83, 0x5d, 0x9e, 0x26, 0x8e, 0xab, 0xff, 0xa8, 0x4a, 0x8a, 0xfb, 0x5f, 0xd2, 0x1c, 0x50, 0x04, 0x33, 0x7b, + 0x8a, 0xf1, 0xb1, 0xcf, 0x32, 0x05, 0xfc, 0xed, 0x7a, 0x6b, 0xc9, 0xc4, 0x2e, 0x69, 0x37, 0x57, 0xc6, 0x2f, 0xb5, + 0x61, 0xc7, 0xc1, 0xb9, 0x01, 0x28, 0xce, 0x1a, 0x1d, 0x96, 0xd7, 0xba, 0x6d, 0x55, 0xa8, 0x40, 0xad, 0xff, 0xbd, + 0x5b, 0x98, 0xf2, 0x36, 0x2f, 0x95, 0xb7, 0x79, 0x68, 0x02, 0x04, 0x22, 0x33, 0xe4, 0x59, 0xd3, 0x31, 0x49, 0xdc, + 0x3b, 0x52, 0xd2, 0xbe, 0x23, 0xc5, 0x0f, 0xde, 0x91, 0x90, 0x6f, 0x09, 0x1d, 0xd9, 0x17, 0x9c, 0x9c, 0x40, 0x99, + 0xc1, 0x5e, 0x5e, 0x33, 0xd9, 0x3f, 0xa0, 0xbd, 0x70, 0x2e, 0xcb, 0x2b, 0xfe, 0x56, 0x78, 0x6b, 0x7f, 0xba, 0x3e, + 0xed, 0xaa, 0x7a, 0xfb, 0x8d, 0x99, 0x79, 0x38, 0x14, 0x87, 0x43, 0x65, 0x82, 0x76, 0x6f, 0xb8, 0x18, 0xe4, 0xec, + 0xce, 0x8d, 0x8f, 0x7f, 0xcb, 0x51, 0xc4, 0x56, 0xca, 0x23, 0xe9, 0x42, 0x25, 0x86, 0x97, 0x06, 0x1e, 0x66, 0xc7, + 0xc7, 0x93, 0xdd, 0xd5, 0xdd, 0x64, 0x30, 0xd8, 0xa9, 0xbe, 0xdd, 0xf2, 0x7a, 0xb6, 0x9b, 0xb3, 0x7b, 0x7e, 0x3b, + 0xdd, 0x06, 0xfb, 0x06, 0xb6, 0xdd, 0xdd, 0x95, 0x38, 0x1c, 0x76, 0xcf, 0xf8, 0x8d, 0xbf, 0xbf, 0x47, 0x40, 0x67, + 0x7e, 0x3e, 0x6e, 0x63, 0xfc, 0x5c, 0xb7, 0x5d, 0xb5, 0x76, 0x00, 0x4f, 0xff, 0xa3, 0x77, 0x3d, 0x5b, 0xcc, 0x7d, + 0xf6, 0x88, 0xdf, 0x83, 0x7f, 0x3e, 0x6e, 0x92, 0x48, 0x7d, 0xa2, 0x5d, 0x26, 0xaf, 0xc1, 0x81, 0x7c, 0xe7, 0xb3, + 0x57, 0xfc, 0x7e, 0xb6, 0x98, 0xf3, 0xe2, 0x70, 0x78, 0x3f, 0x0d, 0x91, 0xac, 0x29, 0xac, 0x88, 0x25, 0xc5, 0xf3, + 0x83, 0xf0, 0xf8, 0xbd, 0x88, 0x0c, 0x91, 0x96, 0x7b, 0x77, 0xc8, 0xae, 0x59, 0xe4, 0x07, 0xf0, 0x41, 0xb6, 0xf3, + 0x27, 0xb2, 0xa6, 0x74, 0xbf, 0x78, 0xe4, 0x1f, 0x0e, 0xf4, 0xd7, 0x2b, 0xff, 0x70, 0x78, 0xcf, 0xee, 0x11, 0x1c, + 0x9d, 0xef, 0xa0, 0x7f, 0xf4, 0xad, 0x03, 0xaa, 0x32, 0x7c, 0x3b, 0xdb, 0xcc, 0xfd, 0x67, 0x2b, 0xb6, 0x04, 0x2e, + 0x14, 0xe5, 0x85, 0x76, 0xcd, 0xee, 0xd1, 0xeb, 0x8c, 0x9c, 0x88, 0x66, 0xbb, 0xb9, 0xcf, 0x62, 0x7c, 0xae, 0xee, + 0x8b, 0xc9, 0x37, 0xef, 0x8b, 0x3b, 0xb6, 0xed, 0xbe, 0x2f, 0xca, 0x37, 0xdd, 0xf5, 0xb3, 0x65, 0x3b, 0x76, 0x0f, + 0x33, 0xec, 0x2d, 0xbf, 0x6e, 0x8e, 0x1d, 0x63, 0xbf, 0x79, 0x63, 0x04, 0x50, 0x66, 0x0b, 0x16, 0x0b, 0x0e, 0x4a, + 0xb5, 0x6a, 0x5b, 0x12, 0x79, 0xa5, 0x03, 0xd5, 0x66, 0x04, 0xf7, 0xd5, 0x42, 0xce, 0x3c, 0x33, 0xd0, 0xb7, 0x15, + 0xa2, 0x85, 0xc3, 0x06, 0xfc, 0x8d, 0xb6, 0x8e, 0x31, 0x4c, 0xb3, 0x9a, 0x69, 0x5b, 0xd4, 0xe5, 0xf7, 0xbd, 0x67, + 0xf2, 0x1b, 0x19, 0xd8, 0x42, 0x24, 0x85, 0xe3, 0xf8, 0xe2, 0xe9, 0x09, 0xff, 0x55, 0xcb, 0xa3, 0x56, 0xfb, 0x85, + 0x52, 0x9f, 0xbe, 0xa4, 0x23, 0x9a, 0xb8, 0x17, 0x6d, 0x19, 0xd6, 0x28, 0x6b, 0x6a, 0xe9, 0x30, 0x8c, 0x6b, 0xd8, + 0x97, 0x07, 0x0e, 0x7d, 0x07, 0x04, 0xda, 0x2a, 0x95, 0x02, 0x2d, 0x1c, 0xc3, 0x28, 0xcc, 0x42, 0xca, 0xc3, 0xc2, + 0x2c, 0xe5, 0x3d, 0x16, 0x68, 0x71, 0xab, 0xee, 0x31, 0xb5, 0xdd, 0x82, 0x08, 0xab, 0xb7, 0x8c, 0xf3, 0xcb, 0x46, + 0x15, 0x6e, 0x0b, 0x50, 0x14, 0x41, 0x19, 0xec, 0x49, 0x6e, 0xbb, 0x51, 0xd2, 0x6c, 0x14, 0xd6, 0x62, 0x59, 0x94, + 0xbb, 0x5e, 0xc3, 0x6e, 0xf0, 0x82, 0xaa, 0x9f, 0x10, 0xb6, 0x65, 0xcf, 0x3a, 0x94, 0x8b, 0xf4, 0xdf, 0xb2, 0xf4, + 0x7c, 0xbf, 0x35, 0xe7, 0x7f, 0xfa, 0x8a, 0x3e, 0x2a, 0xff, 0xfd, 0x4b, 0xfa, 0xc9, 0x60, 0x19, 0x39, 0xa5, 0x7e, + 0x8a, 0x46, 0xb7, 0x69, 0x4e, 0x18, 0x5b, 0xbe, 0x7e, 0xfa, 0x1d, 0x32, 0x05, 0xc9, 0xa1, 0x94, 0xaa, 0x9c, 0xec, + 0xa1, 0x2f, 0xbc, 0xee, 0xc3, 0x4c, 0x30, 0x00, 0xe1, 0x35, 0xda, 0x54, 0x13, 0x26, 0xf1, 0xe0, 0x0a, 0xfe, 0x6f, + 0x04, 0x31, 0x68, 0x9f, 0x28, 0xea, 0xd8, 0x36, 0xd2, 0x75, 0xdb, 0x39, 0x48, 0xee, 0xd4, 0x95, 0x3f, 0x2a, 0x27, + 0xff, 0x8e, 0x86, 0xc8, 0x2b, 0xae, 0x10, 0x2b, 0x0b, 0x2e, 0xb1, 0x18, 0x2a, 0x52, 0x80, 0x6b, 0x08, 0x22, 0x65, + 0x51, 0x52, 0xb8, 0xe5, 0xa0, 0x2a, 0x02, 0x30, 0xae, 0x56, 0x47, 0x9d, 0x08, 0x1f, 0xb7, 0xd6, 0x22, 0x04, 0x2b, + 0x1a, 0xb5, 0xb2, 0x56, 0xe0, 0x0b, 0xd2, 0x97, 0x0e, 0x05, 0x31, 0x3d, 0x0a, 0xa9, 0x2a, 0x1d, 0x0a, 0xa4, 0x39, + 0x54, 0x7c, 0x63, 0xb0, 0x51, 0x54, 0xa4, 0xe7, 0x2f, 0x4d, 0x4a, 0x2e, 0x8d, 0x19, 0x1f, 0x44, 0x19, 0x89, 0xbc, + 0x0e, 0x97, 0x62, 0x5a, 0x20, 0xdf, 0xe8, 0xf1, 0x83, 0xe0, 0x12, 0xde, 0x0d, 0xb9, 0x57, 0x80, 0x2d, 0x01, 0x3b, + 0xc0, 0xbd, 0x32, 0xa3, 0x5c, 0xa7, 0x75, 0xfd, 0xd6, 0x7a, 0x28, 0x86, 0xe1, 0x13, 0x4b, 0x60, 0x3b, 0x5a, 0x47, + 0x47, 0x7a, 0xf8, 0xf0, 0xbf, 0xae, 0x6a, 0x8e, 0x3a, 0x95, 0xcb, 0xd9, 0xf1, 0x84, 0xa5, 0x88, 0x19, 0x74, 0x7f, + 0xdd, 0xbe, 0x14, 0x40, 0xb7, 0xcb, 0x62, 0x9e, 0x8d, 0x76, 0xf2, 0x6f, 0xe9, 0xc6, 0x8a, 0xd2, 0x26, 0xde, 0x65, + 0xbd, 0xb1, 0x3f, 0x1c, 0xfd, 0xc7, 0x93, 0x77, 0x13, 0x42, 0xd5, 0xd9, 0xb0, 0xb5, 0x8e, 0x73, 0xf9, 0x5f, 0xff, + 0x39, 0x26, 0x2b, 0x08, 0x0a, 0xc2, 0xb2, 0x53, 0x4c, 0x54, 0x30, 0x8a, 0x14, 0x6b, 0x3e, 0x9e, 0xac, 0x51, 0x27, + 0xbc, 0xf6, 0x17, 0x5a, 0x27, 0x4c, 0x8c, 0xac, 0x54, 0xfe, 0x9a, 0x55, 0x6c, 0xa9, 0x32, 0x0b, 0xc8, 0x3c, 0xc8, + 0x27, 0x6b, 0xa3, 0xc1, 0x5c, 0xf1, 0x7a, 0xb6, 0x9e, 0x4b, 0xe5, 0x33, 0x98, 0x72, 0x16, 0x83, 0x93, 0xa5, 0xb0, + 0x3b, 0x12, 0x28, 0x5a, 0x33, 0x74, 0xed, 0x4f, 0xb1, 0x55, 0xaf, 0xd2, 0xaa, 0x06, 0x78, 0x40, 0x88, 0x81, 0xa1, + 0xf6, 0x6a, 0xe1, 0xa1, 0xb5, 0x00, 0xd6, 0xfe, 0xa8, 0xf4, 0x83, 0xf1, 0x64, 0xc1, 0x6f, 0x90, 0x7f, 0x39, 0x72, + 0xd4, 0xee, 0xfd, 0xbe, 0x77, 0x07, 0x52, 0x70, 0xe4, 0x5a, 0x28, 0x90, 0x08, 0xe8, 0x86, 0x6f, 0x7c, 0xe5, 0x83, + 0xf1, 0x16, 0xb5, 0xd5, 0xa0, 0xa0, 0x76, 0x74, 0xcb, 0x63, 0x47, 0xef, 0x7c, 0x77, 0x42, 0x5f, 0x7d, 0xa3, 0x85, + 0xe3, 0x6f, 0x9c, 0x91, 0x6b, 0xb6, 0xea, 0x90, 0x23, 0x9a, 0x49, 0x87, 0x10, 0xb1, 0x62, 0x6b, 0xf6, 0x96, 0x54, + 0xce, 0x9d, 0x43, 0x76, 0xfa, 0x08, 0x55, 0x7a, 0xad, 0x87, 0xb7, 0x13, 0xa5, 0xbb, 0x3d, 0xde, 0x4d, 0xbe, 0x67, + 0x13, 0x11, 0x83, 0x01, 0x6d, 0x10, 0xce, 0xc8, 0x3a, 0x44, 0x2a, 0x1d, 0x20, 0x04, 0x8e, 0x09, 0x68, 0xfa, 0xaf, + 0x6f, 0x49, 0x14, 0x70, 0xa4, 0x8d, 0x90, 0xb5, 0xec, 0x70, 0xc8, 0x41, 0xa3, 0xdc, 0xfc, 0xe9, 0x15, 0xea, 0x34, + 0x07, 0xe6, 0xe9, 0x12, 0xf6, 0x1c, 0x3c, 0xd2, 0x8b, 0xe3, 0x23, 0xfd, 0xbf, 0xa3, 0x89, 0x1a, 0xff, 0xfb, 0x9a, + 0x28, 0xa5, 0x45, 0x72, 0x54, 0x4b, 0xdf, 0xa5, 0x8e, 0x82, 0x8b, 0xbc, 0xa3, 0x16, 0xb2, 0x67, 0xd9, 0xb8, 0x51, + 0xcd, 0xfb, 0xff, 0xb5, 0x32, 0xff, 0x5f, 0xd3, 0xca, 0x30, 0x25, 0x3b, 0x96, 0x6a, 0xe6, 0x81, 0x56, 0x31, 0xcc, + 0x7e, 0x21, 0x09, 0x91, 0xe1, 0xd2, 0x80, 0x1f, 0x55, 0xb0, 0x8f, 0xd3, 0x6a, 0x9d, 0x85, 0x3b, 0x54, 0xa2, 0xde, + 0x8a, 0x65, 0x9a, 0x3f, 0xaf, 0xff, 0x25, 0xca, 0x02, 0xa6, 0xf6, 0xb2, 0x4c, 0xe3, 0x80, 0x2c, 0xfc, 0x59, 0x58, + 0xe2, 0xe4, 0xc6, 0x36, 0xfe, 0x22, 0xc7, 0xd3, 0x7e, 0xd5, 0x99, 0x79, 0x20, 0x81, 0x1a, 0xe8, 0x42, 0x72, 0x2e, + 0x2b, 0x8b, 0x7b, 0x84, 0x6e, 0xfe, 0xb1, 0x2c, 0x8b, 0xd2, 0xeb, 0x7d, 0x4a, 0xd2, 0xea, 0x6c, 0x25, 0xea, 0xa4, + 0x88, 0x15, 0x94, 0x4d, 0x0a, 0x30, 0xfa, 0xb0, 0xf2, 0x44, 0x1c, 0x9c, 0x21, 0x50, 0xc3, 0x59, 0x9d, 0x84, 0x00, + 0x34, 0xac, 0x10, 0xf6, 0xcf, 0xa0, 0x85, 0x67, 0x61, 0x1c, 0xae, 0x01, 0x26, 0x27, 0xad, 0xce, 0xd6, 0x65, 0x71, + 0x97, 0xc6, 0x22, 0x1e, 0xf5, 0x14, 0x25, 0xcb, 0xeb, 0xdc, 0x95, 0x73, 0xfd, 0xfd, 0x9f, 0x14, 0xc0, 0x6e, 0xc0, + 0x6c, 0x5b, 0x60, 0x07, 0x00, 0x09, 0x0a, 0x64, 0x0b, 0x75, 0x1a, 0x9d, 0xa9, 0xa5, 0x02, 0xef, 0xb9, 0x1e, 0xe0, + 0xaf, 0x73, 0xc0, 0x32, 0xae, 0x0b, 0x19, 0x30, 0x82, 0x00, 0x46, 0xe0, 0xa0, 0x04, 0x0c, 0x9d, 0x21, 0x6e, 0xab, + 0x72, 0xd6, 0x42, 0x73, 0xa5, 0xdb, 0x92, 0x9b, 0x46, 0x39, 0x5b, 0x89, 0x00, 0xfa, 0xea, 0xa6, 0xc4, 0xe9, 0x62, + 0xd1, 0x4a, 0xc2, 0xbe, 0x7d, 0xdf, 0x4e, 0x15, 0x79, 0x7c, 0x94, 0x86, 0xbc, 0x02, 0xcf, 0x33, 0x8e, 0x24, 0x51, + 0x22, 0x78, 0x9d, 0x37, 0x66, 0x1c, 0x7e, 0x6c, 0x53, 0x4e, 0xed, 0xcd, 0x7a, 0x01, 0x38, 0x4f, 0xd0, 0x96, 0x01, + 0xc6, 0x02, 0x06, 0xe7, 0x42, 0x2c, 0x79, 0x8a, 0xe0, 0x97, 0x4e, 0xa4, 0x30, 0xee, 0x72, 0x18, 0xe6, 0x41, 0xd1, + 0xbb, 0xa4, 0xfe, 0xe8, 0xf7, 0x51, 0x9b, 0x0c, 0x86, 0xa0, 0x12, 0x40, 0x65, 0xdd, 0x20, 0x31, 0xb0, 0x2a, 0xdd, + 0x48, 0x5c, 0x42, 0xbc, 0xcc, 0x57, 0x53, 0x11, 0x05, 0xef, 0xeb, 0x09, 0x21, 0x9c, 0x60, 0x7c, 0x88, 0x1b, 0x20, + 0x60, 0xb0, 0x8a, 0x0b, 0x0c, 0x92, 0xe7, 0x12, 0xdd, 0x1f, 0xcf, 0x77, 0x0c, 0x70, 0xe5, 0xbc, 0xa7, 0xda, 0xd5, + 0x03, 0x7b, 0xb9, 0x4a, 0x97, 0x8c, 0x10, 0x56, 0xfc, 0x5f, 0x44, 0xde, 0xb7, 0xc3, 0x04, 0xd4, 0x36, 0xf2, 0xc7, + 0x20, 0x31, 0x97, 0x89, 0x22, 0x88, 0x47, 0x59, 0xc1, 0x92, 0x34, 0xd8, 0x8c, 0x92, 0x14, 0x34, 0x9a, 0x18, 0x43, + 0xa6, 0x42, 0x3b, 0x24, 0x8d, 0x66, 0x63, 0xb2, 0x8f, 0x21, 0xaf, 0xe1, 0x62, 0xb1, 0xc0, 0xfb, 0x7e, 0x11, 0xaa, + 0x83, 0x6d, 0x69, 0x0e, 0x01, 0x27, 0x09, 0xf6, 0xd4, 0x15, 0x29, 0x09, 0xb3, 0xd1, 0xa7, 0x90, 0x73, 0x03, 0x3a, + 0x4e, 0x1a, 0x43, 0xf5, 0x81, 0x49, 0x78, 0x15, 0xa1, 0x93, 0xb2, 0x42, 0x58, 0xc0, 0x7d, 0x23, 0xa3, 0xd1, 0x4a, + 0x1a, 0x04, 0xde, 0x66, 0xd8, 0x0a, 0x6c, 0x42, 0xc3, 0x7f, 0xcc, 0x3c, 0x4c, 0xab, 0x59, 0x09, 0xe6, 0x7c, 0x03, + 0x95, 0x18, 0x4f, 0x16, 0x57, 0x7c, 0xe3, 0x62, 0x25, 0x26, 0xb3, 0xc5, 0x7c, 0xb2, 0x96, 0x54, 0x73, 0xb9, 0xb7, + 0x66, 0x19, 0x5b, 0xc0, 0xfe, 0x61, 0x60, 0x28, 0x1d, 0xd8, 0xd1, 0x54, 0xd3, 0x26, 0x01, 0x26, 0xd3, 0x39, 0xe7, + 0xc3, 0x4b, 0x44, 0x93, 0xd5, 0xa9, 0x3b, 0x99, 0xaa, 0x76, 0x70, 0x4d, 0xce, 0xe4, 0xf4, 0x48, 0x3d, 0xd5, 0xba, + 0x97, 0x7c, 0xb4, 0x1d, 0x56, 0xa3, 0xad, 0x1f, 0x80, 0x5b, 0xa7, 0xb0, 0xd3, 0x77, 0xc3, 0x6a, 0xb4, 0xf3, 0x35, + 0xec, 0x2e, 0x29, 0x04, 0xaa, 0xbf, 0xca, 0x9a, 0xcc, 0xc5, 0xeb, 0xe2, 0xde, 0x2b, 0xd8, 0x53, 0x7f, 0xa0, 0x7f, + 0x95, 0xec, 0xa9, 0x6f, 0x33, 0xb9, 0xfe, 0x95, 0x76, 0x8d, 0xc6, 0x4c, 0xc7, 0x6b, 0x57, 0x60, 0x85, 0x06, 0xc8, + 0x2f, 0xd8, 0xd1, 0xde, 0xe4, 0x20, 0x10, 0xa0, 0x7b, 0x09, 0x8e, 0xa2, 0x80, 0xa8, 0x69, 0x55, 0x79, 0x74, 0xba, + 0xf7, 0xf7, 0xf8, 0x46, 0x08, 0xd8, 0xe4, 0xa9, 0x75, 0x6f, 0x19, 0xfb, 0x87, 0x03, 0x84, 0xd0, 0xcb, 0xe9, 0x37, + 0xda, 0xb2, 0x7a, 0xb4, 0x63, 0xb9, 0x6f, 0x18, 0xf5, 0x14, 0x8c, 0x61, 0xe8, 0xc2, 0x2a, 0x46, 0xf2, 0x0c, 0xc8, + 0x1a, 0xbf, 0x41, 0x74, 0x01, 0x8b, 0x5e, 0xef, 0xd5, 0x11, 0x0d, 0x22, 0xa0, 0xd2, 0x6b, 0xd2, 0x58, 0xe4, 0x73, + 0x55, 0x88, 0xde, 0x7b, 0x6b, 0xe7, 0xcd, 0x8c, 0x64, 0x99, 0x34, 0x52, 0xed, 0x56, 0x16, 0xeb, 0xca, 0x9b, 0x9d, + 0x90, 0x2e, 0xe6, 0x18, 0x2a, 0x83, 0xc7, 0x01, 0x28, 0x3d, 0xff, 0x11, 0x7a, 0x25, 0x43, 0xa6, 0x59, 0xa2, 0x99, + 0xdd, 0x35, 0xfe, 0x64, 0x95, 0x7a, 0x31, 0x22, 0x66, 0x03, 0x5b, 0x88, 0xdb, 0xa2, 0xd2, 0x6d, 0x51, 0x28, 0x5b, + 0x14, 0xe9, 0x43, 0xed, 0x4c, 0x77, 0x66, 0xe1, 0xb3, 0xca, 0xb4, 0xef, 0x53, 0x66, 0xc6, 0x06, 0x68, 0xbb, 0x08, + 0xdf, 0x40, 0x07, 0x2a, 0x84, 0xfc, 0x0d, 0x22, 0x22, 0x11, 0xb0, 0xcb, 0xa9, 0x3b, 0xb1, 0xe9, 0x90, 0xcc, 0x43, + 0xcc, 0x0a, 0x35, 0xca, 0x0b, 0x9e, 0x1c, 0x0d, 0x48, 0x45, 0xa8, 0xdb, 0xfd, 0xfe, 0xf9, 0xc2, 0x05, 0xb5, 0x5f, + 0x53, 0xec, 0x18, 0xdd, 0x14, 0x70, 0x2e, 0x78, 0x94, 0xf7, 0xdc, 0x3b, 0x07, 0x34, 0xc7, 0xf6, 0x14, 0x59, 0x03, + 0x4e, 0x6f, 0xbb, 0x10, 0x60, 0xfb, 0xac, 0xd9, 0xda, 0x9f, 0xac, 0xae, 0xa2, 0xa9, 0x57, 0xf2, 0x99, 0xee, 0xa2, + 0xc4, 0xed, 0xa2, 0x58, 0x76, 0xd1, 0xa6, 0x81, 0x60, 0xc7, 0x95, 0x1f, 0x00, 0x6f, 0x68, 0xd4, 0xef, 0x97, 0xad, + 0x9e, 0x3d, 0xf9, 0xda, 0x71, 0xcf, 0x66, 0x3e, 0x2b, 0x4d, 0xcf, 0x7e, 0x4e, 0xdd, 0x9e, 0x95, 0x93, 0xbd, 0xe8, + 0x9c, 0xec, 0xd3, 0xd9, 0x3c, 0x10, 0x5c, 0xee, 0xdc, 0xe7, 0xf9, 0x54, 0x4f, 0xbb, 0xca, 0x0f, 0x5a, 0x43, 0x64, + 0xed, 0x72, 0x55, 0xf7, 0xba, 0x82, 0x05, 0x2c, 0xc1, 0xdd, 0x7a, 0x69, 0xfe, 0x19, 0xbb, 0xbf, 0x17, 0xf4, 0xd2, + 0xfc, 0x77, 0xfa, 0x93, 0x02, 0x38, 0x00, 0x8d, 0xa9, 0xdd, 0x02, 0x0f, 0x31, 0x54, 0x50, 0xb8, 0x9b, 0x95, 0x73, + 0xaf, 0x06, 0x38, 0x4c, 0xd2, 0x37, 0xb4, 0x7a, 0xa5, 0xc5, 0xae, 0x97, 0xc9, 0x5e, 0x01, 0x1e, 0xaa, 0x90, 0x87, + 0x87, 0x43, 0xd4, 0x31, 0xec, 0xa0, 0x8e, 0x80, 0x61, 0x0f, 0xa1, 0xb1, 0x05, 0x9e, 0x8f, 0xbf, 0x64, 0x7c, 0x2f, + 0x40, 0x6d, 0x84, 0xf0, 0x78, 0xb5, 0x28, 0x43, 0x6c, 0xd9, 0x1b, 0xa4, 0x92, 0xfa, 0x45, 0x20, 0xca, 0x68, 0x15, + 0xd0, 0x56, 0x7b, 0xcc, 0xd2, 0x78, 0x0d, 0xa1, 0x62, 0xa9, 0x8f, 0x21, 0x34, 0x70, 0xf8, 0x1d, 0x0e, 0x20, 0xc1, + 0x97, 0x5c, 0x93, 0xcd, 0xbd, 0xc9, 0xef, 0x68, 0x9f, 0x3f, 0x1c, 0xce, 0x2f, 0x11, 0x94, 0x2e, 0x85, 0x8f, 0x54, + 0x22, 0xaa, 0xa7, 0xb8, 0x29, 0x21, 0x9b, 0x25, 0x2b, 0xfd, 0xe0, 0xb3, 0xfa, 0x05, 0x00, 0xb2, 0x10, 0x68, 0x13, + 0x99, 0xfd, 0xe9, 0x4c, 0x45, 0x17, 0x00, 0x87, 0xf8, 0xc3, 0x27, 0x88, 0xbe, 0xa1, 0x65, 0x5a, 0x3e, 0x4e, 0x78, + 0x08, 0x5a, 0x5b, 0xd2, 0x49, 0xc4, 0x4a, 0x81, 0x0d, 0x91, 0xf0, 0xfd, 0xfe, 0x79, 0x2c, 0xe9, 0x40, 0xa3, 0x56, + 0xf7, 0xc6, 0xad, 0xee, 0x95, 0xaf, 0xeb, 0x4e, 0x6e, 0x7c, 0x50, 0xb4, 0xcf, 0xe6, 0x8d, 0xca, 0xf7, 0x7d, 0x9d, + 0xb3, 0x3b, 0xdd, 0x3b, 0x72, 0x4e, 0x7c, 0x7f, 0x0f, 0xa1, 0xe8, 0xa1, 0x29, 0xb2, 0x2c, 0x09, 0x03, 0x5a, 0x6b, + 0xd7, 0x9e, 0x65, 0x74, 0xf0, 0xda, 0x37, 0x84, 0x88, 0x3c, 0xc5, 0x27, 0x21, 0xb7, 0x38, 0x3e, 0x28, 0xd0, 0x3f, + 0x33, 0xfe, 0xcc, 0x89, 0x1f, 0xb6, 0xfa, 0x05, 0x70, 0x6e, 0xba, 0xf7, 0xee, 0xc4, 0xac, 0xc7, 0x50, 0xca, 0xc6, + 0xff, 0xfd, 0x3e, 0x91, 0x05, 0x3a, 0x1d, 0xd1, 0x30, 0x10, 0xdc, 0x45, 0xf5, 0x7f, 0xaf, 0x78, 0xdd, 0xb3, 0x56, + 0xe7, 0xcb, 0x4f, 0x9d, 0x9e, 0xf4, 0x7a, 0xe9, 0x56, 0xf8, 0x32, 0x4c, 0x7c, 0xe7, 0x75, 0xbf, 0x61, 0xbb, 0xef, + 0x7e, 0x79, 0x77, 0xf4, 0x32, 0xb0, 0x49, 0xe1, 0x3b, 0x9b, 0x92, 0xcf, 0x7a, 0xa0, 0xf0, 0xeb, 0xb1, 0x5e, 0x5d, + 0xac, 0x7b, 0xac, 0x87, 0x5a, 0x40, 0xf4, 0xb0, 0x00, 0xf5, 0x5f, 0xcf, 0x3e, 0x0d, 0x85, 0x83, 0x6c, 0x9c, 0x2a, + 0x50, 0x64, 0xc1, 0x9f, 0x89, 0xd1, 0xba, 0x20, 0x40, 0x64, 0xb3, 0x7d, 0x7d, 0xac, 0x4e, 0x66, 0xdf, 0x94, 0x5a, + 0x92, 0xc1, 0x37, 0x01, 0x99, 0x1d, 0x58, 0x39, 0x41, 0xe9, 0xb8, 0x35, 0xe0, 0xca, 0x16, 0x91, 0x78, 0xfb, 0xd3, + 0x20, 0x3b, 0x6b, 0x4e, 0x1a, 0xed, 0xc3, 0x3e, 0xcd, 0x03, 0x04, 0x22, 0x99, 0x8a, 0x20, 0xd7, 0xdc, 0x5b, 0xd2, + 0x47, 0x87, 0x73, 0x5e, 0xc8, 0x3f, 0xa7, 0x52, 0x87, 0x38, 0x94, 0x58, 0x03, 0x81, 0xca, 0x33, 0x54, 0x39, 0x6c, + 0x90, 0xe3, 0x8f, 0x8e, 0x64, 0x26, 0x31, 0x59, 0xe4, 0x6e, 0xcd, 0x54, 0xf8, 0x81, 0xe0, 0x63, 0x96, 0x73, 0xe0, + 0x02, 0x9b, 0xcd, 0x7d, 0x35, 0xc5, 0xc5, 0x15, 0xf8, 0x63, 0x0a, 0xbf, 0xe2, 0x29, 0xec, 0xb4, 0xfb, 0x75, 0x51, + 0xa5, 0xa8, 0xdb, 0x28, 0x2c, 0x2a, 0x59, 0x30, 0xad, 0x21, 0x4d, 0x74, 0x18, 0xfd, 0x49, 0xce, 0x40, 0x41, 0xc8, + 0x2f, 0x9b, 0x06, 0x18, 0xa9, 0xe4, 0xf2, 0xa0, 0x4a, 0x02, 0x2f, 0xc0, 0x36, 0xa8, 0xd8, 0xba, 0x80, 0x20, 0xdb, + 0xa4, 0x28, 0xd3, 0xaf, 0x45, 0x5e, 0x87, 0x59, 0x50, 0x8d, 0xd2, 0xea, 0x27, 0xfd, 0x13, 0x98, 0xb7, 0xa9, 0x18, + 0xd5, 0x2a, 0x26, 0xbf, 0xd1, 0xef, 0x17, 0x83, 0xd6, 0x87, 0x0c, 0x3e, 0x7a, 0x6d, 0x1a, 0xfc, 0xda, 0x69, 0xb0, + 0xc3, 0x44, 0x23, 0x00, 0x92, 0x39, 0xb5, 0xe4, 0xa1, 0xe8, 0xcf, 0x20, 0xc7, 0x1a, 0x55, 0x4e, 0xc1, 0x60, 0xfd, + 0xc7, 0xa3, 0x1d, 0x98, 0x7a, 0x71, 0xb4, 0x25, 0x3b, 0x68, 0xe5, 0x1b, 0xe0, 0x7e, 0x8d, 0x6c, 0x31, 0xcb, 0x01, + 0x9a, 0xbd, 0x46, 0x64, 0x7c, 0xf2, 0x02, 0x18, 0xb3, 0x75, 0x16, 0x46, 0x22, 0x0e, 0xc6, 0xaa, 0x31, 0x63, 0x06, + 0x06, 0x2e, 0xd0, 0xb5, 0x4c, 0x4a, 0xd2, 0x90, 0x0e, 0x06, 0xac, 0x94, 0x2d, 0x1c, 0xf0, 0xa2, 0x39, 0x6e, 0xc7, + 0xbb, 0x16, 0x8d, 0x07, 0xb6, 0x8b, 0xed, 0xef, 0x5e, 0x14, 0xdb, 0xb7, 0xe1, 0x96, 0xf4, 0x0a, 0x39, 0x4b, 0xe8, + 0xe7, 0x4f, 0xb2, 0xcf, 0x1a, 0x4e, 0x4e, 0x85, 0x66, 0x68, 0x29, 0x12, 0x4a, 0xf1, 0x4e, 0x4f, 0x0a, 0x8c, 0x65, + 0x2c, 0xfc, 0x3d, 0x70, 0x4e, 0x17, 0x8a, 0xc8, 0x1d, 0x38, 0x8e, 0xaf, 0xa1, 0x82, 0xe0, 0xbf, 0x00, 0xb3, 0x18, + 0x20, 0x4f, 0x67, 0x21, 0xe1, 0x14, 0xc2, 0xc5, 0x2a, 0xeb, 0xf7, 0xe5, 0x2f, 0xea, 0xa2, 0x8b, 0x4c, 0xd6, 0x7d, + 0x12, 0x8e, 0xcc, 0x58, 0x4e, 0xbd, 0x90, 0x3c, 0xef, 0x79, 0x32, 0x4d, 0x9e, 0xe4, 0x41, 0x04, 0x90, 0xcf, 0xe1, + 0x5d, 0x98, 0x66, 0x60, 0x95, 0x26, 0xe5, 0x47, 0x28, 0x7d, 0xf1, 0x79, 0xe5, 0x07, 0x3a, 0x7b, 0x6e, 0x92, 0xe1, + 0xcd, 0xaa, 0xf5, 0x26, 0xb5, 0xae, 0x8b, 0x07, 0xfc, 0xab, 0x33, 0xd8, 0x38, 0xd7, 0x99, 0xe0, 0xc0, 0x8b, 0xa4, + 0xd6, 0x6b, 0xc6, 0x9f, 0x65, 0xb8, 0x2e, 0x55, 0x1b, 0x7d, 0x14, 0xa2, 0x73, 0xc8, 0x54, 0x80, 0x42, 0x91, 0xf6, + 0x0f, 0x4a, 0xad, 0x4c, 0x2a, 0x6d, 0x24, 0x80, 0xee, 0x61, 0xd2, 0x60, 0x8b, 0xa1, 0x8c, 0xa5, 0x49, 0x94, 0x3b, + 0x0d, 0xe2, 0xca, 0x7e, 0xac, 0x24, 0x0e, 0x2d, 0x8b, 0xe4, 0xdf, 0xbb, 0x9e, 0xbe, 0x42, 0xea, 0x4e, 0x16, 0xc8, + 0x8c, 0xf1, 0x3c, 0x8f, 0x3f, 0x01, 0x61, 0x36, 0x68, 0xa3, 0xa2, 0x10, 0x42, 0x36, 0x88, 0x41, 0xe3, 0x79, 0x1e, + 0xbf, 0x50, 0x34, 0x1e, 0xf2, 0x51, 0xe4, 0xab, 0xbf, 0x4a, 0xfd, 0x57, 0xe8, 0x33, 0x13, 0x3c, 0x42, 0x35, 0xd1, + 0xbf, 0x7b, 0x3e, 0xbb, 0x03, 0xb5, 0x61, 0x14, 0x66, 0xa6, 0xfc, 0xca, 0x37, 0xc5, 0xd9, 0xeb, 0xaf, 0xe8, 0x2a, + 0xdb, 0xba, 0x1f, 0xbd, 0x3e, 0x22, 0xb0, 0x36, 0x46, 0x57, 0xdc, 0x18, 0x40, 0x0e, 0x93, 0xf7, 0x2b, 0x4a, 0xcb, + 0x21, 0x0d, 0x42, 0x07, 0x0d, 0x41, 0xaf, 0x24, 0xfa, 0x40, 0x62, 0x11, 0x63, 0x78, 0x21, 0x9e, 0x91, 0x9a, 0x4c, + 0x34, 0xc4, 0x2b, 0x62, 0x3f, 0x44, 0x4b, 0x4e, 0x4d, 0x74, 0x23, 0x4c, 0x31, 0x90, 0xd8, 0x19, 0x24, 0x27, 0x49, + 0xad, 0xfc, 0xe2, 0x99, 0x24, 0x2c, 0xb1, 0xf3, 0x10, 0x83, 0x49, 0x2d, 0xdd, 0xe9, 0x4d, 0x95, 0xbe, 0x1c, 0x69, + 0x39, 0x68, 0x1f, 0x80, 0x5d, 0x4a, 0x7a, 0xff, 0xa4, 0x50, 0xc4, 0x87, 0x30, 0x8e, 0x21, 0x7c, 0x8b, 0xa8, 0xae, + 0xc0, 0xb9, 0x56, 0xa0, 0xb1, 0x1a, 0x78, 0x68, 0x66, 0xd5, 0x7c, 0xc8, 0xe9, 0xa7, 0xd2, 0xf2, 0xc7, 0x88, 0xc6, + 0x46, 0xeb, 0xe6, 0x70, 0xd8, 0xd3, 0xaa, 0x97, 0xce, 0x41, 0x97, 0xcd, 0x24, 0x26, 0x6e, 0x20, 0x5d, 0x3f, 0xfa, + 0xcd, 0x84, 0xbd, 0x88, 0x0a, 0xb9, 0x14, 0x82, 0x82, 0x56, 0x07, 0x02, 0x87, 0xc2, 0x5b, 0x94, 0xf9, 0x22, 0xa6, + 0x0d, 0x84, 0xc1, 0xe7, 0x07, 0xf2, 0xf3, 0x4d, 0x41, 0x2a, 0x76, 0xac, 0x6b, 0xbf, 0xbf, 0x28, 0x3d, 0xc0, 0x93, + 0x33, 0x49, 0x9e, 0x36, 0x43, 0x58, 0x11, 0x40, 0x63, 0x56, 0x93, 0xc5, 0x09, 0x57, 0xe6, 0xf0, 0x75, 0xe5, 0x95, + 0x2c, 0x65, 0xea, 0x3c, 0xd5, 0x0b, 0x20, 0xea, 0x78, 0x83, 0x56, 0xa4, 0x7e, 0x85, 0xce, 0x5e, 0xb3, 0x12, 0x32, + 0x1e, 0x9e, 0x73, 0x9e, 0x8e, 0xee, 0x59, 0xc2, 0x23, 0xfc, 0x2b, 0x99, 0xe8, 0xc3, 0xef, 0x9e, 0xc3, 0xcd, 0x38, + 0xe1, 0x91, 0xdb, 0xec, 0x7d, 0x15, 0xae, 0xe0, 0x66, 0x5a, 0x00, 0x92, 0x5b, 0x90, 0x34, 0x01, 0x25, 0x24, 0x32, + 0x21, 0xb3, 0xa6, 0xe4, 0x8b, 0x96, 0xb6, 0xc1, 0x1a, 0x26, 0x9d, 0x07, 0xbc, 0x68, 0xf5, 0xd1, 0x6a, 0xa2, 0x5d, + 0x66, 0xf9, 0x7c, 0x88, 0x33, 0x54, 0x73, 0xdc, 0x9d, 0xc1, 0xcf, 0x01, 0xaf, 0x58, 0xd5, 0xa4, 0xa3, 0xdd, 0x80, + 0x0b, 0x4f, 0xae, 0xf3, 0x74, 0xb4, 0xc5, 0x5f, 0x72, 0x7f, 0x00, 0xe8, 0x60, 0xea, 0x12, 0xf8, 0x53, 0xb5, 0xd5, + 0x54, 0xea, 0xb7, 0xd6, 0x7e, 0x5d, 0x77, 0x56, 0x2b, 0xf7, 0xac, 0xcb, 0xd0, 0x1e, 0x19, 0x72, 0xc6, 0x0c, 0xf8, + 0x73, 0xc6, 0x92, 0x3f, 0x67, 0xac, 0xf8, 0x73, 0xc6, 0x8d, 0x91, 0x01, 0x94, 0xe0, 0x5e, 0xf2, 0x67, 0x7b, 0xc4, + 0x0c, 0xb1, 0x1a, 0x54, 0x02, 0x2b, 0x4b, 0x39, 0xf7, 0x91, 0x53, 0x4c, 0x39, 0x65, 0x78, 0xe9, 0x74, 0xe6, 0x0e, + 0xe4, 0x3c, 0x98, 0xb9, 0xc3, 0x64, 0xaf, 0xcf, 0x8d, 0x38, 0x96, 0xc6, 0xa4, 0xa8, 0x20, 0x9d, 0xd3, 0xe1, 0xe6, + 0xd5, 0x71, 0x9e, 0xb0, 0x8c, 0x8f, 0xdb, 0x67, 0x0a, 0x84, 0xd8, 0xe2, 0x19, 0x12, 0x29, 0x55, 0xb3, 0xdc, 0xe6, + 0x0f, 0x87, 0x7a, 0x74, 0xaf, 0x77, 0x7a, 0xf8, 0x95, 0xb0, 0xdf, 0x32, 0xcf, 0x3e, 0x41, 0x00, 0x93, 0x44, 0x9e, + 0x49, 0x38, 0xfa, 0xb1, 0x1c, 0xfd, 0x4d, 0xc3, 0xbf, 0x64, 0xa8, 0xee, 0x0e, 0x81, 0x89, 0x2d, 0x3b, 0x70, 0x08, + 0x4e, 0x57, 0x95, 0x48, 0xc0, 0xc1, 0x66, 0xc3, 0x22, 0xbd, 0xc7, 0x43, 0x9c, 0x0f, 0x0a, 0x1f, 0xa1, 0x61, 0x46, + 0xef, 0xf7, 0x37, 0xc2, 0xab, 0x64, 0x2b, 0x0f, 0x87, 0xc4, 0xba, 0x0b, 0x3b, 0xfa, 0x38, 0xda, 0xa3, 0x84, 0xda, + 0x8f, 0x6a, 0xbd, 0xa9, 0xd4, 0x83, 0xdc, 0xec, 0x42, 0x62, 0x50, 0xb1, 0x54, 0x9f, 0x5e, 0xa9, 0x3e, 0xd4, 0xac, + 0xf3, 0xbb, 0x3a, 0xee, 0x53, 0x31, 0x5a, 0xcb, 0x09, 0x01, 0xae, 0x83, 0x44, 0xa3, 0x03, 0x60, 0x9c, 0x6d, 0xb6, + 0xbc, 0xd4, 0xd6, 0x89, 0xd2, 0x71, 0x9c, 0xeb, 0xe3, 0xf8, 0x70, 0x90, 0x62, 0xc6, 0xe5, 0x91, 0x98, 0x71, 0xd9, + 0x00, 0xbc, 0x59, 0xe7, 0x41, 0x7d, 0x38, 0x5c, 0xd2, 0xa5, 0xc8, 0x74, 0xb6, 0x51, 0x7e, 0xd6, 0xa3, 0xfb, 0x27, + 0x09, 0x9a, 0x7b, 0x2b, 0xec, 0xbd, 0x48, 0xb6, 0x67, 0xb2, 0x4e, 0xbd, 0x8c, 0x7c, 0x7a, 0xe1, 0x9e, 0x5d, 0x72, + 0xf5, 0xc3, 0xea, 0xeb, 0xe9, 0x67, 0xe1, 0x45, 0xac, 0xa2, 0xdd, 0xba, 0x64, 0xc2, 0xde, 0x52, 0x2a, 0x69, 0x95, + 0x97, 0x4f, 0x37, 0x7e, 0x80, 0x99, 0x69, 0x4f, 0x1f, 0x64, 0x23, 0xaa, 0x3f, 0x2b, 0x51, 0x2b, 0xc3, 0x64, 0xe1, + 0xbc, 0x64, 0xea, 0xc9, 0x80, 0xc7, 0xac, 0xe4, 0x91, 0xec, 0xf4, 0xc6, 0x20, 0x08, 0x60, 0x9d, 0x93, 0x56, 0x9d, + 0x71, 0x34, 0x5a, 0x55, 0x2e, 0x4e, 0x57, 0xb9, 0xc0, 0x70, 0xbb, 0x35, 0xdb, 0xa8, 0x3a, 0xcb, 0x4d, 0xad, 0x52, + 0xbe, 0x03, 0xf8, 0x58, 0x56, 0xb9, 0xa0, 0x63, 0xca, 0xd4, 0x79, 0x03, 0xc1, 0xd8, 0xaa, 0xc6, 0x85, 0x53, 0xe3, + 0x82, 0x47, 0xd4, 0xee, 0xa6, 0xa9, 0x47, 0x5b, 0x60, 0x29, 0x1d, 0xed, 0x78, 0x89, 0x2a, 0x85, 0x9f, 0x05, 0xdf, + 0x87, 0x71, 0xfc, 0xa2, 0xd8, 0xaa, 0x03, 0xf1, 0xb6, 0xd8, 0x22, 0xed, 0x8b, 0xfc, 0x0b, 0x71, 0xc0, 0x6b, 0x5d, + 0x53, 0x5e, 0x5b, 0x73, 0x1a, 0xd8, 0x1a, 0x46, 0x4a, 0x0a, 0xe7, 0xe6, 0xcf, 0xc3, 0x81, 0x56, 0x76, 0xad, 0xee, + 0x0a, 0xb5, 0x1e, 0x73, 0xd8, 0xb0, 0x6f, 0xb2, 0x70, 0x27, 0x4a, 0x70, 0xe4, 0x92, 0x7f, 0x1d, 0x0e, 0x5a, 0x65, + 0xa9, 0x8e, 0xf4, 0xd9, 0xfe, 0x6b, 0x30, 0x66, 0xe8, 0xd2, 0x04, 0x2c, 0x1b, 0x23, 0xf9, 0x57, 0xd3, 0xcc, 0x1b, + 0x26, 0x6b, 0xa6, 0x70, 0x1c, 0x1a, 0x46, 0x48, 0x03, 0xba, 0x0d, 0x6a, 0xc3, 0x93, 0xf9, 0xa6, 0x2a, 0xbf, 0xba, + 0x23, 0xd5, 0x7e, 0x30, 0xbc, 0x9c, 0x88, 0x73, 0xba, 0x24, 0xa9, 0xa7, 0x12, 0x4a, 0x42, 0xb0, 0x4b, 0x1f, 0xc8, + 0x89, 0x15, 0x90, 0xb5, 0x8c, 0xe5, 0xb7, 0x7a, 0x40, 0xe8, 0x3f, 0xed, 0xd6, 0x0b, 0xfd, 0xa7, 0x69, 0xb6, 0x50, + 0xd7, 0x1f, 0x26, 0xf7, 0x1d, 0xbd, 0xfe, 0xe0, 0xf0, 0x4e, 0x5d, 0x55, 0x5c, 0xc5, 0xa3, 0xda, 0x30, 0xc9, 0x8d, + 0xb2, 0x70, 0x57, 0x6c, 0x6a, 0xb5, 0x3c, 0x1d, 0x87, 0x11, 0x98, 0x11, 0x14, 0x20, 0xeb, 0xba, 0x8d, 0x88, 0x61, + 0x25, 0x97, 0x09, 0xf9, 0x84, 0x80, 0x2c, 0x4a, 0x8d, 0xf3, 0x71, 0x0b, 0x54, 0x22, 0x18, 0x9c, 0x86, 0xd6, 0xaa, + 0x9b, 0xfc, 0xa4, 0xb2, 0xb1, 0x25, 0x90, 0x43, 0x92, 0xc9, 0x62, 0x39, 0xba, 0x15, 0x8b, 0xa2, 0x14, 0xbf, 0x60, + 0x3d, 0x5c, 0xb3, 0x85, 0xfb, 0x0c, 0x08, 0xed, 0x27, 0x4a, 0x7b, 0x13, 0x69, 0x82, 0xee, 0x25, 0x5b, 0x01, 0xc8, + 0x00, 0x8a, 0xba, 0xda, 0xad, 0xcf, 0xf9, 0x39, 0x92, 0x66, 0x38, 0x8c, 0x6e, 0x9f, 0x2e, 0x83, 0xe5, 0xe0, 0x12, + 0xb5, 0xd2, 0x97, 0x2c, 0x6e, 0x61, 0x50, 0xed, 0xcd, 0x12, 0x0e, 0x6a, 0x66, 0xad, 0x8d, 0x40, 0x30, 0xd9, 0x43, + 0x41, 0xc5, 0x5c, 0xc1, 0x3e, 0x28, 0x58, 0x4b, 0x5e, 0x07, 0x87, 0x5b, 0xfb, 0xb2, 0x52, 0x5c, 0x3c, 0xbd, 0x48, + 0x5a, 0x17, 0x96, 0xf2, 0xe2, 0x69, 0x03, 0x06, 0x97, 0x23, 0x6c, 0x2a, 0x30, 0x49, 0x00, 0xe8, 0x56, 0x44, 0x11, + 0x2f, 0x4a, 0x61, 0xdb, 0xca, 0x67, 0x4e, 0xd8, 0x60, 0xc3, 0xee, 0xe1, 0x5e, 0x19, 0x94, 0x0c, 0x2e, 0xc4, 0xb8, + 0xdd, 0xec, 0x02, 0x5c, 0xc1, 0x50, 0x18, 0x5b, 0xf3, 0x77, 0x99, 0x17, 0x29, 0x01, 0x37, 0x43, 0x94, 0xaf, 0x0d, + 0x9c, 0x4c, 0x7a, 0x72, 0x2d, 0x58, 0x0c, 0x58, 0xd0, 0xe0, 0x3b, 0x6a, 0xfd, 0x9d, 0xc9, 0xbf, 0xf1, 0xf4, 0xd0, + 0x0f, 0x5e, 0x64, 0xde, 0xc2, 0x67, 0xef, 0x2a, 0x19, 0xad, 0x49, 0xa2, 0xbc, 0x7a, 0xb8, 0x00, 0xb9, 0x61, 0x31, + 0xba, 0x67, 0x0b, 0x10, 0x27, 0x16, 0xa3, 0x84, 0x32, 0xba, 0xc2, 0xbd, 0xca, 0x6c, 0x99, 0x08, 0xa4, 0x38, 0xb0, + 0x90, 0x72, 0x6f, 0xb1, 0x0e, 0x16, 0xb8, 0x3f, 0x91, 0x5c, 0x40, 0xc9, 0x03, 0x28, 0x57, 0x0a, 0x08, 0xf8, 0x74, + 0x00, 0xe5, 0x4b, 0x79, 0x11, 0xfe, 0xc4, 0x89, 0x1a, 0x2c, 0x46, 0xf7, 0x0d, 0xfb, 0xc9, 0x0b, 0x2d, 0xfb, 0xc3, + 0x52, 0x6b, 0x1a, 0x56, 0x7c, 0x09, 0xd3, 0x62, 0xe2, 0xf6, 0xe5, 0xca, 0xae, 0x8a, 0xcf, 0x56, 0xea, 0xec, 0xa6, + 0x86, 0x24, 0xec, 0x1b, 0xb2, 0x0a, 0x70, 0xb0, 0x2a, 0xe2, 0x9e, 0x75, 0xb9, 0x0f, 0xa3, 0xbf, 0x36, 0x69, 0x29, + 0x2c, 0x54, 0x49, 0x7f, 0xdf, 0x94, 0x02, 0xa9, 0x4c, 0x74, 0xa2, 0x85, 0xe0, 0x0a, 0x0c, 0x02, 0x77, 0x22, 0xaf, + 0x01, 0x30, 0x06, 0x5c, 0x0a, 0x94, 0x65, 0x5b, 0x42, 0x48, 0x75, 0x3f, 0x03, 0xb5, 0x9d, 0xb8, 0x4b, 0x23, 0xb2, + 0x16, 0xa2, 0xaf, 0x82, 0x31, 0x73, 0x5e, 0x4a, 0xb7, 0xd8, 0x74, 0xb5, 0x59, 0x5d, 0xa3, 0x73, 0x69, 0xcb, 0xcd, + 0x4f, 0xd8, 0x62, 0xad, 0x40, 0xd9, 0x84, 0xa4, 0xed, 0x9c, 0xe7, 0x28, 0x9b, 0xd0, 0xd2, 0xde, 0x53, 0x8f, 0x0a, + 0xd5, 0xc9, 0xd6, 0x4b, 0xd5, 0xd4, 0x22, 0xac, 0x16, 0x17, 0x95, 0x1f, 0x80, 0x6e, 0x2a, 0xad, 0x9e, 0xd7, 0x35, + 0x9a, 0x42, 0xad, 0x16, 0x8e, 0x1b, 0xed, 0x6c, 0xba, 0x48, 0x97, 0x88, 0xb3, 0x2a, 0xed, 0xd0, 0x3f, 0x65, 0xda, + 0xf5, 0xb2, 0xa3, 0xdf, 0x8c, 0xab, 0x0b, 0x5c, 0x88, 0x0d, 0xf8, 0x9c, 0xfb, 0xcb, 0xeb, 0x3d, 0x8d, 0x7b, 0xfe, + 0xe1, 0x80, 0xec, 0x49, 0xed, 0x0f, 0xd5, 0xc7, 0xae, 0x60, 0xc8, 0xc2, 0x28, 0xf5, 0x17, 0x29, 0xef, 0x3d, 0xc2, + 0x71, 0xff, 0x52, 0xf5, 0xd8, 0xaf, 0x19, 0xdf, 0xd7, 0xc5, 0x26, 0x4a, 0x28, 0xaa, 0xa1, 0xb7, 0x2a, 0x36, 0x95, + 0x88, 0x8b, 0xfb, 0xbc, 0xc7, 0x30, 0x19, 0xc6, 0x42, 0xa6, 0xc2, 0x9f, 0x32, 0x15, 0x3c, 0x42, 0x28, 0x71, 0xb3, + 0xee, 0x91, 0x76, 0x13, 0xe2, 0x94, 0x6a, 0x51, 0xca, 0x64, 0xfc, 0x5b, 0x3f, 0x81, 0xf2, 0x9c, 0xa2, 0x65, 0xfa, + 0x51, 0xe1, 0x32, 0x7d, 0xb3, 0x3e, 0x2e, 0x3d, 0x13, 0xa1, 0xce, 0x5c, 0x6c, 0x6a, 0x9d, 0x8e, 0xb1, 0x53, 0x3a, + 0xb5, 0x61, 0x5f, 0x2b, 0xc5, 0x65, 0x45, 0xe1, 0xdf, 0x48, 0x64, 0xd5, 0x33, 0xe2, 0xf8, 0x3f, 0xb3, 0xf6, 0x19, + 0x56, 0x81, 0x5f, 0x06, 0xf2, 0x7e, 0x01, 0xf0, 0x71, 0x5d, 0x97, 0xe9, 0xed, 0x06, 0x68, 0x43, 0x68, 0xf8, 0x7b, + 0x3e, 0x32, 0x60, 0xba, 0x8f, 0x70, 0x86, 0xf4, 0x50, 0xe7, 0x9c, 0xce, 0xca, 0x74, 0xce, 0x55, 0x58, 0x4b, 0xb0, + 0x97, 0x93, 0x26, 0x97, 0xeb, 0x12, 0xd4, 0x4c, 0xe0, 0xf6, 0xa1, 0x3d, 0x22, 0x84, 0xda, 0x94, 0xd5, 0xf4, 0x12, + 0x6a, 0xde, 0xc9, 0x69, 0x47, 0x93, 0x12, 0x5c, 0x35, 0x74, 0x56, 0xae, 0xff, 0x3a, 0x1c, 0x7a, 0xb7, 0x59, 0x11, + 0xfd, 0xd9, 0x43, 0x7f, 0xc7, 0xed, 0x75, 0xfa, 0x15, 0xa2, 0x65, 0xac, 0xbf, 0x21, 0x03, 0x3a, 0x9e, 0x0c, 0x6f, + 0x8b, 0x6d, 0x8f, 0x7d, 0x45, 0x0d, 0x96, 0xbe, 0x7e, 0x5c, 0x83, 0x84, 0xaa, 0x6b, 0x5f, 0x58, 0x3c, 0x61, 0x9e, + 0x12, 0x6d, 0x0b, 0x1f, 0xc2, 0x42, 0xbf, 0x42, 0x64, 0x24, 0x84, 0x9b, 0xca, 0xee, 0x51, 0xd2, 0x2e, 0xf4, 0xa5, + 0xaf, 0x65, 0x5f, 0xf9, 0xce, 0x05, 0xc0, 0xca, 0x3e, 0xb5, 0xe1, 0x9e, 0xf4, 0xa7, 0x54, 0x1f, 0xb6, 0xbf, 0x25, + 0x0b, 0x28, 0xb4, 0xb0, 0x9e, 0xca, 0xd9, 0xb9, 0x2c, 0x79, 0x9e, 0x4d, 0xf7, 0x6b, 0xd8, 0xa3, 0xee, 0xd0, 0x6b, + 0x2a, 0x38, 0xbf, 0x34, 0xa3, 0xf7, 0xbb, 0xa1, 0x50, 0x1d, 0x75, 0xee, 0x20, 0xcb, 0xd2, 0xba, 0xe4, 0xfc, 0x65, + 0xe5, 0x8e, 0xc2, 0xfc, 0x2e, 0x04, 0xcf, 0xb0, 0xee, 0xdd, 0xc5, 0x79, 0xef, 0x73, 0x6b, 0x8e, 0xfc, 0x9a, 0xcd, + 0x52, 0xc4, 0x22, 0x99, 0x83, 0xd5, 0x0f, 0xfd, 0x3c, 0xf6, 0xdb, 0x20, 0x87, 0xe3, 0xa6, 0x01, 0x1d, 0x36, 0x64, + 0xd6, 0xbe, 0x44, 0xe0, 0x54, 0x23, 0x48, 0x53, 0x13, 0xd4, 0x2c, 0x0f, 0x91, 0xd8, 0x2e, 0x65, 0xdb, 0x20, 0xd7, + 0x5d, 0x30, 0xcd, 0x91, 0xf6, 0x0c, 0xde, 0x37, 0x69, 0x92, 0x0a, 0xcd, 0xa2, 0x8b, 0x95, 0x8c, 0x7f, 0x47, 0xda, + 0x4c, 0xc9, 0x1e, 0x5b, 0x03, 0xef, 0x25, 0x28, 0x27, 0xc3, 0x14, 0xc3, 0x77, 0x7c, 0xbd, 0xf3, 0xe8, 0x22, 0x7e, + 0x3e, 0x66, 0x9b, 0x94, 0x1d, 0xc1, 0x24, 0xd9, 0xf8, 0x86, 0xe2, 0x0d, 0xdf, 0xdf, 0x56, 0xa2, 0x04, 0xd0, 0xcb, + 0x82, 0x3f, 0x93, 0x36, 0x57, 0xe8, 0x76, 0xf7, 0x8e, 0x52, 0xf8, 0x25, 0x2f, 0x0f, 0x87, 0x6d, 0xea, 0x85, 0xd0, + 0xf9, 0x22, 0x7e, 0x07, 0xe6, 0x30, 0x86, 0xd8, 0x8c, 0x00, 0x61, 0x8e, 0x0f, 0xa8, 0x83, 0xf5, 0x23, 0x00, 0x8d, + 0x13, 0x28, 0xc0, 0xe8, 0xab, 0x6d, 0x41, 0xdf, 0xf2, 0xe2, 0x22, 0x42, 0xd4, 0x28, 0xc0, 0x44, 0x49, 0xb3, 0x18, + 0x86, 0x03, 0x9d, 0xdf, 0x37, 0xb7, 0x75, 0x29, 0x70, 0xe8, 0x1d, 0xcb, 0xf0, 0xdf, 0xfe, 0xc7, 0xda, 0xd2, 0xaa, + 0xb2, 0xdd, 0x1a, 0xa7, 0x99, 0xff, 0xed, 0xb6, 0xd0, 0xf7, 0x5f, 0x0a, 0xc5, 0xf3, 0x8e, 0xd7, 0xed, 0x2f, 0x10, + 0xbd, 0xaf, 0x5b, 0xb9, 0x2a, 0xb5, 0x1b, 0x66, 0xca, 0xef, 0xd3, 0x3c, 0x2e, 0xee, 0x47, 0x71, 0xeb, 0xc8, 0x9b, + 0xa4, 0xe7, 0x9c, 0x7f, 0xa9, 0xfa, 0x7d, 0xef, 0x0b, 0x90, 0xf1, 0xbe, 0x14, 0xc6, 0x11, 0x93, 0x38, 0xf8, 0xf6, + 0x62, 0x14, 0x6d, 0x4a, 0xd8, 0x90, 0xdb, 0xa7, 0x25, 0x68, 0x66, 0xfa, 0x7d, 0x94, 0x28, 0xad, 0xf9, 0xfe, 0x0f, + 0x39, 0xdf, 0x5f, 0x0a, 0x79, 0xb3, 0x92, 0x1f, 0x3e, 0x5a, 0x61, 0xe0, 0x7b, 0x9c, 0x7e, 0x15, 0x3d, 0xb6, 0x2a, + 0x7d, 0xf8, 0xae, 0xb4, 0xf4, 0x59, 0x45, 0xfd, 0x0b, 0x15, 0x35, 0x2f, 0xc5, 0x88, 0x88, 0x07, 0x41, 0x3b, 0xdb, + 0x2e, 0xb5, 0x6b, 0x09, 0xda, 0x05, 0x9b, 0xc2, 0xfe, 0xfe, 0xe0, 0x90, 0xf7, 0xfb, 0x1f, 0x73, 0xaf, 0xc5, 0xeb, + 0x6e, 0xe0, 0x2e, 0x4b, 0x0f, 0x21, 0x80, 0xb5, 0x0c, 0x94, 0x71, 0x84, 0x49, 0x17, 0x79, 0x8d, 0xb2, 0xe9, 0x44, + 0xe0, 0x63, 0x96, 0x5d, 0x39, 0xc9, 0x34, 0xc0, 0x8c, 0x6a, 0x0a, 0x33, 0x01, 0x46, 0xea, 0x23, 0xd6, 0x4d, 0x4f, + 0xab, 0xd0, 0xf2, 0x35, 0x04, 0xeb, 0x22, 0xcb, 0x38, 0x8a, 0x99, 0x00, 0x60, 0xf3, 0x11, 0xe4, 0x2b, 0xba, 0x3a, + 0x24, 0xad, 0x54, 0x79, 0xbf, 0xce, 0x88, 0x8c, 0x26, 0x21, 0x9a, 0xdf, 0xc2, 0x03, 0xfb, 0xb6, 0x99, 0x51, 0xa5, + 0x9e, 0x51, 0x95, 0xcf, 0x70, 0x58, 0x0a, 0xc7, 0x88, 0xff, 0x73, 0xaa, 0x7a, 0x44, 0xa0, 0x57, 0x65, 0x5a, 0x45, + 0x45, 0x9e, 0x8b, 0x08, 0x11, 0xaa, 0xa5, 0x73, 0x38, 0xf4, 0x63, 0xbf, 0x8f, 0x03, 0x61, 0x5e, 0xac, 0x93, 0x07, + 0xba, 0xb2, 0xa6, 0xb5, 0x92, 0x02, 0xa7, 0xa2, 0x46, 0x88, 0x10, 0xde, 0x67, 0xe0, 0x59, 0x4d, 0x7d, 0xbf, 0xb1, + 0x4c, 0x74, 0xbf, 0x67, 0x40, 0xf9, 0x03, 0xf2, 0x75, 0x25, 0xc5, 0x19, 0x91, 0x3c, 0x24, 0xce, 0x38, 0x00, 0x31, + 0xdf, 0x96, 0x68, 0x34, 0xf6, 0x3f, 0x20, 0xc1, 0x50, 0xfd, 0x60, 0xa7, 0x9b, 0x7a, 0xff, 0xcc, 0x24, 0x8e, 0xa2, + 0x4f, 0xdb, 0xe4, 0xb1, 0x64, 0x69, 0xb4, 0x70, 0xf4, 0x1e, 0x31, 0x8c, 0xc3, 0xe9, 0x7c, 0x4c, 0xb2, 0x8d, 0xc9, + 0x2a, 0x80, 0x74, 0x32, 0x53, 0xc7, 0x94, 0x3a, 0x1a, 0xe7, 0x7a, 0x41, 0x15, 0x7a, 0xac, 0x4b, 0x9e, 0x83, 0xf5, + 0xe4, 0x47, 0xaf, 0xf4, 0xa7, 0x42, 0xce, 0x61, 0x23, 0x11, 0x14, 0x7e, 0x80, 0xab, 0xc1, 0x4a, 0x01, 0x83, 0xa9, + 0x6f, 0xe1, 0x6b, 0xe2, 0x39, 0x0a, 0x1e, 0x85, 0x5d, 0x8c, 0xad, 0x95, 0xef, 0x7c, 0x52, 0x50, 0xee, 0x59, 0x31, + 0xe7, 0x15, 0x70, 0x2e, 0x83, 0x42, 0x98, 0x8e, 0x67, 0xf9, 0x3f, 0x93, 0xbc, 0x9e, 0xd8, 0x10, 0x20, 0x83, 0x3f, + 0x25, 0x4e, 0x4b, 0x77, 0xe8, 0xce, 0x43, 0xcf, 0x22, 0x0e, 0x1b, 0x3d, 0x5a, 0x97, 0xc5, 0x36, 0x45, 0xbd, 0x84, + 0xf9, 0x81, 0xfc, 0xbc, 0x25, 0xdf, 0x87, 0x28, 0xde, 0x06, 0x3f, 0x67, 0x2c, 0x16, 0xf8, 0xd7, 0xdf, 0x32, 0x46, + 0x13, 0x2d, 0xf8, 0x7b, 0xd6, 0x20, 0x51, 0x31, 0x60, 0x45, 0x00, 0x97, 0xa9, 0xfa, 0xf0, 0x29, 0x31, 0xde, 0x9a, + 0x0d, 0x0f, 0x7c, 0xb3, 0x02, 0x9d, 0xfa, 0xdc, 0x5d, 0xd9, 0x9e, 0xae, 0x46, 0xaa, 0xaa, 0xf1, 0x73, 0xaa, 0xaa, + 0xf1, 0x73, 0x4a, 0xd5, 0xf8, 0x2b, 0xa3, 0xf8, 0x9d, 0xca, 0x67, 0xc8, 0x9c, 0x6c, 0x62, 0x92, 0x4e, 0xdf, 0x1b, + 0x4e, 0xec, 0xb2, 0xdf, 0xba, 0x4d, 0xa4, 0x99, 0x89, 0x14, 0x72, 0x6f, 0x00, 0x6a, 0x26, 0x7e, 0xcc, 0x0d, 0xa7, + 0xc4, 0xf9, 0xb9, 0x87, 0x2b, 0x36, 0xad, 0x5e, 0xd2, 0x82, 0x05, 0x36, 0x2f, 0xb3, 0x3c, 0xd3, 0x04, 0xb6, 0x4d, + 0x99, 0xf5, 0x97, 0xdc, 0x03, 0x08, 0x66, 0x52, 0x13, 0x00, 0xd2, 0x42, 0x54, 0x0a, 0x91, 0xbf, 0xc4, 0x59, 0x7d, + 0xce, 0x7b, 0x9b, 0x3c, 0x26, 0xd2, 0xea, 0x5e, 0xbf, 0x9f, 0x9e, 0xa5, 0x39, 0x05, 0x35, 0x1c, 0x67, 0x9d, 0xfe, + 0x94, 0x05, 0x22, 0x91, 0xab, 0xf4, 0x1f, 0x6e, 0x90, 0x97, 0xf1, 0x7d, 0xdd, 0xf6, 0xfc, 0x89, 0xfa, 0x7b, 0x67, + 0xfd, 0x6d, 0x81, 0xe0, 0x4e, 0x8e, 0xfd, 0x64, 0x55, 0xca, 0x23, 0xe3, 0xd2, 0xde, 0xf3, 0x9b, 0xba, 0x28, 0xb2, + 0x3a, 0x5d, 0x7f, 0x90, 0x7a, 0x1a, 0xdd, 0x17, 0x7b, 0x30, 0x06, 0xef, 0x00, 0xf0, 0x4c, 0x87, 0x06, 0x48, 0xdf, + 0x33, 0xf2, 0x70, 0x9f, 0x5b, 0xf2, 0x93, 0xca, 0xda, 0x24, 0x61, 0x45, 0xb1, 0x19, 0xc6, 0x08, 0x25, 0xe3, 0x34, + 0xb6, 0x7e, 0xbf, 0xaf, 0xfe, 0xde, 0x61, 0x14, 0x15, 0x15, 0x77, 0x8c, 0x46, 0x65, 0x55, 0x8f, 0xb6, 0x83, 0xc3, + 0xe1, 0x3c, 0xb7, 0x71, 0xb4, 0xf5, 0x0a, 0xd8, 0x5b, 0xa1, 0x52, 0xf6, 0x4a, 0x84, 0xe5, 0x87, 0x2b, 0xbf, 0xdf, + 0x87, 0x7f, 0x65, 0xa4, 0x85, 0xe7, 0x4f, 0xf1, 0xd7, 0x4d, 0x5d, 0x60, 0x78, 0x06, 0xad, 0xd1, 0x0a, 0x82, 0x09, + 0xfe, 0xd1, 0x81, 0x7a, 0x69, 0xa5, 0x7d, 0x04, 0xdd, 0x0a, 0xf4, 0xa0, 0xb1, 0x0f, 0x24, 0xed, 0x0b, 0x89, 0xba, + 0xbd, 0xd5, 0x69, 0xf4, 0x67, 0xc5, 0x72, 0x5e, 0xc1, 0xe4, 0x70, 0x43, 0x9f, 0x56, 0xe1, 0xf6, 0x13, 0x3c, 0xfd, + 0x05, 0x28, 0xb7, 0x0e, 0x87, 0x1c, 0xc4, 0x16, 0x70, 0xf3, 0x58, 0x85, 0x5f, 0x8a, 0x52, 0x46, 0xd4, 0xc7, 0xd3, + 0x12, 0xb4, 0x77, 0x01, 0x3a, 0x60, 0x69, 0x10, 0xaf, 0x90, 0x3c, 0x67, 0x23, 0x80, 0x65, 0x07, 0x96, 0xb3, 0x8c, + 0x53, 0x98, 0x67, 0xf9, 0xac, 0xd2, 0xf8, 0xec, 0x89, 0x57, 0xb3, 0x0c, 0x9c, 0x05, 0x2e, 0x2a, 0x9f, 0x65, 0x5a, + 0xf5, 0x54, 0x24, 0xe8, 0xf3, 0x4a, 0x4e, 0x70, 0x25, 0x38, 0xd9, 0x80, 0xfc, 0x02, 0x24, 0x69, 0x4a, 0x59, 0x53, + 0x3e, 0xbb, 0xa4, 0x1b, 0x32, 0x7a, 0xce, 0x7b, 0x5e, 0x34, 0x0c, 0xfd, 0x0b, 0xaf, 0x84, 0xf0, 0x4d, 0xdc, 0xb6, + 0x51, 0x0a, 0xfb, 0x9b, 0xc0, 0xe2, 0x13, 0xf6, 0xa3, 0xb7, 0xf0, 0xa7, 0xe3, 0x20, 0x1c, 0x22, 0x37, 0x54, 0xcc, + 0x81, 0x3d, 0x0d, 0x58, 0x6c, 0xe2, 0xab, 0xcd, 0x24, 0x1e, 0x0c, 0x7c, 0x9d, 0xb1, 0x98, 0xc5, 0x40, 0x83, 0x1c, + 0x0f, 0x2e, 0xe7, 0xfa, 0x84, 0xd0, 0x0f, 0x23, 0x2a, 0x47, 0x05, 0x3a, 0x07, 0xd1, 0x60, 0x01, 0x78, 0xea, 0xad, + 0x6c, 0x90, 0x64, 0x68, 0xa0, 0x13, 0xd7, 0x9a, 0xa4, 0x3a, 0x9c, 0xd0, 0x3a, 0xd0, 0x71, 0xf5, 0x06, 0x3a, 0x1f, + 0xd7, 0xbd, 0x8f, 0x57, 0xc3, 0x1b, 0x2a, 0xfd, 0x42, 0x0c, 0xbc, 0x7a, 0x3a, 0x0e, 0x2e, 0xe9, 0x56, 0x78, 0xb3, + 0x0a, 0xb7, 0xbf, 0xc8, 0x07, 0x8e, 0x3b, 0x2a, 0x69, 0x08, 0x0c, 0xde, 0x1e, 0xba, 0x9b, 0x19, 0xc7, 0x94, 0xa3, + 0xc3, 0x38, 0x92, 0x43, 0xac, 0x5a, 0x71, 0x21, 0xbd, 0x11, 0x7c, 0xbb, 0x50, 0x8c, 0x65, 0x63, 0x97, 0x86, 0xa2, + 0xf0, 0x67, 0x00, 0x3b, 0xd4, 0xfe, 0x4a, 0x25, 0x1f, 0x23, 0xa3, 0x9a, 0x06, 0x3a, 0x06, 0x60, 0xc9, 0xd2, 0x44, + 0x52, 0x45, 0x1a, 0x89, 0x3f, 0x32, 0x63, 0x1d, 0x35, 0x5d, 0x5f, 0xb0, 0x1c, 0x59, 0x92, 0x6e, 0x67, 0x12, 0xcb, + 0x89, 0x24, 0xb5, 0xdd, 0x47, 0xc4, 0x60, 0xe0, 0x83, 0x8d, 0x98, 0x66, 0x22, 0x1c, 0xf1, 0xa8, 0x44, 0x16, 0x5d, + 0x7e, 0x1b, 0x61, 0xd2, 0xf6, 0x65, 0x45, 0xb6, 0x20, 0x98, 0x9e, 0x44, 0x1f, 0x24, 0x29, 0xa7, 0x22, 0x91, 0x66, + 0x84, 0x00, 0x3f, 0x9e, 0x94, 0x57, 0xfa, 0x73, 0xd0, 0xb4, 0x12, 0xbc, 0x64, 0x90, 0x3c, 0x12, 0x3f, 0x93, 0x82, + 0x59, 0x8c, 0x55, 0x83, 0x01, 0x96, 0x53, 0x3d, 0x71, 0x4c, 0xd2, 0x7f, 0xeb, 0x74, 0xc2, 0x7e, 0xee, 0xe5, 0xb6, + 0x96, 0x37, 0xcd, 0xbd, 0xe7, 0x5e, 0xc5, 0x52, 0x0d, 0xcb, 0xa0, 0xff, 0x9a, 0x68, 0x17, 0x6c, 0x6d, 0x19, 0x13, + 0x56, 0xfd, 0x00, 0xd2, 0x1e, 0xe9, 0xf2, 0xaa, 0x61, 0xce, 0x04, 0x8f, 0x2e, 0xac, 0x79, 0x10, 0x5d, 0x08, 0x1f, + 0xb9, 0xec, 0x26, 0xc9, 0xd5, 0x78, 0xe2, 0x87, 0x83, 0x81, 0x02, 0xa0, 0xa5, 0x75, 0x52, 0x0c, 0xc2, 0x27, 0x42, + 0x0e, 0xa4, 0xd1, 0x51, 0x15, 0x60, 0xb1, 0xcc, 0xae, 0xca, 0x49, 0x36, 0x18, 0xf8, 0x20, 0x36, 0x26, 0x76, 0x43, + 0xb3, 0xb9, 0xcf, 0x4e, 0x14, 0x64, 0xb5, 0x39, 0x6a, 0xcd, 0x74, 0x0b, 0x0c, 0x00, 0x06, 0x11, 0xc1, 0x72, 0x9f, + 0x1a, 0xf9, 0x88, 0x3a, 0x3d, 0x85, 0x11, 0x10, 0xfc, 0x72, 0x22, 0x10, 0xb9, 0x48, 0xa0, 0x1e, 0x60, 0x26, 0xc0, + 0x8c, 0x2a, 0x86, 0x97, 0xc0, 0x2e, 0x9e, 0x9b, 0x57, 0x0c, 0xfa, 0x17, 0x89, 0xd9, 0x89, 0xa6, 0x12, 0x47, 0x63, + 0xe4, 0x54, 0x1a, 0x23, 0x03, 0x62, 0x17, 0xc7, 0xbf, 0xa7, 0xf4, 0x28, 0x48, 0xd9, 0x8b, 0xca, 0x10, 0x87, 0xa3, + 0xf8, 0x0a, 0x56, 0x8d, 0xc3, 0xa1, 0x36, 0xaf, 0xa7, 0xb3, 0x7a, 0x3e, 0x10, 0x01, 0xfc, 0x37, 0x14, 0xec, 0x37, + 0x4d, 0x45, 0x6e, 0x90, 0x3a, 0x0f, 0x87, 0x14, 0xe4, 0x53, 0xdd, 0xe4, 0x9f, 0x2a, 0x77, 0x3f, 0x9d, 0xcd, 0xad, + 0x39, 0x7a, 0x51, 0xe3, 0xba, 0xb5, 0xba, 0xa1, 0x90, 0x68, 0x4d, 0x93, 0xe2, 0xaa, 0x9a, 0x14, 0x03, 0x9e, 0xfb, + 0x42, 0x75, 0xb1, 0x35, 0x82, 0x85, 0x3f, 0xb7, 0x40, 0x98, 0xf4, 0xb7, 0x92, 0x0e, 0xa9, 0x1a, 0x77, 0x6d, 0xb5, + 0xdb, 0x56, 0x36, 0xa4, 0x68, 0x3e, 0xbc, 0x84, 0x5d, 0x3a, 0x45, 0xb4, 0xed, 0x92, 0xe0, 0x0b, 0xd0, 0xb2, 0x7a, + 0x23, 0xf2, 0x98, 0x7e, 0x85, 0xfc, 0x52, 0x0c, 0xff, 0x53, 0xba, 0x37, 0xa7, 0x36, 0xc8, 0x01, 0x6c, 0xf7, 0x1e, + 0x6e, 0xc7, 0xe8, 0x81, 0x0c, 0xde, 0x08, 0x39, 0xe7, 0xfc, 0x72, 0x6a, 0xcd, 0x98, 0x68, 0x58, 0xb0, 0x72, 0x18, + 0xf9, 0x01, 0x32, 0x5e, 0x4e, 0x81, 0x95, 0xfd, 0xa8, 0x88, 0x4b, 0x7f, 0x18, 0xf9, 0x17, 0x4f, 0x83, 0x8c, 0x7b, + 0xd1, 0xb0, 0xe3, 0x0b, 0xb0, 0x57, 0x5f, 0x3c, 0x65, 0xd1, 0x80, 0x57, 0x57, 0xf5, 0x34, 0x0b, 0x86, 0x19, 0x8b, + 0xae, 0x8a, 0x21, 0xf8, 0xd0, 0x3e, 0x2b, 0x07, 0xa1, 0xef, 0x9b, 0x9d, 0x43, 0x77, 0x43, 0x2c, 0x8f, 0xb0, 0x9f, + 0xc0, 0x6d, 0x57, 0x4b, 0xcc, 0x60, 0xb2, 0x59, 0x46, 0xcc, 0x60, 0xcb, 0x5f, 0x3c, 0x35, 0x5c, 0x42, 0xd5, 0x33, + 0xa9, 0xd9, 0x28, 0xd0, 0x9c, 0x5c, 0xa1, 0x39, 0x59, 0x09, 0xb5, 0xe4, 0x93, 0x0a, 0x27, 0xec, 0x7c, 0x92, 0x2b, + 0xbb, 0xd1, 0x18, 0x03, 0x17, 0xad, 0xb9, 0x1d, 0x0a, 0x23, 0x33, 0x9d, 0xa5, 0x68, 0xc0, 0xc2, 0x33, 0x71, 0x4a, + 0x63, 0x40, 0xfb, 0x72, 0x60, 0x69, 0x43, 0x7e, 0x95, 0x33, 0x03, 0x6d, 0x43, 0x4a, 0xa3, 0x66, 0xe0, 0xcf, 0xd4, + 0x84, 0xf9, 0x0c, 0x56, 0x22, 0x88, 0xea, 0x02, 0x4c, 0x92, 0x9c, 0x8c, 0x46, 0xca, 0x4a, 0x24, 0xe7, 0x80, 0xf7, + 0x11, 0x3c, 0x59, 0xc4, 0xb6, 0xf6, 0xa7, 0xf4, 0xbf, 0x3a, 0x7c, 0x2e, 0xfd, 0x27, 0x02, 0x58, 0xc8, 0xa5, 0x41, + 0x64, 0xa0, 0x70, 0x48, 0x2d, 0xc3, 0x7b, 0xe2, 0x78, 0x06, 0xbe, 0x86, 0x0b, 0x34, 0x05, 0xf4, 0x07, 0x35, 0xa3, + 0x88, 0x2c, 0xfc, 0xd5, 0xb3, 0x9b, 0xba, 0xd0, 0xf3, 0xcc, 0x79, 0x0d, 0x9a, 0x19, 0x08, 0xe9, 0x71, 0xaa, 0xde, + 0x86, 0x44, 0xe7, 0xe5, 0xb5, 0x7e, 0x99, 0x10, 0xc9, 0xca, 0xc8, 0xd3, 0xf7, 0x39, 0x98, 0x47, 0x14, 0xa1, 0x83, + 0x2b, 0xf3, 0x70, 0x38, 0x17, 0x14, 0xbe, 0xa3, 0x3c, 0x1f, 0x70, 0x9a, 0x65, 0x09, 0x68, 0x03, 0x59, 0x6e, 0xca, + 0x5c, 0x26, 0x2d, 0x53, 0xf7, 0x1e, 0xac, 0x04, 0x15, 0xba, 0x39, 0x05, 0x85, 0x32, 0x12, 0x94, 0xd2, 0x6a, 0x10, + 0x4a, 0x75, 0x58, 0x04, 0x91, 0x43, 0x16, 0x02, 0x6e, 0xa6, 0xa2, 0xd1, 0x92, 0x86, 0x47, 0x38, 0x37, 0x50, 0x08, + 0x40, 0x62, 0x4f, 0x15, 0x65, 0x5c, 0x0e, 0x01, 0x1f, 0x25, 0x1c, 0xe2, 0xac, 0x49, 0x5b, 0x9e, 0x83, 0x38, 0x96, + 0x0b, 0xbe, 0xac, 0x10, 0x0c, 0x22, 0xf4, 0x19, 0xf2, 0x27, 0xcb, 0xf9, 0x77, 0xeb, 0x30, 0xed, 0x08, 0x1f, 0x76, + 0xb5, 0x1b, 0x2e, 0x66, 0xb7, 0xf3, 0x09, 0xc4, 0xb7, 0xdc, 0xce, 0x8f, 0x31, 0x44, 0x6e, 0xfc, 0xc1, 0x72, 0x28, + 0xb9, 0xa2, 0xd0, 0x65, 0x3d, 0x22, 0x45, 0xf6, 0x74, 0xcd, 0x11, 0x04, 0x07, 0x5a, 0x35, 0xc8, 0xd0, 0x48, 0x7c, + 0xf1, 0x14, 0xb2, 0x06, 0x6b, 0xfe, 0xa2, 0x22, 0x67, 0x75, 0x7f, 0xb2, 0x81, 0x6a, 0x92, 0xc9, 0x5a, 0x51, 0x39, + 0x7f, 0xbb, 0x2a, 0x8b, 0x93, 0x55, 0x19, 0xae, 0x06, 0x5d, 0x55, 0x59, 0x70, 0xa4, 0x36, 0x40, 0x6b, 0xba, 0x42, + 0x0c, 0x85, 0xac, 0xc1, 0xc2, 0xaa, 0xca, 0x9a, 0xfa, 0x04, 0x02, 0x7d, 0x80, 0x65, 0xd4, 0xec, 0xa7, 0xc3, 0x5f, + 0x83, 0x5f, 0x55, 0xc8, 0x52, 0x9d, 0xd6, 0x99, 0xf8, 0x1c, 0x2c, 0x18, 0xfe, 0xf1, 0x7b, 0xb0, 0x06, 0x2c, 0x01, + 0xb2, 0xdc, 0x6d, 0x6c, 0xb4, 0x5e, 0x79, 0x85, 0x78, 0x57, 0xeb, 0x8b, 0x7e, 0xeb, 0x36, 0x51, 0x2b, 0xc0, 0x08, + 0x85, 0x16, 0x01, 0xb6, 0x7a, 0xe0, 0x9e, 0x82, 0x1f, 0x88, 0xe1, 0x5c, 0x93, 0xd6, 0xd4, 0x09, 0xaf, 0xb3, 0x71, + 0x24, 0xa2, 0x7a, 0x0b, 0x17, 0xf7, 0x7a, 0x6b, 0xf1, 0x37, 0x2a, 0x10, 0x00, 0x59, 0x4c, 0xb1, 0x76, 0xde, 0x90, + 0x5e, 0x19, 0x76, 0x12, 0x7a, 0x6f, 0xd8, 0x09, 0xe4, 0xc5, 0x61, 0xa7, 0xd0, 0x25, 0xda, 0x4e, 0x91, 0x9a, 0x68, + 0x3b, 0xe9, 0x66, 0x15, 0x96, 0x10, 0xfc, 0xaa, 0xbd, 0x75, 0x94, 0xed, 0x8b, 0x2c, 0x61, 0xda, 0x02, 0x46, 0xb9, + 0x55, 0x9f, 0x39, 0x45, 0xac, 0x94, 0xbd, 0xd3, 0x49, 0x95, 0xbb, 0xc8, 0xa7, 0x56, 0x53, 0x64, 0xf2, 0x8b, 0xe3, + 0x16, 0xc9, 0x27, 0xbf, 0xb4, 0x1b, 0x26, 0xd3, 0x3f, 0x1e, 0x7d, 0x01, 0x5d, 0x91, 0x9d, 0x3e, 0x81, 0x80, 0x4c, + 0x05, 0xd5, 0xea, 0x56, 0x31, 0xcd, 0xdb, 0x55, 0x76, 0x7b, 0xa1, 0xc4, 0x70, 0x3a, 0x3b, 0x09, 0x8f, 0x36, 0x43, + 0x06, 0x0e, 0x41, 0xa0, 0x10, 0x2a, 0x8a, 0xe1, 0x11, 0xa8, 0x35, 0x92, 0x0f, 0xf0, 0xa3, 0xdd, 0xa9, 0x20, 0x52, + 0xbb, 0xa9, 0xb8, 0x71, 0x72, 0xd3, 0xf5, 0x52, 0xa0, 0xd6, 0x29, 0x59, 0x01, 0x94, 0x10, 0xf5, 0x27, 0xb1, 0xad, + 0x5f, 0xc2, 0x15, 0x9b, 0xef, 0x1b, 0x45, 0x4f, 0xae, 0x4f, 0x51, 0xb7, 0xe2, 0xea, 0x34, 0x6d, 0x35, 0xc7, 0x8e, + 0x33, 0xe4, 0xe0, 0x59, 0x41, 0xb0, 0x1d, 0x95, 0x28, 0xdf, 0xb6, 0x9b, 0x8e, 0x89, 0xad, 0xfe, 0xb9, 0xa9, 0x36, + 0x4b, 0xa8, 0x88, 0x88, 0x8f, 0xb2, 0x9b, 0x27, 0xed, 0x77, 0xb0, 0xc7, 0x5a, 0x0d, 0x22, 0xfb, 0x0c, 0xae, 0x72, + 0x9d, 0x16, 0xb9, 0x2d, 0x83, 0xf3, 0x0f, 0xaf, 0x76, 0x15, 0x36, 0x39, 0xd6, 0xd5, 0xd5, 0x4c, 0x75, 0x52, 0xb1, + 0x81, 0xb1, 0xa6, 0xb5, 0x54, 0xf3, 0x18, 0x92, 0xee, 0xca, 0xe2, 0xac, 0x4a, 0xba, 0xe9, 0xb9, 0x71, 0xa6, 0x10, + 0x03, 0x67, 0xab, 0xd1, 0x72, 0x86, 0x21, 0xba, 0x3e, 0xcc, 0x12, 0xbf, 0xd5, 0x53, 0xee, 0xf3, 0x70, 0xeb, 0x77, + 0xf5, 0x82, 0x93, 0xc9, 0x7e, 0x72, 0x9c, 0xbb, 0x5d, 0xa4, 0xfd, 0xc4, 0xb7, 0x61, 0xfe, 0xf5, 0x0d, 0x62, 0x29, + 0xea, 0x5f, 0x2b, 0x00, 0x1a, 0xdc, 0xe4, 0xb1, 0x44, 0xa9, 0xdf, 0xab, 0xea, 0x07, 0x35, 0x53, 0x35, 0x0d, 0x04, + 0x73, 0x2a, 0x05, 0xfc, 0xe1, 0x76, 0xe1, 0x8a, 0x47, 0xdc, 0xb0, 0x30, 0xfe, 0xe5, 0xd5, 0xec, 0x54, 0x50, 0x19, + 0xb8, 0x19, 0xff, 0xe5, 0x09, 0x76, 0x0a, 0x6b, 0x05, 0x64, 0x85, 0xbf, 0xbc, 0xfc, 0x81, 0xf7, 0x2b, 0xfe, 0x97, + 0x57, 0x3d, 0xf0, 0x3e, 0xe2, 0xbc, 0xfc, 0x85, 0xa4, 0x4e, 0x88, 0xea, 0xf2, 0x17, 0x61, 0x8a, 0xad, 0xd2, 0xfc, + 0x15, 0x29, 0x7c, 0x82, 0x2f, 0xc0, 0x77, 0xb8, 0x0a, 0xb7, 0xe6, 0x37, 0x78, 0xec, 0x58, 0x6c, 0xbb, 0xd4, 0x17, + 0x50, 0x8e, 0xc0, 0x22, 0x72, 0xfb, 0xed, 0xca, 0x7e, 0xb5, 0x30, 0xca, 0x18, 0xbb, 0x2f, 0x59, 0x89, 0xd2, 0x59, + 0xbf, 0x5f, 0x48, 0xc1, 0xc8, 0x2e, 0xac, 0xd1, 0x1e, 0xa5, 0xea, 0xd5, 0xb7, 0x61, 0x1d, 0x25, 0x69, 0xbe, 0x94, + 0xd1, 0x47, 0x32, 0xec, 0x48, 0x5f, 0x49, 0x89, 0xf6, 0x5a, 0x85, 0xe5, 0x68, 0xf6, 0xeb, 0x92, 0x03, 0xe5, 0x75, + 0x2b, 0x28, 0x5f, 0x35, 0x01, 0xf4, 0x4a, 0xb5, 0xcf, 0x40, 0x2b, 0x28, 0x2c, 0x95, 0x07, 0x2b, 0x71, 0x2e, 0xfa, + 0xac, 0x38, 0x1c, 0xd4, 0xc5, 0x90, 0x50, 0xa0, 0x4a, 0x9c, 0x84, 0x46, 0x3c, 0x87, 0x0b, 0xa1, 0x78, 0x96, 0x63, + 0x6c, 0x45, 0x0e, 0x1c, 0xc8, 0xf0, 0x03, 0x02, 0xef, 0x65, 0xff, 0x0a, 0x06, 0xc3, 0x04, 0x37, 0x32, 0xea, 0xe4, + 0x9c, 0xfd, 0x85, 0x81, 0x19, 0xd4, 0x93, 0xda, 0x7d, 0x76, 0xaf, 0x02, 0x7b, 0xe1, 0x0c, 0x68, 0xef, 0xc6, 0xe8, + 0x67, 0x55, 0xac, 0x9d, 0xf4, 0x4f, 0xc5, 0x1a, 0x92, 0xe9, 0xb0, 0x38, 0xda, 0xa6, 0xe1, 0x91, 0x3c, 0x39, 0x8e, + 0x37, 0xfd, 0xc3, 0x61, 0x8c, 0x1f, 0x47, 0xf9, 0xb5, 0x05, 0xbc, 0x8a, 0x5b, 0x48, 0x63, 0x91, 0xa2, 0x77, 0x20, + 0xe6, 0x50, 0xf4, 0x92, 0xfd, 0x96, 0xf1, 0x72, 0x22, 0x28, 0x25, 0x89, 0x0d, 0xef, 0x48, 0x4f, 0xd3, 0x7a, 0xb4, + 0x95, 0x01, 0xfb, 0xf5, 0x68, 0x47, 0x7f, 0x81, 0xe2, 0xd1, 0xc2, 0x5f, 0xd2, 0xdf, 0xc5, 0xdd, 0xdc, 0x73, 0xbe, + 0x69, 0x7c, 0x47, 0x5c, 0xa0, 0x58, 0xb3, 0xfb, 0x6b, 0x5a, 0x3a, 0xeb, 0x40, 0x70, 0xc0, 0x5b, 0xec, 0xa2, 0x7d, + 0xbf, 0x71, 0x9d, 0x9e, 0xf6, 0xdf, 0xbb, 0x35, 0xca, 0xf7, 0x7e, 0x95, 0x28, 0x07, 0xfb, 0x37, 0x2e, 0x9a, 0xbf, + 0xfd, 0x94, 0x21, 0xa9, 0xd0, 0xdc, 0x60, 0x3b, 0xd9, 0x22, 0xac, 0x8d, 0x71, 0x50, 0xb1, 0x65, 0x19, 0x46, 0xc0, + 0xa0, 0x8e, 0xfd, 0x8f, 0x3e, 0x9b, 0x36, 0x64, 0x1f, 0x00, 0x2a, 0x57, 0x21, 0x60, 0x0f, 0xc0, 0x89, 0x46, 0xb8, + 0x01, 0x6e, 0x35, 0x5a, 0xd2, 0x41, 0xdd, 0x16, 0x0c, 0x44, 0x4b, 0xd8, 0xc8, 0xdb, 0xae, 0x4e, 0xdf, 0x10, 0x3e, + 0xd4, 0x4e, 0x4a, 0x87, 0xf2, 0x37, 0xcf, 0xd9, 0x7f, 0xef, 0xb0, 0xa6, 0xa6, 0x5c, 0x03, 0x66, 0xce, 0x4a, 0xe4, + 0x15, 0x42, 0xa7, 0xc8, 0xef, 0x55, 0x5d, 0x89, 0xe1, 0xa2, 0x16, 0x65, 0x67, 0x76, 0xeb, 0x44, 0xef, 0x9c, 0x82, + 0x5a, 0x2a, 0x1b, 0xe4, 0x24, 0xd5, 0xe6, 0x23, 0x6b, 0x05, 0x25, 0xea, 0x1a, 0x05, 0x8e, 0x4f, 0xb9, 0x76, 0xff, + 0xef, 0x9c, 0x09, 0x6a, 0xb6, 0x51, 0xdd, 0x5f, 0xe9, 0xa7, 0xaa, 0x26, 0xb1, 0x00, 0x97, 0x93, 0x34, 0xef, 0x78, + 0x84, 0xd5, 0x3f, 0x4e, 0x96, 0x22, 0xd0, 0xab, 0x88, 0x76, 0x25, 0x20, 0x41, 0x3b, 0x39, 0x0b, 0x15, 0x81, 0x02, + 0x7d, 0xfd, 0xc5, 0x26, 0xcd, 0x62, 0xb9, 0x9a, 0xed, 0x61, 0xa2, 0x2c, 0xd6, 0x43, 0x04, 0x39, 0x33, 0x75, 0xb0, + 0xdf, 0xd3, 0x8c, 0x66, 0xe1, 0x95, 0x29, 0xc1, 0xa5, 0xb8, 0x8a, 0x8a, 0x1c, 0x7c, 0x0e, 0xf1, 0x85, 0x4f, 0x85, + 0xdc, 0x20, 0xa2, 0xe9, 0x4f, 0x12, 0xd5, 0x8e, 0x14, 0xc8, 0xa1, 0xe4, 0x27, 0xc4, 0x5f, 0xb2, 0x36, 0xc6, 0xfd, + 0xd2, 0xa9, 0xf6, 0x4b, 0x85, 0xe0, 0xfe, 0x8b, 0x2d, 0x36, 0xaa, 0x3c, 0xd1, 0x83, 0x4f, 0xb1, 0xfe, 0x27, 0x0b, + 0x28, 0xd5, 0x7d, 0x1b, 0x9c, 0x8a, 0x47, 0xe1, 0xa6, 0x2e, 0xae, 0x11, 0x5a, 0xa0, 0x1c, 0x55, 0xc5, 0xa6, 0x8c, + 0x88, 0x13, 0x76, 0x53, 0x17, 0x3d, 0xcd, 0x81, 0x2e, 0xe7, 0x75, 0x22, 0x4f, 0x84, 0x76, 0x0b, 0xba, 0xa7, 0x39, + 0x56, 0xe2, 0xb9, 0x2c, 0x1d, 0x64, 0x9d, 0x48, 0x13, 0x2a, 0x77, 0x75, 0xd5, 0x51, 0xa9, 0xd4, 0x0d, 0xaf, 0x53, + 0xcd, 0xf8, 0xbb, 0x30, 0x7f, 0x62, 0xd9, 0xaf, 0x5b, 0xbf, 0xd5, 0x6a, 0x6f, 0xac, 0x1e, 0x95, 0xac, 0x39, 0xce, + 0x26, 0x24, 0xa5, 0x4f, 0xd8, 0x6e, 0x26, 0x5d, 0xeb, 0xc0, 0x93, 0xe0, 0x72, 0xe8, 0x09, 0xa8, 0x18, 0x34, 0xf1, + 0x76, 0x17, 0xa8, 0x47, 0xe0, 0x19, 0x28, 0x9f, 0xa8, 0x75, 0xc0, 0xcf, 0x6b, 0x2d, 0x4f, 0x19, 0x61, 0x58, 0xed, + 0x2c, 0x5a, 0x0e, 0xce, 0x3b, 0x45, 0xe0, 0xda, 0x95, 0xc0, 0xf3, 0xa1, 0x7a, 0x2f, 0x04, 0x0c, 0xf7, 0x4f, 0x85, + 0xca, 0x66, 0x37, 0xc3, 0x79, 0xd4, 0x38, 0x3d, 0xd0, 0xde, 0x76, 0xad, 0x87, 0x7a, 0xd7, 0xed, 0xdc, 0x56, 0xba, + 0xf7, 0x6b, 0x27, 0x93, 0x2e, 0xa0, 0xb5, 0xf9, 0xec, 0x3b, 0xbb, 0xd2, 0xba, 0xe9, 0x39, 0x7b, 0xb0, 0x75, 0x4b, + 0x74, 0x2e, 0x88, 0x26, 0xbf, 0x1f, 0x78, 0xd6, 0xb6, 0xa3, 0xdf, 0xa6, 0x1d, 0xdb, 0xdc, 0x43, 0xdd, 0x2b, 0xa8, + 0xf5, 0x86, 0xe6, 0xfd, 0x33, 0xd7, 0xb6, 0xe3, 0xab, 0x5f, 0xd7, 0x1d, 0xae, 0xf3, 0x26, 0x38, 0x6e, 0xba, 0xb6, + 0xd5, 0xce, 0x7e, 0xee, 0xee, 0xad, 0x9b, 0x28, 0xcc, 0xb2, 0x9f, 0x8a, 0xe2, 0xcf, 0x4a, 0xdf, 0x11, 0xe8, 0xe8, + 0xce, 0x8b, 0x3a, 0x5d, 0xec, 0x3e, 0x10, 0xc6, 0x93, 0x57, 0x1f, 0x11, 0xdd, 0xfa, 0x3e, 0x73, 0xbf, 0x02, 0xdc, + 0x08, 0xee, 0x20, 0xda, 0xbb, 0xa5, 0x3e, 0xa9, 0xd5, 0xd7, 0x7a, 0xed, 0x3c, 0x3d, 0xbf, 0xe9, 0xdc, 0x7e, 0xf7, + 0xcd, 0xd1, 0xd6, 0x7b, 0x5c, 0x58, 0x2b, 0x4b, 0x4f, 0x55, 0xc1, 0xde, 0x2c, 0x4f, 0x55, 0xc1, 0xe4, 0x81, 0xd7, + 0xec, 0x17, 0x34, 0xb8, 0xd2, 0xd1, 0xc6, 0x7b, 0xa2, 0x06, 0x6e, 0x51, 0x58, 0x3a, 0xfc, 0x92, 0x9b, 0xc9, 0x4b, + 0xdc, 0x5f, 0x2a, 0x72, 0xb1, 0xef, 0x9c, 0xd1, 0x9d, 0x99, 0x75, 0xaf, 0x2a, 0x5c, 0x2d, 0xc8, 0xd5, 0x81, 0xad, + 0x65, 0x17, 0x87, 0x1b, 0x16, 0x51, 0x80, 0x40, 0x4c, 0xaf, 0xd4, 0xda, 0x1f, 0xd1, 0x20, 0xe4, 0x83, 0x81, 0x5f, + 0x60, 0xb0, 0x2a, 0x50, 0xf8, 0x40, 0x91, 0xfc, 0x8d, 0x27, 0x60, 0x17, 0xcf, 0x00, 0xdd, 0x8a, 0xcd, 0x8a, 0x11, + 0x22, 0x64, 0xb2, 0x9c, 0xd5, 0x74, 0x06, 0xf9, 0xd4, 0x17, 0xdf, 0xd9, 0xaa, 0xd3, 0x79, 0x5b, 0x53, 0xe5, 0xd4, + 0xa1, 0xd0, 0xdd, 0x4d, 0xdd, 0xb9, 0x75, 0x91, 0xa7, 0x0e, 0x21, 0x57, 0x2a, 0x56, 0x62, 0x1a, 0x6a, 0x9e, 0xa4, + 0x19, 0xf5, 0x37, 0x7b, 0xbf, 0xd7, 0x28, 0x9c, 0xf2, 0xa7, 0x63, 0x50, 0x85, 0xab, 0x1a, 0xe2, 0x58, 0xaa, 0xe2, + 0x91, 0x0d, 0x02, 0xcd, 0xab, 0x5b, 0x95, 0x34, 0x21, 0x93, 0x1b, 0xe1, 0x53, 0x93, 0x52, 0x9e, 0xa6, 0x4d, 0x5a, + 0x29, 0x52, 0x07, 0x1f, 0xd4, 0xa9, 0xc6, 0x73, 0xb3, 0x7a, 0x06, 0x60, 0xc6, 0xf9, 0x15, 0xbf, 0x54, 0x5c, 0x46, + 0x6d, 0x65, 0x26, 0xed, 0x4f, 0x8e, 0xc6, 0x46, 0x5d, 0x4e, 0x1b, 0x65, 0x84, 0x95, 0xd2, 0x9c, 0x14, 0xcb, 0xf1, + 0xfc, 0x03, 0x06, 0x6b, 0x9e, 0xc0, 0x0e, 0x26, 0x2a, 0xe5, 0x7d, 0x04, 0xc4, 0xd7, 0x49, 0xba, 0x4c, 0x20, 0x45, + 0xfa, 0x97, 0x2e, 0x78, 0xea, 0x30, 0x36, 0x10, 0x63, 0x56, 0xcc, 0x8c, 0xfe, 0x07, 0x77, 0x49, 0x7f, 0x12, 0x02, + 0xe0, 0x26, 0x9a, 0x42, 0xa7, 0xce, 0x93, 0x8b, 0x3c, 0x58, 0x5c, 0x78, 0x68, 0xc5, 0x88, 0x07, 0xff, 0xf9, 0x2c, + 0x44, 0x10, 0x73, 0x4c, 0xf1, 0xf4, 0x0b, 0xa3, 0xff, 0x08, 0x2e, 0x31, 0x82, 0xd0, 0xdd, 0x3b, 0x87, 0x21, 0xdc, + 0xec, 0x41, 0x06, 0xf5, 0x87, 0x3a, 0x24, 0x6a, 0xf8, 0x6b, 0xe5, 0x41, 0xff, 0xd7, 0x99, 0xb0, 0xd4, 0x7e, 0x7a, + 0x3a, 0x80, 0x0a, 0xde, 0x57, 0xbc, 0x8d, 0x88, 0xef, 0x13, 0x3f, 0x89, 0x07, 0x9b, 0x27, 0x1b, 0xb0, 0xd6, 0x3d, + 0xca, 0x8d, 0x75, 0x95, 0xb0, 0x81, 0x80, 0xaf, 0x31, 0xad, 0x3d, 0xaf, 0xdd, 0xee, 0xc1, 0x7f, 0xfa, 0x17, 0x21, + 0x03, 0x26, 0x4e, 0xdf, 0x67, 0x4e, 0xd6, 0xe8, 0x22, 0x93, 0xe9, 0x43, 0x27, 0x7d, 0xa3, 0xd3, 0x7d, 0x27, 0xfc, + 0xa3, 0x62, 0x16, 0x1f, 0x6e, 0xe9, 0x2b, 0x4d, 0x8a, 0x3b, 0x60, 0x65, 0xf3, 0xa0, 0x20, 0xd4, 0xb9, 0x88, 0xbe, + 0x31, 0xe5, 0x5b, 0x42, 0xcd, 0xbe, 0xb1, 0xa4, 0x94, 0xee, 0x35, 0xf4, 0x3a, 0xad, 0xf5, 0xdb, 0x28, 0xc1, 0x98, + 0xe8, 0x78, 0xf2, 0x32, 0x1e, 0x2b, 0xef, 0xe3, 0x71, 0x23, 0x15, 0xf2, 0x00, 0x44, 0xa0, 0x62, 0xfc, 0xe9, 0xca, + 0x93, 0x93, 0x5e, 0x18, 0xaf, 0x42, 0x29, 0x28, 0x0c, 0xe8, 0x0a, 0xa4, 0x80, 0x47, 0xed, 0x89, 0xce, 0xc2, 0x2e, + 0xe1, 0x1e, 0xdd, 0x04, 0x8c, 0xf5, 0xf9, 0x57, 0x40, 0x73, 0x17, 0xee, 0xf0, 0x62, 0x80, 0xda, 0xd4, 0xab, 0xbb, + 0x8f, 0x6b, 0x75, 0x0e, 0x87, 0xe0, 0x60, 0x35, 0x88, 0xe0, 0x74, 0x3e, 0x75, 0x34, 0xcb, 0x02, 0x54, 0x4e, 0x96, + 0x1b, 0x79, 0xf3, 0x68, 0xd1, 0xab, 0xfb, 0xde, 0x22, 0x2d, 0xab, 0x3a, 0xc8, 0x58, 0x16, 0x56, 0x80, 0xab, 0x43, + 0xeb, 0x07, 0xe1, 0xb2, 0x70, 0xfe, 0x40, 0x08, 0x62, 0xf7, 0x6a, 0x5b, 0xf0, 0x5c, 0xcd, 0xe1, 0x27, 0x4f, 0xd9, + 0x9a, 0x4b, 0xd4, 0x49, 0x67, 0x22, 0x00, 0xb1, 0xa7, 0x66, 0x15, 0x5d, 0x03, 0x49, 0x9d, 0x66, 0x15, 0x5d, 0x53, + 0xb3, 0x8d, 0x71, 0x20, 0x1f, 0xad, 0x52, 0xc0, 0xbe, 0x9b, 0x8e, 0x83, 0xd5, 0x93, 0x58, 0x5e, 0x87, 0x96, 0x4f, + 0x36, 0xca, 0x67, 0x50, 0xb7, 0xda, 0x18, 0x13, 0xdb, 0xcd, 0x97, 0x73, 0xfd, 0x76, 0xb0, 0xf0, 0xed, 0xa0, 0x39, + 0xa7, 0xec, 0xa5, 0x2e, 0x7b, 0x65, 0x97, 0x4d, 0x3d, 0x77, 0x54, 0xb4, 0x1a, 0x03, 0x7a, 0x03, 0x0b, 0xd6, 0xe7, + 0x22, 0xcd, 0x56, 0xa5, 0x2a, 0x01, 0x2f, 0x8c, 0x15, 0x5b, 0xfa, 0x8d, 0xcc, 0x90, 0x84, 0x79, 0x9c, 0x89, 0xb7, + 0x74, 0xaf, 0x85, 0xc9, 0x71, 0x2c, 0x92, 0x29, 0xa1, 0x53, 0xba, 0xb3, 0x0d, 0x9d, 0xab, 0x30, 0x8a, 0x68, 0xad, + 0xa4, 0xd2, 0x48, 0x60, 0x6a, 0x06, 0x28, 0x99, 0x2b, 0x70, 0x4a, 0x97, 0xfb, 0xdf, 0x91, 0x18, 0x67, 0xbe, 0x28, + 0x99, 0x01, 0xdd, 0xf2, 0xeb, 0x62, 0xdd, 0x4a, 0x91, 0x11, 0xe6, 0xcd, 0x71, 0x7b, 0x5d, 0x1f, 0x02, 0xb9, 0x5a, + 0xf6, 0x28, 0x1a, 0x07, 0x85, 0x0e, 0x97, 0x2a, 0x01, 0xf6, 0x45, 0xe2, 0x67, 0x84, 0x2d, 0xed, 0x81, 0xdc, 0x1e, + 0x9d, 0x09, 0x73, 0xce, 0x49, 0x59, 0x76, 0x2e, 0xcd, 0xe0, 0x72, 0xe2, 0x4a, 0x70, 0x91, 0xde, 0xb6, 0xa7, 0x49, + 0x4b, 0xdb, 0xc7, 0x86, 0x73, 0x34, 0xb4, 0x0d, 0xba, 0x63, 0x7f, 0x68, 0x2e, 0x16, 0xb1, 0x75, 0xb1, 0x18, 0x76, + 0x66, 0x3f, 0x5a, 0x2c, 0x40, 0x0e, 0x00, 0x47, 0xdd, 0x86, 0x8f, 0xd9, 0x02, 0x38, 0xad, 0xa6, 0xd9, 0xd4, 0xdb, + 0xf0, 0xea, 0x89, 0xea, 0xe9, 0x05, 0xcf, 0x9f, 0x08, 0x33, 0x16, 0x1b, 0x9e, 0x3f, 0xb1, 0x8e, 0x9c, 0xea, 0x89, + 0x50, 0xa2, 0x75, 0x01, 0xcd, 0xc0, 0x6b, 0x0a, 0x18, 0xb1, 0x64, 0x32, 0xa5, 0x8a, 0x3c, 0xee, 0x4d, 0x37, 0x6a, + 0xf0, 0x82, 0xc2, 0x21, 0x90, 0xd2, 0xe9, 0x17, 0x4f, 0x99, 0x7e, 0xef, 0xe2, 0x69, 0x87, 0xac, 0x6d, 0x98, 0x2e, + 0x37, 0xc3, 0x64, 0x50, 0xfa, 0x4f, 0xcc, 0xc4, 0xb8, 0xb0, 0x26, 0x09, 0x20, 0xfe, 0x8d, 0xfd, 0x0e, 0x29, 0xdc, + 0xbc, 0xbf, 0x18, 0xc6, 0x0f, 0xbc, 0x1f, 0x23, 0x7b, 0x92, 0x66, 0x88, 0x35, 0x93, 0x0a, 0xb9, 0xfb, 0x6a, 0xfd, + 0x63, 0x62, 0x37, 0xd9, 0x03, 0x0b, 0x40, 0x6c, 0x4d, 0x5b, 0xdd, 0xf2, 0x7e, 0xdf, 0x33, 0x45, 0x80, 0x1f, 0x94, + 0x7f, 0x74, 0x67, 0x48, 0x06, 0x65, 0xd7, 0x0d, 0x21, 0x1e, 0x94, 0x4d, 0xd3, 0x5e, 0x6f, 0x7b, 0x67, 0x1e, 0xab, + 0xeb, 0xb4, 0xb3, 0xb8, 0x5a, 0x64, 0x90, 0x56, 0x1f, 0xb2, 0xe3, 0xcc, 0x3e, 0x3b, 0x5a, 0x2a, 0xdd, 0xef, 0x43, + 0x44, 0xdc, 0x51, 0xd6, 0xf6, 0xdb, 0x2d, 0xb8, 0x86, 0xa3, 0x41, 0xe8, 0xca, 0xde, 0x2e, 0xa3, 0x8d, 0x0b, 0x71, + 0xdc, 0x33, 0x9d, 0x2f, 0xf8, 0xf2, 0x28, 0xed, 0x3c, 0x38, 0xd5, 0x13, 0x7d, 0x6e, 0xba, 0xab, 0x4c, 0xae, 0x75, + 0x58, 0x8d, 0x41, 0x6d, 0x16, 0xb6, 0x70, 0x17, 0xb6, 0xd1, 0x41, 0x6b, 0x5f, 0x16, 0xfc, 0x53, 0x06, 0xe0, 0x4b, + 0xcf, 0x96, 0x6d, 0xaf, 0x49, 0xab, 0xd7, 0x32, 0x0a, 0xb1, 0xa5, 0xed, 0xd5, 0xa7, 0xa3, 0x7c, 0xdc, 0x9c, 0x50, + 0x5c, 0xc8, 0x51, 0x7e, 0xf0, 0x1a, 0xa2, 0xae, 0x75, 0x1d, 0x17, 0x8b, 0x0e, 0x37, 0xae, 0xba, 0xed, 0xc6, 0xf5, + 0x23, 0xe2, 0xad, 0xd1, 0x26, 0x85, 0x5a, 0x19, 0x3b, 0x82, 0x97, 0xe5, 0xc3, 0x21, 0x13, 0xc3, 0xa1, 0x84, 0x4c, + 0x7d, 0xe8, 0xde, 0xd0, 0xb4, 0xcf, 0x4f, 0x5b, 0x3f, 0x62, 0xa9, 0x71, 0x14, 0x1b, 0xde, 0xe9, 0x3b, 0x8f, 0xad, + 0x71, 0x25, 0x5f, 0x06, 0xb3, 0x5d, 0x41, 0xb5, 0x35, 0xde, 0xb0, 0x97, 0xf3, 0x9f, 0x2a, 0xa9, 0xe4, 0x6f, 0x7f, + 0x86, 0x6b, 0x78, 0x6b, 0x4b, 0x07, 0x4d, 0x35, 0xcb, 0x59, 0xae, 0xef, 0x05, 0xc7, 0x1f, 0x77, 0xaf, 0x08, 0x06, + 0xbf, 0xa7, 0xa3, 0x20, 0x17, 0x4b, 0xb5, 0x06, 0x14, 0xa4, 0x23, 0x3b, 0xa6, 0xb2, 0xc0, 0x30, 0x80, 0x37, 0x64, + 0x80, 0x3c, 0xa6, 0x70, 0x37, 0x54, 0x78, 0xe1, 0x6f, 0x15, 0xd9, 0x25, 0xb0, 0xad, 0x19, 0x1f, 0x33, 0xdc, 0x41, + 0xc8, 0x3f, 0x82, 0x2d, 0xd9, 0x8a, 0xdd, 0xb2, 0x1b, 0x86, 0x64, 0xe3, 0x38, 0x8c, 0x31, 0x1f, 0x4f, 0xe2, 0x2b, + 0x31, 0x89, 0x07, 0x3c, 0x42, 0xc7, 0x88, 0x35, 0xaf, 0x67, 0xb1, 0x1c, 0x40, 0xb6, 0xe4, 0x4a, 0x07, 0x84, 0xd0, + 0xd8, 0xd0, 0x92, 0xd7, 0x85, 0xc1, 0xc5, 0x8e, 0x7d, 0x46, 0x22, 0x19, 0x87, 0x60, 0xd1, 0xaa, 0x06, 0x16, 0x26, + 0x76, 0xcb, 0x8b, 0xd9, 0x6a, 0x8e, 0xff, 0x1c, 0x0e, 0x08, 0x80, 0x1d, 0xec, 0x1b, 0xb6, 0x8c, 0x10, 0xe9, 0xed, + 0x86, 0x2f, 0x2d, 0x4f, 0x17, 0x76, 0xc7, 0xdf, 0xf2, 0x31, 0x3b, 0xff, 0xd1, 0x83, 0xc8, 0xd9, 0xf3, 0x8f, 0x80, + 0x86, 0x78, 0xc7, 0x6f, 0x53, 0xaf, 0x62, 0xb7, 0x44, 0x41, 0x78, 0x0b, 0xce, 0x40, 0x77, 0x10, 0x01, 0xfb, 0x96, + 0xdf, 0x60, 0xac, 0xd8, 0x59, 0xba, 0xf0, 0x30, 0x23, 0xd4, 0x9e, 0xce, 0x97, 0xb5, 0x9a, 0x84, 0x9b, 0xab, 0xc5, + 0x64, 0x30, 0xd8, 0xf8, 0x3b, 0xbe, 0x06, 0x3e, 0x98, 0xf3, 0x1f, 0xbd, 0x1d, 0x95, 0x0b, 0xff, 0x79, 0x9d, 0x25, + 0xef, 0x7c, 0xf6, 0x76, 0xc0, 0x6f, 0x00, 0x6f, 0x09, 0x1d, 0xb8, 0xee, 0x7c, 0x26, 0xf1, 0xda, 0xde, 0xea, 0x6b, + 0x04, 0x12, 0xf9, 0x02, 0x30, 0x62, 0x62, 0x7e, 0xbf, 0x85, 0x08, 0x8c, 0x18, 0x7c, 0x5b, 0xb5, 0x47, 0xfc, 0x96, + 0x1b, 0xc0, 0xaf, 0xcc, 0x67, 0xf7, 0x3c, 0xd4, 0x3f, 0x13, 0x9f, 0x5d, 0xf3, 0xf7, 0xfc, 0x99, 0x27, 0x25, 0xe9, + 0x72, 0xf6, 0x7e, 0x0e, 0xd7, 0x43, 0x29, 0x4f, 0x87, 0xf4, 0xb3, 0x31, 0x18, 0x40, 0x28, 0x64, 0x5e, 0x7b, 0xc0, + 0x9a, 0x14, 0xe2, 0x5f, 0xc0, 0xb7, 0xa3, 0x84, 0xcd, 0x6b, 0x6f, 0xeb, 0x6b, 0x79, 0xf3, 0xda, 0xbb, 0xf7, 0x29, + 0x0a, 0xb0, 0x0a, 0x4a, 0x59, 0x60, 0x15, 0x84, 0x8d, 0x36, 0xc2, 0x18, 0xb8, 0x7a, 0xd7, 0x18, 0xea, 0x7a, 0x8e, + 0xd8, 0xb6, 0xd2, 0x77, 0xe1, 0x3b, 0xc8, 0x80, 0x0f, 0x5e, 0x17, 0x25, 0xd1, 0xe7, 0xd4, 0x14, 0x49, 0xeb, 0x9e, + 0xfb, 0xad, 0x75, 0x47, 0x6b, 0x4a, 0x7d, 0xe4, 0x6a, 0x7c, 0x38, 0xd4, 0xcf, 0x84, 0x16, 0x09, 0xa6, 0xa0, 0x71, + 0x0d, 0xda, 0x02, 0x04, 0x7d, 0x1e, 0x20, 0x6b, 0x49, 0xb1, 0xe0, 0xdb, 0x5f, 0x21, 0x06, 0xaf, 0x4c, 0xef, 0x5c, + 0xae, 0x32, 0x12, 0xb6, 0x17, 0x7e, 0x39, 0xac, 0xfd, 0x89, 0x53, 0x0b, 0x4b, 0xab, 0x39, 0xa8, 0x9f, 0xd8, 0x72, + 0x9c, 0xaa, 0xda, 0xdf, 0x25, 0x49, 0xb5, 0xab, 0xb4, 0x9c, 0xde, 0xd9, 0x37, 0x5d, 0x26, 0xd8, 0xd8, 0x0f, 0xa8, + 0x3a, 0xb2, 0x1a, 0x76, 0x5f, 0xa8, 0x2f, 0x7a, 0x4a, 0x26, 0x34, 0x1f, 0x55, 0x34, 0xcf, 0xee, 0x37, 0x3b, 0xea, + 0x3f, 0xbd, 0x1c, 0x8a, 0x00, 0xc9, 0x2a, 0x2d, 0x96, 0x22, 0x67, 0x63, 0x3f, 0x1e, 0x26, 0x99, 0x0a, 0x2f, 0x48, + 0x47, 0x77, 0xbf, 0x71, 0x7f, 0xcb, 0x0d, 0x64, 0x85, 0x56, 0x6d, 0x30, 0x56, 0x8a, 0x96, 0xc1, 0xfa, 0x6a, 0xdc, + 0xef, 0x8b, 0xab, 0xf1, 0x54, 0x04, 0x35, 0x10, 0x17, 0x89, 0x67, 0xe3, 0x69, 0x4d, 0x2c, 0xa9, 0x5d, 0x81, 0x31, + 0x7a, 0x5c, 0x15, 0xb5, 0x4f, 0xfd, 0x0c, 0x42, 0x91, 0x6a, 0xcd, 0x1c, 0x6b, 0xdc, 0x18, 0x11, 0x77, 0x58, 0xb9, + 0x76, 0x6a, 0xaf, 0x03, 0xb0, 0xbc, 0x1a, 0x17, 0x84, 0x45, 0x72, 0xec, 0x5c, 0xc0, 0x6a, 0x34, 0xa4, 0xda, 0x0d, + 0xb7, 0x5e, 0x76, 0x7e, 0xf3, 0x4d, 0x62, 0x6b, 0x23, 0xdc, 0x52, 0x40, 0x19, 0xe5, 0x37, 0x96, 0x13, 0x76, 0xa7, + 0x7a, 0x47, 0xaa, 0x76, 0xc4, 0x89, 0x0b, 0x58, 0x6e, 0x78, 0x6a, 0xf5, 0x4d, 0x0c, 0x4e, 0x84, 0xaa, 0x95, 0x0e, + 0x77, 0x32, 0x81, 0xb8, 0x5f, 0xdd, 0xd7, 0xbd, 0x12, 0xfc, 0x24, 0xe4, 0xf5, 0x5b, 0xde, 0x01, 0x60, 0xc5, 0x87, + 0xbc, 0x98, 0x16, 0x8e, 0xd6, 0x65, 0x50, 0x06, 0x88, 0xd0, 0x0c, 0x80, 0x4e, 0xae, 0x0e, 0xa2, 0x34, 0x70, 0xc5, + 0x1d, 0x22, 0xfc, 0x34, 0x7a, 0x92, 0x3f, 0x0b, 0x9f, 0x54, 0xd3, 0xf0, 0x22, 0x0f, 0xa2, 0x8b, 0x2a, 0x88, 0x9e, + 0x54, 0x57, 0xe1, 0x93, 0x7c, 0x1a, 0x5d, 0xe4, 0x41, 0x78, 0x51, 0x35, 0xf6, 0x5d, 0xbb, 0xbb, 0x27, 0xe4, 0x6d, + 0x57, 0x7f, 0xe4, 0x5c, 0xd9, 0x53, 0xa6, 0xe7, 0xe7, 0xb5, 0x5e, 0xa9, 0xdd, 0xe6, 0x7a, 0x8d, 0x9a, 0xa9, 0x8f, + 0xb2, 0xbf, 0xd9, 0xc6, 0xc2, 0xa3, 0x39, 0x84, 0x3e, 0x23, 0x2d, 0xe6, 0x1e, 0xe7, 0x7a, 0xb3, 0x27, 0x85, 0x81, + 0x11, 0x93, 0x4a, 0x46, 0x4e, 0x2f, 0x70, 0x11, 0xaa, 0x10, 0xc3, 0x5a, 0xba, 0xda, 0x67, 0x5d, 0x7a, 0x03, 0x75, + 0x4d, 0xb1, 0xaf, 0x21, 0x03, 0x2f, 0x9a, 0x5e, 0x06, 0x63, 0x40, 0x8e, 0xc0, 0x3b, 0x3e, 0x5b, 0xc0, 0x81, 0xb9, + 0x06, 0xe8, 0x9b, 0x07, 0x7d, 0x5d, 0x96, 0x7c, 0xad, 0xfa, 0x66, 0xba, 0x1e, 0x29, 0xe5, 0xc7, 0x8a, 0x2f, 0x2f, + 0x9e, 0xb2, 0x5b, 0xae, 0x51, 0x51, 0x5e, 0xe8, 0xc5, 0x7a, 0x07, 0x5c, 0x75, 0x2f, 0xe0, 0x36, 0x8b, 0xc7, 0xae, + 0x3c, 0x60, 0xd9, 0x96, 0xdd, 0xb3, 0x6b, 0xf6, 0x9e, 0x3d, 0x62, 0xaf, 0xd8, 0x57, 0x56, 0x23, 0x44, 0x79, 0xa9, + 0xa4, 0x3c, 0xff, 0x86, 0xdf, 0x4a, 0xdb, 0xa3, 0x84, 0x25, 0xbb, 0xb7, 0xed, 0x34, 0xc3, 0x0d, 0x7b, 0xcf, 0x6f, + 0x86, 0x2b, 0xf6, 0x0a, 0xb2, 0xa1, 0x50, 0x3c, 0x58, 0xb1, 0x1a, 0xae, 0xb0, 0x94, 0x41, 0x9f, 0x86, 0xa5, 0x25, + 0x2c, 0x9a, 0x42, 0x51, 0x8a, 0x7e, 0xc5, 0x6b, 0xc2, 0x4e, 0xab, 0xb1, 0x10, 0xf9, 0xa1, 0xe1, 0x8a, 0xdd, 0xf3, + 0x9b, 0xc1, 0x8a, 0xbd, 0xd7, 0x36, 0xa2, 0xc1, 0xc6, 0x2d, 0x8e, 0xc0, 0xac, 0x74, 0x61, 0x52, 0xa0, 0xde, 0xda, + 0x37, 0xc1, 0x0d, 0xbb, 0xc6, 0xfa, 0x3d, 0xc2, 0xa2, 0x51, 0xe6, 0x1f, 0xac, 0xd8, 0x57, 0x2e, 0x31, 0xd4, 0xdc, + 0xf2, 0xa4, 0x63, 0xa8, 0x2e, 0x90, 0xae, 0x08, 0x8f, 0x38, 0xbd, 0xc8, 0xbe, 0x62, 0x19, 0xf4, 0x95, 0xe1, 0x8a, + 0x6d, 0xb1, 0x76, 0xd7, 0xc6, 0xb8, 0x65, 0x55, 0x4f, 0x82, 0x02, 0xa3, 0xac, 0x52, 0x5a, 0x2e, 0x8e, 0x58, 0x36, + 0x75, 0xd4, 0xa0, 0x36, 0x0c, 0xe8, 0x83, 0xd1, 0x7f, 0xf8, 0xfa, 0xdd, 0x0f, 0x5e, 0xa9, 0x6f, 0xbe, 0x2f, 0x1c, + 0xef, 0xca, 0x12, 0xbd, 0x2b, 0x3f, 0xf3, 0x72, 0xf6, 0x62, 0x3e, 0xd1, 0xb5, 0xa4, 0x4d, 0x86, 0xdc, 0x4d, 0x67, + 0x2f, 0x3a, 0xfc, 0x2d, 0x3f, 0xfb, 0x7e, 0x63, 0xf5, 0xb1, 0xfa, 0xae, 0xee, 0xde, 0xfb, 0xc1, 0xa6, 0x71, 0x2a, + 0xbe, 0x3b, 0x5d, 0x71, 0x6c, 0x67, 0xad, 0xbd, 0x33, 0xff, 0x87, 0x6b, 0xbd, 0xc5, 0xb1, 0xbb, 0xe6, 0xdb, 0xe1, + 0xc6, 0x1e, 0x06, 0xf9, 0x7d, 0xe5, 0x97, 0x5f, 0xf3, 0xe7, 0x5e, 0xa7, 0x24, 0x0b, 0xa8, 0x46, 0x9f, 0x8c, 0x34, + 0x74, 0xc9, 0x4c, 0x4c, 0x43, 0x7c, 0x91, 0x01, 0x3a, 0x17, 0x88, 0x67, 0x77, 0x7c, 0x3c, 0xb9, 0xbb, 0x8a, 0x27, + 0x77, 0x03, 0xfe, 0xc9, 0xb4, 0xa0, 0xbd, 0xe0, 0xee, 0x7c, 0xf6, 0x99, 0x17, 0xf6, 0x92, 0x7c, 0xe1, 0xb3, 0x77, + 0xc2, 0x5d, 0xa5, 0x2f, 0x7c, 0xf6, 0x55, 0xf0, 0xcf, 0x23, 0x4d, 0x96, 0xc1, 0xbe, 0xd6, 0xfc, 0xf3, 0x08, 0x59, + 0x3f, 0xd8, 0x17, 0xc1, 0xdf, 0x81, 0xff, 0x77, 0x95, 0xa0, 0x65, 0xfc, 0x4b, 0xad, 0x7e, 0xbe, 0x97, 0xb1, 0x39, + 0xf0, 0x26, 0xb4, 0x82, 0xde, 0xbc, 0xad, 0xe5, 0x4f, 0xe2, 0xe2, 0x48, 0xd5, 0x53, 0xc3, 0x41, 0x8b, 0xc5, 0xdc, + 0xd4, 0x47, 0xe9, 0x54, 0xde, 0xe4, 0x2d, 0x4f, 0xa4, 0x85, 0xf9, 0x0e, 0xc2, 0x81, 0xdf, 0xda, 0x30, 0x05, 0x3b, + 0x8e, 0x9b, 0xc1, 0x5b, 0x06, 0x10, 0x92, 0xd9, 0x74, 0xcb, 0xaf, 0xf9, 0x23, 0xfe, 0x95, 0xef, 0x82, 0x7b, 0xfe, + 0x9e, 0xbf, 0xe2, 0x75, 0xcd, 0x77, 0x6c, 0x21, 0x21, 0x4f, 0xeb, 0xed, 0x65, 0xb0, 0x65, 0xf5, 0xee, 0x32, 0xb8, + 0x67, 0xf5, 0xf6, 0x69, 0x70, 0xcd, 0xea, 0xdd, 0xd3, 0xe0, 0x3d, 0xdb, 0x5e, 0x06, 0x8f, 0xd8, 0xee, 0x32, 0x78, + 0xc5, 0xb6, 0x4f, 0x83, 0xaf, 0x6c, 0xf7, 0x34, 0xa8, 0x15, 0xd2, 0xc3, 0x57, 0x21, 0x99, 0x4e, 0xbe, 0xd6, 0xcc, + 0xb0, 0xea, 0x06, 0x5f, 0x84, 0xf5, 0x8b, 0x6a, 0x19, 0x7c, 0xa9, 0x99, 0x6e, 0x73, 0x20, 0x04, 0xd3, 0x2d, 0x0e, + 0x6e, 0xe9, 0x89, 0x69, 0x57, 0x90, 0x0a, 0xd6, 0xd5, 0xd2, 0xe0, 0xa6, 0x6e, 0x5a, 0x27, 0xb3, 0xe3, 0x9d, 0x18, + 0x77, 0x78, 0x27, 0xde, 0xb0, 0x45, 0xd3, 0xe9, 0xaa, 0x73, 0xfa, 0x3c, 0xd0, 0x47, 0x80, 0xde, 0xfb, 0x2b, 0xe9, + 0x41, 0x53, 0x34, 0x3c, 0x57, 0xba, 0xe3, 0xd6, 0x7e, 0x1f, 0x5a, 0xfb, 0x3d, 0x93, 0x8a, 0xb4, 0x88, 0x45, 0x65, + 0x51, 0x55, 0xc8, 0x27, 0x1e, 0x64, 0x5a, 0xab, 0x96, 0x30, 0x52, 0x67, 0x02, 0x26, 0x7d, 0x41, 0x87, 0x41, 0x4e, + 0x76, 0x05, 0xb6, 0xe0, 0x9b, 0x41, 0xc2, 0xd6, 0x3c, 0x9e, 0x0e, 0x93, 0x60, 0xc1, 0x96, 0x7c, 0xd8, 0x2d, 0x16, + 0xac, 0x54, 0x18, 0x93, 0xbe, 0x3e, 0x1d, 0xed, 0xee, 0xbc, 0xb7, 0x4a, 0xe3, 0x38, 0x13, 0xa8, 0x73, 0xab, 0xf4, + 0x36, 0xbf, 0x75, 0x76, 0xf5, 0xb5, 0xda, 0xe5, 0x41, 0x60, 0xf8, 0x0c, 0x44, 0x3b, 0xc4, 0x7b, 0x07, 0x35, 0x46, + 0xba, 0x25, 0xb3, 0xee, 0x2b, 0x7b, 0x5f, 0xdf, 0x9a, 0xad, 0xfa, 0xdf, 0x2d, 0x82, 0xf6, 0x72, 0xd9, 0xfb, 0x9f, + 0xcc, 0xab, 0xbf, 0x77, 0xbc, 0xba, 0xf1, 0x27, 0xf7, 0xfc, 0x13, 0x46, 0x27, 0x60, 0x22, 0xdb, 0xf1, 0x4f, 0xa3, + 0x6d, 0xe3, 0x94, 0x27, 0xf7, 0xf2, 0xff, 0x2b, 0x05, 0xda, 0xbb, 0x79, 0x65, 0x6f, 0x8a, 0x5b, 0xde, 0xb1, 0x97, + 0x2f, 0xac, 0x3d, 0xd1, 0x20, 0x94, 0x7c, 0xe2, 0x6e, 0x50, 0x34, 0xec, 0x89, 0x2f, 0x78, 0x35, 0xfb, 0x34, 0x9f, + 0x6c, 0xf9, 0xf1, 0x8e, 0xf8, 0xa9, 0x63, 0x47, 0x7c, 0xe1, 0x0f, 0x16, 0xcd, 0xb7, 0x7a, 0xb5, 0x73, 0x27, 0x77, + 0x2a, 0xbd, 0xe3, 0xc7, 0xfb, 0xf8, 0xf0, 0xdf, 0xae, 0xf4, 0xee, 0xbb, 0x2b, 0x6d, 0x57, 0xb9, 0xbb, 0xf3, 0x4d, + 0xc7, 0x37, 0xb2, 0xd6, 0x18, 0x6e, 0x66, 0x14, 0x8c, 0x30, 0x6d, 0x61, 0x9a, 0x06, 0x91, 0xa5, 0x58, 0x84, 0x44, + 0x8d, 0xd2, 0x39, 0xd1, 0x67, 0x41, 0xa7, 0xa0, 0x8b, 0x1b, 0xfd, 0x2d, 0x1f, 0xb3, 0x1b, 0xe3, 0xb2, 0x79, 0x7b, + 0x75, 0x33, 0x19, 0x0c, 0x6e, 0xfd, 0xfd, 0x1d, 0x0f, 0x67, 0xb7, 0x73, 0xf6, 0x96, 0xdf, 0xd1, 0x7a, 0x9a, 0xa8, + 0xc6, 0x17, 0x0f, 0x49, 0x60, 0xb7, 0xbe, 0x3f, 0xb1, 0x88, 0x60, 0xed, 0x1b, 0xe7, 0xad, 0x3f, 0x90, 0x66, 0x69, + 0xb9, 0xb5, 0xbf, 0x7f, 0x58, 0x43, 0x71, 0x0b, 0x42, 0xc6, 0x7b, 0x5b, 0xe5, 0xf0, 0x8a, 0x7f, 0xf4, 0xde, 0xfa, + 0xd3, 0xb7, 0x3a, 0xf8, 0x66, 0xa2, 0xce, 0xa5, 0x57, 0x17, 0x4f, 0xd9, 0x67, 0xfe, 0x49, 0x9e, 0x29, 0xef, 0x84, + 0x9c, 0xb6, 0xd7, 0x48, 0xe2, 0x44, 0x47, 0xc5, 0x57, 0x37, 0x91, 0x40, 0x21, 0x60, 0x57, 0xf8, 0x5a, 0xf3, 0xfb, + 0x49, 0x39, 0xf5, 0x76, 0x40, 0xf2, 0xca, 0x6d, 0x45, 0xf4, 0x2d, 0xe7, 0xfc, 0x66, 0x78, 0x39, 0xfd, 0xda, 0xed, + 0xdb, 0xa3, 0xc2, 0xda, 0x54, 0xc4, 0xdb, 0x2d, 0x06, 0x61, 0x9d, 0xcc, 0x2c, 0x73, 0xc9, 0x97, 0xbe, 0xd6, 0x66, + 0xee, 0x31, 0xbd, 0xe3, 0x4c, 0x33, 0x64, 0xf4, 0x05, 0x66, 0xa6, 0xc3, 0x61, 0x79, 0x8e, 0xe5, 0xf1, 0xe1, 0xab, + 0x27, 0x8f, 0x06, 0x8f, 0x30, 0x84, 0xcb, 0x0a, 0x0b, 0xf9, 0xca, 0x87, 0x59, 0xdd, 0xba, 0x76, 0x5c, 0x3c, 0x1d, + 0xbe, 0x80, 0xbc, 0x41, 0xd7, 0x43, 0x53, 0x44, 0xab, 0xfc, 0x8e, 0xa2, 0x4f, 0x94, 0x1c, 0x74, 0x3c, 0x81, 0xda, + 0x21, 0x17, 0xee, 0xd7, 0x27, 0x1c, 0x14, 0x1d, 0x58, 0x6a, 0xbf, 0x7f, 0xfe, 0x89, 0x08, 0xa5, 0x61, 0xbc, 0x5f, + 0x84, 0xd1, 0x9f, 0x71, 0x59, 0xac, 0xe1, 0x88, 0x1d, 0xc0, 0xe7, 0x9e, 0xe8, 0x6b, 0xd8, 0xd2, 0xf7, 0xfd, 0xc0, + 0xdb, 0xf2, 0x6b, 0xf6, 0x95, 0x7b, 0x97, 0xc3, 0x57, 0xfe, 0x93, 0x47, 0x20, 0x3f, 0x21, 0x4e, 0x0a, 0x86, 0xc4, + 0x76, 0x14, 0xa3, 0xd6, 0xe1, 0x97, 0x1a, 0x62, 0xb5, 0x3e, 0x21, 0x75, 0x17, 0xa4, 0x7f, 0x50, 0xc8, 0x7e, 0x42, + 0x60, 0x35, 0x49, 0x9f, 0x02, 0x93, 0xf8, 0xb6, 0x86, 0x04, 0xd2, 0xb4, 0x40, 0x0c, 0x0e, 0x14, 0x9f, 0x0a, 0xfe, + 0x75, 0xf8, 0x85, 0xe4, 0xbf, 0x9b, 0x9a, 0x8f, 0xe1, 0x6f, 0x18, 0x9a, 0x49, 0x75, 0x9f, 0xd6, 0x51, 0xe2, 0xd5, + 0x70, 0xea, 0x85, 0x95, 0x50, 0x27, 0x43, 0x90, 0x8a, 0x21, 0x17, 0xe2, 0xe2, 0xe9, 0xe4, 0xb6, 0x14, 0xe1, 0x9f, + 0x13, 0x7c, 0x26, 0x57, 0x9a, 0x7c, 0x46, 0x4f, 0x1a, 0x59, 0xc0, 0xbd, 0x7c, 0x5f, 0xf6, 0x6a, 0x70, 0x53, 0x0f, + 0xf9, 0x6d, 0xed, 0xbe, 0x2f, 0xe7, 0x04, 0x3d, 0xb2, 0x1f, 0xd0, 0x1c, 0x0c, 0xd4, 0x0c, 0xa4, 0x0c, 0xc1, 0x2d, + 0x5c, 0xfa, 0x3d, 0x55, 0x90, 0x2f, 0xbf, 0xf7, 0x45, 0xc8, 0xc0, 0x95, 0x1b, 0xc2, 0x94, 0x4b, 0x85, 0x14, 0x38, + 0x6e, 0xeb, 0xc1, 0x17, 0x8d, 0x4e, 0x22, 0xc1, 0xa7, 0x04, 0x24, 0x49, 0xcb, 0x03, 0x49, 0x23, 0xa6, 0x03, 0x71, + 0xa1, 0x34, 0xcd, 0x4a, 0x8a, 0x38, 0xc4, 0xae, 0xfa, 0x16, 0x09, 0xcf, 0x82, 0xf7, 0x0c, 0xd6, 0x8e, 0x14, 0x2d, + 0xbe, 0x1a, 0xd3, 0xb1, 0x0e, 0x1b, 0x5a, 0xca, 0xe2, 0x3e, 0x4b, 0xea, 0x34, 0x12, 0x57, 0xde, 0x09, 0xf9, 0xf3, + 0x9f, 0x4a, 0x04, 0xd2, 0xbb, 0x1a, 0x88, 0x41, 0xf0, 0x03, 0xf4, 0x1f, 0xb0, 0xc8, 0x41, 0x50, 0xaa, 0xcb, 0x30, + 0xaf, 0x32, 0x2a, 0x70, 0xb6, 0x63, 0xdb, 0x39, 0x53, 0x75, 0x0b, 0xbe, 0x08, 0xc3, 0x90, 0x76, 0xb6, 0x6a, 0x4e, + 0x6e, 0xf5, 0x06, 0xea, 0x99, 0xc4, 0x91, 0x5a, 0x8a, 0x23, 0x6d, 0xcd, 0x7d, 0xba, 0xf0, 0xba, 0xe5, 0x05, 0x0d, + 0x17, 0xa0, 0x17, 0xa5, 0xbb, 0xce, 0x27, 0x14, 0xba, 0xac, 0xc6, 0xd5, 0x50, 0xd4, 0xa1, 0x1c, 0x63, 0xed, 0xcf, + 0x95, 0x3c, 0xbf, 0x03, 0xeb, 0x11, 0x1a, 0xbe, 0x2a, 0x75, 0x10, 0xdb, 0x4f, 0xf4, 0xae, 0x53, 0xa9, 0xbf, 0x01, + 0x60, 0xe0, 0xd4, 0xf1, 0x50, 0x1f, 0xb5, 0x53, 0xc8, 0x76, 0xee, 0x2d, 0x31, 0x2a, 0x57, 0xc2, 0x53, 0xa5, 0xe5, + 0x29, 0x65, 0xd5, 0xd7, 0x82, 0x5b, 0xd9, 0x7d, 0x36, 0x80, 0x8c, 0x36, 0x28, 0x90, 0x67, 0xd4, 0xd6, 0x78, 0x90, + 0x6a, 0x9a, 0x25, 0x8e, 0xe1, 0x83, 0x22, 0xcd, 0x2a, 0xb0, 0x78, 0x99, 0x4b, 0xe6, 0xa0, 0x60, 0xb9, 0xde, 0x6c, + 0xa6, 0x99, 0xea, 0x8b, 0xdc, 0xde, 0x68, 0xbc, 0x4c, 0xff, 0xcd, 0x92, 0x01, 0x8f, 0x2e, 0x9e, 0xfa, 0x01, 0xa4, + 0x49, 0x8a, 0x07, 0x48, 0x82, 0xed, 0xc1, 0x2e, 0x76, 0x18, 0xb6, 0x8a, 0x95, 0x3d, 0x79, 0xba, 0xdc, 0xa1, 0x29, + 0x97, 0xe0, 0x92, 0x13, 0x73, 0x39, 0xf5, 0x7d, 0xc9, 0x7a, 0x43, 0x71, 0xca, 0xa6, 0x09, 0x28, 0x09, 0xb4, 0x5b, + 0xf0, 0x5f, 0xf8, 0xd4, 0xd0, 0x69, 0x01, 0x96, 0xda, 0x6e, 0xc0, 0x7f, 0xa1, 0x5f, 0x6c, 0x77, 0x51, 0x3f, 0x30, + 0x0f, 0xf6, 0x66, 0x71, 0x65, 0x0c, 0x38, 0x49, 0x5c, 0x69, 0x1e, 0xb9, 0x7e, 0x50, 0xf4, 0xe9, 0xb2, 0x76, 0xe0, + 0x4c, 0x71, 0x61, 0x95, 0xda, 0x24, 0xbd, 0xf6, 0x5b, 0x6a, 0xe2, 0x4d, 0x94, 0x54, 0x85, 0xed, 0x90, 0xf6, 0x2f, + 0x29, 0x67, 0xaa, 0xb8, 0x43, 0xf4, 0x64, 0x37, 0x71, 0x15, 0x78, 0x61, 0x55, 0xb1, 0x11, 0x6a, 0x33, 0xb2, 0x9c, + 0xc0, 0xe9, 0x1e, 0xab, 0x0b, 0x3e, 0xb6, 0xab, 0xd9, 0x05, 0x2b, 0xd9, 0x9a, 0x49, 0xf7, 0x79, 0x3b, 0xe6, 0x42, + 0x5e, 0xe9, 0x65, 0xd1, 0x0a, 0x68, 0x0f, 0x02, 0x87, 0x5f, 0x68, 0xba, 0x47, 0xcf, 0x36, 0xdb, 0xd4, 0x66, 0x63, + 0x6b, 0x11, 0x42, 0x06, 0xa2, 0xa1, 0x2f, 0xe4, 0x8c, 0x22, 0x5f, 0xa5, 0xe5, 0x5a, 0x6d, 0xac, 0x32, 0x5e, 0x60, + 0x22, 0xc8, 0x70, 0x16, 0xde, 0xa1, 0xa7, 0xf5, 0x48, 0x53, 0x4c, 0x82, 0x93, 0x2e, 0xfe, 0x02, 0x6c, 0x28, 0x4f, + 0x72, 0x73, 0x40, 0x0e, 0xa0, 0x72, 0x29, 0x4a, 0xa5, 0x0c, 0xfe, 0x45, 0xdd, 0x91, 0x6d, 0xd5, 0x7f, 0xa7, 0x81, + 0x0c, 0xee, 0x40, 0xdf, 0xf6, 0x42, 0x6b, 0x47, 0x3b, 0x57, 0xb6, 0xa6, 0x6d, 0x91, 0xe6, 0x31, 0xb2, 0xd8, 0x00, + 0xf2, 0x89, 0x74, 0x0e, 0x44, 0x5e, 0x13, 0x8d, 0x77, 0xf6, 0x8c, 0x8f, 0xa7, 0xe2, 0x21, 0x79, 0xaf, 0xf2, 0x7d, + 0x73, 0xaf, 0x0f, 0xc6, 0xd8, 0xb7, 0xa0, 0x4c, 0x7c, 0xb0, 0xda, 0x5a, 0x97, 0x58, 0x6f, 0x95, 0x26, 0xd1, 0x0d, + 0x57, 0xd0, 0x71, 0x24, 0x6e, 0x10, 0x83, 0x63, 0xc6, 0x6b, 0xab, 0x2c, 0x7d, 0x85, 0x65, 0xae, 0x63, 0x96, 0x0c, + 0x99, 0xd4, 0x79, 0xa2, 0xe0, 0xc9, 0xcf, 0x13, 0x92, 0x11, 0x51, 0xb3, 0x2d, 0x47, 0x29, 0x37, 0x2d, 0xe0, 0x32, + 0x23, 0x03, 0xf8, 0x26, 0x4d, 0x00, 0xca, 0xe5, 0x4b, 0x90, 0x4a, 0x43, 0x04, 0xd7, 0x6c, 0x2f, 0x19, 0xdd, 0x3a, + 0x5a, 0x07, 0x55, 0x92, 0xb9, 0x83, 0x73, 0x3b, 0x8b, 0x94, 0x7a, 0xf3, 0x11, 0x86, 0x9d, 0x7c, 0x08, 0xeb, 0x04, + 0xbf, 0x0d, 0xa8, 0x49, 0x9f, 0x0a, 0x2f, 0x1a, 0x01, 0x9a, 0xfa, 0x4e, 0x95, 0xf1, 0xa9, 0xf0, 0xb2, 0xd1, 0x96, + 0x65, 0x94, 0x42, 0x75, 0xc1, 0xec, 0xd6, 0x74, 0x21, 0xe6, 0x55, 0x35, 0xd0, 0x06, 0xb9, 0x5d, 0xc7, 0x0c, 0x68, + 0xd4, 0x76, 0xe5, 0x91, 0x05, 0xb8, 0x35, 0x13, 0x81, 0x91, 0xf3, 0xef, 0xf3, 0x97, 0x2a, 0x9c, 0xa7, 0xdf, 0x0f, + 0xbd, 0xfd, 0x36, 0x88, 0x46, 0xdb, 0x4b, 0xb6, 0x0b, 0xa2, 0xd1, 0xee, 0xb2, 0x61, 0xf4, 0xfb, 0x29, 0xfd, 0x7e, + 0xda, 0x80, 0xaa, 0x44, 0x98, 0x88, 0x7b, 0xfd, 0x46, 0x2d, 0x5f, 0xa9, 0xf5, 0x3b, 0xb5, 0x7c, 0xa9, 0x86, 0xb7, + 0xf6, 0x24, 0x12, 0x44, 0x96, 0xc6, 0xe6, 0x5e, 0xb2, 0xa5, 0x5a, 0x2a, 0x1d, 0xa3, 0xca, 0x88, 0x5a, 0x3a, 0x9b, + 0x63, 0xc5, 0x48, 0x3b, 0x07, 0x25, 0x03, 0x32, 0x2d, 0xae, 0x6a, 0x4c, 0x37, 0x2b, 0x5a, 0x62, 0x32, 0xc2, 0xca, + 0xb6, 0xbc, 0xdd, 0xa4, 0x6a, 0x3a, 0x27, 0x37, 0xb7, 0x4a, 0xb9, 0xb9, 0x15, 0x3c, 0xff, 0x86, 0x6e, 0xb9, 0xe4, + 0xda, 0xcb, 0x6c, 0x5a, 0x28, 0xdd, 0x32, 0xae, 0xc1, 0xd6, 0xbe, 0x09, 0x64, 0x99, 0x0f, 0x14, 0x35, 0xb6, 0x17, + 0x8d, 0xf2, 0x0d, 0xb2, 0x15, 0x31, 0xea, 0x94, 0x05, 0xe3, 0x6f, 0x77, 0xf4, 0x40, 0x06, 0xaa, 0xaa, 0xda, 0x38, + 0xb8, 0xb3, 0xd2, 0x1f, 0x96, 0x17, 0x4f, 0x59, 0x62, 0xa5, 0x93, 0x0b, 0x55, 0xe8, 0x0f, 0x42, 0x74, 0x53, 0xd9, + 0x70, 0x70, 0xa8, 0x8b, 0xad, 0x0c, 0x08, 0x3d, 0x4c, 0xef, 0x6d, 0xac, 0x64, 0xb9, 0x6b, 0xca, 0x17, 0x33, 0x9e, + 0x70, 0x1c, 0x7d, 0xb9, 0x5a, 0x84, 0xb5, 0x5a, 0x64, 0x27, 0xc0, 0x43, 0x6b, 0xb5, 0x14, 0x72, 0xb5, 0x08, 0x67, + 0xa6, 0x0b, 0x35, 0xd3, 0x33, 0x50, 0x40, 0x0a, 0x35, 0xcb, 0x13, 0x80, 0x85, 0x17, 0x66, 0x86, 0x0b, 0x33, 0xc3, + 0x71, 0x48, 0x8d, 0xff, 0x83, 0xde, 0xeb, 0xdc, 0x73, 0xcb, 0xdd, 0xe8, 0x34, 0xe2, 0xdb, 0xd1, 0x06, 0x73, 0x7c, + 0x10, 0x4e, 0xaa, 0x7e, 0x3f, 0x2d, 0x11, 0xab, 0xc7, 0xc0, 0x08, 0xca, 0xa1, 0x72, 0xb4, 0x5f, 0x16, 0x96, 0x64, + 0x49, 0x58, 0x92, 0x7b, 0x35, 0xce, 0xa5, 0xe5, 0xe2, 0x55, 0x12, 0x88, 0x44, 0xc6, 0x4b, 0x69, 0x82, 0x4f, 0x78, + 0x39, 0x32, 0x52, 0xf3, 0xe4, 0x26, 0xf5, 0x72, 0x96, 0xb1, 0x31, 0x62, 0x18, 0x85, 0x7e, 0x53, 0xf5, 0xfb, 0x79, + 0xe9, 0xe5, 0xd4, 0xce, 0x4f, 0xe0, 0x7a, 0x79, 0xea, 0x2c, 0x72, 0x84, 0xbc, 0x1a, 0x49, 0x85, 0xe5, 0xb5, 0x52, + 0x4f, 0x5f, 0x82, 0x0f, 0xea, 0xee, 0x8d, 0x02, 0x20, 0x2e, 0x72, 0xe9, 0x5f, 0x5b, 0xc2, 0xa5, 0x29, 0x37, 0x30, + 0xe8, 0x21, 0xcf, 0x49, 0x08, 0x95, 0x20, 0x24, 0x85, 0x75, 0xe3, 0xbe, 0x78, 0x3a, 0x71, 0xdd, 0x59, 0x6c, 0x60, + 0x82, 0xc3, 0x01, 0x10, 0x0f, 0xa6, 0x5e, 0x34, 0xe0, 0xa5, 0x9a, 0x33, 0x1f, 0xbd, 0x9c, 0x60, 0x32, 0x40, 0x55, + 0x31, 0x70, 0xca, 0x7a, 0x22, 0x1f, 0x19, 0x37, 0x33, 0xdf, 0x0f, 0xf0, 0xdd, 0xba, 0x90, 0xe8, 0x0f, 0x0a, 0xa0, + 0x20, 0x53, 0x00, 0x05, 0x89, 0x01, 0x28, 0x88, 0x0d, 0x40, 0xc1, 0xa6, 0xe1, 0x4b, 0xa9, 0xc3, 0x8d, 0x80, 0x2e, + 0xc2, 0x87, 0x9e, 0x85, 0x8d, 0x15, 0x8a, 0x67, 0x63, 0x36, 0x66, 0x85, 0xda, 0x79, 0x72, 0x39, 0x15, 0x3b, 0x8b, + 0xb1, 0xae, 0x22, 0xeb, 0xc4, 0x0b, 0x09, 0x45, 0xce, 0xb9, 0x91, 0xa8, 0xbb, 0x9f, 0x7b, 0x2f, 0xc9, 0x58, 0x32, + 0x6f, 0x68, 0xd4, 0x60, 0x5e, 0x76, 0x1d, 0xc0, 0xb4, 0xe4, 0xdb, 0x82, 0x06, 0xd3, 0xa9, 0xf2, 0x88, 0x34, 0x09, + 0x6a, 0xe7, 0x32, 0x29, 0x72, 0x42, 0x98, 0x04, 0xbd, 0x12, 0xfc, 0x46, 0xa2, 0xfc, 0x7f, 0xd3, 0x09, 0x1e, 0xe0, + 0x98, 0x68, 0x95, 0x7c, 0x05, 0x03, 0x66, 0xce, 0x9f, 0x4b, 0xa7, 0x6c, 0x84, 0x62, 0x2c, 0xd3, 0x78, 0xf4, 0x95, + 0x0d, 0x11, 0xda, 0xea, 0x39, 0x9a, 0x98, 0xa0, 0x0e, 0xf0, 0x88, 0xfe, 0x1a, 0x7d, 0x35, 0x14, 0x2a, 0x5d, 0x8d, + 0xd4, 0x35, 0x3b, 0xe7, 0xfc, 0x5d, 0x6d, 0x38, 0x91, 0x31, 0x6d, 0x0a, 0x7c, 0x03, 0x02, 0xf9, 0x06, 0x02, 0xc0, + 0x55, 0xd3, 0x99, 0xbd, 0x02, 0x38, 0x07, 0x02, 0x78, 0x9c, 0x77, 0x3c, 0x7e, 0xa0, 0xbf, 0x8a, 0xe3, 0xde, 0x69, + 0x1a, 0xb6, 0xff, 0x0a, 0x8c, 0xc5, 0x50, 0x8e, 0xe7, 0x3b, 0x05, 0xc9, 0x1e, 0xa5, 0x2c, 0x5d, 0x35, 0x91, 0x1d, + 0x8a, 0xf5, 0x69, 0x4e, 0x19, 0x4b, 0xdb, 0x72, 0x8c, 0x36, 0x5e, 0x3f, 0xc4, 0xe3, 0x9b, 0x1b, 0x3d, 0xf9, 0xa0, + 0x07, 0xb7, 0xb7, 0x37, 0xaf, 0x7a, 0xcc, 0xe6, 0x5b, 0xb1, 0x78, 0x56, 0xc4, 0x89, 0xd3, 0x3a, 0xe4, 0x00, 0x07, + 0x39, 0x09, 0x81, 0x74, 0x8c, 0x4b, 0x2d, 0x3a, 0xa8, 0x59, 0xce, 0x6b, 0x60, 0x99, 0x45, 0x90, 0x0d, 0x10, 0xd5, + 0x34, 0x15, 0xab, 0xe1, 0x41, 0xa9, 0x9a, 0x53, 0x2a, 0xb5, 0x6f, 0x38, 0x5b, 0x9d, 0x3e, 0xb1, 0x6a, 0x13, 0x6e, + 0xfd, 0xb9, 0xf6, 0x04, 0x6d, 0x25, 0x0d, 0x84, 0x7a, 0xbe, 0x4a, 0x97, 0x14, 0xc5, 0xe3, 0xcc, 0xc4, 0x53, 0x15, + 0x18, 0xfb, 0xd6, 0x8e, 0xa0, 0x20, 0x69, 0xba, 0x0e, 0x38, 0x4c, 0xa3, 0x13, 0x16, 0xff, 0x94, 0x3e, 0x94, 0x17, + 0xb5, 0x02, 0x27, 0xf9, 0x87, 0x70, 0x11, 0x49, 0x2c, 0xf4, 0x4b, 0x02, 0x20, 0x91, 0xc1, 0xab, 0x51, 0xb1, 0x16, + 0x2a, 0x40, 0x4e, 0x51, 0x7a, 0xab, 0xf8, 0xb8, 0x14, 0xa5, 0x4a, 0xa9, 0xcc, 0x8d, 0x4a, 0x01, 0x61, 0x6d, 0xe0, + 0xe8, 0x02, 0xbe, 0x80, 0xa0, 0xb5, 0xdc, 0xad, 0x6d, 0xcf, 0x1b, 0x99, 0xcf, 0x4c, 0xf3, 0xb4, 0xfa, 0xa0, 0xfe, + 0x7e, 0xbf, 0xc0, 0x30, 0x1b, 0x4f, 0x7f, 0xdf, 0x66, 0x08, 0x37, 0x7f, 0xc3, 0x10, 0x2d, 0x01, 0x1c, 0xb3, 0xb4, + 0x87, 0x42, 0x16, 0x4c, 0xb0, 0x86, 0xaa, 0x3c, 0xe5, 0xb3, 0x97, 0x4f, 0x6e, 0x00, 0x4d, 0x0d, 0x5d, 0xdc, 0xe8, + 0x54, 0x57, 0x25, 0x08, 0xdf, 0x77, 0x85, 0x7a, 0x6c, 0x0e, 0x38, 0x35, 0x00, 0x14, 0x8b, 0xbc, 0xd6, 0x63, 0xfb, + 0x07, 0xbd, 0x51, 0x6f, 0x80, 0x78, 0x3a, 0xe7, 0x85, 0x7f, 0x44, 0xbf, 0x4e, 0xfd, 0x19, 0x17, 0x82, 0xa8, 0xd7, + 0x93, 0xf0, 0x4e, 0x9c, 0xa5, 0x71, 0x70, 0xd6, 0x1b, 0x98, 0x8b, 0x40, 0x71, 0x96, 0xe6, 0x67, 0x20, 0x96, 0x23, + 0x3c, 0x62, 0xcd, 0x56, 0x80, 0x18, 0x58, 0xea, 0x90, 0x64, 0xd5, 0xb1, 0xfd, 0xfe, 0xeb, 0x91, 0xe1, 0x4d, 0x47, + 0x44, 0x18, 0xfd, 0xbb, 0x02, 0x01, 0x0a, 0x96, 0x99, 0xed, 0xcc, 0xa4, 0xab, 0x3d, 0xab, 0xe7, 0xcd, 0x26, 0xef, + 0xea, 0x1d, 0xab, 0x69, 0x39, 0x35, 0xad, 0xb2, 0x9a, 0x36, 0xc9, 0xa1, 0x66, 0xa2, 0xdf, 0xd7, 0xf8, 0xa8, 0xf9, + 0x1c, 0x70, 0xd9, 0x30, 0xf9, 0xf5, 0xac, 0x9a, 0xf7, 0xfb, 0x9e, 0x7c, 0x04, 0xbf, 0x90, 0xb8, 0xcc, 0xad, 0xb1, + 0x7c, 0xfa, 0x86, 0xf8, 0xcc, 0x0c, 0xe2, 0xd1, 0xea, 0x08, 0xea, 0xeb, 0x5a, 0x78, 0x1d, 0x73, 0x85, 0xcd, 0xc4, + 0xf4, 0x35, 0x0c, 0x9e, 0x27, 0x7c, 0xf0, 0x96, 0xa3, 0xbf, 0x91, 0xce, 0x4c, 0xc1, 0x42, 0xce, 0xfd, 0xc9, 0x6b, + 0x84, 0x4e, 0x46, 0xa4, 0x07, 0x9d, 0x4e, 0xd0, 0x90, 0xfd, 0xfe, 0x2d, 0x74, 0x66, 0x2b, 0x95, 0xb2, 0x55, 0x51, + 0x99, 0xae, 0xeb, 0xa2, 0xac, 0xa0, 0x63, 0xe9, 0xe7, 0xad, 0x90, 0x99, 0xf5, 0x33, 0x0b, 0xf9, 0xe9, 0x56, 0x62, + 0x4d, 0xd9, 0xf6, 0x89, 0xda, 0x20, 0xcd, 0xba, 0x50, 0x5d, 0xe0, 0xdc, 0x59, 0x7b, 0xbd, 0x11, 0xea, 0x9f, 0xf3, + 0xd1, 0xba, 0x58, 0x7b, 0xe0, 0x12, 0x33, 0x4b, 0xe7, 0x8a, 0x43, 0x23, 0xf7, 0x47, 0x5f, 0x8a, 0x34, 0xa7, 0x3c, + 0x40, 0x83, 0x28, 0xe6, 0xf6, 0x5b, 0x20, 0xfd, 0xd0, 0x5b, 0x20, 0xfb, 0xe8, 0x9c, 0x93, 0xd7, 0x00, 0x4e, 0x87, + 0x88, 0xb8, 0x15, 0x09, 0x3a, 0x56, 0x0d, 0x6f, 0x2c, 0xdc, 0xd3, 0x5e, 0x1a, 0xf7, 0xd2, 0xfc, 0x2c, 0xed, 0xf7, + 0x0d, 0x80, 0x66, 0x8a, 0xc8, 0xf0, 0x38, 0x23, 0x77, 0x49, 0x0b, 0xc1, 0x94, 0xf6, 0x5f, 0x8d, 0x21, 0x41, 0x20, + 0xe0, 0xff, 0x10, 0xde, 0x23, 0x40, 0xdb, 0xa4, 0x0d, 0xb8, 0xea, 0x31, 0x1d, 0x98, 0x2d, 0x39, 0x5b, 0x75, 0x36, + 0x00, 0xe5, 0x54, 0x69, 0x3d, 0xe5, 0x71, 0x4d, 0x11, 0x91, 0x2a, 0x0b, 0xf5, 0x1b, 0xeb, 0xc9, 0x64, 0x95, 0x8b, + 0x0c, 0x39, 0x2a, 0xd3, 0xbb, 0x9a, 0x11, 0x62, 0x97, 0x7e, 0x7e, 0x03, 0x4b, 0x36, 0xfe, 0x88, 0x93, 0xb7, 0x04, + 0x48, 0xdb, 0x59, 0xbb, 0xaa, 0x76, 0x39, 0x6e, 0xed, 0xe6, 0x80, 0xe4, 0xeb, 0x8d, 0x46, 0x23, 0xed, 0x27, 0x27, + 0x60, 0xa8, 0x7a, 0x6a, 0x29, 0xf4, 0x58, 0xad, 0xb0, 0x75, 0x3b, 0x72, 0x99, 0x25, 0x83, 0xf9, 0xc2, 0x38, 0x7e, + 0x69, 0x3e, 0xfa, 0x70, 0xa9, 0xac, 0x5d, 0x47, 0x7c, 0xfd, 0x47, 0x59, 0xad, 0xef, 0x79, 0x57, 0x35, 0x01, 0x5f, + 0x54, 0xb1, 0xa5, 0xdf, 0xf1, 0x9e, 0xec, 0x5d, 0x7c, 0xed, 0x1a, 0xbb, 0xe4, 0x7b, 0xde, 0xa2, 0xce, 0xf3, 0x95, + 0xaf, 0x1b, 0x55, 0xba, 0xbd, 0x97, 0xdc, 0xe0, 0xda, 0x3b, 0x6a, 0x1a, 0xeb, 0x99, 0x1f, 0x3d, 0x2c, 0x42, 0xb6, + 0xf3, 0xa1, 0xf7, 0x55, 0xf3, 0xf4, 0xac, 0xa1, 0x37, 0xa9, 0xa1, 0x0f, 0xbd, 0x28, 0xdb, 0xa7, 0xa6, 0x11, 0xbd, + 0x86, 0x0d, 0x7d, 0xe8, 0x2d, 0x39, 0x39, 0x24, 0x18, 0x9c, 0x1a, 0xf3, 0x87, 0x87, 0xd3, 0x19, 0xfe, 0x8e, 0x01, + 0x95, 0x98, 0xcc, 0xa7, 0xc7, 0xb4, 0xa3, 0x00, 0x33, 0xaa, 0xf4, 0xf6, 0xe9, 0x81, 0xed, 0x78, 0x59, 0x0f, 0x2d, + 0xbd, 0x7b, 0x72, 0x74, 0x3b, 0x5e, 0x55, 0xe3, 0x4b, 0x39, 0xe4, 0x79, 0x3e, 0x1b, 0x8d, 0x46, 0xc2, 0xa0, 0x73, + 0x57, 0x7a, 0x03, 0x2b, 0x90, 0xc1, 0x45, 0xf5, 0xa1, 0x5c, 0x7a, 0x3b, 0x75, 0x68, 0x57, 0xfe, 0x24, 0x3f, 0x1c, + 0x8a, 0x91, 0x39, 0xc6, 0x01, 0xe7, 0xa4, 0x50, 0x72, 0x94, 0xac, 0x25, 0x88, 0x4e, 0x69, 0x3c, 0x95, 0xf5, 0xda, + 0x8a, 0xc8, 0xab, 0x11, 0xf2, 0x21, 0xf8, 0xc9, 0x03, 0xb5, 0xf8, 0x33, 0x2d, 0x88, 0x3d, 0xf4, 0xa9, 0x52, 0x3a, + 0xc4, 0xab, 0x02, 0x42, 0x84, 0x01, 0x6f, 0xa0, 0x1d, 0x94, 0xe0, 0xb0, 0xc3, 0x7d, 0x40, 0x84, 0xe8, 0x37, 0x5e, + 0x3e, 0x93, 0xe1, 0xca, 0xbd, 0x41, 0x35, 0x67, 0x80, 0x58, 0xe9, 0x33, 0x70, 0xc1, 0x04, 0xd4, 0x53, 0x7c, 0x8a, + 0xfe, 0xf5, 0xe6, 0x61, 0xd3, 0xf5, 0x69, 0x09, 0xa8, 0x88, 0x9e, 0xfd, 0x7c, 0x0c, 0xe0, 0x9d, 0x5d, 0x9b, 0x91, + 0xf6, 0xf2, 0x37, 0xc0, 0xb0, 0x52, 0x92, 0x68, 0xe7, 0x94, 0x08, 0xdc, 0xf9, 0xc8, 0x96, 0x7e, 0x94, 0x02, 0x31, + 0x77, 0x3c, 0x49, 0x64, 0x0f, 0x36, 0x72, 0x02, 0xb7, 0x18, 0xf0, 0xe8, 0x00, 0x54, 0xae, 0x14, 0xe4, 0x5e, 0x73, + 0x24, 0x77, 0xfc, 0xd0, 0xfb, 0x61, 0x50, 0x0f, 0x7e, 0xe8, 0x9d, 0xa5, 0x24, 0x77, 0x84, 0x67, 0x6a, 0x4a, 0x88, + 0xf8, 0xec, 0x87, 0x41, 0x3e, 0xc0, 0xb3, 0x44, 0x8b, 0xb4, 0xc8, 0xad, 0x26, 0x6a, 0xdc, 0x84, 0x77, 0x89, 0xa4, + 0x21, 0xda, 0x76, 0x1e, 0x11, 0x37, 0x00, 0x92, 0xc5, 0x67, 0xf3, 0x86, 0xa2, 0xde, 0x4d, 0xf8, 0x16, 0xdd, 0x65, + 0xb1, 0xdf, 0xdf, 0xe4, 0x69, 0xdd, 0xd3, 0xa1, 0x32, 0xf8, 0x82, 0x54, 0x13, 0xe0, 0xd1, 0xfe, 0xca, 0x1c, 0xaf, + 0x5e, 0x6d, 0x8e, 0x94, 0x1b, 0x55, 0xa2, 0x7e, 0x8b, 0xd5, 0xac, 0x87, 0x88, 0xdc, 0x59, 0x66, 0xec, 0xed, 0x05, + 0xaf, 0xe4, 0xac, 0x8a, 0xed, 0x72, 0x7c, 0x45, 0x58, 0x5b, 0x49, 0x80, 0x8e, 0xd6, 0x63, 0x6d, 0x8a, 0x91, 0x5f, + 0x29, 0x24, 0xe0, 0xa2, 0x63, 0x6b, 0xa1, 0xd8, 0x78, 0x01, 0xfa, 0x92, 0x9d, 0x69, 0x80, 0xf5, 0x46, 0xaf, 0x22, + 0x6e, 0xcb, 0x07, 0x2a, 0xbc, 0xc9, 0x4d, 0x95, 0x59, 0xd9, 0xdc, 0xb4, 0xfb, 0xa9, 0xe2, 0x15, 0xe2, 0xd6, 0x1b, + 0xb5, 0x47, 0x01, 0x6a, 0x0f, 0x2d, 0x94, 0x01, 0xba, 0x34, 0xcd, 0x00, 0x90, 0x01, 0x40, 0xa6, 0x8a, 0xf8, 0x4c, + 0x80, 0x4a, 0x5b, 0xdd, 0x28, 0x70, 0x22, 0xbd, 0x01, 0xc6, 0x05, 0x56, 0xfa, 0xc8, 0x46, 0x06, 0x8b, 0x2d, 0x02, + 0xdc, 0x72, 0xa4, 0x0f, 0xd3, 0x70, 0xb2, 0x8d, 0xe6, 0x30, 0x49, 0xf3, 0xbb, 0x30, 0x4b, 0x25, 0xb4, 0xc4, 0x8f, + 0xb2, 0xc6, 0x88, 0x05, 0xa4, 0xef, 0xd3, 0x37, 0x45, 0x16, 0x13, 0x24, 0x9c, 0xf5, 0xd4, 0x01, 0x54, 0x93, 0x73, + 0xad, 0x69, 0xf5, 0xac, 0x36, 0x79, 0xc8, 0x02, 0x9d, 0x3d, 0x18, 0x93, 0x5a, 0x6e, 0xe8, 0x91, 0xfd, 0x95, 0xe3, + 0x19, 0xe1, 0xbb, 0x9e, 0xe1, 0xd4, 0x7f, 0xd7, 0x35, 0x90, 0x32, 0x25, 0x80, 0x20, 0x83, 0xa3, 0x09, 0xa1, 0x3c, + 0x1d, 0x93, 0xa9, 0xcd, 0x8f, 0x40, 0x38, 0x22, 0x78, 0x05, 0xcf, 0x0d, 0xad, 0x5b, 0x6e, 0xec, 0x2c, 0xf2, 0x34, + 0x01, 0x64, 0xf1, 0x82, 0xdf, 0x01, 0x32, 0xa7, 0x5e, 0x15, 0xb2, 0x67, 0xcf, 0xc5, 0x74, 0x36, 0x0f, 0xfe, 0x4c, + 0x68, 0xff, 0x62, 0xc2, 0x6f, 0xba, 0xab, 0xe4, 0xca, 0xd4, 0xba, 0x37, 0xd1, 0x63, 0x2e, 0x77, 0xfa, 0xb4, 0xe2, + 0x18, 0xf1, 0x0c, 0x56, 0x01, 0x39, 0x67, 0x43, 0xfe, 0xec, 0x1c, 0xb0, 0x5b, 0x56, 0xc2, 0x8b, 0xf8, 0xb3, 0x50, + 0x56, 0x0b, 0x90, 0x1f, 0x39, 0x8f, 0xcc, 0x2f, 0x5f, 0x6d, 0x87, 0x72, 0x4e, 0x51, 0x44, 0xcb, 0xa9, 0x69, 0x49, + 0x21, 0x3b, 0xf4, 0x14, 0x4c, 0xa6, 0xb6, 0xfc, 0x7d, 0x97, 0xb8, 0x24, 0xdf, 0x4c, 0x22, 0xfb, 0x3a, 0xc0, 0x9a, + 0xb5, 0xea, 0x1e, 0xba, 0x21, 0x18, 0x20, 0x32, 0x42, 0x99, 0xcd, 0xf5, 0xdd, 0x7a, 0x30, 0x50, 0x30, 0xbf, 0x82, + 0x6e, 0x5a, 0x74, 0x8a, 0x03, 0xe4, 0xac, 0x75, 0x8d, 0x4a, 0x55, 0x71, 0xe8, 0x30, 0xef, 0x96, 0x55, 0xd9, 0x65, + 0xe9, 0x85, 0x20, 0x35, 0xea, 0x2a, 0x58, 0xa4, 0x54, 0x44, 0xf1, 0x9e, 0xfc, 0x1a, 0x98, 0x78, 0x66, 0xe5, 0x28, + 0x8d, 0xe7, 0x80, 0x18, 0xa4, 0x80, 0x38, 0xe5, 0x57, 0x80, 0x26, 0xba, 0x88, 0xc2, 0xec, 0x4d, 0x5c, 0x05, 0xb5, + 0xd5, 0xf4, 0x7b, 0x07, 0x32, 0xf6, 0xbc, 0xee, 0xf7, 0x53, 0x62, 0xf4, 0xc3, 0x28, 0x0c, 0xfc, 0x7b, 0x3c, 0xdd, + 0x37, 0x41, 0x6a, 0x5e, 0xf9, 0x13, 0x5e, 0xd1, 0xe5, 0xd6, 0xa6, 0x5c, 0xd1, 0xb8, 0xf0, 0xd7, 0x08, 0x0e, 0x9f, + 0x3a, 0x8a, 0xed, 0x36, 0x55, 0x4e, 0x6d, 0x0c, 0x06, 0x21, 0xdc, 0xb7, 0x32, 0x7e, 0x9f, 0x78, 0xf9, 0x2c, 0x9a, + 0x83, 0xa2, 0x34, 0xd3, 0x7c, 0x21, 0x85, 0x74, 0x13, 0xa0, 0x8f, 0x06, 0xa1, 0x56, 0x57, 0x5e, 0x27, 0x5e, 0xaa, + 0xa6, 0xb5, 0x79, 0x8a, 0x35, 0x0a, 0xc4, 0x2c, 0x9a, 0x37, 0x2c, 0xa3, 0x43, 0x52, 0x5d, 0x2e, 0x4d, 0x33, 0xae, + 0xad, 0x66, 0xa8, 0x56, 0x1c, 0x35, 0x41, 0x8d, 0xd2, 0x35, 0x5c, 0x00, 0x7f, 0xa6, 0x3b, 0x8e, 0x6a, 0x14, 0x29, + 0x1a, 0xf0, 0x09, 0x62, 0xc4, 0x9a, 0xcd, 0x13, 0xd6, 0x9a, 0xba, 0x66, 0xf4, 0xfb, 0x32, 0x64, 0xc8, 0x24, 0x21, + 0x4f, 0x1f, 0x2e, 0xd7, 0x8f, 0xa4, 0xba, 0x00, 0x7e, 0xe5, 0x8a, 0xcd, 0x7a, 0xbd, 0x39, 0xc0, 0xf5, 0xc2, 0xfa, + 0x85, 0x8d, 0x2b, 0x38, 0xbf, 0x24, 0xf8, 0x5d, 0xf5, 0x23, 0xcc, 0x32, 0xa8, 0x02, 0x32, 0xfe, 0x58, 0x28, 0xea, + 0x79, 0x8b, 0xd9, 0x7d, 0xa4, 0x2e, 0x28, 0xb3, 0x74, 0x6e, 0x71, 0x82, 0x80, 0xf3, 0xb0, 0x7a, 0x02, 0xc9, 0xbe, + 0x7c, 0xec, 0xd3, 0x8c, 0x02, 0xd5, 0x11, 0xe0, 0xb3, 0x59, 0x3f, 0x84, 0xfd, 0x03, 0x22, 0x0b, 0xf5, 0x37, 0xdf, + 0xca, 0x59, 0x43, 0xf2, 0x40, 0xaa, 0xb9, 0x8f, 0xe1, 0xd4, 0xb8, 0xc1, 0x97, 0x6e, 0x7a, 0x53, 0xc1, 0x6b, 0x42, + 0xe6, 0xbe, 0x41, 0x6b, 0xdf, 0x0d, 0x1c, 0x21, 0x82, 0xcb, 0x28, 0xc5, 0x69, 0x6f, 0xd7, 0x0b, 0x90, 0xdb, 0xdc, + 0x82, 0xbc, 0x7e, 0xe9, 0xe2, 0x17, 0xa7, 0x48, 0xcf, 0xa2, 0x0b, 0x0c, 0x74, 0x41, 0xe6, 0x8d, 0x7f, 0x56, 0xb0, + 0x72, 0x01, 0xbd, 0x97, 0x8a, 0x95, 0x9c, 0x6c, 0x3b, 0xf5, 0x47, 0xa9, 0xec, 0xb7, 0x67, 0xd6, 0x04, 0x7e, 0x9f, + 0xd8, 0x2f, 0x91, 0xc9, 0x37, 0x3d, 0x36, 0xf9, 0xca, 0xb0, 0xe8, 0xd4, 0x32, 0x38, 0xa7, 0x47, 0x06, 0xe7, 0xde, + 0xce, 0xaa, 0x4d, 0x08, 0x43, 0x41, 0x12, 0x68, 0xba, 0xf0, 0xb0, 0x6e, 0xfa, 0xf3, 0x93, 0x16, 0xd5, 0x56, 0xed, + 0x5b, 0xf7, 0xe3, 0x10, 0xbb, 0xf8, 0x7d, 0xe2, 0x19, 0x22, 0x52, 0x1f, 0xe8, 0xc0, 0x64, 0xf0, 0xc4, 0x65, 0xbf, + 0x0f, 0x85, 0xcd, 0xc6, 0xf3, 0x51, 0x5d, 0xfc, 0x52, 0xdc, 0x03, 0xaa, 0x43, 0x05, 0x76, 0x39, 0x94, 0xa1, 0x8c, + 0xd8, 0xd4, 0x96, 0x7b, 0xfe, 0x78, 0x19, 0xe6, 0x20, 0xef, 0x68, 0x78, 0x9c, 0x33, 0x10, 0xc3, 0xe0, 0xeb, 0x3f, + 0x3c, 0xda, 0xa7, 0xcd, 0x0f, 0x67, 0xf0, 0xdd, 0xd1, 0xd9, 0x07, 0xa4, 0xbb, 0x39, 0x5b, 0x97, 0xc5, 0x5d, 0x1a, + 0x8b, 0xb3, 0x1f, 0x20, 0xf5, 0x87, 0xb3, 0xa2, 0x3c, 0xfb, 0x41, 0x55, 0xe6, 0x87, 0x33, 0x5a, 0x70, 0xa3, 0x3f, + 0xac, 0x89, 0xf7, 0x7b, 0xa5, 0x19, 0xd0, 0x16, 0x10, 0x99, 0xa5, 0xd5, 0x8f, 0xa0, 0x44, 0x54, 0xfc, 0xa8, 0x32, + 0xaa, 0xd5, 0xda, 0x71, 0x3e, 0x24, 0x1a, 0x29, 0x9b, 0x26, 0x24, 0xae, 0x96, 0xb0, 0x0e, 0xf5, 0xec, 0xb4, 0xf9, + 0x76, 0x9c, 0x07, 0xea, 0x80, 0xc8, 0xf9, 0xb3, 0x7c, 0xb4, 0xa5, 0xaf, 0xc1, 0xb7, 0x0e, 0x87, 0x7c, 0xb4, 0x33, + 0x3f, 0x7d, 0xb2, 0x56, 0xca, 0xb8, 0x23, 0x45, 0x2e, 0x84, 0x9c, 0x71, 0xdb, 0x1e, 0x03, 0x0e, 0x00, 0xff, 0x70, + 0xa0, 0xdf, 0x3b, 0xf9, 0x5b, 0xed, 0x96, 0x56, 0x3d, 0x1f, 0xb5, 0xb8, 0x33, 0xde, 0xd4, 0x86, 0xa8, 0x6d, 0x2f, + 0xb1, 0xa5, 0xf7, 0x4d, 0x83, 0x9a, 0x22, 0xfa, 0x09, 0xab, 0x89, 0x55, 0x1c, 0x16, 0xa4, 0x84, 0x24, 0x86, 0x63, + 0xb4, 0x43, 0x8f, 0xd3, 0xc5, 0xd2, 0x93, 0xfb, 0x0e, 0x2f, 0xb7, 0xbe, 0x0f, 0x48, 0x5a, 0x85, 0xf3, 0x0f, 0x5e, + 0x68, 0xe0, 0xd1, 0x8b, 0xbc, 0x2a, 0x32, 0x31, 0x12, 0x34, 0xca, 0x6f, 0x48, 0x9c, 0x39, 0xc3, 0x5a, 0x9c, 0x29, + 0xb0, 0xb0, 0x90, 0xd0, 0xbd, 0x8b, 0x92, 0xd2, 0x83, 0xb3, 0x47, 0xfb, 0xb2, 0xf9, 0x83, 0xe0, 0x21, 0x46, 0x37, + 0xc0, 0x88, 0xb3, 0x6b, 0x97, 0x77, 0x1f, 0x96, 0xb9, 0xf7, 0xc7, 0x9b, 0x65, 0x5e, 0x40, 0x88, 0xe6, 0x99, 0x54, + 0xac, 0x96, 0x67, 0xc0, 0x98, 0x27, 0xe2, 0xb3, 0xb0, 0x92, 0xd3, 0xa0, 0xea, 0x28, 0x56, 0x6f, 0xe3, 0xb9, 0x07, + 0x14, 0xdf, 0x1f, 0x12, 0xe0, 0x72, 0xf7, 0xd9, 0x6b, 0xe5, 0x9a, 0x4a, 0x7a, 0xe4, 0x39, 0x44, 0x4b, 0xbe, 0x4c, + 0x80, 0xe2, 0x19, 0xe2, 0x24, 0x85, 0xd5, 0x73, 0x13, 0xa4, 0x22, 0x5f, 0x9f, 0x50, 0x7c, 0xd1, 0x3c, 0x8a, 0x1a, + 0x16, 0xb2, 0x04, 0x8e, 0x87, 0x64, 0x96, 0xcd, 0x91, 0xa5, 0x3c, 0x6d, 0x4f, 0x91, 0x8e, 0x4e, 0x2c, 0xf1, 0xdb, + 0x9a, 0x5f, 0x2f, 0x52, 0x11, 0x98, 0xb4, 0xb3, 0x95, 0xb9, 0x17, 0xc2, 0x50, 0x25, 0xdc, 0x7b, 0x53, 0xcf, 0x42, + 0xb9, 0x29, 0x5a, 0x15, 0xb3, 0x87, 0x29, 0x31, 0xc3, 0x14, 0xeb, 0x2f, 0x6c, 0xf8, 0xdb, 0xc4, 0x8b, 0xc1, 0x70, + 0xbd, 0xe0, 0xe5, 0x6c, 0x63, 0x16, 0xc2, 0xe1, 0xb0, 0x99, 0x14, 0xb3, 0x05, 0x84, 0xb9, 0x2e, 0xe6, 0x87, 0x43, + 0x57, 0xcb, 0xd6, 0xc2, 0x83, 0x87, 0xaa, 0x85, 0x9b, 0x86, 0xe5, 0xf0, 0x33, 0x99, 0xc5, 0xd8, 0xbe, 0xc6, 0x67, + 0xf6, 0xe7, 0x8b, 0xee, 0x59, 0x82, 0xe4, 0x1b, 0x6b, 0xa0, 0x1d, 0x9b, 0xb5, 0x3b, 0x5c, 0x8d, 0x80, 0xa4, 0x74, + 0x37, 0xfa, 0xbb, 0xb2, 0x93, 0xa7, 0x04, 0xb9, 0xa3, 0x15, 0xd8, 0xef, 0xbe, 0xf1, 0x27, 0x5a, 0xec, 0x41, 0xbb, + 0x8d, 0x2d, 0x21, 0xaa, 0x69, 0xcf, 0xe5, 0x4a, 0xb1, 0x34, 0x6f, 0xa5, 0x8d, 0x9e, 0x0f, 0xeb, 0x73, 0xdf, 0xc8, + 0x81, 0x82, 0x31, 0xe2, 0xa9, 0x75, 0x10, 0xcd, 0xe6, 0x40, 0x83, 0x81, 0xe6, 0x11, 0x9e, 0x5a, 0xe8, 0xa0, 0xcc, + 0xda, 0xb0, 0x9f, 0x27, 0x27, 0xcb, 0xe3, 0xf0, 0x2d, 0xfc, 0xcb, 0x67, 0xd8, 0x24, 0xa6, 0xd8, 0x1e, 0xff, 0xaa, + 0x14, 0x15, 0x1e, 0xdb, 0x11, 0xd7, 0xda, 0xb5, 0xa8, 0x0d, 0x95, 0xc3, 0xbf, 0x84, 0x7d, 0x84, 0xfd, 0x85, 0x26, + 0x08, 0x83, 0x5d, 0x7f, 0x26, 0x10, 0x22, 0x16, 0xe2, 0x05, 0xff, 0xaa, 0x24, 0x15, 0x9d, 0xf0, 0xd9, 0xae, 0x04, + 0xde, 0x3a, 0x0c, 0xe8, 0x13, 0xf2, 0x33, 0x91, 0x30, 0x34, 0x13, 0x7a, 0x47, 0xff, 0x9d, 0xd8, 0xc9, 0x26, 0xb9, + 0x15, 0xf2, 0x81, 0xa4, 0x92, 0x60, 0x82, 0x95, 0x17, 0xca, 0x1f, 0xdd, 0x0b, 0xa5, 0xd6, 0x5a, 0xd0, 0xfa, 0xe5, + 0xcf, 0x13, 0xcf, 0xe0, 0xef, 0x81, 0x8c, 0x41, 0xb7, 0x11, 0xd5, 0x24, 0xc7, 0xf4, 0x51, 0x3a, 0xcf, 0x40, 0x05, + 0x74, 0xb6, 0xce, 0xc2, 0x7a, 0x51, 0x94, 0xab, 0x56, 0xa4, 0xa8, 0x2c, 0x7d, 0xa4, 0x1e, 0x63, 0x5e, 0x98, 0x27, + 0x27, 0xf2, 0xc1, 0x23, 0x00, 0xc6, 0xa3, 0x3c, 0xad, 0x3a, 0x4a, 0xeb, 0x07, 0x96, 0x01, 0x23, 0x70, 0xa2, 0x0c, + 0x78, 0x84, 0x65, 0x60, 0x9e, 0x76, 0x19, 0x6a, 0x10, 0x6b, 0x54, 0x5d, 0xa9, 0x0d, 0xe6, 0x44, 0x51, 0xf2, 0x29, + 0x96, 0x56, 0x18, 0x43, 0x53, 0x57, 0x1e, 0x59, 0x2f, 0x39, 0x61, 0x4f, 0x76, 0x03, 0xe9, 0x16, 0x36, 0x0a, 0x67, + 0xd0, 0xb5, 0x2c, 0x51, 0x2e, 0xba, 0x65, 0x44, 0x99, 0x08, 0xa9, 0x9f, 0x3d, 0x9c, 0x69, 0xb5, 0xdf, 0xd8, 0x49, + 0xfb, 0xf6, 0x48, 0xd1, 0x0b, 0x06, 0xed, 0xd3, 0x1e, 0x29, 0xf5, 0xac, 0x91, 0xcb, 0xc0, 0x96, 0x2e, 0x55, 0x3d, + 0xff, 0x05, 0xca, 0x77, 0x30, 0x33, 0xce, 0x66, 0x7f, 0xe8, 0xcd, 0xed, 0xd1, 0xbe, 0x6e, 0xfe, 0x60, 0xbd, 0x1e, + 0x6c, 0x0d, 0x32, 0xf1, 0xb9, 0x62, 0xa1, 0xb2, 0x0a, 0xb1, 0x82, 0xb4, 0xff, 0x25, 0xbc, 0x3f, 0xe0, 0xad, 0x11, + 0x9a, 0x95, 0xf1, 0x30, 0x1f, 0x3d, 0xda, 0x8b, 0xe6, 0x8f, 0xce, 0xb2, 0xad, 0x5c, 0x95, 0xcc, 0xf6, 0xc7, 0x51, + 0xd2, 0x9c, 0x3d, 0x5c, 0x23, 0xa9, 0x03, 0x7c, 0xb8, 0x3e, 0xc3, 0x07, 0x2a, 0xa1, 0xd4, 0x82, 0xaa, 0x06, 0xad, + 0x8f, 0xfd, 0xd1, 0x7a, 0x4e, 0x1f, 0x3f, 0x96, 0xd3, 0x2d, 0x29, 0xc2, 0xf8, 0x81, 0xc1, 0x94, 0x9d, 0x38, 0x75, + 0xc9, 0x9b, 0x21, 0xbd, 0xeb, 0x56, 0x49, 0x5d, 0xf6, 0x28, 0x11, 0x84, 0x3a, 0x58, 0xbf, 0xd8, 0x0f, 0x61, 0x66, + 0x8b, 0xfe, 0xb0, 0x59, 0xcd, 0x09, 0x10, 0x11, 0xd0, 0x5a, 0xe5, 0x7d, 0xe0, 0x98, 0x2f, 0xcc, 0x9a, 0x1b, 0xd2, + 0xad, 0x37, 0x57, 0xda, 0x2b, 0x29, 0xa0, 0x9f, 0x83, 0xcc, 0xed, 0xa3, 0x5b, 0xae, 0x5a, 0xe6, 0xb9, 0xb4, 0xe5, + 0x80, 0x45, 0x0b, 0x81, 0x9a, 0x9d, 0x4b, 0x87, 0x03, 0x05, 0xa1, 0xae, 0x44, 0x15, 0x71, 0x75, 0x14, 0x2d, 0x44, + 0xad, 0x56, 0xed, 0x72, 0xb2, 0xa9, 0x90, 0x2d, 0x89, 0x20, 0xa3, 0x14, 0x43, 0x97, 0x3e, 0xca, 0xd5, 0x9e, 0x69, + 0x38, 0x40, 0x13, 0xb0, 0x69, 0x83, 0xbf, 0x05, 0xee, 0x65, 0x70, 0x66, 0xda, 0xa7, 0x61, 0x04, 0x9c, 0xe6, 0x10, + 0xf3, 0xe7, 0x77, 0x3d, 0xa8, 0xe0, 0x41, 0x47, 0xfa, 0x9b, 0x7a, 0x56, 0xe0, 0x99, 0x7b, 0xe2, 0xf9, 0xeb, 0x13, + 0xe9, 0x45, 0x0e, 0x0f, 0x34, 0x0d, 0x62, 0xc6, 0x9f, 0x97, 0x65, 0xb8, 0x1b, 0x2d, 0xca, 0x62, 0xe5, 0x45, 0x7a, + 0x1f, 0xcf, 0xa4, 0x18, 0x48, 0xcc, 0x98, 0x19, 0x5d, 0xc5, 0x3a, 0xce, 0x61, 0xdc, 0xdb, 0x93, 0xb0, 0x42, 0xfb, + 0x67, 0x89, 0xbd, 0x2e, 0x00, 0xcb, 0x21, 0x6b, 0xd0, 0x0a, 0xef, 0x74, 0x7b, 0xbb, 0xc7, 0x25, 0x3b, 0x8a, 0x1b, + 0x40, 0x3f, 0xab, 0xa1, 0x65, 0x82, 0x5a, 0x66, 0xdd, 0xc9, 0x64, 0x8a, 0xe4, 0xf2, 0x6d, 0xd8, 0x6b, 0x56, 0xe4, + 0xf3, 0x46, 0x6e, 0x0f, 0xef, 0xc2, 0x95, 0x88, 0xb5, 0x05, 0x9d, 0x74, 0x64, 0x1c, 0xee, 0x85, 0xe6, 0x46, 0xba, + 0x7f, 0x54, 0x25, 0x61, 0x29, 0x62, 0xb8, 0x05, 0xb2, 0xbd, 0xda, 0x56, 0x82, 0x12, 0xf8, 0x60, 0x3f, 0x94, 0x62, + 0x91, 0x6e, 0x05, 0xe0, 0x3a, 0xf0, 0xcf, 0x12, 0x91, 0xd0, 0xdd, 0x79, 0x88, 0x62, 0x8d, 0xbc, 0x6f, 0x10, 0x8d, + 0xfd, 0x15, 0xc8, 0x69, 0x40, 0x26, 0x52, 0x8c, 0x64, 0xc1, 0xc0, 0x07, 0x90, 0xf3, 0x35, 0x98, 0xe4, 0xa6, 0xb9, + 0xe7, 0x07, 0xb9, 0xee, 0x60, 0xda, 0x07, 0xdd, 0x8b, 0x6b, 0xcd, 0x72, 0xf0, 0x8a, 0x89, 0xf8, 0xcf, 0xb5, 0x57, + 0xb2, 0x9c, 0x65, 0x7e, 0x63, 0x2e, 0x3a, 0x19, 0x5c, 0x35, 0x84, 0x5f, 0xcc, 0xb2, 0x39, 0x8f, 0x66, 0x99, 0x8e, + 0xfa, 0x2f, 0x9a, 0xa3, 0x52, 0x00, 0x4e, 0x1d, 0x2f, 0xc0, 0x1a, 0xfa, 0x4a, 0x37, 0xad, 0x78, 0xa0, 0x31, 0x46, + 0x41, 0x85, 0x0e, 0x42, 0x3f, 0xd7, 0x80, 0xb4, 0xc1, 0x24, 0x4d, 0x42, 0xe5, 0x83, 0x0b, 0xba, 0x61, 0x5e, 0xae, + 0x5c, 0xae, 0x9a, 0x54, 0x2d, 0xbf, 0x1c, 0x51, 0xdf, 0xd5, 0x92, 0x4b, 0xb5, 0xf9, 0xd4, 0x28, 0x6b, 0x04, 0x99, + 0x1c, 0xa5, 0xdf, 0xa7, 0x5c, 0xb8, 0x95, 0x31, 0x59, 0x1f, 0x0e, 0x5e, 0xc1, 0x4d, 0x8d, 0xdf, 0xe4, 0x44, 0x28, + 0x6a, 0x0f, 0x89, 0xb0, 0xb5, 0x5b, 0xa1, 0x7b, 0x8f, 0x1b, 0xa5, 0x79, 0x94, 0x6d, 0x62, 0x51, 0x79, 0xbd, 0x04, + 0xac, 0xc5, 0x3d, 0xe0, 0x45, 0xa5, 0xa5, 0x5f, 0xb1, 0x02, 0xd0, 0x03, 0xa4, 0xb0, 0xf1, 0x06, 0x19, 0xb0, 0x3e, + 0x78, 0xa9, 0xdf, 0xef, 0x1b, 0x53, 0xfe, 0xfb, 0xfb, 0x1c, 0x48, 0x0a, 0x45, 0x59, 0xef, 0x60, 0x02, 0xc1, 0xb5, + 0x93, 0xb4, 0x67, 0x35, 0x7f, 0xb6, 0xae, 0x3d, 0xe0, 0xb7, 0xf2, 0x2d, 0x12, 0xab, 0x57, 0xf6, 0xc5, 0x66, 0x9f, + 0x56, 0xd7, 0x46, 0xe3, 0x20, 0x58, 0x5a, 0xbd, 0xd1, 0x2a, 0x87, 0xbc, 0xe1, 0x05, 0x88, 0x54, 0xd6, 0xd5, 0xb5, + 0x72, 0xae, 0xae, 0x05, 0x47, 0x2e, 0xd9, 0x92, 0xe7, 0xf0, 0x5f, 0xc8, 0xbd, 0xf2, 0x70, 0x28, 0xfc, 0x7e, 0x3f, + 0x9d, 0x91, 0x56, 0x16, 0xd8, 0xd3, 0xd6, 0xb5, 0x17, 0xfa, 0x87, 0xc3, 0x1b, 0xf0, 0x1a, 0xf1, 0x0f, 0x87, 0xb2, + 0xdf, 0xff, 0x68, 0x6e, 0x32, 0xe7, 0x63, 0xa5, 0x94, 0xbd, 0x44, 0xa5, 0xfb, 0xa7, 0x84, 0xf7, 0xfe, 0xf7, 0xe8, + 0x7f, 0x8f, 0x2e, 0x7b, 0xb2, 0xeb, 0x7f, 0x49, 0xf8, 0x0c, 0x6f, 0xe8, 0x4c, 0x5d, 0xce, 0x99, 0x74, 0x77, 0x57, + 0x7e, 0xe8, 0x3d, 0x0d, 0x15, 0xdf, 0x9b, 0x9b, 0x36, 0xfe, 0x5c, 0x1d, 0x69, 0x12, 0x3a, 0x2e, 0xfa, 0x87, 0xc3, + 0x2f, 0x89, 0xd6, 0xa7, 0xa5, 0x4a, 0x9f, 0xa6, 0x70, 0x94, 0x0c, 0xb9, 0x9b, 0x5b, 0x98, 0x0e, 0xec, 0xc7, 0xcd, + 0x57, 0xc9, 0x8b, 0xb3, 0x14, 0xae, 0xbd, 0xf9, 0x2c, 0x9d, 0x4f, 0xc1, 0xba, 0x32, 0xcc, 0x67, 0xf5, 0x3c, 0x80, + 0xd4, 0x21, 0xa4, 0x59, 0xd3, 0xf0, 0x1f, 0x95, 0x2b, 0x78, 0x6b, 0x8f, 0x77, 0x03, 0x17, 0xa5, 0x8e, 0xf4, 0x49, + 0x1b, 0x4d, 0x97, 0x54, 0xf2, 0x1f, 0x45, 0x1e, 0x63, 0xcc, 0xc6, 0x1b, 0xe2, 0xfd, 0x2c, 0xf2, 0x97, 0x05, 0x60, + 0x17, 0x01, 0x18, 0x72, 0x3a, 0x77, 0x24, 0xf1, 0x8f, 0xc9, 0xf7, 0x7f, 0x4c, 0x97, 0xf6, 0xa1, 0x2c, 0x96, 0xa5, + 0xa8, 0xaa, 0xa3, 0xd2, 0xb6, 0xb6, 0x5c, 0x0f, 0x4c, 0xa2, 0xfd, 0xbe, 0x64, 0x12, 0x4d, 0x31, 0x14, 0x05, 0x6e, + 0x8d, 0xbd, 0x69, 0xca, 0x15, 0x63, 0xf5, 0xc8, 0x58, 0x3f, 0x5f, 0xec, 0xde, 0xc4, 0x5e, 0xea, 0x07, 0x29, 0x08, + 0xc2, 0x1a, 0x4a, 0x29, 0x45, 0x3e, 0x38, 0x9f, 0x61, 0x2a, 0x51, 0xeb, 0x52, 0xaa, 0xfc, 0x61, 0xa4, 0xf9, 0x30, + 0x05, 0xbd, 0xec, 0xbf, 0x2a, 0x98, 0xff, 0xba, 0x3d, 0x58, 0x9f, 0xd6, 0x65, 0x1a, 0x55, 0x44, 0x95, 0x17, 0xa6, + 0xda, 0x04, 0x22, 0xf8, 0x33, 0x61, 0xf1, 0xfd, 0xfa, 0xe4, 0x48, 0xd0, 0x98, 0xc9, 0xf2, 0xfa, 0xc8, 0xfd, 0xc2, + 0xbe, 0x72, 0x1d, 0xcf, 0xff, 0xdc, 0xcc, 0xff, 0x01, 0x3a, 0x43, 0x16, 0xcf, 0xb8, 0x65, 0xb0, 0xc0, 0xd9, 0x2f, + 0x5d, 0x3d, 0xe0, 0x6f, 0xe6, 0x89, 0x67, 0x40, 0xc7, 0xfc, 0x0c, 0x5d, 0x15, 0xd3, 0x59, 0x31, 0x00, 0x2e, 0x5b, + 0xbf, 0xb1, 0xe6, 0xc4, 0x3b, 0x8b, 0xf2, 0x4a, 0x2e, 0x08, 0x7d, 0x5d, 0x85, 0xd9, 0xb8, 0x2a, 0x36, 0x95, 0x28, + 0x36, 0x75, 0x8f, 0xd4, 0xb2, 0xf9, 0xb4, 0xb6, 0x15, 0xb2, 0x7f, 0x17, 0x2d, 0x06, 0x2f, 0xc3, 0x3a, 0x19, 0x65, + 0xe9, 0x7a, 0x0a, 0xfc, 0x7a, 0x01, 0x9c, 0x45, 0xe6, 0x95, 0xaf, 0xce, 0x1e, 0xb0, 0x45, 0xe3, 0x29, 0x90, 0xa3, + 0xd2, 0x1f, 0x79, 0x63, 0x74, 0x7a, 0xa2, 0xdf, 0xcf, 0xa7, 0x14, 0xf3, 0xf5, 0x77, 0x80, 0xe7, 0xaa, 0xe5, 0x02, + 0xf4, 0x65, 0xa8, 0x83, 0x4a, 0x94, 0x5a, 0x31, 0x8c, 0x58, 0xf8, 0xbb, 0x40, 0x22, 0x67, 0x0a, 0x6c, 0x56, 0x51, + 0x12, 0x2a, 0x51, 0x29, 0xd9, 0x9a, 0xa0, 0x96, 0xde, 0x17, 0x65, 0xbd, 0xaf, 0xc0, 0x51, 0x32, 0xd2, 0x66, 0x39, + 0x69, 0xc6, 0x15, 0x28, 0x73, 0xd1, 0x0f, 0xf6, 0xf7, 0xca, 0xf3, 0x1b, 0x99, 0xcf, 0x72, 0xdf, 0xd1, 0x39, 0x6d, + 0xc7, 0x05, 0xca, 0xdc, 0x72, 0xda, 0x6a, 0xc9, 0x63, 0xf2, 0x9e, 0x85, 0xda, 0xb2, 0x04, 0x29, 0x16, 0x61, 0x3e, + 0xa1, 0xca, 0xe6, 0x5f, 0x10, 0x6a, 0x8b, 0x03, 0x7b, 0xec, 0xc2, 0x44, 0xfc, 0xb7, 0x60, 0x49, 0x0c, 0xb3, 0x52, + 0x84, 0xf1, 0x0e, 0xbc, 0x7f, 0x36, 0x95, 0x18, 0x9d, 0xa1, 0x93, 0xfb, 0xd9, 0x7d, 0x5a, 0x27, 0x67, 0x6f, 0x5e, + 0x9d, 0xfd, 0xd0, 0x1b, 0x14, 0xa3, 0x34, 0x1e, 0xf4, 0x7e, 0x38, 0x5b, 0x6d, 0x00, 0x2d, 0x53, 0x9c, 0xc5, 0x64, + 0x4a, 0x13, 0xf1, 0x19, 0x19, 0x06, 0xcf, 0xea, 0x44, 0x9c, 0xd1, 0xc4, 0x74, 0x5f, 0xa3, 0x34, 0xf9, 0x76, 0x14, + 0xe6, 0xf0, 0x72, 0x29, 0x36, 0x95, 0x88, 0xc1, 0x4e, 0xa9, 0xe6, 0x59, 0xde, 0x3e, 0x8b, 0xf3, 0x51, 0x87, 0xac, + 0xd2, 0x81, 0xbf, 0x3d, 0x91, 0x76, 0x55, 0xba, 0x02, 0x42, 0x0f, 0x80, 0x93, 0xae, 0xfc, 0x79, 0x38, 0xa4, 0x09, + 0x84, 0x5a, 0x30, 0x27, 0xd3, 0x88, 0x6e, 0x48, 0x2f, 0xb1, 0xcf, 0xc0, 0x2c, 0xa4, 0x34, 0x0f, 0x6e, 0xae, 0x16, + 0x43, 0x77, 0xc5, 0xca, 0x51, 0x58, 0xad, 0x45, 0x54, 0x23, 0xeb, 0x31, 0x38, 0xef, 0x40, 0x04, 0x80, 0x22, 0x07, + 0xcf, 0x78, 0xd4, 0xef, 0x47, 0x2a, 0x28, 0x27, 0xa1, 0x5f, 0x14, 0xfa, 0xa5, 0xe1, 0x28, 0x63, 0xfe, 0x25, 0xd4, + 0x1c, 0x01, 0xf5, 0x96, 0x87, 0x8a, 0x2e, 0x00, 0x97, 0x73, 0xc4, 0x8c, 0xf3, 0xde, 0xff, 0xe1, 0xed, 0x4b, 0xb8, + 0xdb, 0xb6, 0xb5, 0x75, 0xff, 0x8a, 0xc5, 0x97, 0xaa, 0x44, 0x04, 0xc9, 0x92, 0x93, 0xf4, 0x9c, 0x52, 0x86, 0x75, + 0xdd, 0x0c, 0x6d, 0x7a, 0x9a, 0xa1, 0x71, 0xd2, 0x49, 0x4f, 0xd7, 0xa5, 0x49, 0xd8, 0x62, 0x43, 0x03, 0x2a, 0x49, + 0x79, 0x88, 0xc4, 0xff, 0xfe, 0xd6, 0xde, 0x18, 0x49, 0xd1, 0x4e, 0xce, 0x79, 0xf7, 0xbd, 0x95, 0xb5, 0x62, 0x11, + 0x04, 0x31, 0x63, 0x63, 0x63, 0x0f, 0xdf, 0x66, 0x4d, 0x60, 0x4e, 0x13, 0x82, 0xc2, 0x5c, 0x07, 0x0b, 0x03, 0x40, + 0xef, 0xda, 0xa3, 0x2d, 0x27, 0x5d, 0x82, 0xc5, 0x73, 0x03, 0x8b, 0x57, 0x17, 0x8b, 0xea, 0x92, 0x6b, 0xb9, 0x85, + 0x4d, 0x29, 0xab, 0x18, 0x02, 0x08, 0x34, 0x63, 0x86, 0xdd, 0x70, 0x97, 0x23, 0x59, 0x17, 0x05, 0x17, 0x3b, 0x81, + 0xa1, 0x9b, 0x71, 0xc9, 0xcc, 0xc1, 0xd5, 0x0c, 0xeb, 0xa4, 0xa2, 0x00, 0xbb, 0xba, 0x00, 0xd9, 0x0b, 0x43, 0x5d, + 0x37, 0xb3, 0xe5, 0x3a, 0xf0, 0x75, 0xe9, 0xc2, 0x97, 0x14, 0xbc, 0x5c, 0x49, 0x51, 0x66, 0x57, 0xfc, 0x27, 0xfb, + 0xb2, 0x19, 0x4b, 0x0a, 0xed, 0x48, 0x5f, 0xb5, 0xbb, 0xa3, 0xc5, 0x38, 0xb6, 0x1c, 0xdf, 0x52, 0xe9, 0x46, 0x8f, + 0xaa, 0x17, 0x42, 0x5b, 0xe7, 0x5a, 0x66, 0x69, 0xca, 0xc5, 0x4b, 0x91, 0x66, 0x89, 0x97, 0x1c, 0xeb, 0x58, 0xd5, + 0x2e, 0x08, 0x96, 0x0b, 0x93, 0xfc, 0x2c, 0x2b, 0x31, 0x76, 0x70, 0xa3, 0x51, 0xad, 0xa8, 0x53, 0x26, 0x06, 0x86, + 0x7c, 0x87, 0xc1, 0xb7, 0x99, 0x4c, 0x80, 0xe1, 0xc7, 0x44, 0x7d, 0x49, 0x4f, 0x21, 0xe0, 0x83, 0x0a, 0xcd, 0xfd, + 0x8c, 0x23, 0xf8, 0xb5, 0x55, 0x99, 0x03, 0x93, 0xad, 0x55, 0x90, 0x88, 0x7b, 0x97, 0xcd, 0xf5, 0x22, 0x5a, 0xa8, + 0xbb, 0x50, 0x2f, 0xde, 0x6e, 0x7b, 0x89, 0xa2, 0x03, 0x4e, 0x7e, 0x1a, 0xbc, 0x88, 0xb3, 0x9c, 0xa7, 0x7b, 0x95, + 0xdc, 0x53, 0x1b, 0x6a, 0x4f, 0x39, 0x73, 0xc0, 0xce, 0xfb, 0xba, 0xda, 0xd3, 0x6b, 0x7a, 0x4f, 0xb7, 0x73, 0x0f, + 0x2e, 0x18, 0xb8, 0x73, 0x2f, 0xb2, 0x2b, 0x2e, 0xf6, 0x40, 0x19, 0x68, 0x8d, 0x07, 0xea, 0xb2, 0x1a, 0xa9, 0x89, + 0xd1, 0x31, 0xac, 0x13, 0x7d, 0x30, 0x07, 0xf4, 0x67, 0x08, 0x6b, 0xdf, 0x7a, 0xbb, 0xd2, 0x07, 0x6d, 0x40, 0xdf, + 0x2d, 0x4d, 0x1f, 0x74, 0xe0, 0x78, 0x15, 0x1d, 0xb8, 0x31, 0xa4, 0x1a, 0xb4, 0xd5, 0xc8, 0x2a, 0x50, 0xbc, 0xe1, + 0x2d, 0xde, 0x9d, 0x6b, 0xc9, 0xc6, 0x7b, 0x89, 0x18, 0x5f, 0x99, 0xa8, 0xe2, 0x4c, 0x1c, 0x7b, 0xa9, 0xbc, 0xd6, + 0x4e, 0x32, 0xc2, 0xf8, 0x96, 0x95, 0xd4, 0xdf, 0x21, 0xe6, 0x16, 0x69, 0x0e, 0x83, 0xe7, 0x61, 0x45, 0x66, 0xbc, + 0xdf, 0x97, 0x33, 0x19, 0x95, 0x33, 0xb1, 0x5f, 0x46, 0x0a, 0xac, 0xed, 0x2e, 0x11, 0xd0, 0xbd, 0x12, 0x20, 0x5f, + 0x00, 0x54, 0xdd, 0x27, 0xfc, 0xb9, 0x4f, 0xea, 0xd3, 0x29, 0xf4, 0x29, 0xb4, 0xf5, 0x8a, 0x2b, 0x88, 0x57, 0x75, + 0x63, 0x64, 0x1b, 0x15, 0xb4, 0x78, 0x2c, 0xcf, 0x6a, 0xc3, 0xd8, 0x9c, 0x5a, 0xff, 0x7a, 0xb3, 0xc1, 0x94, 0xcd, + 0x85, 0x5a, 0x85, 0x21, 0x89, 0x3e, 0x96, 0x5e, 0x24, 0x11, 0x0b, 0x9b, 0xd5, 0xda, 0xfc, 0x26, 0x0c, 0x48, 0x26, + 0x52, 0xdc, 0xcf, 0x96, 0x38, 0x77, 0xf1, 0x78, 0x5e, 0xf5, 0xb5, 0x96, 0x16, 0x99, 0x36, 0xdf, 0xe8, 0xcb, 0x90, + 0xa6, 0xa2, 0x86, 0x34, 0xea, 0xcc, 0xa0, 0xfb, 0x76, 0x79, 0xcb, 0x6a, 0x84, 0x09, 0xf0, 0x4a, 0x67, 0xd0, 0x8d, + 0xc6, 0x03, 0xb1, 0xac, 0x46, 0xc5, 0x5a, 0x08, 0x04, 0x1e, 0x86, 0x1c, 0x33, 0x4b, 0x48, 0xb2, 0x4f, 0xfc, 0x3b, + 0x15, 0x67, 0xa1, 0x88, 0xaf, 0x0d, 0xb2, 0x77, 0x65, 0x5d, 0xbb, 0xeb, 0xc8, 0xcf, 0x89, 0x85, 0xd5, 0xfe, 0x43, + 0xf3, 0xa8, 0x35, 0xce, 0x02, 0xda, 0x9a, 0x56, 0x37, 0x1c, 0xee, 0x51, 0x1d, 0x8b, 0xd2, 0x60, 0x13, 0x7b, 0x64, + 0xb9, 0x68, 0x1d, 0x33, 0x68, 0x40, 0x7f, 0x93, 0x5d, 0xae, 0x2f, 0x11, 0xc0, 0xad, 0x44, 0xd6, 0x49, 0x2a, 0xff, + 0x92, 0xf6, 0xa8, 0x6b, 0x7b, 0x2a, 0xff, 0xdb, 0x36, 0x55, 0x0e, 0x2d, 0xa6, 0x3c, 0x76, 0x73, 0x16, 0xa8, 0x8e, + 0x04, 0x51, 0xa0, 0xb6, 0x5e, 0x30, 0xf5, 0x4e, 0x99, 0xa2, 0x03, 0x04, 0xba, 0x30, 0x67, 0xd8, 0x17, 0x1c, 0x31, + 0x66, 0xa9, 0xc4, 0x60, 0xea, 0x63, 0x8c, 0x6a, 0x5a, 0x2b, 0x40, 0xd7, 0x4f, 0x37, 0xf0, 0x27, 0x2a, 0x6a, 0x34, + 0xd4, 0x1a, 0x49, 0xa1, 0x68, 0xa2, 0x42, 0x91, 0xa5, 0x85, 0x8e, 0xab, 0xd0, 0x49, 0x24, 0x2c, 0x01, 0x0d, 0x13, + 0xa2, 0x93, 0x0a, 0xbc, 0x35, 0x80, 0x33, 0x1f, 0x17, 0xe5, 0xba, 0xd0, 0x06, 0x73, 0x3f, 0xc4, 0x57, 0xfc, 0xe5, + 0x33, 0x67, 0x54, 0xdf, 0xb2, 0xd6, 0xf7, 0xb4, 0x20, 0x3f, 0x84, 0x9c, 0xa2, 0x03, 0x13, 0x3b, 0xda, 0xa0, 0x31, + 0x46, 0x59, 0xeb, 0xa8, 0x17, 0x6f, 0x74, 0x28, 0x16, 0x6d, 0x82, 0x77, 0x8f, 0xa7, 0x88, 0x36, 0x3c, 0x14, 0xc6, + 0xaa, 0x1a, 0x9f, 0x4a, 0xd6, 0xd2, 0x83, 0x15, 0x3c, 0x5d, 0x27, 0x3c, 0x04, 0x3d, 0x12, 0x61, 0x47, 0x61, 0x31, + 0x8f, 0x17, 0x70, 0x9c, 0x14, 0x04, 0xd4, 0x0e, 0xfa, 0x0a, 0x3e, 0x5f, 0xa0, 0xfb, 0xab, 0x44, 0x0f, 0x30, 0xb4, + 0x20, 0x6e, 0x46, 0x41, 0x1d, 0x5d, 0xc6, 0xab, 0x86, 0x8a, 0x84, 0xcf, 0x0b, 0xb0, 0x1d, 0x52, 0xea, 0x29, 0xd0, + 0x42, 0x25, 0x4a, 0x3f, 0x0c, 0x7c, 0x87, 0xc6, 0xc0, 0xd6, 0x3a, 0x40, 0x43, 0x3f, 0x63, 0x9a, 0x5a, 0x67, 0xa8, + 0x7c, 0xe6, 0xdd, 0x33, 0xa3, 0xe5, 0xcc, 0xa2, 0x31, 0xe8, 0xdb, 0x68, 0x8a, 0xe2, 0x9c, 0x7c, 0x16, 0x14, 0x71, + 0x9a, 0xc5, 0x39, 0xf8, 0x6d, 0xc6, 0x05, 0x66, 0x4c, 0xe2, 0x8a, 0x5f, 0xc8, 0x02, 0xb4, 0xdd, 0xb9, 0x4a, 0xad, + 0x6b, 0x10, 0x90, 0xfd, 0x00, 0x56, 0x2f, 0x0d, 0x1d, 0x95, 0xf3, 0xee, 0xd2, 0xa6, 0x10, 0xb1, 0x08, 0xc1, 0xa6, + 0x99, 0x2e, 0xd9, 0x71, 0xa8, 0xb4, 0x39, 0x10, 0xea, 0x08, 0x8d, 0xfb, 0xa7, 0x61, 0x6c, 0x35, 0xc5, 0xd6, 0xee, + 0x6d, 0xbb, 0xfd, 0x57, 0xe9, 0xa5, 0xd3, 0x9c, 0xf4, 0x18, 0xfb, 0x57, 0x19, 0x16, 0x23, 0xdb, 0x11, 0x02, 0x4b, + 0xce, 0xfb, 0xd4, 0x7f, 0x45, 0xcb, 0x79, 0x02, 0xa6, 0x23, 0x3a, 0x58, 0x2e, 0x50, 0x76, 0x0c, 0xe8, 0x0e, 0x0c, + 0xae, 0xe8, 0xf7, 0xc1, 0x2a, 0xc3, 0x5c, 0x48, 0x96, 0x24, 0x65, 0xf0, 0x3c, 0xf5, 0xe0, 0xe0, 0xd7, 0x4c, 0x99, + 0xbb, 0x28, 0xeb, 0xd3, 0x25, 0x99, 0xa6, 0xc8, 0x40, 0xac, 0xc3, 0x4d, 0x96, 0x46, 0x89, 0x12, 0x91, 0x2d, 0xd1, + 0x3f, 0xd2, 0x50, 0x2c, 0x1d, 0xb9, 0x17, 0xa9, 0x12, 0xa1, 0x62, 0x9e, 0xe2, 0x49, 0x9d, 0xd6, 0xe9, 0x08, 0x43, + 0x4f, 0x82, 0x52, 0xae, 0x86, 0x81, 0x2a, 0xa9, 0x5e, 0x0a, 0x9b, 0x62, 0xbb, 0xd5, 0x17, 0x2b, 0x31, 0x8f, 0x17, + 0xf8, 0x52, 0xe0, 0x28, 0xfe, 0x8b, 0x7b, 0x61, 0xa7, 0xd4, 0xf6, 0xa0, 0x76, 0x44, 0x09, 0xfd, 0x17, 0x87, 0x8b, + 0xc4, 0x77, 0x52, 0x87, 0x00, 0x44, 0x8b, 0x90, 0x53, 0x75, 0x90, 0x1a, 0x6e, 0x68, 0x47, 0xf8, 0x6f, 0xb8, 0x3e, + 0xe3, 0x8c, 0xde, 0x54, 0x33, 0x6a, 0x28, 0x5f, 0x0f, 0xda, 0x18, 0xf5, 0xd9, 0xc0, 0x61, 0x85, 0x28, 0xb4, 0x61, + 0x47, 0xa5, 0x12, 0x2d, 0x0c, 0xa5, 0xfa, 0x4b, 0xa8, 0x38, 0xe2, 0xce, 0x8c, 0xb2, 0x64, 0x7c, 0x5a, 0x1e, 0x8a, + 0xe9, 0x60, 0x50, 0x92, 0xca, 0x58, 0xe8, 0xc1, 0xf5, 0xc0, 0xf3, 0xef, 0x81, 0x5b, 0x88, 0x87, 0x8c, 0x2c, 0x86, + 0xdc, 0xe0, 0xe4, 0xb7, 0x38, 0xb9, 0x6a, 0x54, 0xaa, 0x38, 0xd6, 0x44, 0xb5, 0xe0, 0xfb, 0x32, 0x0c, 0xd0, 0x27, + 0x29, 0x00, 0x93, 0xc1, 0x94, 0xdf, 0x80, 0x44, 0xe9, 0x54, 0xdd, 0x90, 0x3e, 0x88, 0x82, 0x9f, 0xf3, 0x82, 0x8b, + 0xc4, 0x15, 0x60, 0x79, 0x07, 0xdb, 0xeb, 0xa8, 0xa2, 0x0a, 0x93, 0xd7, 0xf4, 0x38, 0xe2, 0xc6, 0xfb, 0xcf, 0xf4, + 0xd8, 0x62, 0xb6, 0x5a, 0xc7, 0x06, 0x9f, 0x39, 0x06, 0x17, 0x74, 0x2d, 0xb1, 0x35, 0x54, 0xc3, 0x8a, 0xc0, 0xc0, + 0x05, 0x1c, 0x84, 0x25, 0x8a, 0x63, 0x2b, 0x79, 0x45, 0x1a, 0x52, 0xda, 0x7b, 0x86, 0xa3, 0x4d, 0x72, 0x7c, 0x9b, + 0x65, 0x37, 0x81, 0xf3, 0x45, 0xe7, 0xa4, 0x99, 0xb0, 0x36, 0x78, 0x9f, 0x37, 0xe7, 0xd7, 0xdd, 0x43, 0x42, 0x55, + 0xdc, 0x1b, 0xde, 0x8e, 0x7b, 0xe3, 0x84, 0x5f, 0x73, 0xb1, 0xd0, 0xa1, 0x5a, 0xcc, 0x25, 0xcb, 0x6f, 0xad, 0x77, + 0x4b, 0x92, 0x5a, 0x01, 0xed, 0xb3, 0x2c, 0xa8, 0x89, 0x00, 0x90, 0x3f, 0xfc, 0x05, 0x42, 0x67, 0xf8, 0xdb, 0x63, + 0x70, 0x45, 0x0a, 0xef, 0x1c, 0x02, 0x61, 0x4d, 0x37, 0x77, 0x6a, 0x03, 0xbe, 0x18, 0xf7, 0x67, 0x4c, 0x3d, 0xfd, + 0x36, 0x93, 0xbb, 0xba, 0x6e, 0x8f, 0x2c, 0xc3, 0x47, 0xb8, 0x52, 0x00, 0x37, 0x13, 0xfe, 0x62, 0x98, 0x49, 0xf5, + 0x09, 0x60, 0xaa, 0xe9, 0xe0, 0x3e, 0x41, 0x60, 0x00, 0x95, 0x68, 0x31, 0xba, 0x52, 0x8e, 0x68, 0x06, 0x6e, 0x4d, + 0xb7, 0xc2, 0x78, 0xeb, 0x41, 0x0b, 0x3d, 0xd3, 0x70, 0xe2, 0x3f, 0x68, 0xe6, 0x55, 0x01, 0x01, 0xb4, 0x32, 0x82, + 0xb7, 0xd6, 0x47, 0x73, 0x84, 0xf8, 0x84, 0x25, 0xd1, 0x84, 0xc5, 0x33, 0xc5, 0x8f, 0x09, 0xdd, 0x34, 0xb5, 0x4d, + 0xef, 0x91, 0xfe, 0xe2, 0x9a, 0xf5, 0x53, 0x96, 0xb5, 0x6f, 0x0f, 0x15, 0x2f, 0xa6, 0xcd, 0x38, 0x88, 0x89, 0x2a, + 0xc6, 0xff, 0x82, 0xfb, 0x52, 0x2b, 0x40, 0x64, 0xee, 0xaa, 0xa7, 0xdf, 0x6f, 0x66, 0xcb, 0x81, 0x50, 0xf9, 0x9d, + 0x41, 0xd2, 0xa7, 0x43, 0xfb, 0x81, 0x4d, 0xa2, 0xb6, 0xd0, 0xf3, 0xc7, 0xa5, 0x6e, 0xe2, 0xe5, 0xb5, 0xa9, 0x11, + 0xad, 0x90, 0xa1, 0xb2, 0x75, 0xc0, 0xfa, 0xfe, 0x21, 0xdc, 0x5d, 0xd4, 0x34, 0xd4, 0xba, 0xe7, 0xae, 0x45, 0xc1, + 0x89, 0x3f, 0xc0, 0x58, 0x5c, 0x48, 0x6a, 0x1d, 0x8f, 0x49, 0x3f, 0x5a, 0xc8, 0xe4, 0x46, 0x5d, 0x9d, 0x9c, 0x29, + 0xe6, 0x09, 0x5c, 0x80, 0xcb, 0xb6, 0xbf, 0xa2, 0x52, 0x97, 0x72, 0x7b, 0x45, 0x69, 0x7a, 0x48, 0xdb, 0xab, 0x38, + 0x6f, 0x0b, 0x2e, 0xf8, 0x17, 0x0a, 0x2e, 0xac, 0x83, 0x75, 0xc7, 0x9d, 0xb2, 0x27, 0x3c, 0x51, 0xa6, 0xb5, 0xc1, + 0x5d, 0x37, 0x18, 0x13, 0x63, 0xbf, 0xbb, 0xe4, 0xc9, 0x47, 0x64, 0xc1, 0xbf, 0xcb, 0x04, 0x78, 0x26, 0xbb, 0x57, + 0x2a, 0xff, 0x0f, 0xfe, 0xd5, 0xd6, 0xbe, 0xb3, 0xe6, 0x9f, 0x9e, 0xf5, 0x70, 0xe7, 0x30, 0xf9, 0xb1, 0x3a, 0x03, + 0xba, 0xb9, 0x94, 0x29, 0x07, 0x64, 0x00, 0x6b, 0x91, 0x8c, 0x06, 0x7c, 0x68, 0x65, 0xd9, 0xf6, 0x9d, 0x56, 0x17, + 0x84, 0x3b, 0x09, 0xdc, 0xf4, 0xee, 0xda, 0xcc, 0xcc, 0xe9, 0x5a, 0x89, 0xa6, 0x4b, 0x63, 0x6b, 0x59, 0xaa, 0x30, + 0xde, 0xef, 0x3c, 0xc9, 0xa6, 0xf9, 0xe1, 0x72, 0x9a, 0x5b, 0xea, 0xb6, 0x71, 0xcb, 0x06, 0xd0, 0x10, 0xbb, 0xd6, + 0x56, 0x0e, 0x78, 0xb9, 0x3d, 0x88, 0xe6, 0x6b, 0x45, 0xe8, 0xa9, 0x12, 0xa1, 0x4f, 0xd3, 0x66, 0x1f, 0xec, 0xaa, + 0x5a, 0x37, 0x42, 0x1e, 0x0d, 0x52, 0xcd, 0xc8, 0xbf, 0xb9, 0xe2, 0xc5, 0x79, 0x2e, 0xaf, 0x01, 0x0e, 0x99, 0xd4, + 0x46, 0x61, 0x79, 0x09, 0xee, 0xfc, 0xe8, 0x38, 0xce, 0xc4, 0x28, 0xc7, 0xb8, 0xad, 0x88, 0x94, 0xac, 0x13, 0x67, + 0x80, 0x87, 0xec, 0x4f, 0x9a, 0x0e, 0xed, 0x5a, 0x60, 0x78, 0x5f, 0xe0, 0xae, 0x72, 0x76, 0xb4, 0xc9, 0xed, 0xa2, + 0x6f, 0xce, 0xb0, 0xee, 0x48, 0x69, 0x6d, 0x2c, 0xba, 0xee, 0x60, 0xad, 0x19, 0xb4, 0x45, 0x28, 0xf9, 0x90, 0x3b, + 0x69, 0x3f, 0x05, 0x34, 0x38, 0xcd, 0xd2, 0x1b, 0x6b, 0x95, 0xbf, 0xd1, 0x42, 0x9c, 0x28, 0xa6, 0x4e, 0x7c, 0x13, + 0x25, 0xfa, 0xfc, 0x4c, 0x8c, 0x1b, 0x08, 0xa4, 0xfe, 0x80, 0xf1, 0x35, 0x8a, 0x30, 0x81, 0xeb, 0x40, 0x14, 0xdb, + 0x13, 0xb5, 0xb1, 0x1c, 0x41, 0x27, 0x84, 0x78, 0x07, 0x65, 0x18, 0xab, 0x8b, 0x03, 0x6d, 0xb0, 0xf4, 0x75, 0x6b, + 0x9d, 0x1b, 0x42, 0x61, 0x9c, 0xc0, 0x14, 0x83, 0xa4, 0xce, 0x3a, 0xcb, 0x04, 0x55, 0x76, 0x4c, 0x3a, 0xef, 0x03, + 0x74, 0x77, 0x2d, 0x9a, 0xe2, 0xeb, 0xce, 0x1d, 0x74, 0x17, 0xd7, 0xaf, 0xb5, 0xc8, 0x0d, 0xfe, 0xbc, 0x25, 0xc2, + 0x22, 0x70, 0xd6, 0x9a, 0x7c, 0xd5, 0x08, 0x07, 0xa6, 0x24, 0xd3, 0xb0, 0x97, 0x2b, 0x9b, 0xee, 0xed, 0xb6, 0xd7, + 0xbb, 0x53, 0xc4, 0xd5, 0x63, 0xac, 0xf2, 0x6e, 0xe6, 0xf6, 0x4e, 0xb5, 0x16, 0xbb, 0x37, 0x6d, 0x3f, 0xc5, 0x8e, + 0x5a, 0x6b, 0xb7, 0x1b, 0x4e, 0xa8, 0x21, 0xdf, 0x8a, 0x2a, 0xad, 0x4e, 0x37, 0x06, 0xed, 0x10, 0xda, 0x5a, 0x64, + 0x70, 0xa3, 0x7c, 0xe6, 0x84, 0x4e, 0x2a, 0xe4, 0xaa, 0x53, 0x17, 0x6c, 0x2e, 0x79, 0xb5, 0x94, 0x69, 0x24, 0x28, + 0xda, 0x9c, 0x47, 0x25, 0x4d, 0xe4, 0x5a, 0x54, 0x91, 0xac, 0x51, 0x2f, 0x6a, 0x35, 0x06, 0x08, 0xc8, 0x74, 0xda, + 0xf4, 0xa0, 0x0a, 0x66, 0x43, 0x19, 0xc9, 0xe9, 0x0b, 0xb0, 0xb4, 0x47, 0x8e, 0xb5, 0xbe, 0xab, 0xce, 0x16, 0xdf, + 0xea, 0x09, 0xc1, 0x14, 0x66, 0x0f, 0x44, 0x84, 0x6b, 0x1a, 0x43, 0x4e, 0xbb, 0xc4, 0x65, 0x4d, 0xb7, 0x84, 0x3b, + 0xb8, 0x5d, 0xc9, 0x8e, 0xdc, 0x3c, 0x69, 0x6e, 0xae, 0x60, 0x47, 0xc5, 0x7c, 0x0c, 0xda, 0x2f, 0xa9, 0xae, 0x5d, + 0x9a, 0x5b, 0x8f, 0x07, 0x01, 0x0d, 0x06, 0x85, 0xe1, 0x5f, 0x27, 0xc6, 0xc3, 0x93, 0x06, 0x04, 0x49, 0xb9, 0x08, + 0xc7, 0xbe, 0x11, 0xfd, 0x64, 0x2a, 0x0f, 0x39, 0x5a, 0xbc, 0x43, 0xab, 0x73, 0x08, 0xe8, 0x25, 0x42, 0x49, 0x8c, + 0xaa, 0xd0, 0x88, 0xa0, 0x3c, 0x2d, 0x7f, 0xa9, 0xaa, 0x43, 0x40, 0x21, 0xed, 0x2b, 0x0a, 0x65, 0x9b, 0xc4, 0xd0, + 0x0c, 0xbf, 0x9c, 0x4f, 0x16, 0x7a, 0x06, 0x06, 0x72, 0x7e, 0xb0, 0xd0, 0xb3, 0x30, 0x90, 0xf3, 0x47, 0x8b, 0xda, + 0xad, 0x03, 0x4d, 0x40, 0x3c, 0x17, 0x8e, 0x4e, 0x4a, 0xab, 0xb2, 0x05, 0x74, 0x73, 0x1f, 0x41, 0xff, 0x97, 0x3d, + 0x04, 0x9d, 0x5c, 0x68, 0x47, 0x6e, 0x40, 0xdb, 0x21, 0x09, 0xec, 0x15, 0x93, 0x0a, 0x13, 0x8b, 0xe8, 0x90, 0x8d, + 0xc1, 0x10, 0x5b, 0x7d, 0x70, 0xc8, 0xc6, 0x53, 0x9f, 0x04, 0x01, 0xa3, 0xfb, 0x83, 0x01, 0x07, 0xbf, 0xc1, 0xab, + 0xf4, 0xd1, 0x46, 0xa0, 0x9b, 0xbe, 0xbb, 0x1b, 0x7a, 0x17, 0x57, 0x70, 0xaa, 0x76, 0xf7, 0x24, 0x74, 0x93, 0x69, + 0xc7, 0xea, 0x35, 0xc4, 0x0d, 0xf9, 0x95, 0xd1, 0x68, 0x64, 0x53, 0x42, 0x42, 0x0c, 0xe7, 0xd0, 0xcc, 0x69, 0xb9, + 0x7c, 0x75, 0xeb, 0xd9, 0x80, 0x0c, 0x33, 0xbd, 0x61, 0xb2, 0xbe, 0x87, 0xb2, 0xea, 0x31, 0xb4, 0x43, 0xef, 0x91, + 0xe3, 0xfb, 0x07, 0xdf, 0x64, 0xfc, 0xcc, 0xe1, 0xda, 0xc3, 0xb9, 0xf0, 0x5d, 0xd6, 0x8c, 0xcc, 0xa1, 0xf3, 0xec, + 0xe3, 0x78, 0x0f, 0xe3, 0xe4, 0xf3, 0x2c, 0x94, 0x37, 0x5e, 0xd3, 0xff, 0xa8, 0xf4, 0x66, 0x87, 0x43, 0x4e, 0x57, + 0xb0, 0xe2, 0x66, 0x55, 0x68, 0xf8, 0x59, 0xe4, 0x8d, 0x23, 0x5e, 0x93, 0xa8, 0xea, 0x3e, 0xef, 0x6d, 0xc4, 0xd2, + 0x8e, 0x71, 0x00, 0x70, 0xa2, 0x56, 0x0d, 0xbb, 0xd2, 0xb8, 0x56, 0x07, 0x31, 0x22, 0x25, 0x6c, 0x95, 0x38, 0x12, + 0xca, 0xdf, 0x00, 0x84, 0xc5, 0x50, 0x1c, 0x6f, 0x0d, 0xeb, 0x3d, 0xec, 0x87, 0x2e, 0xd0, 0x34, 0xa7, 0x54, 0x33, + 0x00, 0x48, 0x02, 0xfe, 0xe8, 0xe9, 0xa6, 0xa1, 0xb2, 0xcd, 0xf3, 0xd0, 0xb2, 0xba, 0x82, 0x7b, 0x7a, 0xea, 0x4a, + 0x06, 0xc6, 0x55, 0x1d, 0x7b, 0x9b, 0xbb, 0xdb, 0xa3, 0x55, 0xe4, 0x3b, 0x9b, 0xd4, 0x34, 0x0b, 0x20, 0x45, 0xe3, + 0xd2, 0x17, 0x7a, 0x3a, 0x01, 0x5a, 0xaf, 0x2d, 0x15, 0xed, 0xf7, 0x51, 0x8c, 0x1a, 0x17, 0x0a, 0xac, 0xc2, 0x04, + 0x85, 0x43, 0x84, 0x11, 0x42, 0x7f, 0x2e, 0xc3, 0x8d, 0x2f, 0xc8, 0x20, 0x1a, 0xae, 0x45, 0x87, 0x22, 0x72, 0xbc, + 0x68, 0x5b, 0xaa, 0x6a, 0x4e, 0x9a, 0xb6, 0x04, 0xde, 0x44, 0x06, 0x6c, 0xe7, 0x9f, 0x36, 0x44, 0xae, 0xc2, 0x05, + 0x0c, 0xdf, 0x11, 0xd7, 0x82, 0xe8, 0xa6, 0x36, 0xf5, 0x36, 0xec, 0x10, 0x1d, 0x4d, 0xf1, 0xe8, 0x90, 0x7b, 0xee, + 0x9e, 0xdb, 0x22, 0xbe, 0xfe, 0x0c, 0xb9, 0x6b, 0x3a, 0x7b, 0x29, 0xc2, 0xa0, 0x6e, 0xd9, 0x40, 0xb1, 0x0e, 0x9d, + 0xa0, 0x00, 0x03, 0xb8, 0x7c, 0x02, 0x3a, 0x36, 0x18, 0x54, 0x04, 0x9f, 0x14, 0xb6, 0x4d, 0x83, 0xfc, 0x11, 0xef, + 0x86, 0x0e, 0xaf, 0x2d, 0x79, 0x20, 0x5e, 0x61, 0x9f, 0x29, 0xe1, 0xee, 0x05, 0x05, 0xdd, 0x51, 0x5e, 0xae, 0x0a, + 0x57, 0xa5, 0x01, 0xa8, 0xb2, 0xe3, 0xb9, 0xd6, 0x94, 0xb4, 0x80, 0x95, 0x92, 0xba, 0xf3, 0x9b, 0xe0, 0xb8, 0x25, + 0x53, 0xe1, 0x5b, 0x75, 0xa3, 0xca, 0x43, 0x89, 0x22, 0x1d, 0x7b, 0xb6, 0x73, 0xb0, 0x06, 0xc0, 0x53, 0xd8, 0x5e, + 0x9c, 0x09, 0xf8, 0xdc, 0x69, 0x97, 0x2d, 0x73, 0x09, 0x14, 0xf5, 0xfd, 0x38, 0x2f, 0x3b, 0xbe, 0xdc, 0x1d, 0x6d, + 0xef, 0xa1, 0x37, 0x62, 0x63, 0xbc, 0xbe, 0x8c, 0x9a, 0x7e, 0xf1, 0x0c, 0x57, 0x96, 0x82, 0xdc, 0xd3, 0x54, 0x8f, + 0x30, 0x3a, 0x04, 0xa6, 0x29, 0x3f, 0x62, 0xe3, 0xe9, 0x70, 0x68, 0xc8, 0xa0, 0xd7, 0x4c, 0x0c, 0x05, 0xf6, 0x05, + 0xb4, 0xce, 0x4c, 0x5c, 0xe3, 0xd3, 0xf6, 0x15, 0xb4, 0xba, 0x41, 0x99, 0xdc, 0x29, 0x18, 0x3e, 0xd0, 0x92, 0x29, + 0x98, 0x2a, 0xbc, 0x21, 0x52, 0xc9, 0x3e, 0x2d, 0xad, 0xc3, 0xbe, 0x5d, 0x28, 0xb4, 0xd0, 0xc4, 0xaf, 0x32, 0xc4, + 0x4f, 0x5d, 0x67, 0xfe, 0x6d, 0xda, 0xa7, 0x06, 0xb1, 0x70, 0x24, 0x06, 0x11, 0xbf, 0x38, 0x55, 0xb6, 0x13, 0x42, + 0xc5, 0xc6, 0x43, 0xd7, 0xba, 0x71, 0x24, 0x55, 0x18, 0x4a, 0xa1, 0xf1, 0xd4, 0x70, 0xdf, 0x0b, 0x1d, 0xbe, 0x0e, + 0xb3, 0xb8, 0xcd, 0x1a, 0x49, 0x8d, 0x71, 0x2a, 0x4c, 0x9c, 0x4a, 0xb9, 0x8a, 0x04, 0x06, 0xca, 0xb3, 0x85, 0x41, + 0x80, 0x49, 0x4c, 0x32, 0xb6, 0x16, 0xc2, 0x84, 0xb1, 0x73, 0x85, 0x69, 0xea, 0x22, 0xf5, 0x9b, 0x81, 0xc9, 0x82, + 0x86, 0xfc, 0x1e, 0x8d, 0xd6, 0x54, 0x4d, 0x01, 0x86, 0x71, 0x94, 0x6a, 0xfc, 0x5b, 0x84, 0xda, 0x0c, 0x03, 0x00, + 0xdb, 0xbc, 0x95, 0x99, 0xa8, 0x5e, 0x0a, 0x84, 0x40, 0x73, 0xf6, 0x53, 0x71, 0xb5, 0x33, 0x0b, 0x46, 0xd1, 0x6e, + 0xaf, 0x7c, 0x3e, 0x70, 0x42, 0x79, 0xac, 0x2e, 0x50, 0x2f, 0x64, 0xf1, 0x4a, 0xa6, 0xbc, 0x15, 0x22, 0x73, 0x4f, + 0xb2, 0x9f, 0xf2, 0x11, 0x9c, 0x57, 0xe8, 0x54, 0x6e, 0xb6, 0x89, 0x32, 0x4b, 0x92, 0x8c, 0x05, 0xc6, 0xe6, 0x25, + 0x98, 0x49, 0xcd, 0x8c, 0xe1, 0xd7, 0x10, 0x67, 0x6c, 0xe7, 0x24, 0xdc, 0xdc, 0xcd, 0x03, 0x43, 0x94, 0x72, 0xd1, + 0x12, 0x0d, 0x5b, 0x3b, 0x5e, 0x4f, 0xae, 0x09, 0xf7, 0x61, 0x23, 0xd6, 0x64, 0x8c, 0x71, 0x6d, 0x6e, 0x64, 0xfd, + 0x68, 0x81, 0x07, 0x63, 0xca, 0xfa, 0x13, 0xc8, 0xb4, 0x92, 0xb2, 0xce, 0x17, 0x46, 0xcc, 0xa4, 0x12, 0xbd, 0xdb, + 0x37, 0x3e, 0xab, 0xbb, 0x88, 0xfa, 0xad, 0xfd, 0x9e, 0xd4, 0xc3, 0xad, 0xff, 0xa0, 0xb0, 0x06, 0x95, 0x11, 0x97, + 0x11, 0xe5, 0x99, 0x03, 0xdd, 0x34, 0x29, 0xe2, 0xf4, 0x74, 0x15, 0x17, 0x25, 0x4f, 0xa1, 0x52, 0x4d, 0xdd, 0xa2, + 0xde, 0x04, 0xec, 0x0d, 0x91, 0x24, 0x59, 0x4b, 0x63, 0x2b, 0x76, 0x69, 0x90, 0x9e, 0x3b, 0x23, 0x2e, 0xbd, 0xa8, + 0xd0, 0x90, 0x96, 0x7a, 0x67, 0xa1, 0x92, 0xf9, 0x2b, 0xfe, 0x33, 0xa8, 0x15, 0xe8, 0x68, 0x93, 0x62, 0x3c, 0x05, + 0x46, 0x7c, 0x37, 0x98, 0xd5, 0x3d, 0xc4, 0x45, 0x13, 0x94, 0x7a, 0x47, 0xec, 0xf8, 0xb9, 0xc9, 0xc3, 0xbb, 0x90, + 0x73, 0x06, 0x9f, 0xde, 0xcf, 0x12, 0xb5, 0xd6, 0x91, 0x18, 0xa9, 0x19, 0x40, 0xd3, 0x41, 0x99, 0xf3, 0x58, 0x04, + 0xb3, 0x9e, 0x49, 0x8c, 0x7a, 0x5c, 0xff, 0x02, 0x0d, 0xb5, 0xdf, 0xac, 0x2c, 0xcf, 0xaa, 0xdb, 0x2f, 0xe1, 0xc0, + 0xa6, 0xb6, 0x82, 0x1e, 0xaf, 0x2b, 0x79, 0x71, 0xa1, 0xba, 0xed, 0x17, 0x62, 0xe4, 0x74, 0x8d, 0x6b, 0xe9, 0xbc, + 0x5a, 0xb0, 0x5e, 0x77, 0xba, 0x59, 0xdc, 0xcd, 0x32, 0x1a, 0x08, 0x6b, 0x3b, 0x9f, 0x68, 0xfe, 0xac, 0xd9, 0x76, + 0x1f, 0x6f, 0x41, 0xcc, 0x02, 0x80, 0x48, 0x0f, 0xa2, 0x60, 0x99, 0xa5, 0x3c, 0xa0, 0xf2, 0x2e, 0x8e, 0xb2, 0x50, + 0x7a, 0x39, 0xcb, 0xf8, 0x69, 0xd3, 0x58, 0xeb, 0xac, 0x50, 0x86, 0xd6, 0x46, 0x77, 0xba, 0xca, 0x10, 0xdb, 0x4f, + 0xe2, 0x6c, 0x01, 0xee, 0x8f, 0x19, 0x0a, 0x0d, 0x9d, 0x65, 0xa4, 0x89, 0x86, 0xef, 0xba, 0x63, 0x90, 0x51, 0x9c, + 0xac, 0xf3, 0x4a, 0xba, 0xd1, 0x67, 0x6d, 0x24, 0xcc, 0x3d, 0x44, 0xbf, 0x8a, 0xc1, 0xa3, 0xdc, 0xe7, 0xb5, 0xd1, + 0xc9, 0xb4, 0x8c, 0xb4, 0x3b, 0x3f, 0xa9, 0x97, 0x59, 0xaa, 0x75, 0xd8, 0x3e, 0xc3, 0xde, 0x1a, 0x93, 0xde, 0x84, + 0xd4, 0x30, 0x12, 0x9f, 0xcf, 0xa8, 0x11, 0x02, 0xda, 0x72, 0xfc, 0x1d, 0x3e, 0xc3, 0xd0, 0x14, 0x58, 0xaa, 0xb8, + 0x85, 0xdd, 0xf0, 0x35, 0x9f, 0xac, 0x5a, 0x00, 0x82, 0x59, 0xf9, 0x7a, 0x17, 0xaf, 0x84, 0xfa, 0x54, 0x9b, 0x01, + 0x20, 0x0b, 0x4a, 0xb9, 0xe3, 0xa7, 0x54, 0x3a, 0x58, 0xa2, 0x68, 0x7b, 0x39, 0x7d, 0xa3, 0x63, 0xe3, 0xfb, 0xf4, + 0x5c, 0xc0, 0x76, 0x21, 0xbf, 0x75, 0xa7, 0x5e, 0xa2, 0x22, 0xb5, 0x6d, 0xd6, 0x3d, 0x7c, 0xb9, 0x41, 0x93, 0x30, + 0x82, 0x32, 0x65, 0x0a, 0x60, 0x70, 0x53, 0x8d, 0x82, 0x49, 0xab, 0x91, 0xb0, 0xa5, 0x9e, 0x64, 0xb9, 0xe9, 0x83, + 0x53, 0xdd, 0x21, 0xe8, 0xb9, 0x51, 0xce, 0x17, 0x2d, 0xfb, 0xb5, 0x82, 0xa3, 0x93, 0xab, 0x21, 0x6a, 0xe6, 0xbd, + 0xb6, 0x23, 0x43, 0xca, 0x65, 0x18, 0x08, 0xa6, 0x1c, 0xf3, 0xf4, 0xd8, 0x7a, 0x46, 0x44, 0xf7, 0x9c, 0x7d, 0xa6, + 0x5b, 0x75, 0x25, 0x01, 0xd1, 0xf1, 0x9b, 0xc7, 0x2f, 0x2f, 0xe3, 0x0b, 0x83, 0xa2, 0xd4, 0xb0, 0x88, 0x51, 0xa6, + 0x7d, 0x95, 0x84, 0xc1, 0xfb, 0xf0, 0xee, 0x27, 0x95, 0xa5, 0xf6, 0x7b, 0xb0, 0xb1, 0xa2, 0xaa, 0x0f, 0x25, 0x2f, + 0x9a, 0x02, 0xac, 0xbb, 0x2c, 0x51, 0x20, 0xf7, 0x3b, 0x9b, 0x66, 0xbe, 0x89, 0x1a, 0x37, 0x1b, 0xd6, 0x1b, 0xd7, + 0xed, 0x52, 0x5b, 0xb2, 0x23, 0x2b, 0x91, 0x33, 0x8b, 0xc1, 0x8c, 0x1f, 0x15, 0x06, 0xa5, 0x61, 0x83, 0xaa, 0x54, + 0xfc, 0xde, 0x88, 0xe0, 0xd4, 0xb1, 0xaa, 0x30, 0xa6, 0x01, 0xb3, 0xad, 0xa8, 0x35, 0xa8, 0x83, 0x52, 0xda, 0x9a, + 0x80, 0x6c, 0xbf, 0xb1, 0x82, 0x9a, 0xdf, 0xbf, 0x1b, 0x43, 0xbe, 0xa6, 0x14, 0x54, 0x12, 0xb0, 0x33, 0x68, 0xf4, + 0x54, 0x09, 0x03, 0x29, 0x08, 0x9e, 0x00, 0xe5, 0x8b, 0xa8, 0xb1, 0xda, 0xed, 0xab, 0x53, 0x63, 0xb4, 0x05, 0x84, + 0x16, 0xd2, 0xa3, 0xcb, 0x3e, 0x6e, 0x63, 0x1d, 0x48, 0x3c, 0x38, 0xc1, 0x76, 0xae, 0xae, 0xd1, 0x48, 0x68, 0x7e, + 0xdf, 0x68, 0xc0, 0x6b, 0x5a, 0x81, 0x42, 0x3d, 0xc7, 0xd1, 0xd0, 0xd9, 0x21, 0x05, 0x11, 0x1b, 0xb4, 0xb0, 0xef, + 0x8e, 0x0f, 0xcd, 0xbe, 0x9e, 0x27, 0x0b, 0x52, 0x53, 0xe9, 0x3e, 0x77, 0x4b, 0xc8, 0x5a, 0x75, 0x28, 0x2b, 0x0f, + 0x70, 0xbc, 0x50, 0x32, 0x7f, 0x87, 0x49, 0x8d, 0xd2, 0x98, 0xd0, 0x18, 0xb1, 0x80, 0x25, 0x41, 0x7b, 0x3d, 0x50, + 0xbf, 0x0c, 0x42, 0x85, 0x33, 0x3d, 0x91, 0xf8, 0x94, 0x72, 0xf5, 0x69, 0x41, 0xea, 0x69, 0xc1, 0x1c, 0xe8, 0xa5, + 0x6f, 0xe5, 0x57, 0x36, 0x3e, 0xda, 0xdd, 0xbb, 0xe6, 0xc2, 0x3a, 0x86, 0xb8, 0xd8, 0xc2, 0x6f, 0x4e, 0x4d, 0x01, + 0xd8, 0xf0, 0x58, 0x97, 0xe5, 0x1b, 0x35, 0x91, 0x59, 0x1c, 0x92, 0x08, 0x24, 0xdb, 0xcd, 0xcd, 0x6d, 0x04, 0xdb, + 0xde, 0x42, 0x6d, 0xa8, 0xbf, 0xbc, 0xed, 0x7e, 0xc7, 0xf0, 0x72, 0x4f, 0xee, 0xdd, 0xb4, 0xa1, 0xfc, 0xe1, 0xee, + 0x55, 0xf2, 0x7f, 0x55, 0xc9, 0xdd, 0x56, 0x99, 0x75, 0x5b, 0xbc, 0xdf, 0x75, 0xdc, 0x72, 0x8c, 0x06, 0x81, 0x35, + 0x05, 0x06, 0xd2, 0x93, 0xc6, 0x34, 0xd1, 0xd1, 0x95, 0x19, 0x33, 0x78, 0x74, 0x01, 0x9a, 0xc3, 0x74, 0x9e, 0xc7, + 0x00, 0x1c, 0xe0, 0x1f, 0x79, 0x84, 0xfa, 0xa7, 0xf3, 0x3c, 0x38, 0x0d, 0x06, 0xe5, 0x20, 0xd0, 0x9f, 0xb8, 0xe6, + 0x04, 0x0b, 0xd0, 0xb9, 0xc5, 0x0c, 0xe2, 0x4e, 0x5a, 0x33, 0x87, 0xf8, 0x30, 0x99, 0x0e, 0x06, 0x31, 0xd9, 0x00, + 0x48, 0x5f, 0xbc, 0xb0, 0xce, 0x41, 0x85, 0x5e, 0x90, 0xad, 0xba, 0x8b, 0x66, 0xc5, 0x5e, 0xb5, 0xd3, 0xbc, 0xdf, + 0xcf, 0xe7, 0xe5, 0x20, 0x68, 0x54, 0x58, 0x18, 0xef, 0x3f, 0xda, 0xfc, 0xd2, 0xe8, 0xa4, 0x09, 0x46, 0xac, 0x3d, + 0x46, 0xf5, 0x8a, 0xa7, 0x19, 0x6d, 0xdc, 0x8e, 0x95, 0xf2, 0x05, 0x44, 0xf1, 0xc0, 0x90, 0xb5, 0xf2, 0xee, 0x1c, + 0xbc, 0x2e, 0x37, 0xde, 0x1c, 0x51, 0x80, 0xdd, 0x14, 0xc6, 0x49, 0xcd, 0x45, 0x17, 0x35, 0xf1, 0x0c, 0x76, 0xba, + 0x7a, 0x2b, 0xd1, 0x6a, 0xbc, 0x17, 0xef, 0x9a, 0x8d, 0xbf, 0x96, 0x7b, 0xba, 0xcc, 0xbd, 0x73, 0x40, 0x9c, 0xdd, + 0x8b, 0xab, 0x3d, 0x2c, 0x75, 0x2f, 0x18, 0x58, 0xe4, 0x90, 0x76, 0xb5, 0x7a, 0x28, 0x22, 0x75, 0x1e, 0x83, 0x01, + 0x93, 0x69, 0x48, 0x4d, 0xa6, 0xbd, 0x58, 0x41, 0xda, 0x58, 0x6b, 0x01, 0x6d, 0x38, 0x2c, 0x76, 0xec, 0x86, 0xdd, + 0xe9, 0xd6, 0xa1, 0x50, 0xc2, 0x40, 0xd6, 0x75, 0xf3, 0x50, 0x6b, 0x78, 0x22, 0xe8, 0x41, 0x35, 0xda, 0x4f, 0x0f, + 0xe5, 0x49, 0x7b, 0x2c, 0xc0, 0x45, 0x0f, 0x5f, 0x3e, 0x17, 0x78, 0xd1, 0xde, 0x41, 0x9e, 0x33, 0x9f, 0x2a, 0x1f, + 0xc4, 0x86, 0x5b, 0x86, 0x0f, 0xed, 0xe3, 0x5b, 0x81, 0x4c, 0xea, 0x8e, 0xa6, 0xb6, 0x76, 0x47, 0xe3, 0x98, 0x40, + 0xbf, 0x29, 0x47, 0x29, 0x13, 0x53, 0xcb, 0x92, 0x1d, 0xf5, 0x72, 0xe5, 0x0d, 0x95, 0xb2, 0xa3, 0x65, 0x9b, 0xf3, + 0x4b, 0x1b, 0x09, 0xfd, 0xbe, 0x76, 0x07, 0xc2, 0x37, 0x6a, 0xbd, 0x21, 0x2f, 0x1b, 0x22, 0x96, 0x43, 0xcc, 0xc0, + 0xf1, 0x42, 0x2a, 0xd7, 0xee, 0xa2, 0xa9, 0xaa, 0xdb, 0xd9, 0xca, 0x05, 0x2d, 0xf1, 0x56, 0x0a, 0xac, 0x22, 0x75, + 0x7a, 0x3d, 0x95, 0x78, 0xd7, 0x47, 0xb1, 0xfd, 0x08, 0xd8, 0xc6, 0xc6, 0xd1, 0xd8, 0xb8, 0x45, 0x6c, 0xf0, 0x55, + 0x54, 0xd1, 0x82, 0x03, 0x04, 0x77, 0x5b, 0x52, 0x4b, 0x33, 0x87, 0xb8, 0xaf, 0x78, 0x80, 0xf6, 0x5d, 0x1c, 0x71, + 0x2a, 0xc0, 0xb6, 0xae, 0x75, 0xce, 0x6a, 0x39, 0x60, 0x33, 0xd1, 0xf3, 0x4f, 0xab, 0x46, 0x22, 0x86, 0x55, 0x36, + 0x52, 0x56, 0x68, 0xf7, 0x4a, 0x97, 0x70, 0xf1, 0x05, 0x78, 0xd9, 0xbe, 0x5b, 0xd9, 0x7d, 0xba, 0xc4, 0xfe, 0x61, + 0x5e, 0x35, 0xc1, 0x23, 0xaf, 0xf1, 0xf6, 0x1e, 0x26, 0xbe, 0x54, 0x0a, 0xe1, 0x55, 0x4a, 0x43, 0x09, 0xc0, 0x20, + 0x09, 0x6a, 0xb8, 0xd2, 0xb6, 0x19, 0xa4, 0x32, 0x86, 0xdd, 0xad, 0xde, 0xea, 0xff, 0xb4, 0x0a, 0x17, 0x95, 0x2c, + 0xc6, 0x24, 0xd0, 0x39, 0xd5, 0x72, 0x13, 0x58, 0xf0, 0x74, 0x97, 0x1c, 0x81, 0xc2, 0x4e, 0x00, 0x37, 0x94, 0xb0, + 0xdf, 0xf1, 0x36, 0x94, 0xb3, 0xd7, 0x56, 0xf2, 0xe4, 0xf6, 0x25, 0x15, 0x34, 0x21, 0x53, 0x61, 0xf7, 0x6f, 0x6b, + 0xc3, 0xbe, 0x0c, 0xe5, 0x48, 0x0a, 0x5c, 0x1c, 0x74, 0x0e, 0x60, 0x7f, 0x90, 0xcb, 0xd8, 0x7c, 0x26, 0xfd, 0xbe, + 0x7a, 0xff, 0x34, 0xcf, 0x92, 0x8f, 0x3b, 0xef, 0x0d, 0x4f, 0xb3, 0x64, 0x40, 0x25, 0x62, 0x6a, 0x5d, 0x15, 0xc3, + 0xa5, 0x76, 0x31, 0x6e, 0x90, 0x8c, 0xf8, 0x4e, 0xea, 0x10, 0x23, 0xc6, 0x17, 0xd9, 0x21, 0x29, 0x39, 0x5d, 0xd6, + 0x9d, 0x3d, 0xd7, 0xa2, 0x19, 0x34, 0x86, 0xdb, 0xf1, 0x5e, 0xd2, 0x2b, 0x40, 0x05, 0x88, 0xee, 0x59, 0xe0, 0x1a, + 0xde, 0x5c, 0x12, 0x8d, 0x2d, 0x3d, 0x6d, 0x89, 0x06, 0xee, 0x94, 0x09, 0x49, 0xb5, 0x71, 0x80, 0x45, 0xac, 0xeb, + 0x8f, 0x61, 0x01, 0x40, 0xad, 0x06, 0xe9, 0x95, 0xbe, 0x20, 0x54, 0x25, 0x21, 0x18, 0x9d, 0x48, 0x78, 0x19, 0xd0, + 0x38, 0x33, 0x89, 0x16, 0x36, 0x38, 0xa0, 0x2f, 0x2b, 0x93, 0x68, 0x6c, 0xc8, 0x03, 0xca, 0x6d, 0x1a, 0xc0, 0xe0, + 0x83, 0x24, 0x89, 0xbe, 0x5f, 0x9a, 0x24, 0x10, 0x94, 0xa0, 0x7c, 0x83, 0xfe, 0x51, 0x7a, 0x3e, 0x96, 0x3f, 0x7a, + 0x87, 0xd2, 0x0f, 0x61, 0x01, 0x32, 0x45, 0x5d, 0x31, 0xcd, 0xd8, 0x51, 0xd6, 0x6d, 0x4c, 0xe2, 0x79, 0xda, 0x5d, + 0x15, 0xca, 0xa5, 0x0b, 0xfc, 0xca, 0x32, 0xc4, 0xb1, 0x7e, 0x1a, 0xaf, 0xd8, 0x71, 0xc8, 0x35, 0x5e, 0xfa, 0xd3, + 0x78, 0x85, 0x33, 0x44, 0xab, 0x56, 0x02, 0x51, 0xfe, 0xab, 0x36, 0x70, 0x88, 0xfb, 0x04, 0x83, 0x5c, 0x54, 0xde, + 0x03, 0x81, 0xbc, 0xad, 0x20, 0x22, 0xcd, 0xec, 0x3a, 0x8c, 0x48, 0xb5, 0x93, 0x64, 0xbe, 0xfc, 0x51, 0x66, 0xc2, + 0xfb, 0x06, 0x1e, 0x9b, 0xcd, 0xb2, 0x29, 0xe6, 0x0b, 0x15, 0xcc, 0xc1, 0x7d, 0xa2, 0xe2, 0x52, 0x54, 0xfe, 0x13, + 0x76, 0xc1, 0x8b, 0xf1, 0xe0, 0xf5, 0x1a, 0x01, 0xf6, 0x2b, 0xff, 0xc9, 0x1b, 0xb3, 0xbf, 0xac, 0x1b, 0x5f, 0x66, + 0x22, 0x3e, 0xf0, 0xd1, 0x0d, 0xe5, 0xa3, 0x5b, 0x2f, 0xd3, 0x77, 0x0d, 0x28, 0x91, 0x51, 0x59, 0xf1, 0xd5, 0x8a, + 0xa7, 0xb3, 0xab, 0x24, 0xca, 0x46, 0x15, 0x17, 0x30, 0xbd, 0xe0, 0x78, 0x97, 0xac, 0xcf, 0xb2, 0xe4, 0x25, 0xc4, + 0x1e, 0x58, 0x49, 0x85, 0xc5, 0x0f, 0xcb, 0x4c, 0x2d, 0x66, 0x21, 0x2b, 0x29, 0x78, 0x30, 0xbb, 0x4e, 0xa2, 0xbf, + 0x96, 0x1e, 0x92, 0x9a, 0x99, 0xb2, 0x4d, 0xed, 0x08, 0xb5, 0xf1, 0x75, 0xa4, 0x1b, 0x6d, 0x01, 0x00, 0xf7, 0x6c, + 0x91, 0x46, 0x92, 0x89, 0xe1, 0xa4, 0x66, 0xdc, 0xa4, 0x17, 0x98, 0x1a, 0xd7, 0xac, 0xa2, 0x89, 0xb3, 0x90, 0x01, + 0xbd, 0x3f, 0xcd, 0xf5, 0x73, 0x06, 0xf7, 0x1f, 0xb4, 0x06, 0x2e, 0x0f, 0x8b, 0x7e, 0x5f, 0x1e, 0x16, 0xdb, 0x6d, + 0x79, 0x14, 0xf7, 0xfb, 0xf2, 0x28, 0x36, 0xfc, 0x83, 0x52, 0x6c, 0x1b, 0x73, 0x83, 0x84, 0xe6, 0x12, 0xa2, 0x16, + 0x8d, 0xe0, 0x0f, 0xcd, 0x72, 0x2e, 0xa2, 0xfc, 0x30, 0xe9, 0xf7, 0x7b, 0xcb, 0x99, 0x18, 0xe4, 0xc3, 0x24, 0xca, + 0x87, 0x89, 0xe7, 0x84, 0xf8, 0x8b, 0xe7, 0x84, 0xa8, 0x68, 0xe0, 0x0a, 0xce, 0x0c, 0x40, 0x14, 0xf0, 0xe9, 0x1f, + 0xd5, 0xb5, 0x14, 0xba, 0x96, 0x58, 0xd5, 0x92, 0xe8, 0x0a, 0x6a, 0x76, 0x5d, 0x84, 0x25, 0x96, 0x42, 0x97, 0xec, + 0xbb, 0x25, 0xf0, 0x44, 0x39, 0xaf, 0x36, 0xc0, 0xc0, 0x46, 0x78, 0xe7, 0x30, 0xe1, 0x24, 0xd6, 0x35, 0xa0, 0x9d, + 0x6e, 0x6a, 0x7a, 0x4e, 0x57, 0xf4, 0x02, 0xf9, 0xd9, 0x73, 0x30, 0x58, 0x3a, 0x64, 0xf9, 0x74, 0x30, 0x38, 0x27, + 0x2b, 0x56, 0xce, 0xc3, 0x78, 0x10, 0xae, 0x67, 0xf9, 0xf0, 0x3c, 0x3a, 0x27, 0xe4, 0xab, 0x62, 0x41, 0x7b, 0xab, + 0x51, 0xf9, 0x31, 0x83, 0xf0, 0x7e, 0xe9, 0x2c, 0xcc, 0x4c, 0x9c, 0x8f, 0xd5, 0xe8, 0x86, 0xae, 0x20, 0x7e, 0x0d, + 0xdc, 0x48, 0x48, 0x04, 0x1d, 0xb9, 0xa0, 0x2b, 0xba, 0xa6, 0xd2, 0xcc, 0x30, 0x46, 0xeb, 0xb6, 0xc7, 0x49, 0x02, + 0x8e, 0xc9, 0xae, 0xf8, 0x68, 0xac, 0x0a, 0xef, 0xfa, 0x8e, 0xd0, 0x5e, 0x2f, 0x71, 0x83, 0xf4, 0x43, 0x7b, 0x90, + 0x80, 0x11, 0x19, 0xa9, 0x81, 0x32, 0x23, 0x23, 0xa9, 0x99, 0x54, 0x1c, 0x92, 0xd8, 0x1f, 0x12, 0x35, 0x0e, 0x89, + 0x3f, 0x0e, 0xb9, 0x1e, 0x07, 0xe4, 0xee, 0x97, 0x6c, 0x4c, 0x53, 0x36, 0xa6, 0x6b, 0x35, 0x2a, 0xf4, 0x92, 0x9e, + 0x69, 0xea, 0x78, 0xca, 0x5e, 0xc1, 0x81, 0x3d, 0x08, 0xf3, 0x59, 0x3c, 0x7c, 0x15, 0xbd, 0x22, 0xe4, 0x2b, 0x49, + 0xaf, 0xd4, 0xa5, 0x0c, 0x02, 0x21, 0x5e, 0x82, 0x73, 0xa9, 0x0b, 0x75, 0x72, 0x69, 0x76, 0x1c, 0x3e, 0x5d, 0x34, + 0x9e, 0xce, 0x20, 0xa2, 0x0f, 0x5a, 0xa9, 0xf4, 0xfb, 0xe1, 0x39, 0x2b, 0xe7, 0xa7, 0xe1, 0x98, 0x00, 0x0e, 0x8f, + 0x1e, 0xce, 0xf3, 0xd1, 0x0d, 0x3d, 0x1f, 0xdd, 0x12, 0xb0, 0xf0, 0x1a, 0x4f, 0xd7, 0x87, 0x2c, 0x9e, 0x0e, 0x06, + 0x6b, 0xa4, 0xea, 0x2a, 0xf7, 0x9a, 0x2c, 0xe8, 0x39, 0x4e, 0x04, 0x01, 0x86, 0x3e, 0x13, 0x6b, 0x43, 0xc3, 0x5f, + 0x31, 0xf8, 0xf8, 0x96, 0x9d, 0x8f, 0x6e, 0xe9, 0x0d, 0x7b, 0xb5, 0x1d, 0x4f, 0x81, 0x99, 0x5a, 0xcd, 0xc2, 0xdb, + 0xc3, 0x8b, 0xd9, 0x05, 0xbb, 0x8d, 0x6e, 0x8f, 0xa0, 0xa1, 0x97, 0xec, 0x16, 0x01, 0x97, 0xd2, 0x87, 0xcb, 0xc1, + 0x2b, 0xb2, 0x3f, 0x18, 0xa4, 0x24, 0x0a, 0xaf, 0x42, 0xaf, 0x95, 0xaf, 0xe8, 0x2d, 0xa1, 0x2b, 0x76, 0x83, 0xa3, + 0x71, 0xc1, 0xf0, 0x83, 0x33, 0x76, 0x5b, 0x5f, 0x85, 0xde, 0x6e, 0x4e, 0x44, 0x27, 0x88, 0x11, 0xfa, 0x1a, 0x38, + 0x9a, 0xe5, 0xc2, 0x4c, 0xc0, 0x93, 0xb9, 0xc8, 0x68, 0x51, 0x68, 0x06, 0xe2, 0xac, 0x04, 0xc4, 0x92, 0xa8, 0xfb, + 0xcd, 0x46, 0xa7, 0xb0, 0x9c, 0xfb, 0xfd, 0x5e, 0x65, 0xe8, 0x01, 0x22, 0x67, 0x76, 0xd2, 0x83, 0x9e, 0x4f, 0x0f, + 0xf0, 0x13, 0xbd, 0x6a, 0x10, 0x27, 0xf3, 0x87, 0x65, 0xf4, 0x8b, 0x47, 0x1f, 0x3e, 0x74, 0x53, 0x9e, 0x32, 0xff, + 0xf7, 0x29, 0x8f, 0xcc, 0xa3, 0x57, 0x95, 0x07, 0x82, 0xe7, 0xad, 0x49, 0xa5, 0x91, 0xa8, 0x46, 0xa7, 0xab, 0x18, + 0xb4, 0x91, 0xa8, 0x6d, 0xd0, 0x4f, 0x68, 0x61, 0x05, 0x11, 0x72, 0x0e, 0x9e, 0x81, 0x41, 0x2a, 0x84, 0xca, 0x51, + 0x8b, 0x12, 0x0d, 0x41, 0x72, 0x59, 0x72, 0x15, 0x3e, 0x87, 0x50, 0x75, 0xfa, 0x38, 0x13, 0x61, 0x43, 0x8f, 0x43, + 0x1f, 0x00, 0xfe, 0xf7, 0x1d, 0x72, 0x51, 0xf2, 0x0b, 0x3c, 0x9b, 0xdb, 0x04, 0xa3, 0x60, 0x89, 0x68, 0x86, 0xb6, + 0x41, 0xec, 0xc7, 0x92, 0x60, 0x3d, 0x92, 0xc6, 0xa3, 0xd2, 0x1c, 0x11, 0x7e, 0x14, 0x1f, 0x45, 0x4f, 0x63, 0x43, + 0x22, 0x39, 0x92, 0x48, 0x3e, 0x00, 0xc2, 0x49, 0xd0, 0x5f, 0xdc, 0x35, 0xd9, 0xb5, 0x90, 0x18, 0xf4, 0xa7, 0x25, + 0xd3, 0xb2, 0x7b, 0xd5, 0x63, 0x5f, 0x11, 0xe4, 0x8e, 0xe9, 0xdf, 0xbc, 0x3e, 0xfc, 0xbd, 0xc4, 0x19, 0xb4, 0x9e, + 0x2f, 0xaa, 0x33, 0x33, 0x6f, 0x70, 0x23, 0xaf, 0xcb, 0xda, 0x75, 0xf9, 0x9c, 0xef, 0xf1, 0x9b, 0x8a, 0x8b, 0xb4, + 0xdc, 0xfb, 0xb9, 0x6a, 0xe3, 0x39, 0x95, 0xeb, 0x95, 0x8b, 0xb3, 0xa2, 0x8c, 0x53, 0x3d, 0xa9, 0x8b, 0xb1, 0x86, + 0x6d, 0xf8, 0x3d, 0xa2, 0xae, 0xa4, 0xe5, 0xe8, 0x29, 0xe5, 0xaa, 0x99, 0x72, 0xbe, 0xce, 0xf3, 0x9f, 0x76, 0x52, + 0x71, 0x8a, 0x9b, 0x29, 0x48, 0x95, 0x5a, 0x2e, 0xa0, 0x7a, 0x8e, 0x5a, 0xee, 0x96, 0x66, 0x07, 0x38, 0xb7, 0x4d, + 0xf5, 0xb1, 0x32, 0xbb, 0xf0, 0x92, 0x1b, 0xf7, 0x27, 0x53, 0x86, 0x05, 0xa3, 0xd0, 0x66, 0xd5, 0x95, 0xb6, 0x2f, + 0xb4, 0x4e, 0xc3, 0x70, 0xe5, 0xc7, 0x0b, 0x48, 0x17, 0x30, 0x8e, 0x17, 0x25, 0x13, 0xe3, 0xf6, 0xe8, 0xad, 0x20, + 0xbe, 0x64, 0x2b, 0x90, 0x7e, 0xbf, 0x27, 0xbc, 0x5d, 0xd7, 0xd1, 0x76, 0x4f, 0x9c, 0x32, 0x2a, 0x57, 0xb1, 0xf8, + 0x3e, 0x5e, 0x19, 0xc8, 0x64, 0x75, 0x3c, 0x36, 0xc6, 0x74, 0xfa, 0x7d, 0x12, 0xfa, 0x85, 0x50, 0xf0, 0x59, 0x2f, + 0xad, 0x3c, 0xb9, 0x3d, 0x2c, 0xe3, 0x1a, 0xbd, 0x12, 0x57, 0xba, 0x6f, 0x46, 0x0a, 0xa9, 0x47, 0xbe, 0x6a, 0x0a, + 0xe8, 0xcd, 0xd8, 0x37, 0x53, 0x61, 0xde, 0xee, 0x18, 0x73, 0x85, 0x60, 0xa5, 0xca, 0x6e, 0xdf, 0xa9, 0x31, 0x15, + 0x33, 0x98, 0x62, 0xdb, 0x59, 0x4c, 0xba, 0x95, 0x7f, 0xda, 0xb9, 0x4f, 0xf3, 0x0e, 0x77, 0x45, 0xfd, 0x16, 0xb8, + 0xd0, 0xac, 0x28, 0xab, 0xb6, 0x6c, 0xd8, 0x36, 0xde, 0xc8, 0x42, 0xb1, 0x01, 0x96, 0x3d, 0xf7, 0x2d, 0x3c, 0x40, + 0xdc, 0x84, 0x7b, 0x76, 0x51, 0xc3, 0x8d, 0xe1, 0xcb, 0x4a, 0xf2, 0x5d, 0x69, 0xcc, 0xa5, 0x4f, 0x95, 0x26, 0x86, + 0x93, 0xc5, 0x88, 0x8b, 0x74, 0x51, 0x67, 0x76, 0x2d, 0x7c, 0xc6, 0xcb, 0x70, 0xce, 0x17, 0x46, 0x37, 0xa5, 0x4b, + 0x2f, 0x58, 0xa2, 0x3b, 0xbd, 0x59, 0x69, 0xac, 0x94, 0x88, 0x5b, 0xb3, 0x4c, 0xa0, 0x2c, 0x65, 0xad, 0x84, 0x37, + 0x45, 0xcb, 0x56, 0xd2, 0xc8, 0x7b, 0xe6, 0xe0, 0x3e, 0xf6, 0x01, 0x31, 0x91, 0x4d, 0x60, 0x52, 0x34, 0x74, 0x40, + 0xbb, 0xea, 0xc2, 0x37, 0xa3, 0x1e, 0x0c, 0x72, 0x4b, 0x12, 0xb1, 0x82, 0x14, 0x2b, 0x58, 0xd7, 0xac, 0x98, 0xe7, + 0x0b, 0x7a, 0xce, 0xe4, 0x3c, 0x5d, 0xd0, 0x15, 0x93, 0xf3, 0x35, 0xde, 0x84, 0xce, 0xe1, 0x84, 0x24, 0x9b, 0x58, + 0x29, 0x60, 0xcf, 0xf1, 0xf2, 0x86, 0x67, 0xaa, 0xa6, 0x65, 0x17, 0x8a, 0x03, 0x8c, 0xcf, 0xca, 0x30, 0x2c, 0x87, + 0xe7, 0x60, 0x2d, 0xb1, 0x1f, 0xae, 0xe6, 0x7c, 0xa1, 0x7e, 0x43, 0xd4, 0xf9, 0x24, 0x54, 0xec, 0x82, 0xdd, 0x0b, + 0x64, 0x7a, 0x39, 0xe7, 0x0b, 0x35, 0x12, 0xba, 0xe0, 0x4b, 0x6b, 0x6c, 0x12, 0x7b, 0x82, 0x96, 0x59, 0x3c, 0x1f, + 0x2f, 0xa2, 0xb8, 0x86, 0x65, 0x78, 0xa2, 0x66, 0xa6, 0x25, 0xff, 0x49, 0xd4, 0x86, 0x26, 0xfa, 0x06, 0xab, 0xc8, + 0x1f, 0x1e, 0x1f, 0x5d, 0x02, 0x19, 0x3b, 0xbb, 0x92, 0x99, 0x0f, 0x7d, 0x1f, 0x19, 0xdc, 0x73, 0x53, 0xce, 0xb8, + 0x0a, 0x12, 0x65, 0xe0, 0xee, 0xd5, 0x2c, 0x19, 0x6b, 0x11, 0xbe, 0x7b, 0x54, 0x14, 0x7d, 0x26, 0x4d, 0x03, 0xba, + 0x8f, 0x04, 0x73, 0xa0, 0xf7, 0x0a, 0x1d, 0x2e, 0xab, 0x6d, 0x26, 0xe0, 0x2f, 0x12, 0xe4, 0xb7, 0x42, 0xaf, 0x6a, + 0x0c, 0xaa, 0x68, 0x17, 0xb1, 0xf4, 0xef, 0x23, 0x7e, 0x94, 0xcd, 0xdf, 0xcc, 0x3d, 0x5e, 0x49, 0x18, 0xfc, 0x90, + 0x9a, 0x4d, 0x32, 0x6f, 0xaf, 0xd8, 0x77, 0xd0, 0x51, 0x8f, 0x5a, 0xe3, 0x7d, 0xf5, 0x9c, 0x53, 0x88, 0x51, 0x42, + 0xd1, 0x49, 0x30, 0x80, 0xdb, 0x25, 0xa4, 0xb8, 0x1b, 0xec, 0xa6, 0x79, 0xcd, 0x8b, 0x82, 0xb3, 0x75, 0x55, 0x05, + 0x7e, 0x40, 0xc3, 0xf9, 0x62, 0x37, 0x84, 0xe1, 0x98, 0xb6, 0xae, 0x61, 0x10, 0x66, 0x0c, 0x23, 0x21, 0x78, 0xfd, + 0x8b, 0x1e, 0xd1, 0x24, 0x5e, 0x7d, 0xc7, 0x3f, 0x65, 0xbc, 0x50, 0x44, 0x1a, 0x44, 0x48, 0xdd, 0xc4, 0x37, 0x32, + 0x4d, 0x0a, 0x28, 0x04, 0x18, 0x05, 0x54, 0x62, 0x43, 0x53, 0xf1, 0xb7, 0x5a, 0x7c, 0xf0, 0x53, 0xd3, 0xf1, 0x68, + 0x5c, 0xb7, 0x3a, 0xa3, 0x82, 0xce, 0x40, 0x8f, 0x5a, 0x51, 0x4f, 0x83, 0x56, 0x82, 0x69, 0xa4, 0x79, 0xeb, 0x1e, + 0x02, 0xaf, 0x4c, 0x8b, 0x77, 0x1e, 0xd0, 0xcd, 0xa9, 0x0f, 0x9e, 0x3c, 0xa6, 0xa7, 0x0e, 0x3d, 0xb9, 0x62, 0x47, + 0x55, 0x0f, 0xb5, 0xf7, 0x66, 0x84, 0x82, 0x7e, 0x1f, 0x53, 0xa0, 0x1b, 0x41, 0xed, 0x5d, 0xdd, 0x2b, 0xb9, 0xcb, + 0xe1, 0x3b, 0xce, 0x72, 0x03, 0x58, 0x2a, 0xb2, 0x56, 0xe0, 0x51, 0x80, 0xba, 0x54, 0x86, 0xb0, 0xc5, 0x1c, 0x0e, + 0x95, 0xdd, 0xaa, 0xd5, 0x50, 0x92, 0xc3, 0x72, 0x04, 0x0e, 0xa1, 0xeb, 0x72, 0x50, 0x8e, 0x96, 0x59, 0xf5, 0x0e, + 0x7f, 0x6b, 0xd6, 0x21, 0xc9, 0xee, 0x62, 0x1d, 0xb8, 0x65, 0x1d, 0xa6, 0x1f, 0x0d, 0x52, 0x00, 0x9a, 0x6c, 0x04, + 0x2e, 0x01, 0x78, 0x6f, 0xff, 0x11, 0xa1, 0x56, 0xa6, 0x77, 0x32, 0x16, 0xea, 0xfb, 0x46, 0x12, 0x94, 0xd0, 0x4c, + 0xa8, 0x1c, 0x4b, 0xc1, 0x3b, 0x8f, 0x74, 0x4e, 0xea, 0x4c, 0xbc, 0x03, 0x71, 0x5a, 0x78, 0xcf, 0xde, 0x82, 0xe0, + 0x9c, 0x05, 0xbd, 0xc5, 0xdb, 0xac, 0x96, 0xda, 0xe8, 0x81, 0x02, 0xf8, 0xdd, 0xe0, 0x16, 0x41, 0xbe, 0x1a, 0xc3, + 0xb5, 0x92, 0xd7, 0x21, 0x1f, 0x16, 0xf4, 0x80, 0x0c, 0xec, 0xb3, 0x18, 0xc6, 0xf4, 0x80, 0x1c, 0xda, 0x67, 0xe9, + 0x06, 0x70, 0x20, 0xf5, 0xa8, 0xd2, 0x03, 0x68, 0xd0, 0x6f, 0xb6, 0x45, 0xee, 0x00, 0x94, 0x46, 0x11, 0x03, 0x55, + 0x82, 0x88, 0x5a, 0xfc, 0x7e, 0x6f, 0xae, 0x5b, 0xcc, 0x05, 0xc2, 0x1c, 0x0c, 0x38, 0x88, 0xdb, 0x20, 0x34, 0x07, + 0xcc, 0xe6, 0x26, 0x12, 0xf4, 0xd6, 0x1a, 0x66, 0x76, 0xf4, 0x87, 0x5b, 0x09, 0xbe, 0xc9, 0x5a, 0xa3, 0xce, 0x8b, + 0x43, 0x20, 0x08, 0xde, 0x14, 0xaa, 0xda, 0xab, 0x1e, 0xd8, 0x78, 0xab, 0x7e, 0x6c, 0xb7, 0xe3, 0xa9, 0x70, 0xd7, + 0x7e, 0x41, 0xe1, 0xe4, 0x53, 0xf2, 0xaf, 0x77, 0x26, 0x83, 0x03, 0x23, 0xc3, 0x97, 0xde, 0xfe, 0x85, 0xaf, 0xb5, + 0x74, 0x4f, 0x0c, 0x4a, 0xf2, 0xf0, 0x40, 0xd1, 0xbf, 0x3b, 0x65, 0xe5, 0x53, 0x3b, 0xfd, 0xdb, 0xad, 0x59, 0x9f, + 0x87, 0xa3, 0xc9, 0x76, 0xdb, 0x8b, 0x2b, 0xed, 0xb1, 0xa6, 0x17, 0x04, 0x3a, 0xd7, 0x93, 0xfd, 0x03, 0x88, 0x8a, + 0xd0, 0x8c, 0xbb, 0x59, 0x36, 0x24, 0x32, 0x7e, 0x9c, 0xce, 0xb2, 0x21, 0xd8, 0xe1, 0x5e, 0x54, 0xe2, 0x72, 0xd4, + 0xda, 0xe0, 0xf4, 0x36, 0x09, 0x21, 0x94, 0x03, 0x56, 0x76, 0xa3, 0xfe, 0xdc, 0x2a, 0x33, 0x21, 0x35, 0x59, 0xdd, + 0x4e, 0xe9, 0x1e, 0xa6, 0xf9, 0x9e, 0x19, 0xc1, 0x01, 0xf7, 0xf6, 0x57, 0xfd, 0x31, 0x4c, 0x32, 0x4d, 0x4e, 0x91, + 0xfc, 0x22, 0x3d, 0x85, 0xa4, 0x1d, 0x7a, 0xaa, 0x08, 0xe0, 0x84, 0xda, 0x8f, 0xe1, 0x37, 0x8c, 0xfb, 0x77, 0xcd, + 0xd7, 0x6e, 0x2a, 0xa2, 0xc7, 0x14, 0xcb, 0xd4, 0xe4, 0x34, 0xc9, 0x8a, 0x04, 0xa2, 0x36, 0xaa, 0x66, 0x44, 0x8f, + 0x5c, 0xcc, 0x47, 0x45, 0xf8, 0xbc, 0x5a, 0xff, 0x67, 0x08, 0x9f, 0x51, 0xb8, 0x01, 0x5c, 0x5e, 0x71, 0x71, 0x16, + 0x3e, 0x79, 0x4c, 0xf7, 0x26, 0xdf, 0x1c, 0xd0, 0xbd, 0x83, 0x47, 0x4f, 0x08, 0xc0, 0xa2, 0x5d, 0x9c, 0x85, 0x07, + 0x4f, 0x9e, 0xd0, 0xbd, 0x6f, 0xbf, 0xa5, 0x7b, 0x93, 0x47, 0x07, 0x8d, 0xb4, 0xc9, 0x93, 0x6f, 0xe9, 0xde, 0x37, + 0x8f, 0x1b, 0x69, 0x07, 0xe3, 0x27, 0x74, 0xef, 0x9f, 0xdf, 0x98, 0xb4, 0x7f, 0x40, 0xb6, 0x6f, 0x0f, 0xf0, 0x3f, + 0x93, 0x36, 0x79, 0xf2, 0x88, 0xee, 0x4d, 0xc6, 0x50, 0xc9, 0x13, 0x57, 0xc9, 0x78, 0x02, 0x1f, 0x3f, 0x82, 0xff, + 0xfe, 0x41, 0x60, 0x13, 0x48, 0x96, 0x0b, 0xd4, 0x9f, 0xa1, 0x88, 0x13, 0x55, 0x13, 0x09, 0x0f, 0x31, 0xb3, 0xfa, + 0x26, 0x0e, 0x03, 0xe2, 0xd2, 0xa1, 0x20, 0xba, 0x37, 0x1e, 0x3d, 0x21, 0x81, 0x0f, 0x4f, 0xf7, 0xd1, 0x07, 0x19, + 0xcb, 0xc5, 0x3c, 0xfb, 0x2a, 0x37, 0xb1, 0x15, 0x3c, 0x00, 0xab, 0x13, 0x3f, 0x17, 0x97, 0xf3, 0xec, 0x2b, 0x2e, + 0x77, 0x73, 0xfd, 0xab, 0x05, 0x28, 0xef, 0xaf, 0x5a, 0xf6, 0xb1, 0x50, 0xa1, 0xd3, 0x5a, 0xa3, 0xcf, 0x4e, 0x30, + 0x7d, 0x30, 0xf0, 0x6e, 0xd8, 0xdf, 0xef, 0x94, 0xd3, 0xfa, 0x46, 0xa3, 0x50, 0xa3, 0xf2, 0x90, 0xb0, 0x23, 0x28, + 0x7a, 0x30, 0x00, 0x9e, 0xc0, 0xc3, 0x7d, 0xfb, 0x37, 0xcb, 0x38, 0xe9, 0x28, 0xe3, 0x0f, 0x94, 0x21, 0xa0, 0x51, + 0x0f, 0xb3, 0x9b, 0x1e, 0x36, 0xba, 0xd5, 0x4b, 0x96, 0xea, 0x64, 0x6a, 0x7a, 0x06, 0xfb, 0x5a, 0xd7, 0x72, 0xcf, + 0x88, 0xa2, 0xe5, 0xf9, 0x5e, 0xca, 0x67, 0x15, 0xfb, 0x7e, 0x89, 0xea, 0xad, 0xa8, 0xf1, 0x46, 0x66, 0xb3, 0x8a, + 0xfd, 0x6c, 0xde, 0x00, 0x37, 0xc3, 0xfe, 0xa5, 0x9e, 0xfc, 0xc0, 0x19, 0x99, 0xb4, 0xed, 0x51, 0x26, 0x46, 0x80, + 0x15, 0x90, 0x81, 0x03, 0x0f, 0x80, 0x0e, 0xfa, 0xa3, 0xbd, 0xdd, 0xaa, 0x94, 0x66, 0x9f, 0x2d, 0x0c, 0xa0, 0x61, + 0xde, 0x26, 0x1e, 0xaa, 0x59, 0x43, 0x5e, 0x82, 0xc2, 0xad, 0x66, 0x79, 0x3b, 0x85, 0x21, 0x84, 0x60, 0x95, 0x32, + 0x00, 0x1c, 0x08, 0x30, 0x18, 0x6b, 0x19, 0x50, 0xb3, 0xe5, 0xa3, 0x0d, 0x57, 0xea, 0x49, 0xe0, 0x0c, 0xce, 0x65, + 0x91, 0xf0, 0x37, 0x5a, 0xec, 0x8f, 0xd6, 0x8f, 0xbe, 0x6f, 0x8f, 0x07, 0x6b, 0xdf, 0xe3, 0x23, 0xfd, 0x59, 0xe3, + 0x3a, 0xb0, 0x69, 0xf9, 0xc6, 0x8b, 0xda, 0x4a, 0x3c, 0x4a, 0xe0, 0x0d, 0x4c, 0x44, 0x0a, 0x83, 0x54, 0x0b, 0x1c, + 0x83, 0xf2, 0xc6, 0x42, 0x2c, 0x55, 0x57, 0x37, 0x74, 0x4b, 0x86, 0xe0, 0xe1, 0xf6, 0xe3, 0x52, 0x05, 0x8e, 0xea, + 0xf7, 0x33, 0xe9, 0xbb, 0x3d, 0x19, 0x3b, 0x72, 0x9c, 0xfa, 0xa9, 0x70, 0xf0, 0xdf, 0xa4, 0xae, 0x8d, 0xdd, 0x7d, + 0xca, 0x2c, 0xcb, 0xc2, 0x8e, 0x42, 0x2d, 0xf7, 0xa8, 0x3c, 0x48, 0xbe, 0x90, 0x43, 0x24, 0x0b, 0x8c, 0x42, 0x41, + 0x86, 0x13, 0x2a, 0x46, 0x6b, 0x51, 0x2e, 0xb3, 0xf3, 0x2a, 0xdc, 0x28, 0x85, 0x32, 0xa7, 0xe8, 0xdb, 0x0d, 0x0e, + 0x24, 0x24, 0xca, 0xca, 0xd7, 0xf1, 0xeb, 0x10, 0xc1, 0xea, 0xb8, 0xb6, 0x85, 0xe2, 0xde, 0xfe, 0xcc, 0xd2, 0x2e, + 0xfe, 0xc8, 0xb8, 0x80, 0xba, 0x58, 0x4c, 0xc3, 0x89, 0xd5, 0xef, 0xb8, 0x2f, 0xac, 0xa6, 0x07, 0xa0, 0xbe, 0x4b, + 0x25, 0x46, 0x50, 0x5f, 0x19, 0xfb, 0xd8, 0x1e, 0x63, 0x72, 0x06, 0xb1, 0x86, 0xf5, 0xdd, 0x4e, 0xf5, 0x8d, 0xb0, + 0x23, 0x00, 0x6e, 0x84, 0xd6, 0xe8, 0xc8, 0x24, 0x55, 0x88, 0xe7, 0xa5, 0x0a, 0xdf, 0x9a, 0x11, 0x3a, 0x06, 0x6f, + 0x2a, 0xdb, 0x48, 0x21, 0x7d, 0xc1, 0xa0, 0x39, 0xb6, 0x75, 0x14, 0x56, 0x5b, 0x59, 0x76, 0x04, 0x70, 0x03, 0xd9, + 0xa1, 0xb9, 0x78, 0xce, 0xaa, 0x79, 0xb6, 0x88, 0x4c, 0x50, 0xc0, 0xa5, 0xb0, 0x0c, 0xda, 0xeb, 0x3b, 0x64, 0x3b, + 0x0e, 0xa1, 0x1b, 0xee, 0x23, 0x18, 0x4f, 0xbb, 0x29, 0x58, 0x41, 0x34, 0x42, 0x3c, 0xcc, 0x98, 0xc5, 0xf7, 0x4a, + 0x53, 0x9e, 0xaa, 0x96, 0x40, 0xe0, 0x28, 0x84, 0xba, 0xd8, 0x35, 0x4a, 0x70, 0x99, 0x1a, 0xc1, 0x0c, 0x76, 0xec, + 0x48, 0x6d, 0x97, 0x9c, 0xd3, 0xa1, 0x9a, 0xd2, 0x52, 0x4f, 0xa9, 0xf6, 0x35, 0x14, 0xf3, 0x12, 0x3d, 0xf4, 0xc0, + 0xf5, 0x40, 0x3b, 0xe4, 0x95, 0x74, 0x62, 0x22, 0xe8, 0xb4, 0xda, 0x84, 0x9d, 0x1b, 0xe9, 0x96, 0xd5, 0xc8, 0x3b, + 0x86, 0x66, 0x47, 0x3c, 0xf7, 0x03, 0x75, 0x01, 0x44, 0xc8, 0x9d, 0x2d, 0x32, 0xb3, 0xcf, 0xb2, 0xf2, 0x05, 0x94, + 0xc5, 0x11, 0x5b, 0x57, 0xc0, 0xb5, 0x14, 0x4c, 0x2e, 0x79, 0x94, 0xa5, 0x88, 0x08, 0x78, 0xac, 0xb4, 0xeb, 0x3b, + 0x2d, 0x21, 0x54, 0xa4, 0x40, 0xdc, 0x5c, 0x14, 0xe7, 0xda, 0x06, 0xb2, 0x00, 0xfa, 0xf6, 0x53, 0x76, 0xe9, 0x85, + 0x83, 0xdd, 0x5c, 0x66, 0xe2, 0x19, 0x3f, 0xcf, 0x04, 0x4f, 0x11, 0xec, 0xea, 0xc6, 0x3c, 0x70, 0xc7, 0xb6, 0x81, + 0xe5, 0xdb, 0x77, 0xb0, 0x60, 0xca, 0x50, 0x2b, 0x25, 0x32, 0x11, 0x09, 0xc8, 0xec, 0x33, 0x77, 0xaf, 0x32, 0xf1, + 0x2a, 0xbe, 0x01, 0x6f, 0x8a, 0x06, 0x3f, 0x3d, 0x3a, 0xc3, 0x2f, 0x11, 0x49, 0x14, 0x62, 0xd8, 0x62, 0x44, 0x2c, + 0x44, 0x8e, 0x1d, 0x13, 0xca, 0x95, 0xa0, 0xb5, 0x35, 0x04, 0x5e, 0xfc, 0x69, 0xd5, 0xbd, 0xcb, 0x4c, 0x18, 0xfb, + 0x8c, 0xcb, 0xf8, 0x86, 0x95, 0x0a, 0xcc, 0x02, 0xe3, 0xdc, 0xb7, 0xa5, 0x24, 0x97, 0x99, 0x30, 0x02, 0x92, 0xcb, + 0xf8, 0x86, 0x36, 0x65, 0x1c, 0xda, 0x8a, 0xce, 0x8b, 0xf3, 0xbb, 0x3b, 0xfc, 0x12, 0x43, 0xad, 0x8c, 0xfb, 0x7d, + 0x90, 0x98, 0x49, 0xdb, 0x94, 0x99, 0x8c, 0xa4, 0x46, 0x0b, 0xa9, 0x28, 0x1f, 0x4c, 0xc8, 0xee, 0x4a, 0xb5, 0x8c, + 0xa8, 0xfd, 0x2a, 0x14, 0xb3, 0x71, 0x34, 0x21, 0x74, 0xd2, 0xb1, 0xde, 0x4d, 0x6b, 0x21, 0xd3, 0xe8, 0x49, 0xe4, + 0xf9, 0x74, 0x16, 0xac, 0x9a, 0x16, 0x87, 0x8c, 0x4f, 0x8b, 0xc1, 0x80, 0x68, 0x97, 0xc2, 0x0d, 0xd6, 0x03, 0xa6, + 0x34, 0x2e, 0xde, 0x9a, 0x69, 0xf5, 0x0b, 0xa9, 0x42, 0xd2, 0x7b, 0x06, 0x24, 0x42, 0xba, 0x60, 0xb7, 0x20, 0x51, + 0xf4, 0xfc, 0xef, 0xd4, 0x16, 0xdc, 0xf5, 0x60, 0x6c, 0x46, 0xf7, 0xf5, 0x8c, 0xff, 0x50, 0xdb, 0x82, 0xa8, 0x4f, + 0x25, 0xeb, 0x75, 0x24, 0xaa, 0x90, 0x8b, 0xf0, 0xb3, 0xa3, 0x21, 0x86, 0xa8, 0xf6, 0x58, 0x20, 0xd6, 0x97, 0x67, + 0xbc, 0xc0, 0xe9, 0x67, 0xee, 0x72, 0x05, 0xdb, 0x82, 0x56, 0x86, 0x46, 0xbd, 0x8e, 0x5f, 0x47, 0xf6, 0xb2, 0xa0, + 0x8b, 0x7c, 0x86, 0x42, 0xd6, 0x3c, 0x0c, 0xab, 0x61, 0x7b, 0x10, 0xc9, 0x7e, 0x7b, 0x12, 0x1a, 0x8d, 0x81, 0x05, + 0xb2, 0x43, 0x23, 0x70, 0x11, 0x5a, 0xf9, 0xdb, 0x21, 0xb8, 0x70, 0x59, 0x44, 0x96, 0xa1, 0x8e, 0xdf, 0xd4, 0x6e, + 0x82, 0xea, 0x15, 0x3a, 0x4d, 0x61, 0x55, 0xca, 0x24, 0x1f, 0x7e, 0xbd, 0x90, 0x05, 0x66, 0xf2, 0xba, 0xec, 0xd1, + 0xd7, 0x76, 0x7b, 0x07, 0xa6, 0x60, 0xdd, 0x27, 0xef, 0xeb, 0x87, 0x9d, 0x3d, 0x01, 0xa3, 0x58, 0x95, 0xa3, 0x29, + 0xa4, 0xd4, 0x3e, 0x28, 0xf5, 0xc7, 0x70, 0x29, 0x34, 0xc7, 0x6e, 0x01, 0x93, 0x80, 0x7d, 0x86, 0x54, 0x8f, 0x69, + 0xc7, 0x3e, 0x47, 0x1b, 0x58, 0x12, 0x70, 0xf8, 0x47, 0x42, 0xd6, 0xfe, 0xd5, 0xbd, 0x4c, 0x9b, 0x21, 0x5b, 0xe6, + 0x0b, 0xe0, 0xf3, 0x61, 0xd7, 0x46, 0x25, 0xca, 0x26, 0x22, 0x49, 0x61, 0xcb, 0x63, 0x90, 0xf6, 0x28, 0xa6, 0xab, + 0x82, 0x27, 0x19, 0x4a, 0x29, 0x12, 0xed, 0x13, 0x9c, 0xc3, 0x1b, 0xdc, 0x8f, 0x2a, 0x20, 0xbc, 0x0a, 0x39, 0x1d, + 0xa5, 0x54, 0x5b, 0xc0, 0x28, 0xea, 0x01, 0xa2, 0xbc, 0x0c, 0xe4, 0x78, 0xdb, 0xed, 0x84, 0xae, 0xd8, 0x72, 0x38, + 0xa1, 0x48, 0x4a, 0x2e, 0xb0, 0xdc, 0x4b, 0xd0, 0x79, 0x9c, 0xb1, 0xde, 0x73, 0xc0, 0x22, 0x38, 0x85, 0xbf, 0x31, + 0xa1, 0x57, 0xf0, 0x37, 0x27, 0xf4, 0x15, 0x0b, 0x2f, 0x87, 0x17, 0x64, 0x3f, 0x4c, 0x07, 0x13, 0x25, 0x18, 0xbb, + 0x65, 0x69, 0x19, 0xaa, 0xc4, 0xd5, 0xfe, 0x39, 0x79, 0x78, 0x4e, 0x6f, 0xe8, 0x35, 0x3d, 0xa1, 0x6f, 0x80, 0xf0, + 0xdf, 0x1e, 0x4e, 0xf8, 0x70, 0xf2, 0xb8, 0xdf, 0xef, 0x9d, 0xf5, 0xfb, 0xbd, 0x53, 0x63, 0x40, 0xa1, 0x77, 0xd1, + 0x45, 0x4d, 0xf5, 0xaf, 0xcb, 0x7a, 0x31, 0x7d, 0xa3, 0x36, 0x6e, 0xc2, 0xb3, 0x3c, 0xbc, 0xdc, 0xbf, 0x25, 0x43, + 0x7c, 0x3c, 0xcf, 0xa5, 0x2c, 0xc2, 0x8b, 0xfd, 0x5b, 0x42, 0xdf, 0x1c, 0x81, 0xde, 0x14, 0xeb, 0x7b, 0xf3, 0xf0, + 0x56, 0xd7, 0x46, 0xe8, 0xf3, 0x30, 0x81, 0x6d, 0x72, 0xc3, 0xec, 0x5d, 0x7b, 0x32, 0x86, 0x58, 0x26, 0xb7, 0x5e, + 0x79, 0xb7, 0x0f, 0x6f, 0xc8, 0xfe, 0x0d, 0x78, 0x8a, 0x5a, 0xf2, 0x37, 0x0b, 0xaf, 0x59, 0xab, 0x86, 0x87, 0xb7, + 0xf4, 0xa4, 0xd5, 0x88, 0x87, 0xb7, 0x24, 0x0a, 0xaf, 0xd9, 0x05, 0x3d, 0x61, 0x97, 0x84, 0x9e, 0xf5, 0xfb, 0xa7, + 0xfd, 0xbe, 0xec, 0xf7, 0xbf, 0x8f, 0xc3, 0x30, 0x1e, 0x16, 0x64, 0x5f, 0xd2, 0xdb, 0xfd, 0x09, 0x7f, 0x44, 0x66, + 0xa1, 0x6e, 0xbe, 0x5a, 0x70, 0x56, 0xe5, 0xad, 0x72, 0xdd, 0x52, 0xb0, 0x56, 0xb8, 0x65, 0xea, 0xe9, 0x0d, 0xbd, + 0x66, 0x05, 0x3d, 0x61, 0x31, 0x89, 0xae, 0xa0, 0x15, 0x67, 0xb3, 0x22, 0xba, 0xa6, 0x27, 0xec, 0x74, 0x16, 0x47, + 0x27, 0xf4, 0x0d, 0xcb, 0x87, 0x13, 0xc8, 0x7b, 0x32, 0xbc, 0x26, 0xfb, 0x6f, 0x48, 0x14, 0xbe, 0xd1, 0xbf, 0x6f, + 0xe9, 0x05, 0x0f, 0xdf, 0x50, 0xaf, 0x9a, 0x37, 0xc4, 0x54, 0xdf, 0xa8, 0xfd, 0x0d, 0x89, 0xfc, 0xc1, 0x7c, 0x63, + 0xed, 0x69, 0x1e, 0x38, 0xda, 0xb8, 0x2e, 0xc3, 0x5b, 0x42, 0xd7, 0x65, 0x78, 0x4d, 0xc8, 0xb4, 0x39, 0x76, 0x30, + 0xa0, 0xb3, 0x07, 0x51, 0x42, 0xe8, 0xb5, 0x5f, 0xea, 0x35, 0x8e, 0xa1, 0x19, 0x21, 0x95, 0x76, 0x82, 0x69, 0xb8, + 0x0e, 0x9e, 0x69, 0xb0, 0x8e, 0xb3, 0x7e, 0x3f, 0x5c, 0xf7, 0xfb, 0x10, 0xe9, 0xbe, 0x98, 0x99, 0xd8, 0x6e, 0x8e, + 0x6c, 0xd2, 0x6b, 0xd0, 0xfe, 0x3f, 0x1b, 0x0c, 0xa0, 0x33, 0x5e, 0x49, 0xe1, 0xf5, 0xe0, 0xd9, 0xc3, 0x5b, 0xa2, + 0xea, 0x28, 0x68, 0x29, 0xc3, 0x82, 0xbe, 0xa2, 0x19, 0x00, 0x7e, 0x3d, 0x1b, 0x0c, 0x48, 0x64, 0x3e, 0x23, 0xd3, + 0x67, 0x87, 0x6f, 0xa6, 0x83, 0xc1, 0x33, 0xb3, 0x4d, 0x3e, 0xb1, 0x3b, 0x4a, 0x81, 0xf5, 0x77, 0xda, 0xef, 0x7f, + 0x3a, 0x8a, 0xc9, 0x59, 0xc1, 0xe3, 0x8f, 0xd3, 0x66, 0x5b, 0x3e, 0xb9, 0xa8, 0x6a, 0xa7, 0xfd, 0xfe, 0xba, 0xdf, + 0x3f, 0x01, 0xec, 0xa2, 0x99, 0xf3, 0xf5, 0x04, 0x69, 0xcb, 0xdc, 0x51, 0x24, 0x4d, 0x72, 0x68, 0x0c, 0x6d, 0x8b, + 0x55, 0xdb, 0x66, 0x1d, 0x19, 0x58, 0x1c, 0x35, 0x2b, 0x8a, 0x6b, 0x12, 0x85, 0xbd, 0xd3, 0xed, 0xf6, 0x84, 0x31, + 0x16, 0x13, 0x90, 0x7e, 0xf8, 0xaf, 0x4f, 0xea, 0x46, 0x0c, 0xb1, 0x52, 0x89, 0xef, 0x36, 0x4b, 0x7b, 0x08, 0x44, + 0x1c, 0x36, 0xfd, 0x3b, 0x73, 0x2f, 0x17, 0xb5, 0xe3, 0x5b, 0xff, 0x00, 0x10, 0x22, 0xc9, 0x42, 0x3e, 0xc3, 0x31, + 0x28, 0x33, 0x00, 0x32, 0x8f, 0xd4, 0xcc, 0x4b, 0x00, 0x01, 0x26, 0xdb, 0xed, 0x68, 0x3c, 0x9e, 0xd0, 0x82, 0x8d, + 0xfe, 0xf1, 0xe4, 0x61, 0xf5, 0x30, 0x0c, 0x82, 0x41, 0x46, 0x5a, 0x7a, 0x0a, 0xbb, 0x58, 0xab, 0x7d, 0x30, 0x82, + 0xd7, 0xec, 0xe3, 0x55, 0xf6, 0xc5, 0xec, 0x23, 0x12, 0xd6, 0x06, 0xe3, 0xc8, 0x45, 0xda, 0xd2, 0xdb, 0xdd, 0xc1, + 0x60, 0x72, 0x91, 0x7e, 0x86, 0xed, 0xf4, 0xf9, 0x37, 0x0f, 0xc6, 0x13, 0x0e, 0x46, 0x77, 0x51, 0xd0, 0x67, 0xda, + 0x76, 0x5b, 0xf9, 0x97, 0xc0, 0xd7, 0x98, 0x0a, 0x3a, 0x36, 0xcb, 0xc2, 0x0d, 0x2a, 0xa2, 0x8e, 0x96, 0x41, 0x55, + 0x2b, 0xdb, 0x39, 0xa0, 0x96, 0x58, 0x95, 0x89, 0x5b, 0x60, 0x18, 0x32, 0xd4, 0xe5, 0x1e, 0x57, 0x7f, 0xf0, 0x42, + 0x1a, 0xf8, 0x0c, 0x27, 0x22, 0xf4, 0xb8, 0x35, 0xee, 0x73, 0x6b, 0xe2, 0x33, 0xdc, 0x5a, 0x89, 0x24, 0xd6, 0xc0, + 0x92, 0x9a, 0xcb, 0x51, 0xc2, 0x8e, 0x4a, 0xc6, 0x67, 0x65, 0x94, 0xd0, 0x18, 0x1e, 0x24, 0x13, 0x33, 0x19, 0x25, + 0x68, 0x9f, 0xe8, 0x22, 0x0c, 0xfe, 0x0d, 0x98, 0xfd, 0x34, 0x87, 0xbf, 0x92, 0x4c, 0x93, 0x43, 0x08, 0x08, 0x71, + 0x38, 0x9e, 0xc5, 0xe1, 0x98, 0x44, 0xc9, 0x11, 0x3c, 0xc1, 0x7f, 0x45, 0x38, 0x26, 0xb5, 0xbe, 0xc3, 0x48, 0x75, + 0xb9, 0x4d, 0x18, 0xc0, 0x95, 0x8d, 0x67, 0x93, 0xc8, 0x4a, 0x77, 0xe5, 0xc3, 0xd1, 0xf8, 0x09, 0x99, 0xc6, 0xa1, + 0x1c, 0x24, 0x84, 0x82, 0x77, 0x6f, 0x58, 0x0e, 0x13, 0x0d, 0xcf, 0x06, 0x6c, 0x5e, 0xe9, 0xd8, 0x3c, 0x09, 0x27, + 0x20, 0x0c, 0x13, 0x72, 0xac, 0x77, 0x20, 0xa5, 0xe8, 0xf3, 0x1c, 0xfb, 0xa9, 0x8f, 0x20, 0xcc, 0x8e, 0x5a, 0x2a, + 0xbe, 0x02, 0xa0, 0x4b, 0x1c, 0x1c, 0x6a, 0xcf, 0x7c, 0x31, 0x0b, 0x4b, 0x8f, 0x4a, 0x99, 0xea, 0xf6, 0x45, 0x83, + 0xf2, 0x9b, 0x06, 0xed, 0x0b, 0x32, 0x98, 0xd0, 0xf2, 0x68, 0xc2, 0x1f, 0x41, 0x00, 0x8f, 0x46, 0xc4, 0x2f, 0x85, + 0x13, 0x03, 0xe1, 0x55, 0x90, 0x81, 0x4a, 0x6b, 0xd5, 0x98, 0x91, 0xad, 0x78, 0x0f, 0xc2, 0xa4, 0xec, 0x5d, 0xcb, + 0x75, 0x9e, 0x42, 0x54, 0xb0, 0x75, 0x5e, 0xed, 0x5d, 0x80, 0x25, 0x7b, 0x5c, 0x41, 0x9c, 0xb0, 0xf5, 0x0a, 0xb0, + 0x73, 0x1f, 0x6c, 0xca, 0x7a, 0x4f, 0x7d, 0xb7, 0x87, 0x2d, 0x87, 0x57, 0x95, 0xdc, 0x9b, 0x8c, 0xc7, 0xe3, 0xd1, + 0x9f, 0x70, 0x74, 0x00, 0xa1, 0x25, 0x91, 0xe1, 0x93, 0x01, 0x1a, 0x77, 0x5d, 0x71, 0x6f, 0x5c, 0x28, 0xca, 0x4a, + 0x27, 0x13, 0x02, 0xe2, 0x67, 0xd3, 0x37, 0xd8, 0x57, 0x5c, 0xc7, 0x3f, 0xd9, 0xfd, 0xc4, 0xac, 0x68, 0xb5, 0x52, + 0x47, 0x6f, 0xdf, 0x9c, 0xbc, 0x7c, 0xff, 0xf2, 0x97, 0xe7, 0xa7, 0x2f, 0x5f, 0xbf, 0x78, 0xf9, 0xfa, 0xe5, 0xfb, + 0xdf, 0xef, 0x61, 0xb0, 0x7d, 0x5b, 0x11, 0x3b, 0xf6, 0xde, 0x3d, 0xc6, 0xab, 0xc5, 0x17, 0xce, 0x1e, 0xb8, 0x5b, + 0x2c, 0xc0, 0x26, 0x18, 0x6e, 0x41, 0x50, 0xcd, 0x68, 0x54, 0xfa, 0x9e, 0x80, 0x8c, 0x46, 0x85, 0x6c, 0x3c, 0xac, + 0xd8, 0x0a, 0xb9, 0x78, 0xc7, 0x70, 0xf0, 0x91, 0xfd, 0xad, 0x38, 0x13, 0x6e, 0x47, 0x5b, 0xb3, 0x22, 0xe0, 0xf3, + 0xb5, 0x16, 0x95, 0xc7, 0x85, 0xa8, 0xbd, 0x6d, 0x9f, 0x43, 0x42, 0x3d, 0x22, 0xd7, 0xc1, 0xfb, 0x36, 0xc8, 0x1e, + 0x1f, 0x79, 0x4f, 0xca, 0x33, 0xd4, 0xe7, 0x68, 0xf8, 0xa8, 0xf1, 0x8c, 0x4e, 0xcc, 0xb5, 0xd1, 0xa1, 0x9e, 0x16, + 0xb0, 0xbf, 0x95, 0x18, 0x9b, 0x16, 0xac, 0x4c, 0x11, 0xeb, 0xc3, 0xe9, 0x7e, 0x77, 0x6f, 0x46, 0x3f, 0xc3, 0xf1, + 0xa3, 0x54, 0x13, 0x48, 0x8b, 0x02, 0xa5, 0x2b, 0x43, 0x6e, 0x7b, 0x16, 0x16, 0xe6, 0x67, 0xd8, 0x20, 0x80, 0xf6, + 0xb2, 0x63, 0x49, 0xa0, 0x59, 0xbc, 0xd6, 0xf5, 0xcf, 0xcb, 0x97, 0x89, 0x76, 0xbe, 0xf8, 0x06, 0x42, 0x0c, 0xfb, + 0x57, 0x84, 0xc6, 0x84, 0xbb, 0x49, 0x76, 0x97, 0x16, 0x73, 0xaf, 0xba, 0x8c, 0xf1, 0xb8, 0xbb, 0xe3, 0x4a, 0xd1, + 0xbc, 0x75, 0x81, 0x3d, 0x50, 0xf3, 0x3a, 0x5e, 0xb2, 0x10, 0xb0, 0x19, 0xf7, 0xed, 0x22, 0x71, 0x7e, 0xef, 0x74, + 0x42, 0xf6, 0x0f, 0xa6, 0x7c, 0xc8, 0x4a, 0x2a, 0x06, 0xac, 0xac, 0x77, 0xa8, 0x39, 0x6f, 0x13, 0x72, 0xb1, 0x4b, + 0xc3, 0xc5, 0x90, 0xdf, 0x77, 0x49, 0x7a, 0xcf, 0x1b, 0x0e, 0xd5, 0xb6, 0xb9, 0x18, 0xd2, 0x94, 0xd3, 0x5d, 0x2a, + 0x03, 0x42, 0xa4, 0xcb, 0xb8, 0x22, 0xb5, 0x3e, 0xaa, 0x52, 0x27, 0xe9, 0xb8, 0xca, 0x36, 0x9f, 0xb9, 0x64, 0xab, + 0xdb, 0xb5, 0x7f, 0xad, 0x6e, 0x5f, 0x98, 0x81, 0xfc, 0xfd, 0x85, 0xa8, 0x26, 0x06, 0xa2, 0x0b, 0xa8, 0xe0, 0x5f, + 0xe0, 0xe5, 0xc9, 0x23, 0xad, 0x00, 0xbd, 0xeb, 0xec, 0xe8, 0xda, 0xe3, 0x8d, 0x59, 0x6c, 0x2d, 0x71, 0xce, 0x2a, + 0xdf, 0x59, 0x5e, 0x95, 0xad, 0xd0, 0x75, 0x04, 0xfb, 0x23, 0xec, 0xe8, 0xbb, 0xb7, 0x0d, 0x80, 0x28, 0x85, 0x95, + 0x3b, 0xfb, 0x85, 0x77, 0xf6, 0x0b, 0x7b, 0xf6, 0xdb, 0x4d, 0xa0, 0x7c, 0x58, 0xa1, 0x65, 0x2f, 0xa4, 0xa8, 0x4c, + 0x93, 0xc7, 0x4d, 0x5d, 0x16, 0xd2, 0x62, 0xbe, 0x6f, 0x69, 0xd7, 0xe3, 0x31, 0x95, 0xa8, 0x1e, 0xf9, 0x01, 0x5b, + 0xb5, 0x5f, 0x92, 0xfb, 0xef, 0x99, 0xff, 0xb3, 0x37, 0xc8, 0xbb, 0xee, 0x76, 0xff, 0x37, 0x17, 0x3a, 0xb8, 0xad, + 0xa5, 0xc2, 0x53, 0x57, 0xc7, 0x05, 0xde, 0xd5, 0xd2, 0xfb, 0xef, 0x6a, 0x6f, 0x33, 0xbd, 0xec, 0x2a, 0x40, 0x0d, + 0x12, 0xeb, 0x4b, 0x5e, 0x64, 0x49, 0x6d, 0x15, 0x1a, 0x6f, 0x38, 0x84, 0xf6, 0xf0, 0x0e, 0x2e, 0x90, 0xc3, 0x12, + 0x42, 0x3f, 0x56, 0x46, 0x00, 0xe8, 0xb3, 0xd8, 0x6f, 0x78, 0x98, 0x91, 0x81, 0x2f, 0xf1, 0x93, 0xd2, 0x17, 0x17, + 0xef, 0xef, 0x64, 0x26, 0xe8, 0x55, 0xe2, 0xa2, 0xe6, 0xca, 0x76, 0xcc, 0x0f, 0xff, 0x0b, 0x8c, 0x06, 0xe1, 0xb5, + 0x25, 0xdb, 0x17, 0x1d, 0xb3, 0x5c, 0xc1, 0x51, 0x5b, 0xba, 0x32, 0x65, 0xeb, 0xfa, 0x59, 0x0d, 0x33, 0x7d, 0xa6, + 0xbc, 0x01, 0xd9, 0x17, 0x72, 0xf7, 0x53, 0x5d, 0xb1, 0x20, 0x47, 0x93, 0xf1, 0x94, 0x88, 0xc1, 0xa0, 0x95, 0x7c, + 0x88, 0xc9, 0xc3, 0xe1, 0x0e, 0x73, 0x29, 0x74, 0x3f, 0xbc, 0x3e, 0x40, 0x7d, 0x8d, 0x2d, 0x49, 0x36, 0x15, 0xfb, + 0x1b, 0xcc, 0x62, 0x81, 0x38, 0x3a, 0xf8, 0xc5, 0xf9, 0x02, 0x40, 0x96, 0x61, 0x99, 0x69, 0x61, 0x91, 0x4c, 0x95, + 0x8f, 0x6c, 0xc1, 0xe4, 0xe1, 0x78, 0xe6, 0xf7, 0xdc, 0x31, 0x38, 0x84, 0x44, 0x13, 0x6b, 0xfc, 0xe2, 0x67, 0xc1, + 0x38, 0x0e, 0xe5, 0x91, 0x6c, 0x7c, 0x57, 0x92, 0x68, 0x6c, 0x4c, 0x95, 0xf5, 0x55, 0xa2, 0x1a, 0x26, 0xe4, 0x61, + 0x41, 0xf6, 0x0b, 0xba, 0xf4, 0xc7, 0x12, 0xd3, 0xf7, 0xe3, 0xfd, 0xc9, 0x98, 0x3c, 0x8c, 0x1f, 0x4e, 0x0c, 0xdc, + 0xb0, 0x9f, 0x23, 0x1f, 0x2e, 0xc9, 0x7e, 0xb3, 0x4a, 0x30, 0x45, 0x35, 0x3d, 0xf3, 0x2b, 0x49, 0x06, 0xcb, 0x41, + 0xfa, 0xb0, 0x95, 0x17, 0x6b, 0xd5, 0xe3, 0xbd, 0x3e, 0xe4, 0x53, 0x22, 0x1a, 0x37, 0x86, 0x35, 0xbd, 0x8c, 0xff, + 0x92, 0x45, 0x24, 0x25, 0x20, 0x12, 0x82, 0x7a, 0x3b, 0x3b, 0xcf, 0x92, 0x58, 0xa4, 0x51, 0x5a, 0x13, 0x9a, 0x1e, + 0xb1, 0xc9, 0x78, 0x96, 0xb2, 0xf4, 0x70, 0xf2, 0x64, 0x36, 0x79, 0x12, 0x1d, 0x8c, 0xa3, 0x74, 0x30, 0x80, 0xe4, + 0x83, 0x31, 0xb8, 0xd8, 0xc1, 0x6f, 0x76, 0x00, 0x43, 0x77, 0x84, 0x2c, 0x61, 0x01, 0x4d, 0xfb, 0xb2, 0x26, 0xe9, + 0xe1, 0x3c, 0x57, 0x3d, 0x89, 0x6f, 0xe8, 0xda, 0x73, 0x70, 0xf1, 0x5b, 0x78, 0xee, 0x5a, 0x78, 0xbe, 0xdb, 0x42, + 0xa1, 0xc9, 0x76, 0x2c, 0xff, 0x7f, 0xdc, 0x30, 0xee, 0xba, 0x4b, 0x98, 0xc5, 0x75, 0x95, 0x8d, 0x56, 0x85, 0xac, + 0x24, 0xdc, 0x26, 0x94, 0x28, 0x6c, 0x14, 0xaf, 0x56, 0xb9, 0x76, 0x11, 0x9b, 0x57, 0x14, 0xc0, 0x5d, 0x20, 0x4e, + 0x31, 0xb0, 0xd0, 0xc6, 0x40, 0xee, 0x13, 0x2f, 0x24, 0xb3, 0x6a, 0x1f, 0x73, 0x8f, 0xfc, 0x2b, 0x04, 0x63, 0x54, + 0x71, 0x34, 0x9e, 0x29, 0xac, 0x8b, 0xcf, 0xc9, 0x7b, 0xff, 0x8d, 0xa3, 0xc8, 0x1e, 0xcd, 0xa0, 0x27, 0x88, 0x9c, + 0x47, 0x9c, 0x3d, 0x99, 0xbc, 0x0c, 0xdc, 0xcf, 0x60, 0xa5, 0xbf, 0xee, 0x36, 0x63, 0x6d, 0x7b, 0x74, 0x2f, 0x8c, + 0x50, 0xf4, 0x13, 0xbe, 0x33, 0xf5, 0x02, 0x2e, 0xa1, 0x1a, 0xd8, 0xf5, 0xc5, 0x05, 0x2f, 0x01, 0x44, 0x28, 0x13, + 0xfd, 0x7e, 0xef, 0x2f, 0x03, 0x4d, 0x5a, 0xf2, 0xe2, 0x55, 0x26, 0xac, 0x33, 0x0e, 0x34, 0x15, 0xa8, 0xff, 0xc7, + 0xca, 0x3e, 0xd3, 0x31, 0x99, 0xf9, 0x8f, 0xc3, 0x09, 0x89, 0x9a, 0xaf, 0xc9, 0x67, 0x4e, 0xd3, 0xcf, 0x5c, 0xd1, + 0xfe, 0x03, 0x99, 0xb9, 0xe1, 0x90, 0xa1, 0xfe, 0xd2, 0x31, 0x4f, 0x46, 0xaf, 0x13, 0xb3, 0x23, 0xc1, 0xaa, 0x19, + 0x44, 0x61, 0x2f, 0xe0, 0x41, 0x5d, 0xcb, 0xe2, 0x29, 0xcc, 0x3e, 0xa8, 0x11, 0xc5, 0x21, 0x1b, 0xcf, 0x42, 0x19, + 0x4e, 0xc0, 0xbe, 0x77, 0x32, 0x86, 0xfb, 0x80, 0x0c, 0x3f, 0x56, 0x21, 0x76, 0x0e, 0xd2, 0x3e, 0x56, 0xa8, 0x98, + 0x00, 0x88, 0x40, 0xc8, 0xdb, 0xef, 0x4b, 0x95, 0x84, 0xaf, 0x4b, 0x4c, 0x29, 0xd4, 0x07, 0xff, 0x89, 0x54, 0xdd, + 0x31, 0xfd, 0x6a, 0xfd, 0xf8, 0x33, 0xa1, 0xf8, 0x74, 0x97, 0x12, 0xdf, 0x40, 0x70, 0xe7, 0x02, 0x74, 0x10, 0x15, + 0x9a, 0xb1, 0xdd, 0xcf, 0xef, 0x8a, 0xbb, 0xf9, 0x5d, 0xf1, 0xff, 0x8e, 0xdf, 0x15, 0xf7, 0x31, 0x86, 0x95, 0x85, + 0x86, 0x9f, 0x05, 0xe3, 0x20, 0xfa, 0xcf, 0xf9, 0xc4, 0x3b, 0x79, 0xea, 0xcb, 0x4c, 0x4c, 0xef, 0x60, 0x9a, 0x7d, + 0x82, 0x82, 0xb0, 0x8a, 0xbb, 0xf4, 0x64, 0x5d, 0xd9, 0x5b, 0x2b, 0x19, 0x62, 0x9e, 0x7b, 0x58, 0xa3, 0xb0, 0xf2, + 0x80, 0xee, 0x51, 0xb5, 0x41, 0x9c, 0x08, 0x1e, 0xc6, 0xcc, 0x4a, 0xdf, 0xb7, 0x5b, 0xa3, 0xc2, 0xbc, 0x97, 0x8b, + 0x82, 0xec, 0xe6, 0xe3, 0xd9, 0x38, 0x0a, 0xb1, 0x01, 0xff, 0x31, 0x63, 0xd5, 0x90, 0xcd, 0x77, 0x32, 0x52, 0x3b, + 0x26, 0x4f, 0x93, 0x5d, 0xd2, 0x3b, 0xe0, 0x1d, 0xf2, 0xf3, 0xfa, 0x63, 0x18, 0x4b, 0xc3, 0x6f, 0xc9, 0x8b, 0xb8, + 0xc8, 0xaa, 0xe5, 0x65, 0x96, 0x20, 0xd3, 0x05, 0x2f, 0xbe, 0x98, 0xe9, 0xf2, 0x3e, 0xd6, 0x07, 0x8c, 0xa7, 0x14, + 0xaf, 0x1b, 0xa2, 0xf4, 0x75, 0xcb, 0xb3, 0x42, 0x5d, 0x9e, 0x54, 0xcc, 0xf6, 0xac, 0x04, 0xa7, 0x53, 0x30, 0xc1, + 0xd7, 0x3f, 0x5d, 0xef, 0x13, 0xc0, 0x05, 0x85, 0x9a, 0xd3, 0x42, 0xae, 0x0c, 0x96, 0x93, 0x85, 0xee, 0x04, 0xcc, + 0x50, 0x29, 0xf0, 0x02, 0x05, 0x7f, 0xd1, 0xc0, 0x88, 0xbe, 0x70, 0xbf, 0xc9, 0xc0, 0x20, 0x5d, 0x9a, 0x13, 0x61, + 0xec, 0xb8, 0x9d, 0x38, 0x6d, 0x45, 0x39, 0xe3, 0xec, 0x9d, 0xba, 0x52, 0x80, 0x01, 0xde, 0xe6, 0x3a, 0x3a, 0x4d, + 0xd0, 0x6b, 0x41, 0xe9, 0xbc, 0x81, 0xbb, 0x59, 0x46, 0x46, 0xb8, 0xf8, 0xb0, 0xf2, 0x58, 0x70, 0xcf, 0x7e, 0x21, + 0xb1, 0xb6, 0x7e, 0x60, 0xcc, 0xe6, 0x05, 0x0b, 0x14, 0x2a, 0x50, 0x60, 0x39, 0xd3, 0x96, 0xa6, 0xd5, 0x90, 0xef, + 0x1f, 0xa0, 0xb5, 0x69, 0x35, 0xe0, 0xfb, 0x07, 0x75, 0x94, 0x1d, 0x42, 0x96, 0x23, 0x3f, 0x83, 0x7a, 0x5d, 0x47, + 0x26, 0xc5, 0x64, 0xf7, 0xeb, 0x4b, 0xfd, 0x51, 0xdd, 0x80, 0xeb, 0x07, 0x20, 0x80, 0x0d, 0xc0, 0x21, 0x50, 0x0d, + 0x96, 0x46, 0x04, 0x8b, 0x32, 0x85, 0xf6, 0x35, 0xf4, 0xde, 0x68, 0xf8, 0x2f, 0x70, 0x17, 0x91, 0x2b, 0xff, 0x13, + 0x04, 0xfe, 0x8a, 0x32, 0xad, 0x4c, 0xf1, 0x3f, 0xd1, 0xea, 0x15, 0xca, 0x59, 0xd3, 0x9a, 0x0f, 0xa2, 0x35, 0x11, + 0xaa, 0x19, 0x43, 0xf0, 0x6f, 0x65, 0x99, 0xb6, 0x54, 0x55, 0xea, 0x43, 0xe3, 0xb5, 0x56, 0x38, 0xcb, 0xc7, 0x91, + 0xf7, 0x1a, 0x43, 0xc7, 0x26, 0xce, 0x52, 0x4e, 0xa5, 0xce, 0x5e, 0xef, 0xcb, 0xc8, 0x01, 0x4e, 0x27, 0x6c, 0x3c, + 0x4d, 0x0e, 0xe5, 0x34, 0x71, 0x90, 0xf9, 0x39, 0xc3, 0xc8, 0xaa, 0x06, 0x84, 0x45, 0xd9, 0x50, 0xda, 0x02, 0x4c, + 0x72, 0x42, 0xc8, 0x14, 0x43, 0x51, 0xe4, 0x23, 0xdd, 0x0f, 0xeb, 0xcd, 0xea, 0xbe, 0x78, 0xab, 0x01, 0x4e, 0xc3, + 0x04, 0x02, 0x81, 0x17, 0xf1, 0x75, 0x26, 0x2e, 0xc0, 0x63, 0x78, 0x00, 0x5f, 0x82, 0x9b, 0x5c, 0xca, 0x7e, 0xab, + 0xc2, 0x1c, 0xd7, 0x16, 0x30, 0x68, 0xb0, 0x7a, 0x10, 0x1d, 0x2e, 0xa5, 0xcd, 0xae, 0x02, 0xc4, 0xc6, 0x14, 0x62, + 0x59, 0xb0, 0xb5, 0x65, 0xcf, 0x7e, 0x56, 0x4d, 0x43, 0xeb, 0x84, 0x63, 0x71, 0x91, 0x43, 0x14, 0x95, 0x41, 0x0c, + 0xee, 0x48, 0x1e, 0x9f, 0xf7, 0x40, 0x84, 0xe7, 0x04, 0xdc, 0xca, 0x12, 0x19, 0xae, 0xe8, 0x72, 0x74, 0x43, 0xd7, + 0xa3, 0x6b, 0x3a, 0xa6, 0x93, 0x7f, 0x8e, 0xd1, 0x22, 0x5b, 0xa5, 0xde, 0xd2, 0xf5, 0x68, 0x49, 0xbf, 0x1d, 0xd3, + 0x83, 0x7f, 0x8c, 0xc9, 0x34, 0xc7, 0xc3, 0x84, 0x9e, 0x83, 0x63, 0x17, 0xa9, 0xd1, 0x53, 0xd3, 0x37, 0x38, 0xac, + 0x46, 0xf9, 0x90, 0x8f, 0x72, 0xca, 0x47, 0xc5, 0xb0, 0x1a, 0x81, 0xa7, 0x63, 0x35, 0xe4, 0xa3, 0x8a, 0xf2, 0xd1, + 0xd9, 0xb0, 0x1a, 0x9d, 0x91, 0x66, 0xd3, 0x5f, 0x56, 0xfc, 0xb2, 0x64, 0x6b, 0xd8, 0x16, 0xb0, 0x7c, 0xdd, 0x2a, + 0xcb, 0x53, 0x7f, 0x55, 0x9b, 0x93, 0xd9, 0x72, 0xf6, 0xf6, 0xba, 0xcb, 0x89, 0xc5, 0xe3, 0xb6, 0xe9, 0x70, 0xf5, + 0xe5, 0x44, 0x9d, 0xf4, 0x0a, 0xf9, 0x61, 0x3c, 0x15, 0xea, 0x1c, 0x02, 0x33, 0x89, 0x59, 0x18, 0x33, 0x6c, 0xa6, + 0x4e, 0x03, 0x05, 0x4e, 0x36, 0xf2, 0x5c, 0x14, 0xb3, 0x51, 0x4e, 0xe1, 0x7d, 0x4c, 0x48, 0x24, 0xe0, 0xac, 0x3a, + 0xaa, 0x46, 0x05, 0xc4, 0x1c, 0x61, 0x21, 0x3e, 0x42, 0xbf, 0xd4, 0x47, 0x1e, 0x12, 0x78, 0x86, 0x7d, 0x2d, 0x06, + 0x31, 0x1c, 0xf1, 0xb6, 0xb2, 0x6a, 0x16, 0x26, 0x50, 0x59, 0x35, 0x2c, 0x4d, 0x65, 0x05, 0xcd, 0x46, 0x95, 0x5f, + 0x59, 0x85, 0x63, 0x94, 0x10, 0x12, 0x95, 0xba, 0x32, 0x50, 0x9f, 0x24, 0x2c, 0x2c, 0x75, 0x65, 0x67, 0xea, 0xa3, + 0x33, 0xbf, 0xb2, 0x33, 0x70, 0x21, 0x1d, 0x24, 0xfe, 0x55, 0x6a, 0x99, 0xb6, 0xaf, 0x83, 0x8d, 0x55, 0x45, 0x37, + 0xfc, 0xa6, 0x2a, 0xe2, 0xa8, 0xa4, 0x2e, 0x06, 0x34, 0x2e, 0x8c, 0x48, 0x52, 0xbd, 0x46, 0xc1, 0x1f, 0x12, 0x44, + 0xa5, 0x31, 0x78, 0x75, 0x26, 0x5d, 0x2b, 0xb5, 0xa2, 0x62, 0x50, 0x0e, 0x0a, 0xb8, 0x3f, 0xe5, 0xad, 0x85, 0xf4, + 0x33, 0x44, 0x54, 0x86, 0xf2, 0x06, 0x1f, 0x30, 0x78, 0x32, 0xbb, 0x48, 0xc3, 0x64, 0x74, 0x4b, 0xe3, 0xd1, 0x12, + 0xe1, 0x60, 0xd8, 0x79, 0xaa, 0xf0, 0xd6, 0x57, 0x90, 0x7e, 0x43, 0xe3, 0xd1, 0x35, 0x4d, 0xad, 0xcd, 0xa9, 0x81, + 0xba, 0xea, 0x8d, 0xe9, 0x4d, 0x04, 0xaf, 0x6f, 0xa3, 0x25, 0x85, 0xad, 0x74, 0x9c, 0x67, 0x17, 0x22, 0x4a, 0x29, + 0x22, 0x10, 0xae, 0x11, 0x39, 0x70, 0xa9, 0xd1, 0x06, 0xd7, 0x03, 0x28, 0x43, 0xc3, 0x05, 0x2e, 0x07, 0xf1, 0x68, + 0xe9, 0x91, 0xa9, 0x54, 0x5f, 0x64, 0x11, 0x3e, 0xda, 0xd9, 0x68, 0x29, 0x9e, 0x11, 0x0b, 0xe3, 0x0a, 0x86, 0x50, + 0x17, 0x56, 0x9a, 0x82, 0xa4, 0x0b, 0x1c, 0xd9, 0x0b, 0xe3, 0x2a, 0xdc, 0x80, 0x69, 0xd1, 0x2d, 0x98, 0x47, 0x81, + 0xc2, 0xc1, 0x25, 0x48, 0x3f, 0xa1, 0x6c, 0xe7, 0x28, 0x4d, 0x0e, 0x6f, 0x82, 0xd6, 0x3b, 0x13, 0x84, 0xb4, 0xab, + 0x9b, 0x6c, 0x49, 0xdf, 0x60, 0x7b, 0x87, 0x4e, 0x45, 0x05, 0xd5, 0xe7, 0x16, 0x4c, 0x96, 0x6c, 0x10, 0xb6, 0x84, + 0xe9, 0x99, 0x5e, 0x03, 0xf6, 0xf4, 0xfe, 0xc1, 0xce, 0x7c, 0x17, 0xb3, 0xd7, 0xfb, 0x65, 0x34, 0x56, 0x16, 0xbc, + 0xb9, 0x25, 0x76, 0x4b, 0x36, 0x9e, 0x2e, 0x0f, 0xcb, 0xe9, 0x12, 0x89, 0x9d, 0xa1, 0x5b, 0x8c, 0xcf, 0x97, 0x0b, + 0x9a, 0xe0, 0xd9, 0xc6, 0xaa, 0xf9, 0xd2, 0xa0, 0xa5, 0xa4, 0x0c, 0xd7, 0xdb, 0x12, 0xfd, 0xff, 0xd5, 0xc5, 0x2f, + 0x05, 0x78, 0x09, 0xc6, 0x02, 0x40, 0xb8, 0x07, 0xd3, 0x82, 0xd4, 0x46, 0xd9, 0x48, 0xd3, 0x30, 0xc5, 0x45, 0x60, + 0x52, 0xfa, 0xfd, 0x30, 0x67, 0x29, 0xf1, 0xa0, 0x43, 0xed, 0x28, 0x9d, 0xa7, 0xbe, 0x10, 0x04, 0x78, 0x24, 0x75, + 0x8e, 0x4d, 0xfe, 0x39, 0x9e, 0x05, 0x6a, 0x20, 0x82, 0x28, 0x3b, 0xc4, 0x47, 0x0c, 0x5c, 0x14, 0xe9, 0xb8, 0x9d, + 0xae, 0x88, 0xd5, 0xee, 0x31, 0x0b, 0x71, 0x92, 0x30, 0xd7, 0x2c, 0x1b, 0xb2, 0x2a, 0xc2, 0x04, 0x5d, 0x18, 0xd8, + 0xaf, 0x0d, 0x59, 0xb5, 0x7f, 0x00, 0x91, 0x5a, 0x6d, 0x19, 0x17, 0x5d, 0x65, 0x7c, 0x0b, 0x40, 0xd6, 0x8c, 0xb1, + 0x83, 0x7f, 0x8c, 0x67, 0xea, 0x9b, 0x28, 0xe4, 0x47, 0x07, 0xff, 0x80, 0xe4, 0xc3, 0x6f, 0x91, 0x99, 0x83, 0xe4, + 0x46, 0x41, 0x97, 0xcd, 0x59, 0xd7, 0x50, 0x9a, 0xb8, 0xf6, 0x4a, 0xbd, 0xf6, 0xa4, 0x59, 0x7b, 0x05, 0xba, 0x53, + 0x1b, 0xde, 0x43, 0xd9, 0xce, 0x82, 0x09, 0x3a, 0x9a, 0xdd, 0x81, 0x0e, 0xde, 0x29, 0x82, 0x5e, 0x26, 0xa1, 0xf1, + 0x08, 0x55, 0x46, 0xbd, 0x18, 0x0f, 0xaa, 0x93, 0x75, 0xc9, 0x3c, 0x03, 0xe6, 0xd8, 0x9e, 0x43, 0x62, 0x98, 0xab, + 0x83, 0x3a, 0x65, 0xe5, 0x30, 0xc7, 0x03, 0x78, 0xcd, 0xe4, 0x50, 0x0c, 0x72, 0x8d, 0xf2, 0x7d, 0xce, 0x8a, 0x61, + 0x39, 0xc8, 0x35, 0x37, 0x33, 0x6d, 0xc6, 0xa6, 0x4d, 0x74, 0x78, 0xe6, 0x15, 0x3b, 0x5a, 0xf5, 0x80, 0x8f, 0x05, + 0x4f, 0x66, 0xdf, 0xf3, 0xf1, 0x29, 0x70, 0x32, 0x9b, 0x9b, 0x68, 0x49, 0x6f, 0xa3, 0x94, 0x5e, 0x47, 0x6b, 0xba, + 0x8c, 0xce, 0x8d, 0x89, 0x71, 0x52, 0xc3, 0x39, 0x00, 0xad, 0x02, 0x48, 0x3c, 0xf5, 0xeb, 0x1d, 0x4f, 0xaa, 0x70, + 0x49, 0x53, 0x70, 0x1b, 0xf6, 0xed, 0x33, 0xcf, 0x7c, 0x89, 0xd4, 0x06, 0x31, 0xd6, 0xac, 0xa1, 0xe2, 0xc6, 0x5b, + 0xf7, 0x91, 0xa8, 0x61, 0xe7, 0xba, 0xd8, 0x44, 0xd5, 0x70, 0x32, 0x2d, 0x01, 0xb1, 0xb5, 0x1c, 0x0e, 0xdd, 0x11, + 0xb2, 0x7b, 0xfc, 0xe8, 0x40, 0xcf, 0x3d, 0x69, 0xb1, 0x6d, 0x5b, 0xfe, 0xc0, 0x10, 0xa6, 0xf4, 0xf3, 0x47, 0x3e, + 0x20, 0x56, 0x5c, 0xc2, 0xd9, 0x08, 0xd4, 0xd1, 0x0a, 0x9d, 0x7e, 0xab, 0xc2, 0x42, 0x1f, 0xe0, 0x9b, 0x9b, 0x28, + 0xa1, 0xb7, 0x51, 0xee, 0x91, 0xb5, 0x65, 0xcd, 0xe4, 0xf4, 0x34, 0x0b, 0x79, 0xfb, 0x40, 0x2f, 0x17, 0x00, 0xa2, + 0x35, 0x88, 0x7d, 0xa9, 0xeb, 0x01, 0x38, 0x0d, 0xa1, 0x49, 0x68, 0x04, 0x57, 0x15, 0x84, 0x11, 0x70, 0x25, 0xe1, + 0x6f, 0x30, 0x51, 0x81, 0x2f, 0xc0, 0x45, 0x26, 0x4d, 0x73, 0x1e, 0xd4, 0xfe, 0x48, 0xbe, 0x2a, 0xda, 0xde, 0xae, + 0x30, 0x9a, 0x60, 0xec, 0x89, 0xf6, 0x79, 0xa4, 0x1c, 0xc5, 0x45, 0x12, 0x66, 0xa3, 0x1b, 0x75, 0x9e, 0xd3, 0x6c, + 0x74, 0xab, 0x7f, 0x55, 0x74, 0x4c, 0x7f, 0xd1, 0x01, 0x6d, 0x94, 0xf4, 0xad, 0xe3, 0x6c, 0x40, 0xeb, 0xc5, 0xd2, + 0xf8, 0x5f, 0xcb, 0xd1, 0x0d, 0x95, 0xa3, 0x5b, 0xdf, 0x92, 0x6a, 0x32, 0x2d, 0x0e, 0x05, 0x1a, 0x52, 0x75, 0x7e, + 0x5f, 0x00, 0x3f, 0x57, 0x1a, 0xdf, 0x69, 0xf3, 0xbd, 0xd7, 0xfe, 0xd3, 0x4e, 0x9e, 0x40, 0xb1, 0x44, 0x05, 0xab, + 0x46, 0x60, 0xc7, 0xbe, 0xce, 0xe3, 0xc2, 0x8c, 0x52, 0x4c, 0xad, 0x49, 0x3f, 0x06, 0xae, 0x98, 0xf6, 0x0a, 0x70, + 0xb5, 0x04, 0x27, 0x01, 0x88, 0xa1, 0x09, 0x7b, 0x76, 0x0c, 0x51, 0xcf, 0x8d, 0x63, 0x94, 0x6c, 0xb8, 0x07, 0xc4, + 0x5a, 0xe6, 0xad, 0x5c, 0x02, 0x12, 0x78, 0xeb, 0x61, 0x52, 0x00, 0xc6, 0x60, 0xb9, 0x24, 0x3a, 0x8f, 0x87, 0x3e, + 0xa1, 0x5e, 0x68, 0xd4, 0x09, 0xd9, 0xd8, 0x12, 0x38, 0xfe, 0xb0, 0x3e, 0x04, 0x82, 0x57, 0x79, 0xae, 0xbf, 0xd2, + 0xba, 0xfe, 0x52, 0xe9, 0xb9, 0x63, 0xb9, 0xae, 0xdf, 0xb6, 0xa9, 0xd1, 0x0b, 0xb0, 0xf0, 0xdd, 0x28, 0xf3, 0x48, + 0x6e, 0x11, 0x52, 0x15, 0x58, 0xa9, 0x5b, 0x48, 0x30, 0xff, 0x4a, 0xce, 0x56, 0x65, 0xbe, 0x7a, 0xe4, 0x5e, 0x39, + 0x9b, 0x9e, 0xfe, 0x86, 0x04, 0xed, 0xb6, 0x23, 0xcd, 0xe3, 0x2d, 0x3a, 0x7c, 0x76, 0xad, 0x25, 0xe6, 0x4e, 0xa2, + 0xe2, 0xf9, 0x14, 0xb0, 0xd5, 0xb3, 0xec, 0x52, 0xf9, 0x58, 0xed, 0xe2, 0xf8, 0x99, 0xf3, 0x27, 0xa9, 0xc2, 0xb5, + 0x68, 0x28, 0x41, 0xc0, 0x9b, 0xc3, 0xd8, 0x15, 0xaa, 0x80, 0x86, 0xe6, 0x06, 0x8e, 0x73, 0x35, 0xac, 0x34, 0x01, + 0xd3, 0x52, 0x1e, 0x1d, 0xe0, 0xd0, 0xe4, 0x51, 0xbb, 0x69, 0x58, 0x19, 0xba, 0xd6, 0xe8, 0x73, 0x5b, 0xe9, 0x8c, + 0x37, 0x1b, 0xbe, 0x7f, 0x30, 0xa8, 0xf0, 0x27, 0x69, 0x8e, 0x46, 0x3b, 0x37, 0xdc, 0x69, 0x04, 0x66, 0xae, 0xe4, + 0x8a, 0xec, 0x8e, 0x92, 0x97, 0xdf, 0xd3, 0x0b, 0x0b, 0xe8, 0xcf, 0x7f, 0x2e, 0x26, 0x9c, 0xb4, 0xc4, 0x84, 0x68, + 0xe9, 0xa0, 0x45, 0x07, 0x3b, 0xca, 0x2b, 0xfb, 0x12, 0x2f, 0x9d, 0xe3, 0x7f, 0x5f, 0x8f, 0xb5, 0xab, 0x40, 0x68, + 0x75, 0x72, 0xbf, 0x3d, 0x59, 0x20, 0x6a, 0x40, 0x35, 0xbb, 0x2a, 0x47, 0x99, 0x76, 0x56, 0x64, 0xd3, 0x90, 0xb9, + 0xee, 0x66, 0x69, 0xd8, 0x4c, 0x76, 0x2c, 0x2c, 0x33, 0x0c, 0xd6, 0x4e, 0x15, 0x7d, 0x0e, 0x5a, 0x7e, 0x04, 0x2f, + 0x9b, 0xca, 0x33, 0x9f, 0xcd, 0x32, 0xe2, 0x05, 0x3a, 0xe7, 0x54, 0x2c, 0x9a, 0xd2, 0xb1, 0x72, 0xbb, 0x2d, 0xd1, + 0x58, 0xa2, 0x8c, 0x82, 0xa0, 0xb6, 0x41, 0xd8, 0x75, 0xe9, 0x9e, 0xf4, 0x69, 0x17, 0x9f, 0x56, 0xa0, 0xef, 0xf1, + 0x5d, 0x06, 0x12, 0x53, 0x4f, 0xf2, 0x50, 0x35, 0x9a, 0xa3, 0x93, 0x67, 0x49, 0xaa, 0xf1, 0xf9, 0x95, 0xec, 0xac, + 0x79, 0xb7, 0x1a, 0x53, 0xfc, 0x47, 0xea, 0xf6, 0x9d, 0xcb, 0xd0, 0x44, 0x7f, 0x2d, 0x0f, 0x5a, 0x0a, 0x0b, 0x8e, + 0xdb, 0xc6, 0x5f, 0xbf, 0xcd, 0x1c, 0x62, 0x58, 0xba, 0x1c, 0xde, 0x84, 0x0e, 0xdd, 0x5d, 0x65, 0x67, 0xae, 0x0f, + 0xa8, 0x53, 0x17, 0xeb, 0x36, 0xa0, 0x64, 0xc9, 0xbb, 0x75, 0x7a, 0x62, 0xa5, 0x5f, 0xf6, 0xc3, 0x9d, 0x79, 0xd4, + 0xec, 0xee, 0x76, 0x3b, 0x21, 0x6d, 0xfb, 0x60, 0xbc, 0x2f, 0x61, 0x21, 0xce, 0x3b, 0x6c, 0xef, 0xe7, 0xb0, 0x7a, + 0xc8, 0x07, 0x7f, 0xe0, 0x38, 0xc3, 0xe8, 0x67, 0xca, 0xd0, 0xe7, 0x45, 0x21, 0x2f, 0x55, 0xa7, 0x7c, 0xa1, 0x5b, + 0xcb, 0xd4, 0xfb, 0x75, 0xfc, 0xba, 0x15, 0x20, 0xc6, 0xeb, 0x8a, 0x95, 0xe2, 0x0d, 0xad, 0x30, 0xae, 0x81, 0xdb, + 0xe4, 0x50, 0x4b, 0xb5, 0x40, 0xd4, 0xe5, 0x27, 0x0f, 0x79, 0x64, 0xd4, 0x99, 0xf0, 0xdd, 0x43, 0xee, 0x4b, 0xd7, + 0x76, 0x9b, 0xf8, 0xb9, 0xa6, 0xed, 0xef, 0x0e, 0x74, 0x47, 0xeb, 0xee, 0x6f, 0x9e, 0xcd, 0xcf, 0x23, 0xf3, 0xc5, + 0x00, 0x9b, 0xb5, 0xcb, 0xb8, 0xec, 0x18, 0xee, 0x7b, 0xd3, 0x83, 0xb1, 0x80, 0x40, 0x62, 0x86, 0x5e, 0x06, 0x2e, + 0x70, 0x81, 0xbb, 0xc2, 0x80, 0x21, 0xae, 0x69, 0xc9, 0xad, 0xb6, 0xb2, 0xf5, 0x91, 0xb7, 0x51, 0x21, 0x58, 0xd7, + 0x1d, 0x37, 0x49, 0x0e, 0xc1, 0x09, 0x5b, 0xee, 0x7d, 0xed, 0xb5, 0x33, 0xfc, 0x30, 0x10, 0xce, 0x2d, 0xd1, 0x33, + 0x6a, 0x7b, 0xa8, 0xd5, 0xbd, 0x86, 0x57, 0xb9, 0x8d, 0x3c, 0xeb, 0x37, 0xf3, 0xd2, 0xb0, 0x2f, 0x78, 0x2d, 0x05, + 0x87, 0xc6, 0x76, 0x2b, 0xdc, 0x62, 0xf1, 0x8e, 0x56, 0x2b, 0x6b, 0x6d, 0xb5, 0xd7, 0x4a, 0x45, 0xef, 0x5e, 0x73, + 0x9c, 0x38, 0x4b, 0x61, 0xfb, 0xe1, 0xfd, 0x05, 0xbb, 0x26, 0x80, 0x41, 0x8b, 0xc9, 0x02, 0x25, 0xa8, 0x64, 0xad, + 0x6a, 0xb7, 0x53, 0xe2, 0x97, 0xfb, 0x45, 0x97, 0xd9, 0xce, 0xe3, 0xd7, 0x4d, 0xda, 0x67, 0x3e, 0x47, 0x3f, 0xcc, + 0xef, 0xac, 0x93, 0x92, 0x33, 0x8c, 0x6b, 0xf9, 0xff, 0x55, 0xf4, 0xa2, 0xc8, 0xd2, 0x68, 0x63, 0x78, 0x30, 0x1b, + 0x6a, 0xd3, 0x87, 0xc6, 0xa8, 0xdc, 0xb2, 0x51, 0x44, 0xb4, 0xba, 0x01, 0xc1, 0x8c, 0xe2, 0xbe, 0x44, 0x9b, 0x57, + 0xaa, 0x2c, 0xbc, 0xc3, 0x67, 0x36, 0x7a, 0xc3, 0xf6, 0x84, 0x50, 0xbe, 0x7b, 0x5a, 0x98, 0x55, 0x4b, 0x45, 0x83, + 0xed, 0x12, 0xde, 0xc5, 0xa8, 0xd2, 0x4f, 0x98, 0x6c, 0x59, 0x30, 0xd5, 0xff, 0xef, 0x8b, 0x2c, 0x6d, 0x53, 0x74, + 0x60, 0x3a, 0x9b, 0x3e, 0x9d, 0x74, 0x83, 0xeb, 0x0c, 0x58, 0x44, 0xb0, 0xa5, 0xc2, 0xf1, 0x28, 0xb5, 0x1b, 0x24, + 0x4c, 0x04, 0x37, 0x51, 0x2f, 0x3b, 0x5a, 0xa6, 0x64, 0x55, 0xc0, 0xf3, 0x2b, 0x57, 0x99, 0x8e, 0xa3, 0xa1, 0xdf, + 0x3f, 0x4b, 0x4d, 0xe8, 0x57, 0xea, 0xa5, 0x2a, 0xce, 0xc3, 0xa8, 0x3a, 0x54, 0x18, 0xa3, 0x25, 0x4d, 0xe1, 0x18, + 0xcc, 0xce, 0xc3, 0x14, 0x2f, 0x67, 0x9b, 0x84, 0x7d, 0xc1, 0x40, 0x2e, 0xb5, 0x41, 0xbd, 0xa6, 0x44, 0x6b, 0xd6, + 0xde, 0xcc, 0x29, 0xa1, 0xe7, 0xac, 0xf4, 0xef, 0x42, 0x6b, 0x10, 0x28, 0xca, 0x66, 0xca, 0xf4, 0x54, 0xb7, 0xf3, + 0x9c, 0x26, 0xb4, 0xa0, 0x2b, 0x52, 0x83, 0xbe, 0xd7, 0xc9, 0xd9, 0xd1, 0xc9, 0xce, 0xcc, 0x7a, 0xcc, 0x8a, 0xe1, + 0x64, 0x1a, 0xc3, 0x35, 0x2d, 0x76, 0xd7, 0xb4, 0x65, 0xf3, 0xc6, 0xd5, 0xd8, 0x38, 0x0d, 0xda, 0x05, 0xd2, 0x36, + 0xcd, 0xed, 0xa7, 0x1e, 0xb7, 0xbf, 0xae, 0xd9, 0x72, 0xda, 0x5b, 0x6f, 0xb7, 0xbd, 0x14, 0x6c, 0x44, 0x3d, 0x3e, + 0x7e, 0xad, 0xa4, 0xeb, 0x96, 0xcb, 0x4f, 0xe1, 0xd9, 0xe3, 0xeb, 0x97, 0x3e, 0xb8, 0x1c, 0xad, 0xda, 0xdc, 0xfd, + 0x72, 0x17, 0x59, 0xee, 0x8b, 0x86, 0x96, 0xeb, 0x19, 0x6a, 0x92, 0x67, 0xa3, 0xbd, 0x43, 0x2d, 0x58, 0xce, 0xba, + 0x09, 0x4f, 0x0c, 0x76, 0xec, 0x55, 0x63, 0x73, 0x54, 0xe6, 0x92, 0xd5, 0x20, 0x81, 0x3e, 0xc9, 0x33, 0x4d, 0x7f, + 0x2f, 0xc3, 0x7c, 0x74, 0x43, 0x73, 0xc0, 0x15, 0xab, 0xec, 0x25, 0x83, 0xd4, 0x55, 0x7b, 0x89, 0x2b, 0x5f, 0xe1, + 0x90, 0x6c, 0xf0, 0xc9, 0x30, 0x55, 0x9f, 0x5d, 0xf2, 0xe0, 0xff, 0x6d, 0xd5, 0x2a, 0x3d, 0x37, 0xc9, 0x0d, 0xc7, + 0xbf, 0x4e, 0xda, 0x3e, 0x26, 0x06, 0x09, 0x78, 0x6a, 0x17, 0x43, 0x35, 0xaa, 0x8a, 0x58, 0x94, 0xb9, 0x89, 0x39, + 0x76, 0x67, 0xd7, 0xd0, 0x41, 0x19, 0xfc, 0xba, 0xe1, 0x13, 0x73, 0x07, 0xb6, 0x02, 0x1d, 0x9d, 0x68, 0x2e, 0xc3, + 0xcc, 0x5c, 0x86, 0x69, 0xd7, 0x56, 0x81, 0xe1, 0x55, 0x5b, 0x25, 0x51, 0xae, 0x46, 0x3d, 0x6e, 0x66, 0xa9, 0xd9, + 0x8b, 0xbc, 0x7b, 0x4d, 0x7a, 0x12, 0x7f, 0xba, 0xf4, 0xe4, 0xf5, 0x30, 0x20, 0xf2, 0x4b, 0x96, 0x86, 0x6b, 0x14, + 0x04, 0xa7, 0x56, 0x3b, 0x90, 0xe6, 0x23, 0x40, 0xe6, 0xc7, 0x69, 0xf8, 0x4e, 0x8b, 0x73, 0xc8, 0x46, 0x69, 0x9c, + 0xd8, 0xd2, 0xa8, 0x87, 0xe0, 0xce, 0x7b, 0xc9, 0x63, 0x08, 0x7c, 0xf8, 0x1e, 0x37, 0x83, 0x8a, 0x6e, 0x4b, 0x4c, + 0x94, 0x36, 0x8f, 0xba, 0xe5, 0xa3, 0x86, 0x50, 0xc9, 0xca, 0xf0, 0x12, 0x68, 0xef, 0x8e, 0xc0, 0xa8, 0x72, 0x02, + 0x99, 0x61, 0xb1, 0x7f, 0x30, 0x4c, 0x95, 0xa0, 0x68, 0x28, 0x87, 0x4b, 0x94, 0x03, 0x62, 0x12, 0x08, 0x8c, 0x8a, + 0x41, 0xaa, 0x2b, 0x53, 0x2f, 0x06, 0xa9, 0xbe, 0x55, 0x91, 0xfa, 0x34, 0x0b, 0x2b, 0xaa, 0x5b, 0x44, 0xc7, 0x74, + 0x28, 0xe9, 0xd2, 0xec, 0xd4, 0x5c, 0x4b, 0x2f, 0xd4, 0x72, 0x7c, 0xaa, 0xd3, 0x60, 0x14, 0x4f, 0x5c, 0x8a, 0x7e, + 0xab, 0xf6, 0xb3, 0xff, 0x16, 0x53, 0x6a, 0xc4, 0xa6, 0xf6, 0x16, 0x31, 0xac, 0xda, 0xf7, 0x59, 0x95, 0x83, 0x76, + 0x17, 0x94, 0x8d, 0x95, 0x71, 0x9e, 0x6f, 0x04, 0x33, 0x07, 0x6d, 0x63, 0xd5, 0xf4, 0xa1, 0x37, 0x62, 0xd4, 0xde, + 0x98, 0x6a, 0xdc, 0x13, 0xf8, 0x69, 0x83, 0xa6, 0x7b, 0x91, 0xe7, 0xa8, 0x47, 0xde, 0xfd, 0xcf, 0x1c, 0xd9, 0x99, + 0x7c, 0x16, 0xcb, 0xa4, 0x6e, 0x1f, 0x93, 0x60, 0xa1, 0xea, 0x18, 0x5d, 0xb8, 0x91, 0x29, 0xed, 0xe7, 0xce, 0xf4, + 0x23, 0x9e, 0xc9, 0xfd, 0x76, 0x68, 0xd4, 0x97, 0x86, 0xb5, 0xa4, 0x88, 0xfa, 0x82, 0xde, 0x9a, 0xea, 0xe8, 0x80, + 0x7a, 0x1d, 0x81, 0xd5, 0x15, 0x6d, 0x50, 0x03, 0x30, 0x19, 0xd7, 0xb6, 0x36, 0x9f, 0x83, 0xa9, 0xad, 0xaa, 0xe0, + 0x09, 0xdd, 0x15, 0x4a, 0xf7, 0x26, 0x75, 0xdd, 0x1a, 0x62, 0x0b, 0x18, 0x10, 0xb8, 0xd1, 0x53, 0xd3, 0x1f, 0x34, + 0x51, 0x01, 0x68, 0xd0, 0xb8, 0x9d, 0xe9, 0x1c, 0x89, 0x7e, 0xa7, 0x36, 0x6d, 0x33, 0xd5, 0xab, 0xca, 0x07, 0x50, + 0xf1, 0x67, 0xe9, 0xf4, 0xdc, 0x8c, 0x58, 0x00, 0xe3, 0x1e, 0x38, 0x53, 0xbd, 0xe3, 0x0c, 0xac, 0x27, 0xf2, 0x3c, + 0x2b, 0x79, 0x22, 0x05, 0xcc, 0x88, 0xbc, 0xbc, 0x94, 0x02, 0x86, 0x41, 0x0d, 0x00, 0x5a, 0x34, 0x97, 0xd1, 0x84, + 0x3f, 0xaa, 0xe9, 0x5d, 0x79, 0xf8, 0x23, 0x9d, 0xeb, 0x9b, 0x71, 0x0d, 0x86, 0xca, 0xeb, 0x8a, 0xef, 0x64, 0xfa, + 0x86, 0x3f, 0xf6, 0x32, 0x2d, 0xe5, 0xba, 0xd8, 0xc9, 0xf2, 0xe8, 0x1b, 0xfe, 0x44, 0xe7, 0x39, 0x78, 0x5c, 0xd3, + 0x34, 0xbe, 0xdd, 0xc9, 0xf2, 0xcf, 0x6f, 0x1e, 0xdb, 0x3c, 0x8f, 0xc6, 0x35, 0xbd, 0xe6, 0xfc, 0xa3, 0xcb, 0x34, + 0xd1, 0x55, 0x8d, 0x1f, 0xff, 0xd3, 0xe6, 0x7a, 0x5c, 0xd3, 0x4b, 0x29, 0xaa, 0xe5, 0x4e, 0x51, 0x07, 0xdf, 0x1c, + 0xfc, 0x93, 0x7f, 0x63, 0xba, 0x77, 0x50, 0xd3, 0xbf, 0xd7, 0x71, 0x51, 0xf1, 0x62, 0xa7, 0xb8, 0x7f, 0xfc, 0xf3, + 0x9f, 0x8f, 0x6d, 0xc6, 0xc7, 0x35, 0xbd, 0xe5, 0x71, 0x47, 0xdb, 0x27, 0x4f, 0x1e, 0xf3, 0x7f, 0xd4, 0x35, 0xfd, + 0x95, 0xf9, 0xc1, 0x51, 0x8f, 0x33, 0x4f, 0x0f, 0x9f, 0xcb, 0x26, 0x6a, 0xc0, 0xd0, 0x43, 0x03, 0x58, 0x4a, 0xab, + 0xa6, 0xb9, 0xc3, 0x2b, 0x17, 0xdc, 0xbe, 0x4f, 0xe3, 0x34, 0x5e, 0xc1, 0x41, 0xb0, 0x41, 0xe3, 0xac, 0x02, 0x38, + 0x55, 0xe0, 0x3d, 0xa3, 0x92, 0x66, 0xa5, 0xfc, 0x95, 0xf3, 0x8f, 0x30, 0x68, 0x08, 0x69, 0xa3, 0x22, 0x03, 0xbd, + 0x59, 0xe9, 0xc8, 0x46, 0xe8, 0xbf, 0xd9, 0x8c, 0x83, 0xe3, 0xc3, 0xe8, 0xf5, 0xfb, 0x61, 0xc1, 0x44, 0x58, 0x10, + 0x42, 0xff, 0x0a, 0x0b, 0x70, 0x28, 0x29, 0x98, 0x97, 0xcf, 0xf8, 0x9e, 0x6b, 0xa3, 0xb0, 0x10, 0x44, 0x77, 0x91, + 0x7d, 0x40, 0xd5, 0xa3, 0xef, 0xd0, 0x0d, 0xf1, 0xb2, 0xc2, 0x82, 0xa1, 0x55, 0x0d, 0xcc, 0x10, 0x14, 0xff, 0x8a, + 0x87, 0x12, 0x7c, 0xe2, 0x01, 0x3e, 0x7a, 0x4c, 0x66, 0x5c, 0x5d, 0x6b, 0xdf, 0x9c, 0x87, 0x05, 0x0d, 0x74, 0xdb, + 0x21, 0xe8, 0x40, 0xe4, 0xbf, 0x00, 0x4f, 0x81, 0x81, 0x0f, 0x0b, 0xbb, 0xee, 0xc0, 0xf3, 0xf9, 0xd5, 0xb0, 0x8e, + 0x2e, 0xfc, 0xe8, 0xaf, 0xd6, 0x85, 0x3d, 0x23, 0x53, 0x79, 0x58, 0x0e, 0x27, 0xd3, 0xc1, 0x40, 0xba, 0x38, 0x6e, + 0xc7, 0xd9, 0xfc, 0xd7, 0xb9, 0x5c, 0x2c, 0x50, 0xf7, 0x8d, 0xf3, 0x3a, 0xd3, 0x7f, 0x23, 0xed, 0x7c, 0xf0, 0xea, + 0xf8, 0xb7, 0xd3, 0x93, 0xe3, 0x17, 0xe0, 0x7c, 0xf0, 0xfe, 0xf9, 0xf7, 0xcf, 0xdf, 0xa9, 0xe0, 0xee, 0x6a, 0xce, + 0xfb, 0x7d, 0x27, 0xf5, 0x09, 0xf9, 0xb0, 0x22, 0xfb, 0x61, 0xfc, 0xb0, 0x50, 0x46, 0x0f, 0xe4, 0x90, 0x59, 0x28, + 0x64, 0xa8, 0xa2, 0xb6, 0xbf, 0xcb, 0xe1, 0xc4, 0x03, 0xb3, 0xb8, 0x69, 0x88, 0x70, 0xfd, 0x96, 0xdb, 0x20, 0x6b, + 0xf2, 0xc8, 0xeb, 0x07, 0x27, 0x53, 0xe9, 0xd8, 0xc2, 0x82, 0x41, 0xd9, 0xd0, 0xa6, 0xe3, 0x6c, 0x5e, 0x2c, 0x6c, + 0xbb, 0xdc, 0x02, 0x19, 0xa5, 0xd9, 0xf9, 0x79, 0xa8, 0xa0, 0xab, 0x8f, 0x40, 0x03, 0x60, 0x1a, 0x55, 0xb8, 0x16, + 0xf1, 0x99, 0x5f, 0x7e, 0x34, 0xf6, 0x9a, 0x77, 0x85, 0xba, 0x27, 0xd3, 0xac, 0xaa, 0x31, 0xa0, 0x83, 0x09, 0xe5, + 0x6e, 0xd0, 0x4d, 0x30, 0x19, 0xd5, 0x96, 0x5f, 0xe7, 0xd5, 0xc2, 0x34, 0xc7, 0x0d, 0x43, 0xe5, 0x95, 0x7c, 0x2e, + 0x1b, 0x88, 0x0c, 0x24, 0xc3, 0xb0, 0x47, 0x63, 0x14, 0xa9, 0xef, 0xed, 0x7a, 0xc7, 0x6f, 0x72, 0x09, 0xd1, 0x14, + 0x33, 0x90, 0xce, 0x1f, 0x0b, 0xe5, 0x5c, 0x2e, 0x19, 0x9f, 0x8b, 0xc5, 0x11, 0xb8, 0x9d, 0xcf, 0xc5, 0x22, 0xc2, + 0xa0, 0x7c, 0x19, 0xc4, 0x2a, 0x01, 0xbb, 0x17, 0x07, 0xe1, 0xdb, 0x09, 0x6d, 0x60, 0x37, 0x90, 0x64, 0x83, 0xd2, + 0xae, 0x34, 0x44, 0xb9, 0x53, 0x1e, 0x6d, 0x10, 0x79, 0x88, 0x55, 0xf3, 0xaa, 0xed, 0xc9, 0x66, 0x2e, 0x26, 0xb8, + 0xca, 0x62, 0x26, 0xa7, 0xf1, 0x21, 0x2b, 0xa6, 0x31, 0x94, 0x12, 0xa7, 0x69, 0x18, 0xd3, 0x09, 0x15, 0x84, 0x24, + 0x8c, 0xcf, 0xe3, 0x05, 0x4d, 0x50, 0x4a, 0x10, 0x42, 0xc8, 0x8f, 0x11, 0xda, 0xe6, 0xc0, 0x92, 0xb7, 0xdb, 0xcf, + 0xd3, 0xcf, 0xed, 0x18, 0x2e, 0xa3, 0x22, 0x74, 0x83, 0xce, 0x1a, 0xfe, 0x8d, 0xa8, 0xa0, 0x31, 0x56, 0x0c, 0x41, + 0xc0, 0x0b, 0x8c, 0x4a, 0x58, 0x90, 0x98, 0x55, 0x10, 0x45, 0xa0, 0x9c, 0xc7, 0x0b, 0x56, 0xd0, 0xa6, 0xcd, 0x69, + 0xac, 0x4d, 0x82, 0x7a, 0x0e, 0x4b, 0x6d, 0x4f, 0x2a, 0x15, 0x62, 0x8f, 0xcf, 0x44, 0x74, 0xad, 0x0d, 0x0d, 0x00, + 0x05, 0x4a, 0xc9, 0xc5, 0xaf, 0xbf, 0xdc, 0xc3, 0x4d, 0x41, 0xff, 0xb3, 0x8d, 0x89, 0x76, 0x96, 0xab, 0x43, 0x6f, + 0xbe, 0xa0, 0x71, 0x9e, 0x43, 0x28, 0x36, 0x83, 0x40, 0x2e, 0xb2, 0x0a, 0x22, 0x5a, 0xdc, 0x06, 0x26, 0x24, 0x1c, + 0xb4, 0xe9, 0x03, 0xa4, 0x36, 0xc4, 0xe4, 0xca, 0x13, 0x03, 0xbb, 0xad, 0x12, 0x04, 0x1c, 0xe9, 0x79, 0xf6, 0xa9, + 0x89, 0xb1, 0xa6, 0xa9, 0x99, 0x89, 0xb7, 0xa1, 0x10, 0x0d, 0x5a, 0x10, 0xcd, 0xe0, 0xfd, 0x73, 0xc9, 0xf1, 0xaa, + 0x03, 0x3f, 0xe0, 0x9d, 0x8b, 0x33, 0xaf, 0x66, 0x1e, 0x91, 0x53, 0x8f, 0x73, 0x44, 0xbf, 0xe4, 0x61, 0x35, 0xd2, + 0xc9, 0x18, 0x2b, 0x89, 0x83, 0xde, 0x06, 0x0b, 0xe6, 0x84, 0xae, 0x78, 0x68, 0xf9, 0xf8, 0x17, 0xc8, 0x64, 0x94, + 0xd4, 0x58, 0xd1, 0x95, 0x16, 0x23, 0xce, 0x6b, 0x98, 0xa5, 0xc9, 0x8a, 0x2e, 0x16, 0x9a, 0x34, 0x0b, 0x65, 0x1a, + 0xe0, 0x13, 0x68, 0x31, 0x72, 0x0f, 0x35, 0x6d, 0x20, 0x34, 0xec, 0x0e, 0x01, 0x1f, 0xb9, 0x87, 0x0e, 0xff, 0x3f, + 0xcf, 0x2e, 0x10, 0x69, 0xef, 0xd2, 0x44, 0xc6, 0x23, 0x75, 0x03, 0x07, 0xc5, 0xf8, 0xd8, 0x37, 0x13, 0xbf, 0x70, + 0x46, 0xef, 0x93, 0xca, 0x77, 0xf8, 0x60, 0xf9, 0xe3, 0x4d, 0xcd, 0xac, 0x8c, 0x60, 0x3d, 0x6c, 0xb7, 0xb8, 0x20, + 0xda, 0x2e, 0x80, 0xd4, 0x33, 0x5e, 0x2d, 0x7c, 0xe3, 0xd5, 0xf8, 0x0e, 0xe3, 0x55, 0x67, 0x85, 0x15, 0xe6, 0x64, + 0x83, 0xfa, 0x2c, 0x25, 0xcf, 0xcf, 0x51, 0x26, 0xd8, 0x74, 0x39, 0x2b, 0xa9, 0x4a, 0x25, 0xb4, 0x17, 0xfb, 0x19, + 0xe3, 0x1b, 0x82, 0x71, 0x56, 0x1c, 0x46, 0x02, 0x55, 0xa9, 0xa4, 0x0e, 0x7b, 0x05, 0xa8, 0xc7, 0xe0, 0xbd, 0xc1, + 0x10, 0x35, 0x32, 0x76, 0xd3, 0x06, 0x42, 0x43, 0x63, 0x3d, 0xda, 0xb3, 0xd6, 0xa3, 0xdb, 0x6d, 0x65, 0xfc, 0xed, + 0xe4, 0xba, 0x48, 0x10, 0x55, 0x58, 0x8d, 0x26, 0xc0, 0x9b, 0x26, 0xf6, 0xb6, 0xe4, 0x94, 0x16, 0x18, 0x3e, 0xfb, + 0xaf, 0xb0, 0x74, 0x2a, 0x89, 0x92, 0xcc, 0xca, 0x68, 0xe0, 0xce, 0xc1, 0x67, 0x71, 0x05, 0x6b, 0x00, 0x22, 0x39, + 0xa2, 0x87, 0xeb, 0x5f, 0xa1, 0x74, 0x99, 0x25, 0x99, 0x49, 0xc8, 0xcc, 0x45, 0xda, 0xce, 0x3a, 0x98, 0x38, 0x93, + 0x5a, 0x6f, 0x2c, 0xe4, 0xd0, 0x20, 0x3f, 0x80, 0x32, 0xc4, 0xe1, 0x93, 0x0f, 0x26, 0x54, 0xaa, 0x50, 0xaa, 0x8d, + 0x6e, 0x76, 0x03, 0xaf, 0xbc, 0xcf, 0x2e, 0x79, 0x59, 0xc5, 0x97, 0x2b, 0x63, 0x49, 0xcc, 0xd9, 0x5d, 0x6e, 0x7b, + 0x54, 0x98, 0x57, 0xaf, 0x9f, 0x7f, 0x7f, 0xdc, 0x78, 0xb5, 0x8b, 0x38, 0x1a, 0x82, 0x6d, 0xc5, 0x18, 0xa3, 0xb7, + 0xf8, 0x34, 0x98, 0x28, 0xd7, 0x08, 0xf4, 0x2e, 0x05, 0xfd, 0xf6, 0x97, 0x7a, 0x02, 0x5e, 0x72, 0xbd, 0xfc, 0x92, + 0x8f, 0x80, 0x25, 0x2a, 0xf4, 0xac, 0x30, 0x37, 0x2b, 0xb3, 0x3b, 0xbb, 0x15, 0x99, 0x69, 0x57, 0x1a, 0x19, 0x88, + 0x57, 0xdb, 0x61, 0x2c, 0x5c, 0xba, 0xa6, 0xdb, 0xc1, 0xae, 0x96, 0x9e, 0x25, 0xf2, 0x76, 0x5b, 0x42, 0x87, 0xec, + 0x80, 0x7b, 0x2f, 0xe3, 0x1b, 0x78, 0x59, 0x7a, 0xdd, 0x6c, 0x06, 0x4f, 0x00, 0x33, 0xe1, 0xc2, 0x59, 0x16, 0xc7, + 0x2c, 0x4b, 0x42, 0x15, 0x9b, 0xab, 0x21, 0xf2, 0x56, 0x84, 0xd6, 0xec, 0xaf, 0x50, 0x8c, 0xc0, 0xee, 0xe4, 0xe4, + 0x63, 0xb6, 0x9a, 0xad, 0x01, 0x35, 0xff, 0x32, 0x13, 0x40, 0x73, 0xed, 0x5a, 0xb0, 0x4d, 0xa1, 0xcd, 0x75, 0xfd, + 0x34, 0x5e, 0xc5, 0x09, 0xa8, 0x6e, 0xc0, 0x5b, 0xe4, 0x46, 0x8b, 0xae, 0x0c, 0xba, 0x28, 0xbd, 0xa7, 0x1c, 0x4b, + 0x0a, 0x1d, 0x7d, 0xef, 0x09, 0x75, 0xee, 0x19, 0xc0, 0x25, 0x8d, 0x9a, 0xa7, 0x5a, 0xca, 0x58, 0x00, 0x2c, 0x74, + 0x30, 0x53, 0x64, 0x2b, 0xba, 0x32, 0x98, 0x14, 0xf0, 0xd6, 0x00, 0x7f, 0x88, 0xac, 0x52, 0x77, 0xc5, 0x32, 0x2c, + 0x3d, 0xfb, 0xeb, 0x7e, 0x3f, 0xf6, 0xec, 0xaf, 0x57, 0x9a, 0xd6, 0xc5, 0xed, 0x06, 0x90, 0x1a, 0x03, 0x88, 0x1c, + 0xeb, 0x81, 0x30, 0x11, 0xc5, 0x9a, 0xbe, 0x7f, 0xc7, 0x26, 0x8b, 0x02, 0xa1, 0xdf, 0xa9, 0xd7, 0x93, 0x92, 0x80, + 0x4e, 0xad, 0x62, 0x47, 0x03, 0x6d, 0xf6, 0x01, 0x01, 0x51, 0xfd, 0x8c, 0x6c, 0xbe, 0x50, 0xce, 0xc5, 0x2a, 0x7c, + 0xf8, 0x98, 0x42, 0x40, 0xe1, 0x8e, 0x1a, 0x9d, 0xb7, 0x21, 0x12, 0x28, 0x2b, 0x14, 0xb1, 0xe6, 0xc5, 0x5a, 0x12, + 0x32, 0x1f, 0x2f, 0x50, 0x70, 0xe5, 0x80, 0x5d, 0x39, 0x9b, 0x0c, 0xcb, 0x88, 0xb3, 0xf0, 0xee, 0x6f, 0x26, 0x0b, + 0x82, 0x9a, 0x2b, 0x3f, 0x90, 0xe3, 0x4e, 0xa6, 0xc6, 0x9e, 0x6a, 0xd4, 0x20, 0x98, 0x8c, 0x20, 0x30, 0xdc, 0xf0, + 0x0b, 0x3e, 0x3e, 0x58, 0x10, 0x50, 0x91, 0x59, 0xb3, 0x10, 0xf3, 0xe2, 0xf0, 0x11, 0xa0, 0xc6, 0x8c, 0x0e, 0x9e, + 0x4c, 0x39, 0x83, 0x43, 0x94, 0x8e, 0x41, 0x46, 0x2b, 0xe0, 0xb7, 0x50, 0xbf, 0x5b, 0x27, 0xbe, 0x0f, 0xfd, 0x2a, + 0xe8, 0x79, 0x0c, 0x0c, 0x47, 0x34, 0xd9, 0x0f, 0xf9, 0x60, 0x32, 0x00, 0x6d, 0x89, 0xb7, 0xfb, 0x5a, 0x5a, 0x71, + 0x73, 0xba, 0x74, 0xba, 0x7f, 0xd2, 0x26, 0x48, 0x22, 0x95, 0xac, 0x54, 0xc4, 0x00, 0x42, 0x59, 0xaa, 0x6d, 0xb2, + 0x06, 0xcb, 0x0a, 0xb3, 0xa4, 0xb9, 0x41, 0x49, 0xdc, 0xdd, 0x0c, 0x1c, 0xa3, 0x66, 0x1d, 0x87, 0x65, 0xcb, 0x8d, + 0x1a, 0xe0, 0x73, 0x12, 0x56, 0xd8, 0x1b, 0xce, 0x4c, 0x7a, 0x67, 0x3a, 0x5c, 0x1d, 0x73, 0xf6, 0x8a, 0x23, 0x18, + 0x47, 0x82, 0x37, 0x1e, 0xba, 0x64, 0x1a, 0x2a, 0x32, 0x65, 0x1c, 0x4c, 0x7b, 0x80, 0x7b, 0xcf, 0xc1, 0x38, 0x8c, + 0x0d, 0x2a, 0x4b, 0xea, 0x53, 0xef, 0x2e, 0x04, 0x82, 0xb4, 0xd6, 0xcb, 0x7c, 0x86, 0xa7, 0x67, 0x84, 0xb2, 0x3f, + 0xe4, 0xf0, 0x05, 0xd8, 0x51, 0x90, 0xa3, 0x09, 0x7f, 0xf2, 0x70, 0x37, 0x50, 0x15, 0x1f, 0x04, 0x7b, 0xb1, 0x48, + 0xf7, 0x82, 0x81, 0x80, 0x5f, 0x05, 0xdf, 0xab, 0xa4, 0xdc, 0x3b, 0x8f, 0x8b, 0xbd, 0x78, 0x15, 0x17, 0xd5, 0xde, + 0x75, 0x56, 0x2d, 0xf7, 0x4c, 0x87, 0x00, 0x9a, 0x37, 0x18, 0xc4, 0x83, 0x60, 0x2f, 0x18, 0x14, 0x66, 0x6a, 0x57, + 0xac, 0x6c, 0x1c, 0x67, 0x26, 0x44, 0x59, 0xd0, 0x0c, 0x10, 0xd6, 0x38, 0x0d, 0x80, 0x4f, 0x5d, 0xb3, 0x94, 0x9e, + 0x63, 0xb8, 0x01, 0x31, 0x5d, 0x43, 0x1f, 0x80, 0x47, 0x5e, 0xd3, 0x18, 0x96, 0xc0, 0xf9, 0x60, 0x40, 0xce, 0x21, + 0x72, 0xc1, 0x9a, 0xda, 0x20, 0x0e, 0xe1, 0x5a, 0xd9, 0x69, 0xef, 0x02, 0x33, 0x6d, 0xb7, 0x80, 0xa8, 0x3c, 0x21, + 0xfd, 0xbe, 0xfd, 0x86, 0xfa, 0x17, 0xec, 0x25, 0xd8, 0x5f, 0x15, 0x55, 0x98, 0x4b, 0xa5, 0xf9, 0xbe, 0x60, 0x47, + 0x03, 0x15, 0x71, 0x78, 0xc7, 0x91, 0xa2, 0x8d, 0xca, 0x65, 0xd9, 0x93, 0x65, 0xc3, 0x57, 0xe2, 0x92, 0x3b, 0x3f, + 0xae, 0x4a, 0xca, 0xbc, 0xca, 0x56, 0x8a, 0xfd, 0x9b, 0x71, 0xcd, 0xfd, 0x81, 0xf5, 0x67, 0xf3, 0x15, 0x5c, 0x5b, + 0xbd, 0x77, 0x4d, 0xae, 0x11, 0x39, 0x4b, 0x28, 0x97, 0xd4, 0x36, 0x0f, 0x6f, 0xe9, 0xfb, 0xfc, 0xea, 0xdb, 0x4c, + 0xa7, 0xf1, 0x59, 0x85, 0x85, 0x0b, 0xd1, 0x8a, 0xe0, 0xd0, 0x90, 0x8b, 0xe6, 0x11, 0x60, 0xae, 0x7d, 0xb6, 0x82, + 0x82, 0xd4, 0xa7, 0x15, 0x7a, 0xb7, 0x42, 0xc2, 0x0b, 0xcd, 0x2e, 0xdd, 0x0f, 0xa4, 0x8c, 0xdb, 0x43, 0x4b, 0x98, + 0xb4, 0xbc, 0x08, 0xef, 0xbd, 0xe6, 0x26, 0xf7, 0x32, 0xc4, 0xe8, 0x45, 0x9e, 0x9d, 0x80, 0xb1, 0xee, 0x92, 0x9d, + 0x0d, 0x4f, 0xfc, 0x86, 0xe7, 0xac, 0x45, 0xa3, 0xe9, 0x92, 0x25, 0xfd, 0x7e, 0x0c, 0x26, 0xde, 0x29, 0xcb, 0xe1, + 0x57, 0xbe, 0xa0, 0x6b, 0x06, 0x98, 0x62, 0xf4, 0x1c, 0x12, 0x52, 0x44, 0x22, 0x59, 0xab, 0x93, 0xe4, 0x33, 0xdd, + 0x05, 0x60, 0xf4, 0xf3, 0x59, 0x1a, 0x2d, 0xef, 0x34, 0xb3, 0x40, 0xf2, 0x0c, 0x7d, 0xd7, 0xc1, 0xf6, 0xc6, 0x3e, + 0x48, 0x39, 0x3f, 0x14, 0xd3, 0xc1, 0x80, 0x13, 0x0d, 0x37, 0x5e, 0x2a, 0x71, 0xad, 0x6e, 0x71, 0xc7, 0x30, 0x96, + 0xfa, 0xb6, 0x88, 0xc1, 0x01, 0xbb, 0x68, 0x65, 0xb7, 0x0f, 0xb0, 0xaf, 0x1c, 0xef, 0x52, 0x65, 0x77, 0x7a, 0xcc, + 0x34, 0x97, 0xad, 0x26, 0x9d, 0x54, 0xdc, 0x4d, 0xe4, 0x9b, 0xdc, 0x41, 0x97, 0xcb, 0xb1, 0xe6, 0x2d, 0x07, 0xa0, + 0xa2, 0x1f, 0x29, 0xaa, 0xfb, 0x05, 0x8e, 0x30, 0xf7, 0xd6, 0x6d, 0x3e, 0xd9, 0x37, 0x05, 0x0e, 0x91, 0x27, 0x6d, + 0x34, 0x05, 0x74, 0xef, 0xe2, 0x61, 0x57, 0xbf, 0x2d, 0xdd, 0x05, 0x4a, 0xb4, 0x53, 0x71, 0xc3, 0x8f, 0x89, 0x3a, + 0x9d, 0x69, 0x43, 0xe8, 0x5f, 0x19, 0x71, 0x7f, 0x69, 0x5c, 0xc5, 0x9b, 0xde, 0xe5, 0x33, 0x0e, 0x75, 0x76, 0x43, + 0x28, 0x00, 0x57, 0xed, 0xe9, 0xd4, 0x8d, 0x21, 0xbd, 0x52, 0xa2, 0xdb, 0xe0, 0x60, 0x77, 0xfa, 0x8c, 0xa3, 0xe8, + 0xc7, 0xa8, 0x91, 0xaf, 0x23, 0xf1, 0x50, 0x0e, 0xe2, 0x87, 0x05, 0x5d, 0x46, 0xe2, 0x61, 0x31, 0x88, 0x1f, 0xca, + 0xba, 0xde, 0x3d, 0x57, 0xee, 0xee, 0x23, 0xf2, 0xac, 0x3b, 0x7b, 0xa9, 0x84, 0x8d, 0x81, 0x67, 0xd7, 0x02, 0xc2, + 0x29, 0x78, 0x22, 0x5b, 0x4b, 0x1f, 0x3a, 0xb7, 0xfb, 0xd8, 0x32, 0x49, 0x10, 0xf4, 0xbc, 0xcd, 0x26, 0x51, 0xec, + 0x6c, 0xf3, 0xe8, 0xc3, 0x29, 0x90, 0xd0, 0xed, 0xb6, 0x59, 0x57, 0x6b, 0x40, 0x31, 0x0d, 0xc7, 0x7c, 0xbf, 0x18, + 0x5d, 0xfb, 0xee, 0xfa, 0xfb, 0xc5, 0x68, 0x49, 0x86, 0x13, 0x33, 0xf9, 0xf1, 0xd1, 0x78, 0x16, 0x47, 0x93, 0xba, + 0xe3, 0xb4, 0xd0, 0xf8, 0xa7, 0xde, 0x2d, 0x14, 0x81, 0x53, 0x31, 0x82, 0x23, 0xa7, 0x42, 0x39, 0x29, 0x35, 0x30, + 0xfc, 0xf7, 0xaa, 0x1d, 0x6d, 0xda, 0xab, 0xb8, 0x4a, 0x96, 0x99, 0xb8, 0xd0, 0xe1, 0xc3, 0x75, 0x74, 0x71, 0x1b, + 0xd0, 0xce, 0xbb, 0x4c, 0x3b, 0x7e, 0x9d, 0x34, 0xe8, 0x89, 0xab, 0x99, 0x01, 0xb7, 0xee, 0x47, 0x68, 0x86, 0xc0, + 0x68, 0x79, 0xfe, 0x16, 0x31, 0xb7, 0x7f, 0x51, 0x36, 0xbf, 0x8a, 0xf6, 0x39, 0x32, 0x52, 0xb6, 0xc9, 0x48, 0x05, + 0x46, 0x98, 0x52, 0x24, 0x71, 0x15, 0x42, 0x20, 0xfb, 0x2f, 0x29, 0xae, 0xc5, 0xd2, 0x7b, 0x0d, 0xc2, 0x04, 0xdb, + 0x05, 0xed, 0x57, 0xb7, 0x73, 0x5b, 0x69, 0xb1, 0x47, 0xea, 0xfb, 0xdc, 0xd9, 0xae, 0x68, 0xf2, 0xf7, 0x65, 0x03, + 0xda, 0x00, 0xa2, 0xbc, 0xab, 0x8f, 0x4a, 0xe0, 0x64, 0xc4, 0x0d, 0x25, 0x46, 0x2f, 0xe8, 0xea, 0x44, 0xee, 0xd9, + 0xa9, 0x79, 0x53, 0x31, 0x53, 0x71, 0xe5, 0x9b, 0x3d, 0xf3, 0x1f, 0x0c, 0x05, 0x2d, 0xc1, 0xc0, 0xdb, 0x9c, 0xf1, + 0xe8, 0x40, 0x77, 0x6d, 0x74, 0x5a, 0xb0, 0x59, 0x50, 0x97, 0x75, 0xdd, 0xc6, 0x83, 0x46, 0x1c, 0x14, 0xc5, 0xaa, + 0x50, 0x23, 0xe1, 0x89, 0x40, 0xc0, 0x94, 0x5d, 0xf2, 0xc8, 0x08, 0x6a, 0x7a, 0x13, 0x0a, 0x1b, 0x0a, 0xfe, 0x2a, + 0x51, 0x4d, 0x6f, 0x42, 0x9b, 0x4c, 0x9c, 0x66, 0x10, 0xc1, 0x8c, 0xd8, 0xee, 0xb7, 0x80, 0x36, 0xb7, 0x66, 0xb4, + 0xa9, 0x6b, 0xab, 0xad, 0x42, 0x2e, 0x29, 0x52, 0x96, 0xff, 0x4e, 0x4d, 0x05, 0x25, 0xb5, 0x5c, 0xf4, 0x26, 0x4d, + 0x17, 0x3d, 0x9e, 0x19, 0x49, 0xa0, 0x72, 0xcb, 0x1d, 0xa3, 0x3f, 0x84, 0x05, 0x1e, 0x31, 0x71, 0x62, 0xc1, 0xdc, + 0xea, 0x88, 0x65, 0x73, 0xb1, 0x18, 0xad, 0x24, 0x84, 0x0d, 0x3e, 0x64, 0xd9, 0xbc, 0xd4, 0x0f, 0xa1, 0x2f, 0x2c, + 0x7d, 0x03, 0x76, 0xb1, 0xc1, 0x4a, 0x96, 0x01, 0xf8, 0x5e, 0xd0, 0xcd, 0x4a, 0x96, 0x91, 0x54, 0xdd, 0x8f, 0x6b, + 0x2c, 0x41, 0xa5, 0x15, 0x2a, 0x2d, 0xa9, 0xb1, 0x20, 0xf0, 0x55, 0xd5, 0xe5, 0x43, 0xb2, 0xab, 0x40, 0x3d, 0x75, + 0xd4, 0x80, 0x53, 0xa0, 0xaa, 0xc0, 0x82, 0x24, 0xa8, 0x0c, 0x5d, 0x15, 0x98, 0x56, 0x60, 0x9a, 0xa9, 0xc2, 0x45, + 0x99, 0x1d, 0x4a, 0xb3, 0x5e, 0xf2, 0x59, 0x3c, 0x08, 0x93, 0x61, 0x4c, 0x1e, 0x22, 0xd4, 0xfe, 0x7e, 0x1e, 0xc5, + 0x5a, 0x2e, 0x79, 0xe1, 0xfc, 0xe2, 0xaf, 0x3f, 0x63, 0xaf, 0x7b, 0x8a, 0xc1, 0x02, 0x9c, 0xa5, 0xed, 0x65, 0x26, + 0xde, 0xca, 0x56, 0x70, 0x1c, 0xcc, 0xa2, 0x1c, 0x56, 0x3d, 0x39, 0xa2, 0xb9, 0xc8, 0xb5, 0x77, 0x11, 0x22, 0x07, + 0x99, 0x3d, 0x06, 0xd8, 0x8d, 0xf0, 0x75, 0x68, 0x6d, 0x6e, 0x75, 0x85, 0xf8, 0x1b, 0x25, 0x12, 0x3f, 0x49, 0xf9, + 0x71, 0xbd, 0x52, 0xb9, 0x2a, 0x83, 0xc7, 0xaa, 0x9b, 0xc1, 0x33, 0xed, 0x7b, 0xac, 0xfd, 0x5b, 0xdb, 0xcd, 0xf1, + 0xde, 0x83, 0x07, 0xad, 0xff, 0xad, 0x27, 0x21, 0xb4, 0x57, 0x4e, 0x52, 0x77, 0xd4, 0xe8, 0x99, 0xc9, 0x1a, 0x51, + 0x09, 0x53, 0xbb, 0x53, 0x39, 0x06, 0x6a, 0x3a, 0x80, 0x6b, 0x89, 0x9a, 0xa0, 0x27, 0x05, 0x1b, 0xc3, 0x11, 0x67, + 0x71, 0xd0, 0x0e, 0x63, 0x14, 0x2f, 0xe7, 0x4a, 0xbc, 0x9c, 0x1f, 0x31, 0x0e, 0xd0, 0x5a, 0x80, 0x54, 0xaf, 0x61, + 0x3f, 0x73, 0x05, 0x0b, 0x6c, 0xee, 0x7c, 0x07, 0x16, 0xc8, 0x10, 0x27, 0x9b, 0xe3, 0x64, 0x8f, 0x6b, 0x3d, 0xf7, + 0x02, 0x1f, 0x27, 0xf5, 0xc2, 0xab, 0xab, 0x6c, 0xd7, 0xb5, 0x64, 0xe5, 0xbc, 0x18, 0x4c, 0x20, 0x28, 0x4b, 0x39, + 0x2f, 0x86, 0x93, 0x05, 0xcd, 0xe1, 0xc7, 0xa2, 0x81, 0x0e, 0xb1, 0x1c, 0x24, 0x70, 0xe9, 0xec, 0x31, 0xe0, 0x0d, + 0xa5, 0x16, 0x77, 0x63, 0x1d, 0x39, 0xd6, 0x51, 0xec, 0x87, 0x31, 0xe0, 0xca, 0x3a, 0x81, 0xf7, 0xdd, 0xd7, 0xc7, + 0x26, 0x20, 0xab, 0x76, 0x85, 0x57, 0xa3, 0xdc, 0x75, 0xa5, 0xd1, 0x97, 0x94, 0x9e, 0xf0, 0x82, 0xa7, 0x92, 0xed, + 0xb6, 0x67, 0xe0, 0x6c, 0x89, 0x87, 0xc4, 0x3b, 0x46, 0xf4, 0x62, 0xda, 0xc8, 0xcc, 0x09, 0x9c, 0xd9, 0xee, 0xb2, + 0x8d, 0xf9, 0xb1, 0x03, 0x1c, 0x2c, 0x82, 0x90, 0xb8, 0x21, 0x0c, 0x13, 0x3b, 0x2a, 0x87, 0x5a, 0x08, 0xd7, 0xb5, + 0xf0, 0x3a, 0x4e, 0xcb, 0x18, 0x5c, 0xa4, 0xb5, 0x6d, 0xe2, 0x1d, 0x74, 0xdd, 0xf3, 0x63, 0x6e, 0x75, 0x8c, 0xb6, + 0x90, 0x7e, 0x3b, 0x3a, 0xbd, 0xe7, 0x30, 0x00, 0x4d, 0x0f, 0x66, 0x55, 0xfb, 0x4c, 0xe2, 0xe6, 0xb4, 0x13, 0x84, + 0x44, 0x20, 0x8a, 0xd2, 0x19, 0x61, 0xfa, 0x77, 0x9a, 0xcb, 0x2a, 0x5a, 0xdd, 0xcb, 0x33, 0x87, 0x3c, 0x0b, 0xbd, + 0xed, 0x41, 0xab, 0xe6, 0x6e, 0x30, 0x4e, 0xdc, 0x6e, 0xef, 0xfc, 0xbf, 0x65, 0x5d, 0x5b, 0xad, 0x11, 0x0f, 0xdb, + 0xd5, 0x0f, 0x1a, 0x7b, 0xb5, 0xa7, 0x62, 0xc0, 0x5c, 0x48, 0xef, 0x8c, 0x2a, 0x79, 0x91, 0xf1, 0x12, 0x4f, 0xaa, + 0x8b, 0x86, 0x8f, 0xf7, 0x75, 0x36, 0x32, 0x0f, 0x64, 0x0a, 0x88, 0xe7, 0x1f, 0x53, 0xa3, 0x3e, 0x4e, 0x51, 0x02, + 0xfe, 0x56, 0xc7, 0x37, 0xa2, 0x27, 0xf6, 0xc5, 0x05, 0xaf, 0xde, 0x5c, 0x0b, 0xf3, 0xe2, 0x99, 0xd5, 0xf9, 0xd3, + 0xa7, 0x85, 0x0f, 0x1d, 0x8e, 0xda, 0x3b, 0x28, 0xb2, 0x64, 0xe2, 0x68, 0x62, 0x64, 0x6d, 0x62, 0x76, 0xa2, 0xe0, + 0x62, 0xa2, 0x0a, 0x3d, 0xeb, 0xec, 0x09, 0x53, 0x80, 0xbe, 0x71, 0x8c, 0x4a, 0xc6, 0xb0, 0x60, 0xa0, 0x4e, 0x53, + 0x42, 0xf4, 0x50, 0xcc, 0x30, 0x5e, 0x31, 0x80, 0xc2, 0x14, 0x0a, 0x44, 0xd1, 0xd9, 0x87, 0x03, 0x4d, 0xe8, 0xf7, + 0x3f, 0xa6, 0x3a, 0x03, 0x2d, 0xeb, 0x69, 0x01, 0xa2, 0x3a, 0x88, 0xb6, 0x0a, 0x84, 0x39, 0xa5, 0x65, 0x46, 0x97, + 0x82, 0xa6, 0x82, 0x26, 0x19, 0x3d, 0xe7, 0x4a, 0x54, 0x7c, 0x2e, 0x98, 0xa2, 0xed, 0x86, 0xb0, 0xff, 0xd8, 0xa0, + 0xeb, 0xad, 0x58, 0x6b, 0x68, 0x77, 0x82, 0x8c, 0xd0, 0x7c, 0xa1, 0x83, 0x90, 0xa1, 0x72, 0x12, 0xf1, 0xe1, 0x35, + 0x5e, 0x81, 0x4b, 0xa6, 0xd9, 0x68, 0x19, 0x97, 0x61, 0x60, 0xbf, 0x0a, 0x2c, 0x26, 0x07, 0x26, 0x9d, 0xac, 0xcf, + 0x9e, 0xca, 0xcb, 0x95, 0x14, 0x5c, 0x54, 0x0a, 0xa2, 0xdf, 0xe0, 0xbe, 0x9b, 0xb8, 0xea, 0xac, 0x59, 0x2b, 0xbd, + 0xef, 0x5b, 0x9f, 0xb5, 0x71, 0x5f, 0x18, 0x1c, 0x83, 0x9d, 0x8f, 0x88, 0x81, 0x34, 0xa8, 0x74, 0x8b, 0x43, 0x13, + 0xa0, 0x4b, 0x87, 0x14, 0xb2, 0x64, 0x2a, 0x53, 0x25, 0xa8, 0xf8, 0xc6, 0xef, 0xa4, 0xac, 0x46, 0x7f, 0xaf, 0x79, + 0x71, 0x7b, 0xc2, 0x73, 0x8e, 0x63, 0x14, 0x24, 0xb1, 0xb8, 0x8a, 0xcb, 0x80, 0xf8, 0x96, 0x57, 0xc1, 0x41, 0x6a, + 0xc2, 0xc6, 0xec, 0x54, 0x8d, 0x5a, 0xaf, 0x02, 0x7d, 0x65, 0x94, 0x6f, 0x0c, 0x86, 0x26, 0xa2, 0x0a, 0xfa, 0x5e, + 0xab, 0x7b, 0x5a, 0xdd, 0xb0, 0x80, 0xf8, 0x73, 0xa5, 0x17, 0x6a, 0xbd, 0x6e, 0xc6, 0xdc, 0x30, 0x11, 0x82, 0x46, + 0x8f, 0xea, 0x85, 0xc3, 0xcf, 0xdf, 0x28, 0x4b, 0x22, 0x78, 0xb1, 0x49, 0xd7, 0x85, 0x89, 0xa5, 0x41, 0x75, 0xc0, + 0xdc, 0x68, 0x93, 0xf3, 0x0b, 0x10, 0xfd, 0x39, 0x2b, 0xa2, 0x49, 0x5d, 0x53, 0x85, 0x60, 0x18, 0x6d, 0x6e, 0x1a, + 0xe9, 0xf4, 0x16, 0xbc, 0xdc, 0x8c, 0x35, 0x92, 0xf6, 0x74, 0xac, 0x69, 0xc1, 0xcb, 0x95, 0x14, 0x25, 0x44, 0x77, + 0xee, 0x8d, 0xe9, 0x65, 0x9c, 0x89, 0x2a, 0xce, 0xc4, 0x71, 0xb9, 0xe2, 0x49, 0xf5, 0x0e, 0x2a, 0xd4, 0xc6, 0x38, + 0xd8, 0x7a, 0x35, 0xea, 0x2a, 0x1c, 0xf2, 0xcb, 0xf3, 0xe7, 0x37, 0xab, 0x58, 0xa4, 0x30, 0xea, 0xf5, 0x5d, 0x2f, + 0x9a, 0xd3, 0xb1, 0x8a, 0x0b, 0x2e, 0x4c, 0xd4, 0x62, 0x5a, 0xb1, 0x80, 0xeb, 0x8c, 0x01, 0xe5, 0x2a, 0x76, 0x67, + 0xa6, 0x62, 0x19, 0xc6, 0x65, 0xf9, 0x53, 0x56, 0xe2, 0x1d, 0x00, 0x5a, 0x03, 0xa7, 0xc5, 0xcc, 0x80, 0x80, 0xdc, + 0xe6, 0x06, 0x17, 0x81, 0x05, 0x07, 0x8f, 0xc7, 0xab, 0x9b, 0x80, 0x7a, 0x6f, 0xa4, 0xba, 0x1e, 0xb2, 0x60, 0x3c, + 0x7a, 0x12, 0x38, 0xe4, 0x10, 0xff, 0xa3, 0xc7, 0x07, 0x77, 0x7f, 0x33, 0x09, 0x48, 0x3d, 0x05, 0x55, 0x85, 0x51, + 0x88, 0xc2, 0xb4, 0xbf, 0x5a, 0xab, 0x5b, 0xee, 0x9b, 0xb3, 0x92, 0x17, 0x57, 0x10, 0xad, 0x9d, 0x4c, 0x33, 0x20, + 0xe7, 0x52, 0x25, 0xc0, 0xa2, 0x88, 0xab, 0xaa, 0xc8, 0xce, 0xc0, 0x44, 0x09, 0x0d, 0xc0, 0xcc, 0xd3, 0x0b, 0x74, + 0xf8, 0x88, 0xe6, 0x01, 0xf6, 0x29, 0x58, 0xd4, 0xa4, 0x2e, 0xa1, 0xb0, 0x64, 0x0f, 0x83, 0xd5, 0xa9, 0xb8, 0xd2, + 0x0e, 0xe0, 0xbb, 0xfa, 0x33, 0x5a, 0x4a, 0x8c, 0x35, 0xab, 0xe7, 0x29, 0x3e, 0x2b, 0x65, 0xbe, 0xae, 0x40, 0x7b, + 0x7e, 0x5e, 0x45, 0x07, 0x8f, 0x57, 0x37, 0x53, 0xd5, 0x8d, 0x08, 0x7a, 0x31, 0x55, 0x38, 0x6f, 0x49, 0x9c, 0x27, + 0xe1, 0x64, 0x3c, 0xfe, 0x6a, 0x6f, 0xb8, 0x07, 0xc9, 0x64, 0xfa, 0x69, 0xa8, 0x1c, 0xb9, 0x86, 0x93, 0xf1, 0xb8, + 0xfe, 0xb3, 0x36, 0x61, 0xbe, 0x4d, 0x3d, 0x4f, 0xff, 0x3c, 0x54, 0xeb, 0xff, 0xe8, 0x70, 0x5f, 0xff, 0xf8, 0xb3, + 0xae, 0xa7, 0x4f, 0x8b, 0x70, 0xfe, 0x7b, 0xa8, 0xd6, 0xf7, 0x71, 0x51, 0xc4, 0xb7, 0x35, 0x44, 0x36, 0x15, 0xce, + 0xbb, 0x86, 0x7a, 0x64, 0x81, 0x1e, 0x90, 0xe9, 0xb9, 0x60, 0xf0, 0xcd, 0xbb, 0x2a, 0x0c, 0x78, 0xb9, 0x1a, 0x72, + 0x51, 0x65, 0xd5, 0xed, 0x10, 0xf3, 0x04, 0xf8, 0xa9, 0xc5, 0x33, 0x2b, 0x0c, 0xf1, 0x3d, 0x2f, 0x38, 0xff, 0xc4, + 0x43, 0x65, 0x2c, 0x3e, 0x46, 0x63, 0xf1, 0x31, 0x55, 0xdd, 0x98, 0x7c, 0x43, 0x75, 0xdf, 0x26, 0xdf, 0x80, 0x49, + 0x56, 0xd6, 0xfe, 0x46, 0x19, 0x6b, 0x46, 0x63, 0x7a, 0xf5, 0x22, 0xcf, 0x56, 0x70, 0x29, 0x58, 0xea, 0x1f, 0x35, + 0xa1, 0xef, 0x78, 0x3b, 0xfb, 0x68, 0x34, 0x7a, 0x53, 0xd0, 0xd1, 0x68, 0xf4, 0x31, 0xab, 0x09, 0x5d, 0x89, 0x8e, + 0xf7, 0xef, 0x38, 0x3d, 0x93, 0xe9, 0x6d, 0x14, 0x04, 0x74, 0x99, 0xa5, 0x29, 0x17, 0xaa, 0xac, 0x57, 0x69, 0x3b, + 0xaf, 0x6a, 0x21, 0x02, 0x21, 0xe9, 0x36, 0x22, 0x24, 0x13, 0xa1, 0x6f, 0x77, 0x7a, 0x36, 0x1a, 0x8d, 0x5e, 0xa5, + 0xa6, 0x5a, 0x77, 0x41, 0x79, 0x8a, 0xe6, 0x14, 0xce, 0x4f, 0x01, 0xac, 0x91, 0x4c, 0xf4, 0x97, 0xfd, 0xff, 0x1e, + 0xce, 0xe6, 0xe3, 0xe1, 0xb7, 0xa3, 0xc5, 0xc3, 0x7d, 0x1a, 0x04, 0x7e, 0xe8, 0x86, 0x50, 0x5b, 0xb7, 0x4c, 0xcb, + 0xc3, 0xf1, 0x94, 0x94, 0x03, 0xf6, 0xd8, 0xfa, 0x16, 0x7d, 0xf5, 0x18, 0x90, 0x59, 0x51, 0xa4, 0x1c, 0x38, 0x69, + 0x28, 0x5e, 0xcd, 0x5e, 0x0a, 0xc0, 0x8b, 0xb3, 0x91, 0x1d, 0x8c, 0x56, 0x74, 0x1c, 0x41, 0x79, 0xb5, 0x35, 0x15, + 0xe9, 0x31, 0x96, 0x99, 0x28, 0xa9, 0xe3, 0x69, 0x79, 0x9d, 0x55, 0xc9, 0x12, 0x03, 0x3d, 0xc5, 0x25, 0x0f, 0xbe, + 0x0a, 0xa2, 0x92, 0x1d, 0x3c, 0x99, 0x2a, 0xb8, 0x63, 0x4c, 0x4a, 0xf9, 0x05, 0x24, 0x7e, 0x3b, 0x46, 0x48, 0x58, + 0xa2, 0x3d, 0x38, 0xb1, 0xc6, 0x17, 0xb9, 0x8c, 0xc1, 0xa3, 0xb5, 0xd4, 0x3c, 0x9c, 0x3d, 0x19, 0xad, 0x3d, 0x4a, + 0xab, 0x39, 0x12, 0x9a, 0x13, 0x4a, 0x26, 0xf7, 0x4b, 0x2a, 0xbf, 0x9a, 0xa0, 0x97, 0x14, 0xb8, 0x99, 0x47, 0x70, + 0xfc, 0x5b, 0x4b, 0x0f, 0xbd, 0x7c, 0x52, 0xb6, 0x3f, 0xff, 0xdf, 0x25, 0x5d, 0x0c, 0xf6, 0xdd, 0xd0, 0xbc, 0xd5, + 0xee, 0xbc, 0x15, 0x32, 0x8e, 0x55, 0xf8, 0x26, 0x25, 0xd6, 0x18, 0x97, 0xb3, 0xa3, 0x8d, 0xe9, 0xce, 0xa8, 0x2a, + 0xb2, 0xcb, 0x90, 0xe8, 0x5e, 0x39, 0x90, 0xd0, 0x20, 0xca, 0x46, 0xb8, 0x7e, 0xc0, 0x7a, 0xc6, 0xeb, 0xe4, 0x15, + 0x2f, 0xaa, 0x2c, 0x51, 0xef, 0xaf, 0x1a, 0xef, 0xeb, 0xda, 0x04, 0x54, 0x7d, 0x50, 0x30, 0x98, 0xe7, 0xb7, 0x05, + 0x80, 0x98, 0x22, 0x0d, 0xf0, 0x09, 0x66, 0x10, 0xd4, 0xae, 0x99, 0x97, 0x8d, 0xe0, 0x1b, 0xf0, 0xd5, 0x83, 0x02, + 0x30, 0x48, 0x42, 0x90, 0x22, 0x43, 0x68, 0x20, 0x10, 0x68, 0x18, 0x72, 0x81, 0xc1, 0x4f, 0xbc, 0x38, 0x92, 0xca, + 0x29, 0x91, 0x87, 0x01, 0xfe, 0x08, 0xa8, 0x0a, 0x40, 0x62, 0x3c, 0x0e, 0xe1, 0x85, 0xfa, 0xe5, 0xde, 0xa8, 0x3d, + 0xc2, 0x9e, 0xa6, 0x21, 0x04, 0x1b, 0xc2, 0x87, 0x00, 0x96, 0x14, 0xa1, 0x6f, 0x91, 0xcb, 0x08, 0x83, 0xf3, 0x3c, + 0x5b, 0xe9, 0xa4, 0x6a, 0xd4, 0xd1, 0x7c, 0x28, 0xb5, 0x23, 0x39, 0xa0, 0x5e, 0x7a, 0x8c, 0xe9, 0x85, 0x4a, 0x57, + 0x45, 0x39, 0xa3, 0x9c, 0x07, 0x7a, 0x62, 0x5c, 0xd8, 0x42, 0x0e, 0x91, 0x70, 0x1e, 0x14, 0x2a, 0x14, 0x0e, 0x5f, + 0x00, 0x18, 0x18, 0x48, 0x3b, 0x76, 0xe3, 0xdd, 0xa8, 0xec, 0xa7, 0x9c, 0xed, 0xff, 0xf7, 0x3c, 0x1e, 0x7e, 0x1a, + 0x0f, 0xbf, 0x5d, 0x0c, 0xc2, 0xa1, 0xfd, 0x49, 0x1e, 0x3e, 0xd8, 0xa7, 0x2f, 0xb8, 0xe5, 0xd2, 0x60, 0xe1, 0x37, + 0x82, 0xfd, 0xa8, 0x95, 0x10, 0x44, 0x01, 0xde, 0xb0, 0xdc, 0x6a, 0x9c, 0x00, 0xe0, 0x61, 0xf0, 0x5f, 0x01, 0x1a, + 0x4d, 0xb9, 0x8b, 0x17, 0xe8, 0x4b, 0xd4, 0xef, 0xa3, 0x47, 0x0d, 0x83, 0x41, 0x10, 0xd7, 0xa8, 0x98, 0x30, 0x44, + 0x97, 0x31, 0x51, 0x30, 0xc8, 0x36, 0xfb, 0x76, 0xdb, 0x6b, 0x4b, 0xc2, 0xf0, 0x4b, 0x3f, 0xd3, 0xc4, 0xcc, 0x3b, + 0xdc, 0xd8, 0x56, 0x72, 0x15, 0x22, 0x56, 0xa0, 0xfe, 0x95, 0x33, 0x88, 0xbd, 0x79, 0x95, 0x81, 0x4f, 0x87, 0xfd, + 0x62, 0x3c, 0x03, 0x36, 0x0a, 0xee, 0x7c, 0x05, 0x3f, 0xcf, 0xc0, 0xcd, 0x5b, 0xc4, 0x28, 0x70, 0xb0, 0x4b, 0xa2, + 0xdf, 0xef, 0xe5, 0x59, 0x98, 0x6b, 0xdc, 0xe9, 0xbc, 0x36, 0x6a, 0x08, 0xd4, 0x91, 0x83, 0xfa, 0x41, 0x0f, 0xc1, + 0x50, 0x0d, 0x41, 0xd1, 0xd1, 0x16, 0x57, 0xaf, 0xad, 0xa7, 0x30, 0xbd, 0x55, 0xf5, 0x15, 0xa3, 0xbf, 0x64, 0x26, + 0xb0, 0x90, 0x76, 0xcd, 0xb1, 0xae, 0x39, 0x46, 0xda, 0xd3, 0xef, 0x8b, 0x06, 0xf9, 0xe9, 0x2c, 0x3c, 0x08, 0x54, + 0xa9, 0x72, 0xa7, 0x2c, 0xca, 0x6d, 0x69, 0xde, 0x18, 0xd6, 0x34, 0xcf, 0x6c, 0x9c, 0x9b, 0x59, 0xaf, 0x17, 0x86, + 0xe8, 0xe0, 0x89, 0xa5, 0x62, 0x6d, 0x10, 0xee, 0xc8, 0x24, 0x8c, 0x2e, 0x41, 0x76, 0x19, 0x9e, 0x72, 0x82, 0x7c, + 0x2a, 0xb0, 0x0f, 0xaa, 0x5a, 0x2f, 0x27, 0x3c, 0x36, 0xf2, 0x65, 0x23, 0x68, 0x90, 0x97, 0x14, 0xf5, 0x26, 0x6e, + 0xc7, 0x1e, 0xb7, 0x90, 0x2b, 0x37, 0xf5, 0xb4, 0xa7, 0x49, 0x45, 0x8f, 0xf5, 0x2a, 0xf5, 0x0b, 0x2c, 0x2d, 0x2c, + 0xf9, 0x20, 0xb4, 0xa7, 0x69, 0x05, 0x66, 0xb8, 0xb2, 0x19, 0x0c, 0xfd, 0x70, 0xfc, 0x04, 0x74, 0x46, 0x6d, 0x4b, + 0x08, 0x63, 0x37, 0x08, 0x2b, 0xef, 0x89, 0x7c, 0xf5, 0xd8, 0xbb, 0x18, 0x84, 0xdc, 0x6c, 0x66, 0xd1, 0xc0, 0x74, + 0x3f, 0x93, 0xcd, 0xe6, 0xe9, 0xe6, 0x7a, 0x51, 0x42, 0x05, 0x6c, 0xb7, 0x95, 0x20, 0xf8, 0xf7, 0x63, 0x36, 0xc3, + 0xbf, 0x59, 0xbf, 0xdf, 0x0b, 0xf1, 0x17, 0xc7, 0x60, 0x46, 0x73, 0xb1, 0x60, 0x1f, 0x41, 0xc6, 0x44, 0x22, 0x4c, + 0x55, 0xc6, 0x80, 0xac, 0x02, 0x8b, 0x40, 0xf3, 0x81, 0xca, 0x85, 0x99, 0xec, 0x65, 0xce, 0x35, 0xe4, 0x79, 0x6b, + 0x9c, 0xb2, 0x51, 0x96, 0x28, 0x57, 0x8e, 0x6c, 0x14, 0xe7, 0x59, 0x5c, 0xf2, 0x72, 0xbb, 0xd5, 0x87, 0x63, 0x52, + 0x70, 0x60, 0xd7, 0x15, 0x95, 0x2a, 0x59, 0x47, 0xaa, 0x07, 0x5e, 0x1a, 0x16, 0xb8, 0x4f, 0xf9, 0xbc, 0x30, 0x34, + 0x62, 0x0f, 0x84, 0x19, 0x4c, 0xdd, 0xd2, 0x7b, 0x61, 0x01, 0xcd, 0x2b, 0x09, 0xd9, 0x60, 0xaa, 0x67, 0xe1, 0x1b, + 0x33, 0x31, 0x2f, 0x16, 0x10, 0x56, 0xa7, 0x58, 0x68, 0x66, 0x93, 0x26, 0x2c, 0x06, 0xd8, 0xbc, 0x98, 0x4c, 0x21, + 0xbe, 0xbb, 0x2a, 0x27, 0x5e, 0x98, 0xfb, 0x76, 0xe2, 0x90, 0x43, 0xe0, 0x55, 0x6d, 0xd0, 0xd5, 0x6c, 0xc3, 0x51, + 0x47, 0xca, 0x89, 0xc9, 0xef, 0xa7, 0x0a, 0x42, 0xdc, 0x89, 0x23, 0xe1, 0xf2, 0x66, 0xbb, 0xf0, 0xb2, 0x03, 0x41, + 0x47, 0x0d, 0x4e, 0xf9, 0x99, 0xc1, 0xd1, 0x98, 0xa4, 0x1b, 0xef, 0x04, 0x29, 0xc2, 0x98, 0x6c, 0x24, 0x3b, 0x93, + 0xa1, 0x98, 0xc7, 0x0b, 0x50, 0x5e, 0xc6, 0x0b, 0xb0, 0x34, 0x32, 0x06, 0xa9, 0x20, 0xbf, 0xe3, 0x5e, 0x28, 0x2c, + 0x8a, 0x2b, 0x44, 0x7a, 0x56, 0xbf, 0xc7, 0x45, 0x3b, 0x14, 0x08, 0x8a, 0x3b, 0x94, 0x79, 0x72, 0xd6, 0x63, 0x81, + 0xc4, 0x86, 0x80, 0xf1, 0x95, 0x4e, 0x53, 0xad, 0x75, 0x6f, 0x6c, 0xf4, 0xaa, 0x69, 0x36, 0x12, 0xb2, 0x3a, 0x3d, + 0x07, 0x91, 0x92, 0x8f, 0x8e, 0x8f, 0xfc, 0x22, 0xee, 0x2c, 0xf3, 0xd6, 0xb6, 0xa8, 0x64, 0x47, 0x1b, 0x00, 0x2d, + 0xd4, 0xd1, 0xb3, 0x94, 0xdc, 0xa6, 0x24, 0xb5, 0xdb, 0x14, 0xb0, 0x92, 0xfc, 0x05, 0x0c, 0xc1, 0xd7, 0xf6, 0x84, + 0xd3, 0xb1, 0x42, 0xbc, 0xa6, 0x29, 0x22, 0x4d, 0x86, 0x25, 0xc5, 0xb1, 0x2d, 0x11, 0x05, 0xd5, 0x96, 0x65, 0x07, + 0xc3, 0x44, 0x09, 0x7e, 0x96, 0x7a, 0x94, 0x28, 0x08, 0xa8, 0x1e, 0x72, 0x90, 0x60, 0xdb, 0x06, 0xc2, 0x03, 0xf2, + 0x88, 0xde, 0x58, 0x7f, 0x9f, 0x75, 0x9e, 0x5d, 0x68, 0x9e, 0xcb, 0xf5, 0xae, 0x30, 0x63, 0x84, 0x27, 0x99, 0x09, + 0x1b, 0xe0, 0x9d, 0x67, 0x46, 0x6d, 0xd3, 0xf3, 0xf0, 0xda, 0x9e, 0x63, 0x84, 0xbe, 0x3b, 0x06, 0xdd, 0x04, 0xf3, + 0xea, 0xb0, 0x59, 0xaf, 0x14, 0xa4, 0x86, 0xa9, 0x45, 0x13, 0xb3, 0x9e, 0x35, 0x28, 0xdf, 0x6e, 0x7b, 0x7a, 0xae, + 0xee, 0x9e, 0xbb, 0xed, 0xb6, 0x87, 0xdd, 0x7a, 0x96, 0x76, 0x5b, 0xc5, 0x57, 0xea, 0x83, 0xf6, 0xf8, 0x73, 0x37, + 0xfe, 0xdc, 0x20, 0x9b, 0x94, 0x8e, 0x66, 0xda, 0xfa, 0x20, 0x3c, 0x70, 0x7a, 0xdb, 0x68, 0xd2, 0xf7, 0x59, 0x28, + 0xe9, 0x4a, 0x34, 0xaa, 0x33, 0x21, 0xcc, 0x58, 0x75, 0xff, 0xfa, 0xbf, 0x7f, 0x15, 0xe0, 0x11, 0xa7, 0x76, 0xf6, + 0x9d, 0x0d, 0x2a, 0x1a, 0x6d, 0xe1, 0x48, 0x11, 0x7a, 0x40, 0x12, 0xee, 0x6a, 0x59, 0x8b, 0xdb, 0x3c, 0xc9, 0xee, + 0xa7, 0x4f, 0xef, 0x53, 0xdf, 0x0b, 0xc1, 0x2d, 0xb3, 0xcc, 0x1c, 0x78, 0x15, 0xc5, 0x01, 0x8d, 0xba, 0x68, 0xdf, + 0x65, 0x56, 0x96, 0xe0, 0xf5, 0x02, 0xf7, 0xca, 0x13, 0xee, 0xc3, 0xef, 0x5d, 0x54, 0xcd, 0x4d, 0x7a, 0x92, 0xcd, + 0xb3, 0xc5, 0x76, 0x1b, 0xe2, 0xdf, 0xae, 0x16, 0x39, 0x9a, 0x3c, 0x07, 0x9d, 0x26, 0x46, 0x32, 0x62, 0xba, 0x71, + 0xde, 0xe6, 0x7f, 0x2d, 0x1a, 0x4e, 0x13, 0xcf, 0x81, 0x5e, 0xcc, 0x8e, 0x41, 0x26, 0x65, 0x40, 0x0e, 0xc4, 0x4c, + 0xaf, 0x19, 0x88, 0x46, 0x26, 0x22, 0xc0, 0x15, 0xc6, 0x46, 0xa2, 0xd1, 0x09, 0x27, 0x35, 0x01, 0x0b, 0x56, 0x5b, + 0xde, 0x4f, 0x96, 0xb6, 0x55, 0xc5, 0xad, 0xb7, 0xa4, 0x39, 0xae, 0x03, 0xe7, 0xeb, 0x60, 0x86, 0xd8, 0x94, 0x5d, + 0x2d, 0x90, 0xfb, 0xe5, 0x35, 0xed, 0x8d, 0xeb, 0x04, 0x66, 0x6d, 0x53, 0x5b, 0xc6, 0xcf, 0x96, 0xfe, 0x4e, 0x0f, + 0xae, 0x32, 0x06, 0x9b, 0x1b, 0x2b, 0x0d, 0xbb, 0x6f, 0x3c, 0x5f, 0x0a, 0x08, 0x4f, 0xe7, 0xd3, 0xe3, 0x93, 0xcc, + 0xa3, 0xc7, 0x40, 0x74, 0xcc, 0x47, 0xa5, 0xfb, 0xc8, 0xee, 0x5e, 0x3f, 0x20, 0xe0, 0xbc, 0x6a, 0x17, 0x34, 0x2f, + 0x17, 0x10, 0x58, 0xd5, 0x2b, 0xaf, 0xb0, 0x7c, 0x66, 0xcc, 0x2e, 0x80, 0x0c, 0x15, 0x04, 0x02, 0x77, 0x77, 0x9d, + 0x0b, 0xb1, 0xea, 0xb0, 0x32, 0xa7, 0x49, 0xd8, 0x51, 0x88, 0xe6, 0xad, 0xc1, 0x2c, 0xf8, 0xaf, 0x60, 0x50, 0x0e, + 0x82, 0x28, 0x88, 0x82, 0x80, 0x0c, 0x0a, 0xf8, 0x85, 0xb8, 0x6b, 0x04, 0x63, 0xb6, 0x40, 0x87, 0xdf, 0x72, 0xe6, + 0x33, 0x22, 0x2f, 0xfd, 0xb0, 0x9e, 0xde, 0x00, 0x9c, 0x49, 0x99, 0xf3, 0x18, 0x7d, 0x4e, 0xde, 0x72, 0x96, 0x11, + 0xfa, 0xd6, 0x3b, 0x95, 0x1f, 0xf0, 0x46, 0xb0, 0xbf, 0xdd, 0x61, 0x7b, 0x01, 0xf2, 0x8a, 0xde, 0x98, 0xbe, 0xe5, + 0x24, 0xca, 0x1a, 0xce, 0xd4, 0x1c, 0x7a, 0x56, 0x59, 0xd6, 0x8a, 0x1a, 0x72, 0x83, 0x62, 0x6e, 0x64, 0x99, 0x9c, + 0x4c, 0x5b, 0xcd, 0xa9, 0xc0, 0x75, 0x67, 0xd7, 0x0b, 0x48, 0x0e, 0x85, 0x66, 0xe9, 0x6c, 0x38, 0x6f, 0xdb, 0xb2, + 0x67, 0xad, 0x53, 0xc8, 0x6b, 0x88, 0x8a, 0x06, 0xe9, 0x08, 0xa8, 0xa1, 0x15, 0x17, 0x15, 0xb8, 0x30, 0x9b, 0xf6, + 0x70, 0xd3, 0x1e, 0xd3, 0x8c, 0x9f, 0x20, 0x66, 0x1e, 0xc7, 0x96, 0x81, 0x1d, 0x89, 0xc3, 0xf7, 0x71, 0xbe, 0x40, + 0xbb, 0xf4, 0xd6, 0xd5, 0xe2, 0x11, 0xd6, 0x9e, 0xb7, 0x42, 0x42, 0x80, 0xf8, 0x34, 0x95, 0x6e, 0xb7, 0x41, 0x00, + 0x03, 0xdc, 0xef, 0xf7, 0x80, 0x6b, 0x35, 0xec, 0xa4, 0xb9, 0x35, 0x5b, 0x62, 0xaf, 0x28, 0x3c, 0x06, 0xe6, 0xd4, + 0xfc, 0x67, 0x10, 0x50, 0x3c, 0x77, 0x43, 0xb0, 0x37, 0x65, 0x47, 0x1b, 0x88, 0x38, 0x54, 0xe0, 0x03, 0xca, 0x85, + 0x41, 0xcc, 0xad, 0xe3, 0x78, 0x18, 0xf6, 0x49, 0x7d, 0x88, 0x63, 0x91, 0x67, 0xa1, 0x23, 0x2c, 0x95, 0x21, 0x2c, + 0x5c, 0x31, 0xd2, 0x41, 0x1c, 0xd4, 0xa4, 0x73, 0xb0, 0x2a, 0x17, 0x7c, 0xb9, 0xd7, 0x7b, 0x0d, 0x30, 0xe9, 0x99, + 0x37, 0x2c, 0x2f, 0x3c, 0x40, 0xb4, 0x5e, 0x0f, 0x17, 0x8a, 0x47, 0x26, 0x1a, 0x68, 0x9c, 0xf8, 0xd2, 0xb2, 0xeb, + 0x33, 0x2d, 0x2b, 0x19, 0x8d, 0x46, 0x55, 0xad, 0x24, 0x1f, 0xf6, 0xbb, 0x4f, 0x2d, 0x14, 0x4f, 0x19, 0xa7, 0x3c, + 0x05, 0xcb, 0x77, 0x43, 0xe9, 0xe6, 0x0b, 0xba, 0xe2, 0x22, 0x55, 0x3f, 0x3d, 0xf4, 0xcd, 0x06, 0x71, 0xcd, 0x9a, + 0x3a, 0x1c, 0x3b, 0xfc, 0x10, 0x00, 0xd3, 0x3e, 0xcc, 0x5c, 0xba, 0x86, 0xe9, 0x05, 0xf1, 0x6c, 0x5c, 0xf0, 0xd0, + 0xe5, 0x01, 0xec, 0x43, 0x73, 0x48, 0xe2, 0xa7, 0xf0, 0x73, 0x66, 0xd2, 0x3a, 0x3e, 0xc3, 0xd9, 0x8c, 0x4a, 0x75, + 0x23, 0x68, 0xbf, 0x86, 0x44, 0x62, 0x90, 0x9e, 0x1b, 0x0c, 0x45, 0xeb, 0x6e, 0x03, 0x57, 0x7e, 0x4b, 0xef, 0x7c, + 0x1a, 0x04, 0x58, 0xdf, 0x58, 0x0c, 0x00, 0xa8, 0xe2, 0x0f, 0x54, 0x5d, 0x99, 0x2b, 0x8a, 0x69, 0x98, 0x4a, 0xb4, + 0x77, 0x1c, 0xd7, 0x51, 0xe3, 0x3a, 0x2c, 0x58, 0x69, 0x6d, 0x9b, 0xdd, 0x5b, 0x88, 0x3f, 0xa2, 0x4b, 0x40, 0xb5, + 0x20, 0xee, 0x04, 0xf0, 0xa1, 0x91, 0xea, 0x40, 0x90, 0xdd, 0x07, 0x07, 0x00, 0xbc, 0xe1, 0x79, 0x18, 0xc2, 0x1f, + 0x58, 0x38, 0xb0, 0x2c, 0x55, 0x3f, 0x97, 0xd3, 0x18, 0xce, 0xdd, 0x5c, 0xed, 0xf0, 0xd9, 0x12, 0x14, 0x9b, 0x6a, + 0x4e, 0xcd, 0xe5, 0x2b, 0x6f, 0xec, 0xf7, 0x98, 0x60, 0x1e, 0x33, 0xdb, 0xf0, 0x5b, 0x4f, 0xb7, 0xf5, 0x0d, 0x76, + 0x03, 0x27, 0xed, 0x85, 0xd3, 0x5e, 0x6c, 0x97, 0x06, 0xf2, 0xaf, 0x6e, 0x08, 0x11, 0xde, 0x6b, 0x62, 0x91, 0x35, + 0x64, 0x3a, 0x16, 0x2b, 0x44, 0xb5, 0xa9, 0x78, 0xaa, 0x0d, 0x04, 0xca, 0xa9, 0xba, 0x30, 0xb5, 0x52, 0x99, 0x30, + 0x88, 0x3b, 0x25, 0x2c, 0xaa, 0x0c, 0x30, 0x0c, 0x2a, 0xa4, 0xb8, 0xb6, 0x9e, 0xbf, 0x70, 0xf9, 0x66, 0xa6, 0xcd, + 0xf6, 0xd3, 0x17, 0x79, 0x7c, 0xb1, 0xdd, 0x86, 0xdd, 0x2f, 0xc0, 0x1c, 0xb5, 0x54, 0x1a, 0x46, 0x70, 0x02, 0x51, + 0x92, 0xeb, 0x3b, 0x72, 0x4e, 0x1c, 0x27, 0xd7, 0x6e, 0xde, 0x6c, 0x27, 0xc5, 0x08, 0x2c, 0xe0, 0xc4, 0x45, 0x3a, + 0xd0, 0x52, 0x49, 0x6a, 0x4f, 0x01, 0x6f, 0xd3, 0x3b, 0x4a, 0x85, 0x57, 0x0b, 0x4d, 0x42, 0x2a, 0x77, 0x2f, 0xb1, + 0xa3, 0x06, 0x9c, 0x93, 0xba, 0x83, 0x80, 0xd3, 0x9e, 0x6e, 0xac, 0x55, 0x24, 0x9b, 0x04, 0xef, 0x95, 0x1e, 0xba, + 0x44, 0x3b, 0xb5, 0xbb, 0x6d, 0x55, 0xb6, 0x50, 0x30, 0xf7, 0x72, 0x96, 0xa8, 0xe3, 0x01, 0x85, 0x2e, 0xea, 0x68, + 0xc8, 0x17, 0xa4, 0xd0, 0x2b, 0x47, 0xab, 0x9a, 0x77, 0x25, 0x03, 0xa5, 0x5a, 0x05, 0x79, 0x4d, 0xac, 0xfb, 0x5a, + 0xd6, 0x58, 0x5c, 0x39, 0x21, 0x85, 0x4d, 0xf8, 0xd2, 0x52, 0x2c, 0xcc, 0x62, 0x6f, 0x4c, 0x7d, 0xe1, 0x12, 0xa1, + 0xed, 0x6e, 0x43, 0x8c, 0x36, 0x58, 0x37, 0xdb, 0xed, 0xfb, 0x22, 0x9c, 0x67, 0x0b, 0x2a, 0x47, 0x59, 0x8a, 0x90, + 0x6a, 0xc6, 0x63, 0xd9, 0x76, 0xc1, 0x4c, 0x0c, 0x75, 0xed, 0xf1, 0x92, 0x4c, 0xb1, 0x36, 0x49, 0x8e, 0xe2, 0x33, + 0x59, 0xa8, 0xb5, 0x46, 0x08, 0x1e, 0xee, 0xdf, 0xa5, 0x10, 0xd3, 0xce, 0xac, 0xbb, 0x5f, 0x76, 0x6e, 0x88, 0xdf, + 0x41, 0x60, 0x85, 0x92, 0xbd, 0x2f, 0x46, 0x67, 0x99, 0x48, 0x71, 0xa7, 0xaa, 0x28, 0xc1, 0x6a, 0x1d, 0x34, 0x5b, + 0x6e, 0xef, 0xc5, 0x96, 0x28, 0x40, 0x9c, 0x67, 0xa1, 0x19, 0xcf, 0xca, 0x59, 0xce, 0x64, 0x14, 0x1b, 0x12, 0x95, + 0x5e, 0x94, 0x78, 0x9f, 0xa7, 0x31, 0x3d, 0x74, 0x6b, 0x10, 0x5c, 0x57, 0x77, 0x36, 0xd2, 0x7c, 0x41, 0x88, 0x9a, + 0x00, 0x09, 0x1b, 0xd5, 0x9c, 0x5a, 0x17, 0xe2, 0x7e, 0x56, 0xf9, 0x56, 0x1f, 0xc4, 0x17, 0x02, 0x78, 0x58, 0x6f, + 0x7b, 0x5f, 0x0a, 0x8f, 0xb5, 0xc1, 0xb7, 0xdb, 0xed, 0x85, 0x98, 0x07, 0x81, 0xc7, 0x68, 0xfe, 0xa0, 0x24, 0xe6, + 0xbd, 0x31, 0x85, 0x15, 0xef, 0xbb, 0xf8, 0x75, 0x93, 0x5a, 0x6b, 0x91, 0xbb, 0xc3, 0xf5, 0x01, 0xcf, 0x53, 0xe2, + 0x68, 0x47, 0xe5, 0x54, 0x5a, 0xdb, 0x01, 0xec, 0x8a, 0xc0, 0x40, 0xd9, 0x1f, 0x52, 0xb6, 0x01, 0xf3, 0x44, 0xb0, + 0x3e, 0x42, 0xbf, 0x2d, 0xa5, 0x3f, 0x19, 0xa3, 0x71, 0x8f, 0x5c, 0x57, 0xd1, 0x01, 0xd7, 0xd1, 0xec, 0x79, 0xf4, + 0x8f, 0x27, 0x63, 0x5a, 0xc4, 0x22, 0x95, 0x97, 0xa0, 0x82, 0x00, 0x65, 0x08, 0x3a, 0x42, 0x68, 0x6a, 0x00, 0x1a, + 0x04, 0x37, 0x00, 0xbf, 0x76, 0x3a, 0x51, 0xda, 0x9a, 0x7c, 0x8c, 0x56, 0x55, 0xe4, 0xac, 0x0d, 0xed, 0xa6, 0x92, + 0x43, 0xf2, 0xb0, 0x04, 0x7c, 0x4b, 0x6c, 0x96, 0xb2, 0x41, 0x51, 0x9b, 0x4d, 0xbd, 0x56, 0xec, 0xc8, 0x4d, 0xa3, + 0x68, 0xb3, 0x16, 0xb5, 0xdd, 0xc8, 0x7c, 0x31, 0xbd, 0xb1, 0xc2, 0xc0, 0xa9, 0x69, 0xcd, 0xf5, 0x0e, 0x94, 0x9c, + 0xad, 0xcf, 0xe4, 0x26, 0x40, 0x1c, 0x60, 0xb8, 0x6e, 0xe6, 0xd7, 0x0b, 0x42, 0x6f, 0xd8, 0x8d, 0x15, 0xab, 0x5e, + 0x5b, 0xb9, 0x88, 0x49, 0xbb, 0x1e, 0x4c, 0xe0, 0x32, 0xce, 0x0a, 0xfb, 0x42, 0xab, 0x1b, 0x8a, 0x8e, 0xb6, 0x49, + 0xfb, 0x79, 0x47, 0xbb, 0xe1, 0x82, 0x6f, 0xc5, 0x3a, 0xce, 0x2d, 0x6b, 0xaa, 0xd0, 0xb4, 0x03, 0xbd, 0x1d, 0x02, + 0x9a, 0xb3, 0x31, 0x5d, 0xd2, 0x14, 0x2f, 0xd0, 0x74, 0x0d, 0x66, 0x3a, 0xe7, 0xd0, 0xd7, 0x6e, 0x1f, 0xed, 0x73, + 0xd5, 0x13, 0xe1, 0x2d, 0x51, 0xf0, 0x6d, 0x49, 0xc1, 0x4b, 0x2d, 0xe7, 0xb1, 0x99, 0x43, 0xc0, 0xa7, 0x51, 0x25, + 0x7a, 0x27, 0xc5, 0x05, 0x68, 0x33, 0xe1, 0x08, 0x34, 0x55, 0x23, 0xb6, 0x72, 0x80, 0xdb, 0x8b, 0xa7, 0x01, 0xa1, + 0x20, 0xd5, 0x5d, 0xdb, 0x15, 0x79, 0xc3, 0x8e, 0x36, 0x37, 0x60, 0x26, 0x5c, 0xad, 0xcb, 0xd6, 0x57, 0x36, 0xd9, + 0x7d, 0x5c, 0x13, 0x6c, 0xbb, 0xb7, 0x41, 0xc2, 0x1b, 0x7a, 0x4d, 0x36, 0xd7, 0xfd, 0x7e, 0x08, 0xfd, 0x21, 0x54, + 0x77, 0xe8, 0xa6, 0xb3, 0x43, 0x37, 0x5e, 0x3b, 0xcf, 0xac, 0x9e, 0x4f, 0x79, 0x87, 0xbc, 0x47, 0x93, 0x35, 0xba, + 0x8a, 0x6f, 0x61, 0x53, 0x47, 0x15, 0x55, 0x95, 0x47, 0x09, 0x05, 0x95, 0x78, 0xc6, 0xcb, 0x13, 0x8e, 0xb1, 0x5e, + 0xf5, 0xd3, 0x5b, 0xcd, 0xab, 0xad, 0xcd, 0xda, 0x2c, 0xd7, 0x67, 0x60, 0x21, 0x71, 0xc6, 0xa3, 0x4b, 0x4d, 0x4b, + 0x2e, 0x7c, 0x28, 0x4d, 0x1c, 0x95, 0xe0, 0x3c, 0xce, 0x72, 0x50, 0xe3, 0x9e, 0x37, 0xfb, 0x1f, 0x6a, 0xdb, 0xb1, + 0x65, 0xe3, 0xcc, 0xbd, 0x0a, 0xc9, 0xe6, 0x7f, 0x6c, 0xa0, 0x5e, 0x85, 0x18, 0x21, 0xd6, 0x2c, 0xe8, 0x37, 0x0c, + 0x62, 0x85, 0x06, 0xe5, 0x3a, 0x49, 0x78, 0x59, 0x06, 0x46, 0xa9, 0xb5, 0x66, 0x6b, 0x73, 0x9e, 0x3d, 0x60, 0x47, + 0x0f, 0x7a, 0x8c, 0xdd, 0x10, 0x9a, 0x68, 0x9d, 0x90, 0xa9, 0x31, 0xf2, 0xb4, 0x40, 0xba, 0x43, 0x51, 0x76, 0x1e, + 0xbe, 0x41, 0x21, 0x4b, 0x7b, 0x9f, 0x9b, 0x13, 0x59, 0x7d, 0xa3, 0x8d, 0x50, 0x22, 0x95, 0x08, 0xb2, 0xf1, 0x6b, + 0x04, 0x30, 0x86, 0x66, 0x07, 0x64, 0xb3, 0x64, 0x27, 0xf4, 0xd4, 0x9a, 0x04, 0xc1, 0xeb, 0x37, 0x2a, 0xd1, 0x8c, + 0xb2, 0x22, 0xba, 0xca, 0xe8, 0xe7, 0x36, 0x24, 0xd1, 0x69, 0x48, 0xfc, 0xdc, 0xb0, 0xb4, 0xae, 0x42, 0x14, 0x33, + 0x9b, 0x0d, 0xaf, 0x15, 0x51, 0x8d, 0x6d, 0x65, 0x7c, 0xcc, 0x6f, 0x6c, 0x1a, 0x99, 0x42, 0x5f, 0x87, 0x93, 0x7e, + 0x1f, 0xfe, 0x6a, 0xfa, 0x81, 0xb7, 0x14, 0xfc, 0xc5, 0x1e, 0x90, 0x3a, 0x61, 0x01, 0xc0, 0x33, 0xe6, 0xbc, 0x6a, + 0x4e, 0xe0, 0x03, 0x76, 0xb4, 0x79, 0x10, 0x9e, 0x34, 0x66, 0xee, 0x36, 0xc4, 0x4b, 0x55, 0xd2, 0xf3, 0xe6, 0xc9, + 0x0c, 0xc4, 0xca, 0x6a, 0xcd, 0x6f, 0x98, 0xd5, 0x27, 0x00, 0x91, 0xba, 0xb1, 0x0e, 0xb6, 0xf8, 0xb1, 0xe9, 0x32, + 0xd9, 0xa4, 0xac, 0xcd, 0x44, 0x29, 0x15, 0x49, 0x73, 0x11, 0x40, 0xbf, 0x61, 0x38, 0x6a, 0x80, 0x3b, 0xd7, 0x63, + 0x6f, 0x86, 0xc6, 0x1b, 0x53, 0x43, 0xcf, 0x36, 0x7a, 0x79, 0x3b, 0x0a, 0x61, 0xc6, 0x22, 0xba, 0x71, 0xc7, 0x62, + 0x78, 0x42, 0xdf, 0x40, 0x85, 0xaf, 0x42, 0x8c, 0x2e, 0x4c, 0xea, 0x7a, 0xba, 0x56, 0x5b, 0xe9, 0x9a, 0xd0, 0x1c, + 0xa3, 0x1a, 0x79, 0x6d, 0xbb, 0xa5, 0x46, 0x68, 0x4f, 0x28, 0x0f, 0x6f, 0x68, 0x45, 0xaf, 0x2d, 0x8b, 0xe0, 0xe4, + 0xc7, 0x5e, 0x7e, 0x42, 0xcf, 0x3c, 0x81, 0x49, 0xd1, 0xd6, 0x00, 0x7e, 0x40, 0xfd, 0x70, 0x56, 0x4f, 0xad, 0x94, + 0xc3, 0x53, 0xf8, 0x92, 0x0d, 0xc8, 0x15, 0xf4, 0x62, 0x8d, 0xd9, 0x51, 0x0c, 0x3a, 0xa8, 0x9d, 0xdd, 0xe1, 0x4d, + 0x4a, 0x19, 0xa2, 0x35, 0xa2, 0x83, 0xbc, 0xfa, 0x15, 0x34, 0x7d, 0x90, 0x16, 0xa6, 0x74, 0x8d, 0x02, 0x1e, 0xd0, + 0x37, 0xf5, 0xfb, 0x39, 0x3e, 0xd7, 0x9e, 0x65, 0x9a, 0xb2, 0x40, 0x26, 0x74, 0xe9, 0xc5, 0xed, 0x02, 0x69, 0xb3, + 0x63, 0x15, 0x80, 0x15, 0x49, 0xa0, 0x11, 0x09, 0x58, 0x2e, 0x79, 0xe2, 0xb2, 0x0d, 0x1a, 0xd4, 0x44, 0x25, 0x85, + 0x2c, 0x91, 0x04, 0x7e, 0x18, 0x41, 0x99, 0xa2, 0x18, 0xc4, 0xbd, 0x7a, 0x79, 0xc5, 0x35, 0x35, 0x60, 0x4d, 0x11, + 0x4c, 0xb0, 0x4e, 0xa7, 0x40, 0x6c, 0xc5, 0x7a, 0x05, 0x9e, 0xa8, 0xee, 0x22, 0x89, 0x2c, 0x01, 0x1a, 0xe8, 0xf9, + 0xd2, 0x69, 0xb7, 0xbc, 0x3d, 0xd1, 0x52, 0xc5, 0xe6, 0xde, 0x8b, 0x85, 0xe5, 0x1e, 0x2b, 0x7f, 0x3b, 0xd0, 0x5e, + 0x58, 0xed, 0x88, 0xa8, 0xc1, 0xea, 0xb0, 0x6d, 0xe7, 0x87, 0xd2, 0x50, 0xdd, 0x2b, 0xc7, 0x04, 0x54, 0x74, 0x15, + 0x57, 0xcb, 0x28, 0x1b, 0xc1, 0x9f, 0xed, 0x36, 0xd8, 0x0f, 0xc0, 0x22, 0xf4, 0xc3, 0xbb, 0x9f, 0x22, 0x0c, 0x57, + 0xf5, 0xe1, 0xdd, 0x4f, 0xdb, 0xed, 0x93, 0xf1, 0xd8, 0x70, 0x05, 0x4e, 0xad, 0x03, 0xfc, 0x81, 0x61, 0x1b, 0xec, + 0x92, 0xdd, 0x6e, 0x9f, 0x00, 0x07, 0xa1, 0xd8, 0x06, 0xb3, 0x8b, 0x95, 0x63, 0x9b, 0x62, 0x35, 0xf4, 0x8e, 0x04, + 0xec, 0xbe, 0x1d, 0x96, 0x62, 0x97, 0xfa, 0xa8, 0x90, 0x94, 0x7a, 0xd1, 0x3f, 0xef, 0x14, 0x58, 0x52, 0x30, 0xe5, + 0x0d, 0x96, 0x55, 0xb5, 0x2a, 0xa3, 0xfd, 0xfd, 0x78, 0x95, 0x8d, 0xca, 0x0c, 0xb6, 0x79, 0x79, 0x75, 0x01, 0x00, + 0x13, 0x01, 0x6d, 0xbc, 0x5b, 0x8b, 0xcc, 0xbc, 0x58, 0xd0, 0x65, 0x86, 0x6b, 0x12, 0xcc, 0x0e, 0x72, 0x6e, 0x75, + 0x93, 0x53, 0x62, 0x1f, 0xc0, 0x06, 0x73, 0xbb, 0x6d, 0xf0, 0x0b, 0x47, 0xa3, 0x27, 0xb3, 0x65, 0xa6, 0x0d, 0x5c, + 0xb9, 0xd9, 0xff, 0x24, 0xf2, 0xd2, 0x50, 0xf1, 0x49, 0xa6, 0xcf, 0x33, 0xe0, 0xf3, 0xd8, 0x27, 0x11, 0xfa, 0x2c, + 0x57, 0xa3, 0x35, 0xc0, 0xc6, 0x66, 0xe7, 0xb7, 0xa3, 0x94, 0x43, 0x84, 0x8e, 0xc0, 0xaa, 0x6b, 0x96, 0x19, 0xf1, + 0x6d, 0x2a, 0x6e, 0x5a, 0xaa, 0xb0, 0x4f, 0xc2, 0x73, 0xde, 0xe1, 0xc6, 0x71, 0xa8, 0x37, 0x89, 0xc2, 0xe7, 0x28, + 0x44, 0xe5, 0x68, 0x5c, 0xe8, 0xe4, 0x6b, 0x99, 0xc7, 0x84, 0x62, 0x0e, 0xf7, 0xee, 0xf7, 0xd4, 0x99, 0xcb, 0xf8, + 0xc2, 0xbd, 0xe7, 0xbe, 0xcc, 0xe4, 0x4a, 0x02, 0x48, 0x94, 0xaa, 0xfd, 0xe7, 0xcf, 0x48, 0x8d, 0xff, 0x4e, 0xb5, + 0x06, 0xa0, 0xf7, 0x33, 0xd4, 0xe4, 0x08, 0x02, 0xb6, 0x62, 0xea, 0x47, 0x17, 0xb0, 0x92, 0xf9, 0x9f, 0x50, 0xb7, + 0x23, 0xd8, 0x46, 0xc5, 0x13, 0x8a, 0x2a, 0x5a, 0xf0, 0x74, 0x2d, 0xd2, 0x58, 0x24, 0xb7, 0x11, 0xaf, 0xa7, 0x58, + 0x12, 0xb3, 0x11, 0xc3, 0x7e, 0x6e, 0x76, 0xe1, 0x5d, 0xd1, 0x30, 0x89, 0xa7, 0xa5, 0xbf, 0xad, 0xbc, 0xcd, 0x64, + 0x19, 0x67, 0x64, 0xca, 0x15, 0x82, 0xb9, 0xd5, 0xf7, 0x98, 0x13, 0xfc, 0xf1, 0xc1, 0x63, 0x42, 0xaf, 0xe4, 0xb4, + 0x44, 0x90, 0x3e, 0x91, 0x5a, 0xd7, 0x55, 0xec, 0xd7, 0x14, 0xa2, 0x5a, 0x08, 0x06, 0xa1, 0x4c, 0x4d, 0xfb, 0x14, + 0xdf, 0x67, 0xcb, 0xfe, 0x64, 0xca, 0x96, 0x64, 0x23, 0xa0, 0x63, 0xd2, 0x79, 0xbf, 0x7a, 0x7b, 0x76, 0xe6, 0xfd, + 0x06, 0x4d, 0x38, 0xa8, 0x6e, 0xa0, 0x5d, 0x05, 0x99, 0xc6, 0x28, 0x36, 0x8b, 0xb1, 0x76, 0x6b, 0x22, 0x82, 0x20, + 0xdc, 0xe5, 0x2c, 0x6c, 0xb7, 0x13, 0xe2, 0x6d, 0x20, 0x81, 0x02, 0xd7, 0x36, 0xca, 0x49, 0x48, 0xd4, 0x85, 0xcc, + 0x1c, 0x13, 0x92, 0x05, 0x7a, 0x8d, 0x1d, 0x04, 0xf4, 0x98, 0xdb, 0xa7, 0x80, 0xbe, 0x28, 0xd8, 0x31, 0x1f, 0x04, + 0x43, 0x8c, 0x37, 0x1b, 0xd0, 0x8f, 0x52, 0x3d, 0x82, 0xc7, 0x34, 0xb0, 0x5c, 0xf4, 0x75, 0xc1, 0x10, 0x66, 0xe9, + 0xb7, 0x94, 0x4d, 0xbe, 0xf9, 0xa7, 0x9b, 0xdf, 0x33, 0x2d, 0x66, 0x07, 0xa1, 0xb8, 0xbd, 0x9e, 0x00, 0xf1, 0xab, + 0xf8, 0x25, 0x58, 0x9b, 0x6b, 0x89, 0xb7, 0x27, 0x79, 0x10, 0xbe, 0x1c, 0xdd, 0x7e, 0x52, 0x9a, 0x4f, 0x20, 0x68, + 0x8f, 0x93, 0x94, 0xbb, 0xef, 0x4e, 0xa4, 0xab, 0x08, 0x46, 0x0b, 0x10, 0xfc, 0xee, 0xac, 0xe4, 0xb4, 0x29, 0xfc, + 0xc7, 0x3a, 0x5f, 0x60, 0x2c, 0x15, 0x79, 0x82, 0xd3, 0xdf, 0x04, 0x07, 0xf7, 0x6f, 0x65, 0xd6, 0x90, 0xe8, 0x4c, + 0x7d, 0x04, 0xf4, 0x7f, 0xac, 0xc7, 0xef, 0x18, 0x25, 0x7d, 0x49, 0x9c, 0x23, 0x7c, 0x13, 0x2f, 0xd1, 0x74, 0xb1, + 0x37, 0xae, 0xe9, 0xa7, 0xc2, 0xbc, 0xd0, 0x0a, 0x0e, 0xfb, 0xd6, 0x28, 0x3c, 0xf0, 0xcc, 0xfb, 0x4e, 0x34, 0x04, + 0xdd, 0xff, 0xc2, 0xbd, 0xf1, 0x9d, 0x60, 0x19, 0xde, 0x94, 0xb3, 0xcc, 0xdc, 0xe1, 0xae, 0x33, 0x91, 0xca, 0x6b, + 0xc6, 0x82, 0xb5, 0x50, 0xe6, 0xbc, 0x69, 0x30, 0xdb, 0xd4, 0x91, 0x4a, 0x76, 0xdf, 0xff, 0xd5, 0x38, 0x61, 0xb3, + 0x41, 0x70, 0x52, 0xc9, 0x22, 0xbe, 0xe0, 0xc1, 0x54, 0xab, 0x28, 0x32, 0xb0, 0x2b, 0x04, 0xa4, 0x1c, 0xa7, 0xbd, + 0x83, 0x27, 0x4b, 0xcd, 0x4c, 0xc8, 0x6f, 0xab, 0xb3, 0x80, 0xb7, 0x66, 0x34, 0x8f, 0x2b, 0xd8, 0x65, 0xbe, 0x92, + 0xe2, 0xbb, 0x96, 0x24, 0x1b, 0xeb, 0x6f, 0xc8, 0xb0, 0xad, 0x7c, 0xe6, 0x0c, 0x30, 0x77, 0x3e, 0x4a, 0x15, 0xf4, + 0xaf, 0xc7, 0xd8, 0xb5, 0x44, 0x22, 0x20, 0x9c, 0xc5, 0xc4, 0xad, 0x30, 0xe1, 0x30, 0x5d, 0xa0, 0xa0, 0x18, 0x03, + 0x05, 0x9d, 0xc8, 0x90, 0xd3, 0x63, 0x3e, 0x48, 0x1a, 0xb3, 0xf5, 0x97, 0x2a, 0x91, 0x5e, 0x4b, 0x42, 0x4f, 0xe1, + 0xf7, 0xb8, 0xc5, 0x03, 0x35, 0x82, 0x75, 0xba, 0x9b, 0xd3, 0xfe, 0xeb, 0x82, 0x0c, 0x7f, 0x03, 0x6f, 0xb7, 0xd8, + 0x5e, 0x96, 0x13, 0x58, 0xdc, 0xb1, 0x57, 0x3c, 0xcd, 0x55, 0x8b, 0x13, 0xe2, 0x11, 0x8b, 0xdc, 0x27, 0x16, 0x30, + 0xa2, 0x86, 0xd1, 0xf8, 0xf1, 0xe4, 0xcd, 0x6b, 0x8d, 0x61, 0x95, 0xfb, 0x1f, 0xc0, 0x88, 0x6a, 0x69, 0xbb, 0x1d, + 0xf0, 0xe5, 0x08, 0x0d, 0xd8, 0x53, 0x37, 0xd8, 0xfd, 0xbe, 0x49, 0x3b, 0x2a, 0xbd, 0x6c, 0x4e, 0x0c, 0xba, 0xa3, + 0xb4, 0x59, 0x2a, 0x03, 0xe3, 0xae, 0xc2, 0xd1, 0x9c, 0xd8, 0x88, 0x55, 0xbd, 0x0f, 0xc3, 0x25, 0x8d, 0xad, 0xac, + 0xdc, 0xee, 0x26, 0x1c, 0xd9, 0x04, 0xb8, 0x3e, 0x05, 0xed, 0xd5, 0x9c, 0x83, 0x16, 0x94, 0x28, 0x70, 0x44, 0xdb, + 0x6d, 0x08, 0x11, 0x49, 0x8a, 0xe1, 0x64, 0x16, 0x16, 0xc3, 0xa1, 0x1a, 0xf8, 0x82, 0x90, 0xe8, 0x53, 0x31, 0xcf, + 0x16, 0x0a, 0xc1, 0xc8, 0xdf, 0x49, 0xbf, 0x14, 0x8a, 0x53, 0xee, 0x7d, 0x27, 0xc8, 0xe6, 0x5f, 0x29, 0xc6, 0x60, + 0x74, 0x9a, 0xcd, 0x0c, 0x24, 0xac, 0xc7, 0x15, 0x51, 0xeb, 0xc8, 0xce, 0x06, 0xa8, 0x62, 0xd1, 0x34, 0x18, 0xd4, + 0x2d, 0x9e, 0x58, 0xcf, 0xe8, 0x3d, 0xa8, 0x04, 0x51, 0x2d, 0xd8, 0x8d, 0xe1, 0x5a, 0x7b, 0x2d, 0x42, 0x49, 0x39, + 0x69, 0x32, 0x33, 0x56, 0x34, 0x58, 0x80, 0x90, 0x34, 0x2e, 0xab, 0x57, 0x32, 0xcd, 0xce, 0x33, 0x40, 0x90, 0x70, + 0xfe, 0x84, 0xb2, 0xf1, 0xe6, 0xa9, 0x9a, 0x97, 0xae, 0xc4, 0x99, 0x85, 0x3d, 0xe9, 0x7a, 0x4b, 0x0b, 0x12, 0x15, + 0x40, 0xa3, 0x7c, 0x2d, 0xcf, 0xf7, 0x3b, 0x56, 0x21, 0xbb, 0x1f, 0x4e, 0x95, 0xed, 0x10, 0x3f, 0x62, 0x15, 0xf1, + 0x4e, 0xeb, 0x4a, 0x89, 0x34, 0x3a, 0xda, 0x06, 0xc4, 0xb0, 0x65, 0xdf, 0xa2, 0x86, 0x0f, 0xc2, 0x2e, 0x3a, 0xc9, + 0x0f, 0x7a, 0x8a, 0xc7, 0xd6, 0x40, 0xd2, 0xd7, 0x22, 0xf8, 0x1a, 0x1d, 0xe9, 0x44, 0x99, 0x46, 0x62, 0x0a, 0x89, + 0x7e, 0xbd, 0xd0, 0x1a, 0xcb, 0x28, 0xfb, 0x8a, 0xfc, 0x9f, 0x75, 0xf7, 0xbe, 0x13, 0xdb, 0x2d, 0x4c, 0xb2, 0xe7, + 0x81, 0x06, 0x9b, 0x1a, 0xb5, 0x42, 0x38, 0x3b, 0xc7, 0x15, 0x6a, 0xc7, 0x7a, 0x61, 0x09, 0xe4, 0x01, 0x6c, 0x45, + 0x1a, 0x94, 0x41, 0xb2, 0x4f, 0xc5, 0x5c, 0x2c, 0x9c, 0x28, 0x47, 0x2a, 0xfc, 0x33, 0x39, 0x4a, 0x39, 0x5c, 0xc5, + 0xc2, 0x82, 0x21, 0xbf, 0x3a, 0x3a, 0x2f, 0xe4, 0x25, 0x48, 0x4a, 0x0c, 0x43, 0x65, 0x79, 0x5d, 0x5c, 0xb5, 0x25, + 0xa1, 0xbd, 0x53, 0x00, 0xa5, 0x29, 0x40, 0xf0, 0xd2, 0xa8, 0x21, 0x66, 0x1b, 0xb5, 0xbb, 0xa2, 0x3b, 0xc9, 0x01, + 0x75, 0xba, 0x6b, 0xb7, 0xde, 0x94, 0xad, 0xba, 0x15, 0x17, 0xfe, 0x05, 0xa5, 0x1f, 0xf3, 0x41, 0xe1, 0x53, 0x09, + 0xdc, 0xf8, 0x6a, 0x93, 0x65, 0xe7, 0xb7, 0xb8, 0xf4, 0xab, 0xc6, 0xf8, 0xf5, 0xfb, 0x3d, 0xb5, 0x10, 0x1a, 0xa9, + 0xc0, 0x7c, 0xfb, 0xcc, 0x54, 0x65, 0x34, 0xa5, 0xf6, 0x12, 0x5c, 0x39, 0xfb, 0x11, 0x54, 0xc4, 0x75, 0x45, 0x6a, + 0x53, 0x03, 0xb4, 0xe7, 0x65, 0x85, 0x5b, 0x59, 0x80, 0xc7, 0x4e, 0x40, 0xb6, 0x5b, 0x1e, 0x06, 0xfa, 0xd0, 0x09, + 0xfc, 0x2d, 0xf9, 0x0a, 0x99, 0x35, 0xfb, 0xf8, 0x87, 0x16, 0xfc, 0x63, 0x0b, 0x7e, 0x42, 0x71, 0xa7, 0x95, 0xf9, + 0xb7, 0xd2, 0xba, 0xc5, 0xfd, 0x3b, 0x99, 0x26, 0x14, 0x95, 0x09, 0xb5, 0x5f, 0xe9, 0x8f, 0x26, 0x78, 0x94, 0xca, + 0xfe, 0x5e, 0xc2, 0x07, 0xb3, 0xc6, 0x13, 0x6b, 0x3c, 0x19, 0x4e, 0xb7, 0xd2, 0xb0, 0x0c, 0x28, 0xf4, 0xf3, 0x32, + 0x57, 0x54, 0x3f, 0xff, 0xbc, 0xe6, 0x6b, 0xde, 0x6c, 0xb1, 0x4d, 0xba, 0xa7, 0xc1, 0x5e, 0x1e, 0x4d, 0x29, 0x9c, + 0x44, 0x9d, 0x1b, 0x89, 0xba, 0xa8, 0x59, 0x86, 0xea, 0x04, 0xaf, 0xe6, 0xa9, 0x1e, 0xf6, 0x66, 0x22, 0x5a, 0x2b, + 0x29, 0x4b, 0x0c, 0x58, 0xeb, 0xc8, 0x43, 0x72, 0xb7, 0xd6, 0x71, 0xa7, 0xa1, 0x2e, 0x4d, 0xa1, 0x26, 0x58, 0xe1, + 0x02, 0x1c, 0x41, 0xef, 0x8a, 0x90, 0xc3, 0x35, 0x55, 0xe9, 0x17, 0x34, 0x25, 0x4f, 0x3c, 0x45, 0xad, 0x56, 0xa4, + 0xdb, 0x8f, 0x72, 0xec, 0x86, 0x6f, 0x9c, 0x90, 0x13, 0x23, 0xf4, 0x77, 0xc7, 0x52, 0xce, 0xd0, 0xe2, 0x41, 0x9d, + 0x60, 0xbd, 0xbc, 0xa5, 0x40, 0x31, 0x47, 0x97, 0x55, 0xd7, 0xbc, 0x44, 0xdb, 0x97, 0x65, 0xbf, 0x9f, 0xdb, 0x7a, + 0x52, 0x76, 0xb4, 0x59, 0x9a, 0x7d, 0x88, 0x8a, 0x29, 0xdc, 0xf5, 0x89, 0xe6, 0xaf, 0x42, 0x7d, 0xd5, 0x96, 0x39, + 0x1f, 0x71, 0xc4, 0x09, 0xc9, 0x49, 0xfd, 0x87, 0x9a, 0x7a, 0x25, 0xee, 0x57, 0x95, 0xfc, 0x22, 0x8c, 0x15, 0xa3, + 0x25, 0x86, 0x28, 0xd2, 0xee, 0x8d, 0xe9, 0xcb, 0x02, 0xe0, 0xaf, 0x04, 0xfb, 0x94, 0x86, 0x5a, 0xf9, 0x2d, 0xda, + 0x02, 0xfe, 0x8d, 0xe2, 0x06, 0xac, 0x02, 0x03, 0x8c, 0x26, 0xdb, 0x73, 0x9a, 0xc0, 0x01, 0x27, 0xb4, 0x8a, 0x82, + 0x0a, 0x33, 0x34, 0xd4, 0x16, 0x46, 0x5f, 0xa1, 0x8c, 0x5b, 0x65, 0xf6, 0x6e, 0x8c, 0x9d, 0x16, 0x78, 0x0d, 0xff, + 0x46, 0x2f, 0x14, 0xb3, 0x51, 0x07, 0xe9, 0xd1, 0x49, 0x4c, 0x7f, 0xdc, 0xc2, 0xc9, 0xcd, 0xc2, 0x59, 0xd6, 0x2c, + 0x81, 0xee, 0xc0, 0x05, 0x31, 0xee, 0xf7, 0x73, 0x38, 0x32, 0xcd, 0xc8, 0x17, 0x2c, 0xa7, 0x31, 0x5b, 0x52, 0xed, + 0x79, 0x78, 0x51, 0x85, 0x39, 0x5d, 0x5a, 0x19, 0x6f, 0xca, 0x40, 0x65, 0xb4, 0xdd, 0x86, 0xf0, 0xa7, 0xdb, 0xda, + 0x25, 0x9d, 0x2f, 0x21, 0x03, 0xfc, 0x01, 0x89, 0x28, 0x62, 0x81, 0xff, 0x5b, 0x8d, 0x53, 0x7a, 0xa2, 0xb4, 0x66, + 0x09, 0x5d, 0x33, 0x5d, 0x3f, 0x3d, 0x67, 0xeb, 0xc6, 0x52, 0xd8, 0x6e, 0xc3, 0x66, 0x02, 0xd3, 0x9c, 0x2b, 0x99, + 0x9e, 0xa3, 0x4e, 0x0a, 0xa8, 0x58, 0x78, 0x8e, 0xcb, 0x2f, 0x25, 0x14, 0x9a, 0x3b, 0x5f, 0x2e, 0x8c, 0x12, 0x13, + 0x5a, 0x25, 0xbf, 0x7c, 0xa8, 0xcc, 0xd7, 0xc6, 0x43, 0xf0, 0xc7, 0x34, 0x4c, 0x4c, 0x91, 0xa8, 0x10, 0x9d, 0xfd, + 0x02, 0xb2, 0x1c, 0x01, 0xb8, 0x9e, 0xaf, 0x20, 0x0a, 0xdc, 0x1a, 0xe2, 0xc2, 0x43, 0x83, 0xde, 0x16, 0xf2, 0x32, + 0x2b, 0x79, 0x88, 0xf7, 0x04, 0x4f, 0x33, 0x7a, 0xb7, 0xc1, 0x87, 0xb6, 0xf6, 0xe8, 0x09, 0xb2, 0xf1, 0x94, 0xfb, + 0xf5, 0x2f, 0x22, 0x9c, 0x43, 0xf4, 0xce, 0x05, 0xd5, 0xea, 0x6a, 0x07, 0xc8, 0xe5, 0xd9, 0x5e, 0x3d, 0x80, 0xd3, + 0x4d, 0x5f, 0xdf, 0xaa, 0xd0, 0x99, 0x03, 0x48, 0x7b, 0x48, 0xd6, 0x35, 0xd7, 0x3b, 0xc0, 0x3b, 0x12, 0xd7, 0x40, + 0x63, 0xdd, 0xd6, 0xec, 0xb4, 0x47, 0xf1, 0x98, 0xc8, 0xcc, 0x58, 0xa4, 0x18, 0x73, 0xb7, 0x4e, 0x8b, 0xa2, 0x0d, + 0x9a, 0x21, 0xec, 0xde, 0x75, 0xb2, 0x75, 0x2b, 0xe2, 0xfc, 0xdd, 0xb6, 0x2f, 0x30, 0x1a, 0xc6, 0x5c, 0xbb, 0xe7, + 0x1b, 0xba, 0xad, 0xdd, 0xc8, 0x68, 0x24, 0xc8, 0x4c, 0x1d, 0x88, 0xb2, 0xb6, 0x06, 0x6c, 0x0f, 0xb8, 0xde, 0xb4, + 0xc0, 0xcf, 0x9b, 0x18, 0xbc, 0x3d, 0x6b, 0x9c, 0xd2, 0xfa, 0x1a, 0xd7, 0x1c, 0x57, 0x85, 0x88, 0xda, 0x22, 0x05, + 0xc0, 0xb0, 0xf3, 0x05, 0xee, 0xcc, 0x0a, 0x83, 0x39, 0x61, 0xa9, 0x64, 0xa7, 0x72, 0xfd, 0x39, 0x6c, 0x71, 0x90, + 0xca, 0x97, 0x5e, 0x7f, 0xff, 0xf0, 0xc5, 0x17, 0xe8, 0xb6, 0xe7, 0xfc, 0x08, 0x82, 0x4c, 0xa0, 0x83, 0x9a, 0x52, + 0x3d, 0xfe, 0x50, 0x00, 0xb5, 0x87, 0x79, 0xf8, 0xa1, 0x60, 0x22, 0xbe, 0xca, 0x2e, 0xe2, 0x4a, 0x16, 0xa3, 0x2b, + 0x2e, 0x52, 0x59, 0x58, 0xa9, 0x71, 0x70, 0xbc, 0x5a, 0xe5, 0x3c, 0x00, 0x53, 0x79, 0xcb, 0x28, 0x3b, 0xb9, 0xa4, + 0x1e, 0x5c, 0x2d, 0x4f, 0xaf, 0xb4, 0xe8, 0xbc, 0xbc, 0xba, 0x08, 0x22, 0xfc, 0x75, 0x66, 0x7e, 0x5c, 0xc6, 0xe5, + 0xc7, 0x20, 0xb2, 0x36, 0x75, 0xe6, 0x07, 0x4a, 0xe5, 0xc1, 0xdf, 0x09, 0x64, 0xba, 0x3f, 0x14, 0x60, 0x99, 0x6d, + 0x2b, 0x3e, 0x8c, 0xb1, 0xd6, 0xe1, 0x84, 0xcc, 0x54, 0x89, 0xde, 0xbb, 0x64, 0x5d, 0x80, 0xb5, 0x9f, 0xc2, 0x76, + 0x56, 0xb9, 0x66, 0x58, 0x99, 0xaa, 0xc8, 0x10, 0xb4, 0x35, 0xdb, 0x0f, 0xad, 0x13, 0xcd, 0x1c, 0xbd, 0x05, 0xf4, + 0x03, 0xd9, 0xbf, 0xa0, 0x72, 0xcd, 0x3c, 0x1f, 0x9b, 0xc6, 0xeb, 0x07, 0xfb, 0x17, 0x9e, 0x40, 0xc9, 0xde, 0xc9, + 0x51, 0x98, 0x08, 0x9e, 0xc6, 0x66, 0x7c, 0x91, 0x67, 0x05, 0xec, 0xa0, 0xc9, 0x78, 0x4c, 0xbd, 0xa5, 0xd5, 0xba, + 0x39, 0x3a, 0x64, 0xdb, 0xec, 0x61, 0xf5, 0x90, 0x93, 0x7d, 0xde, 0x32, 0xb5, 0x6d, 0x5b, 0xc7, 0x79, 0x9a, 0x7c, + 0x65, 0xba, 0x5f, 0xae, 0x6d, 0x84, 0x78, 0xe5, 0xec, 0xe8, 0xbc, 0xa4, 0x5b, 0xdf, 0x94, 0x86, 0x5e, 0x4b, 0x00, + 0xe6, 0xd3, 0x06, 0xfc, 0x05, 0x93, 0xeb, 0x51, 0xc5, 0xcb, 0x0a, 0x24, 0x2c, 0x28, 0xc2, 0x9b, 0x62, 0x6f, 0x0a, + 0x77, 0xe3, 0xf4, 0x1c, 0x76, 0xe0, 0x62, 0x8a, 0xee, 0x38, 0x31, 0x99, 0x95, 0x46, 0x2b, 0x1a, 0xe9, 0x5f, 0xae, + 0x2f, 0xb1, 0xee, 0x8b, 0x56, 0xe6, 0xd9, 0x9c, 0x0a, 0x9b, 0xde, 0x55, 0x2e, 0x9d, 0xa8, 0xdf, 0x32, 0xe1, 0xca, + 0x95, 0x20, 0x20, 0xd3, 0x82, 0xf5, 0x0a, 0xb3, 0x8b, 0x0a, 0x24, 0x64, 0x60, 0xf8, 0x1a, 0xac, 0x45, 0xc9, 0x8d, + 0x15, 0xac, 0x77, 0xcf, 0xd7, 0x09, 0x42, 0x0a, 0x1e, 0xb8, 0x09, 0xfa, 0xd0, 0xba, 0x79, 0x3b, 0x4a, 0x94, 0x41, + 0x7c, 0x72, 0xed, 0x94, 0x83, 0x04, 0x02, 0x70, 0x60, 0x55, 0x48, 0x12, 0x05, 0x3a, 0x0f, 0xae, 0x66, 0x1c, 0xc1, + 0xe6, 0x95, 0x33, 0x17, 0x37, 0x80, 0xf3, 0xca, 0x9f, 0xcb, 0x06, 0x5b, 0xd6, 0x23, 0xaa, 0xcc, 0x19, 0xa7, 0x18, + 0xd4, 0xc9, 0x12, 0xf4, 0x95, 0xa5, 0xb4, 0x17, 0xa0, 0x69, 0xbc, 0x64, 0x2b, 0xe5, 0x03, 0x40, 0xcf, 0xd8, 0x4a, + 0x19, 0xfb, 0xe3, 0xd7, 0xa7, 0x6c, 0xa5, 0xa5, 0xc1, 0xd3, 0xcb, 0xd9, 0xd9, 0xec, 0x74, 0xc0, 0x0e, 0xa2, 0x50, + 0x1b, 0x30, 0x04, 0x2e, 0x32, 0x41, 0x30, 0x08, 0x35, 0xfe, 0xcb, 0x40, 0x05, 0x08, 0x23, 0x1e, 0x8f, 0x8d, 0x38, + 0x62, 0xe1, 0x78, 0x88, 0xc1, 0xc0, 0x9a, 0x2f, 0x48, 0x40, 0xa8, 0x29, 0x0d, 0x7d, 0x3d, 0xc3, 0xe1, 0x64, 0x6f, + 0x02, 0xa9, 0x98, 0x99, 0xa9, 0xc2, 0xd8, 0x98, 0x44, 0x10, 0xff, 0xb5, 0xb3, 0x5e, 0x28, 0xb7, 0xbb, 0x46, 0x03, + 0x41, 0x33, 0xf8, 0xa2, 0x8a, 0x27, 0x7b, 0xc3, 0xae, 0x8a, 0x71, 0x14, 0xae, 0x8c, 0xf2, 0xed, 0xf4, 0x10, 0xc0, + 0x7c, 0x4f, 0x87, 0xbe, 0x5c, 0xe2, 0x74, 0xff, 0x31, 0x79, 0xf8, 0x98, 0xd0, 0x53, 0x76, 0xfa, 0xd5, 0x63, 0x7a, + 0xaa, 0xc8, 0xc9, 0xde, 0x24, 0xba, 0x62, 0x16, 0x03, 0xe7, 0x40, 0x35, 0x81, 0x5e, 0x8c, 0xd6, 0x42, 0x2d, 0x30, + 0xed, 0xd0, 0x14, 0x7e, 0x3b, 0xde, 0x0b, 0x06, 0x57, 0xed, 0xa6, 0x5f, 0xb5, 0xdb, 0xea, 0x79, 0x75, 0xed, 0x1d, + 0x44, 0xbb, 0xc5, 0x4c, 0xfe, 0x39, 0xde, 0x73, 0x73, 0x80, 0xf5, 0xdd, 0x3f, 0x26, 0xa6, 0x49, 0x3b, 0xa3, 0xe2, + 0xd7, 0xf4, 0x08, 0xfb, 0xd0, 0x2c, 0xb2, 0xa3, 0x0f, 0xc3, 0x7f, 0xab, 0x13, 0xf5, 0xe9, 0x57, 0x07, 0x40, 0x8e, + 0x40, 0x06, 0x8a, 0x25, 0x82, 0x19, 0x0e, 0x34, 0x05, 0x14, 0x64, 0x7a, 0xdc, 0xa9, 0x1e, 0x7e, 0x35, 0x6a, 0x6a, + 0x46, 0xae, 0x60, 0x6a, 0xb0, 0x2d, 0xf8, 0x81, 0xea, 0x86, 0xfe, 0x46, 0xa3, 0x3d, 0x69, 0x27, 0x33, 0xf3, 0x92, + 0xda, 0x38, 0x77, 0x57, 0x10, 0xd0, 0xd9, 0xc1, 0x2d, 0x4a, 0xf6, 0xf5, 0xe1, 0xc5, 0x1e, 0xae, 0x22, 0x40, 0x0d, + 0x63, 0xc1, 0xd7, 0x83, 0x0b, 0xbd, 0xb9, 0xf7, 0x02, 0x32, 0xf8, 0x3a, 0x38, 0xfa, 0x7a, 0x20, 0x07, 0xc1, 0xe1, + 0xfe, 0xc5, 0x51, 0xe0, 0x8c, 0xfb, 0x21, 0xe4, 0xa5, 0xaa, 0x28, 0x66, 0xc2, 0x54, 0x91, 0xd8, 0xda, 0x73, 0x5b, + 0xaf, 0x32, 0x3e, 0xa3, 0xe9, 0xd4, 0x22, 0xa1, 0x87, 0x29, 0x8b, 0xcd, 0xef, 0x60, 0xc2, 0x2f, 0x83, 0xc8, 0x05, + 0x85, 0x9d, 0xe5, 0x51, 0x4c, 0x97, 0xec, 0x46, 0x84, 0x29, 0x4d, 0xf6, 0x73, 0x42, 0xa2, 0x70, 0xa9, 0xc0, 0x04, + 0xd5, 0xeb, 0x04, 0xe2, 0xda, 0xba, 0xcf, 0x6f, 0x44, 0xb8, 0xa4, 0xf9, 0x7e, 0x42, 0x5a, 0x45, 0xb8, 0x08, 0x35, + 0x9b, 0x9a, 0x9e, 0xb3, 0x70, 0x45, 0x2f, 0xd0, 0x54, 0x73, 0x1d, 0x5e, 0x00, 0x97, 0xb7, 0x9e, 0xaf, 0x16, 0xec, + 0xa2, 0x21, 0x7d, 0x33, 0x7c, 0xf1, 0xb9, 0xf5, 0xc9, 0x03, 0x1e, 0xd2, 0xf9, 0xe1, 0xa5, 0x60, 0x03, 0x70, 0x95, + 0xf1, 0xeb, 0xef, 0xe4, 0x8d, 0x9e, 0x97, 0xf6, 0x14, 0xe3, 0xcc, 0xb4, 0x13, 0x93, 0x76, 0x42, 0xee, 0xdf, 0xb7, + 0x7d, 0xf7, 0xe2, 0xb5, 0x72, 0x59, 0xb5, 0x0c, 0x49, 0xbc, 0x56, 0xae, 0xd3, 0x28, 0x39, 0xb5, 0x02, 0x4f, 0x76, + 0xce, 0xab, 0x64, 0xe9, 0x1f, 0x54, 0xd6, 0x6a, 0xc0, 0x1e, 0x23, 0x96, 0x85, 0xc2, 0xb1, 0x7f, 0x95, 0xb1, 0x78, + 0xdd, 0x40, 0x06, 0x46, 0xee, 0xed, 0x55, 0xc6, 0xbc, 0x18, 0xb4, 0xf9, 0xda, 0x0b, 0xdd, 0xe7, 0xa5, 0x2f, 0x5b, + 0xbc, 0x97, 0x53, 0x6a, 0x18, 0x89, 0xe8, 0xde, 0x58, 0x99, 0x51, 0xaa, 0x44, 0xad, 0x41, 0x23, 0x82, 0x8d, 0x5d, + 0x30, 0x50, 0x70, 0x42, 0xe5, 0x9e, 0x3a, 0xdb, 0xb7, 0x53, 0x2a, 0x3d, 0xa0, 0x5d, 0x6a, 0x54, 0xe5, 0x6e, 0x99, + 0x49, 0x56, 0x0d, 0x82, 0xd1, 0x5f, 0xa5, 0x14, 0x33, 0xbc, 0x33, 0xb2, 0x60, 0x0a, 0x56, 0x82, 0xaa, 0x96, 0x61, + 0x39, 0xe4, 0xa8, 0xc5, 0x33, 0x3e, 0xa9, 0x52, 0xff, 0xe8, 0x08, 0x1a, 0x9c, 0xae, 0x5b, 0x41, 0x83, 0x1f, 0x8f, + 0x1f, 0xeb, 0x81, 0x5e, 0xaf, 0xb5, 0xe3, 0xa1, 0xcf, 0x6f, 0x23, 0xde, 0xb8, 0xee, 0x3d, 0xd5, 0x5a, 0x85, 0x32, + 0xd0, 0x62, 0x45, 0xe5, 0x4a, 0x2d, 0xe9, 0xdd, 0x2e, 0x02, 0x60, 0x11, 0x1b, 0xb3, 0xf1, 0xae, 0x6d, 0x56, 0x08, + 0x1a, 0x5d, 0x76, 0xb4, 0x89, 0x07, 0x2c, 0xd1, 0xad, 0x1d, 0x4c, 0x68, 0x7c, 0xc4, 0xca, 0x7e, 0x3f, 0x3f, 0x02, + 0x7a, 0xaa, 0x8d, 0x98, 0x0a, 0x38, 0xf2, 0xbf, 0xb4, 0x22, 0x53, 0x14, 0xd8, 0xac, 0xa9, 0xbb, 0x35, 0x96, 0x91, + 0xe8, 0xcb, 0x94, 0x2e, 0x4f, 0x78, 0x06, 0x4c, 0xe7, 0xeb, 0x96, 0xe3, 0xca, 0xae, 0xe2, 0xc8, 0x53, 0x61, 0x59, + 0x71, 0x5e, 0x85, 0xe3, 0xad, 0xc7, 0x37, 0xd8, 0x37, 0x6c, 0xda, 0xca, 0x1f, 0x42, 0x58, 0x08, 0xaf, 0x32, 0xb8, + 0x8d, 0x68, 0x3b, 0x09, 0x54, 0xde, 0x98, 0xeb, 0x84, 0xb2, 0xb9, 0x3d, 0x5f, 0x7b, 0x06, 0xe9, 0xc4, 0x1c, 0x28, + 0xd5, 0x08, 0x5a, 0xa3, 0x59, 0x50, 0x35, 0xe2, 0x91, 0x33, 0xff, 0x72, 0x06, 0xb1, 0x5a, 0xbe, 0xa4, 0xa9, 0x14, + 0x0d, 0xc0, 0xb8, 0x00, 0x2e, 0x4f, 0x1f, 0xde, 0xfd, 0x74, 0xc2, 0xe3, 0x22, 0x59, 0xbe, 0x8d, 0x8b, 0xf8, 0xb2, + 0x0c, 0x37, 0x6a, 0x8c, 0xe2, 0x9a, 0x4c, 0xc5, 0x80, 0x49, 0xb3, 0x92, 0x9a, 0xbb, 0x52, 0x13, 0x62, 0xac, 0x33, + 0x59, 0x97, 0x95, 0xbc, 0x6c, 0x54, 0xba, 0x2e, 0x32, 0xfc, 0xb8, 0xe5, 0x73, 0xba, 0x0f, 0xc0, 0xa6, 0xc6, 0x85, + 0x34, 0x92, 0xba, 0x10, 0x63, 0x2e, 0xe2, 0x75, 0x7d, 0x3c, 0x6e, 0x74, 0xbd, 0x64, 0x4f, 0xc6, 0x8f, 0xa6, 0xaf, + 0xb2, 0x30, 0x1b, 0x08, 0x32, 0xaa, 0x96, 0x5c, 0xb4, 0x4c, 0x39, 0x95, 0x49, 0x00, 0xfa, 0x78, 0xf6, 0x18, 0x3b, + 0x18, 0x8f, 0xc9, 0xa6, 0x2d, 0x1e, 0xe0, 0x61, 0xba, 0x0e, 0x0b, 0x32, 0xd3, 0x75, 0x44, 0x81, 0xe0, 0x37, 0x55, + 0x00, 0xc8, 0x96, 0xb6, 0x2a, 0xc3, 0xa5, 0xb1, 0x27, 0xe3, 0x09, 0x95, 0xd8, 0xed, 0x90, 0xd4, 0x5e, 0x85, 0x6e, + 0xe6, 0xa5, 0xef, 0x51, 0x24, 0x8d, 0xcb, 0xd2, 0x4e, 0xa5, 0x52, 0xed, 0x99, 0x99, 0xeb, 0x1a, 0xc4, 0xa4, 0x08, + 0x75, 0xdd, 0xa5, 0x57, 0xf7, 0x6e, 0x73, 0xad, 0xd9, 0x0e, 0x78, 0xaf, 0x41, 0x33, 0x94, 0xbc, 0xc5, 0xbc, 0x75, + 0x45, 0xd4, 0xf4, 0x62, 0x0d, 0x66, 0xc5, 0x28, 0x5b, 0x8a, 0xd6, 0x6b, 0x0a, 0x4a, 0xc1, 0x68, 0xb5, 0xf6, 0x16, + 0xee, 0x53, 0xd9, 0xb8, 0xb0, 0x64, 0x7a, 0xb5, 0x28, 0x29, 0xa1, 0xba, 0xa9, 0x18, 0x29, 0x61, 0xa4, 0x34, 0x3c, + 0x95, 0xef, 0x05, 0x1e, 0xe7, 0x79, 0x10, 0xb5, 0xbc, 0xc0, 0x8e, 0x2b, 0x72, 0x0c, 0x8e, 0x5e, 0x26, 0xa7, 0xa1, + 0xc0, 0x3f, 0x66, 0x0a, 0xd4, 0x75, 0xa8, 0xee, 0x37, 0xb8, 0xf9, 0x7f, 0x2d, 0x58, 0xe0, 0xf1, 0xad, 0x97, 0xb8, + 0x8d, 0x7e, 0x2d, 0x7c, 0x5a, 0xfa, 0x46, 0xfa, 0xae, 0x2e, 0x9e, 0xb4, 0x37, 0x1b, 0x25, 0xcb, 0x2c, 0x4f, 0x5f, + 0xcb, 0x94, 0x83, 0xc8, 0x0c, 0xad, 0x41, 0xd9, 0x91, 0x68, 0xdc, 0xf0, 0xc0, 0x88, 0xb1, 0x71, 0xe3, 0xfb, 0x31, + 0x03, 0xd9, 0x30, 0x58, 0x7d, 0xb3, 0x54, 0x26, 0x6b, 0x40, 0xd8, 0xd0, 0xf2, 0x13, 0x8d, 0xb7, 0x11, 0xea, 0xeb, + 0x17, 0xb8, 0xcd, 0x95, 0xbe, 0xcf, 0xf9, 0x8f, 0x19, 0xfd, 0x11, 0x81, 0x5f, 0xe2, 0x15, 0xc8, 0x3d, 0x9e, 0x42, + 0xdd, 0x08, 0xdb, 0xcb, 0x31, 0x58, 0x12, 0xa2, 0xa3, 0x88, 0x8a, 0x05, 0x0a, 0x9a, 0xc2, 0x20, 0x8a, 0xa8, 0x0b, + 0xe6, 0xf0, 0x2c, 0x97, 0xc9, 0xc7, 0xa9, 0xf1, 0x99, 0x1f, 0xc6, 0x18, 0x43, 0x3a, 0x18, 0x84, 0xd5, 0x2c, 0x18, + 0x8e, 0x47, 0x93, 0x83, 0x27, 0x70, 0x6e, 0x07, 0xe3, 0x80, 0x0c, 0x82, 0xba, 0x5c, 0xc5, 0x82, 0x96, 0x57, 0x17, + 0xb6, 0x0c, 0xfc, 0xb8, 0x0e, 0x06, 0xbf, 0x16, 0x9e, 0xe2, 0x1d, 0x34, 0x27, 0xb7, 0x32, 0x0c, 0x02, 0x7a, 0xb1, + 0x26, 0x20, 0x29, 0xeb, 0x69, 0x7e, 0x52, 0x1f, 0x6e, 0x4c, 0x69, 0xff, 0xcc, 0xe1, 0x05, 0x87, 0x1d, 0x12, 0x28, + 0x90, 0xc6, 0xd3, 0x6c, 0xf4, 0x52, 0x29, 0x72, 0xdf, 0x16, 0x1c, 0xee, 0xcc, 0x3d, 0x67, 0x7a, 0xe4, 0x14, 0x12, + 0xcd, 0x2c, 0xe0, 0x46, 0xfe, 0x52, 0x5c, 0xc5, 0x79, 0x96, 0xee, 0x35, 0xdf, 0xec, 0x95, 0xb7, 0xa2, 0x8a, 0x6f, + 0x46, 0x81, 0xb1, 0x26, 0xe4, 0xbe, 0xea, 0x09, 0xd0, 0x13, 0x60, 0x0b, 0x80, 0x01, 0xf1, 0x8e, 0x99, 0xc9, 0x8c, + 0x47, 0xe0, 0x11, 0xd8, 0xf4, 0x81, 0x2c, 0x6e, 0x9d, 0x4b, 0x92, 0xbf, 0x99, 0x4a, 0x7b, 0xd5, 0x2b, 0x77, 0x0a, + 0xb2, 0x5e, 0x6d, 0xe5, 0xae, 0x5b, 0x9f, 0x7d, 0xd3, 0xe1, 0x15, 0x78, 0x2a, 0xc1, 0x2d, 0xb2, 0xdf, 0x6f, 0x0a, + 0x2a, 0x85, 0x51, 0x11, 0xef, 0x24, 0xd7, 0xe8, 0xdf, 0xee, 0x8d, 0x8d, 0x22, 0xb9, 0xe5, 0xfd, 0x03, 0xa8, 0x33, + 0x79, 0x57, 0xdc, 0xce, 0x21, 0x6a, 0xeb, 0x6e, 0x3c, 0xf0, 0xde, 0xa0, 0x5d, 0xd6, 0x1c, 0xc1, 0x96, 0x17, 0x7b, + 0x19, 0x8c, 0x05, 0xce, 0xca, 0x48, 0xa9, 0x71, 0xad, 0x8c, 0x06, 0xd4, 0x26, 0x77, 0x90, 0xa5, 0x9e, 0x04, 0x45, + 0x8e, 0x67, 0x31, 0x64, 0x1a, 0x6f, 0x03, 0xb1, 0xdf, 0xc8, 0x10, 0xa4, 0x69, 0xdb, 0x6d, 0x73, 0x04, 0xca, 0xee, + 0x81, 0x29, 0x49, 0x5d, 0x1b, 0x53, 0x03, 0x0d, 0x3d, 0x88, 0x1a, 0xa9, 0x88, 0xb3, 0xa3, 0xa7, 0xa0, 0x43, 0x04, + 0xdf, 0xef, 0x34, 0x2b, 0x3b, 0x5e, 0x4c, 0x08, 0x9e, 0xbc, 0xcf, 0x6f, 0xb2, 0xb2, 0x2a, 0xa3, 0x17, 0x29, 0x1a, + 0x42, 0x25, 0x52, 0x44, 0xaf, 0x21, 0xbe, 0x60, 0x89, 0xbf, 0xcb, 0xe8, 0x5d, 0x4a, 0xe3, 0x34, 0xc5, 0xf4, 0x67, + 0x05, 0xfc, 0x7c, 0x0a, 0x28, 0x97, 0xb8, 0x13, 0xa2, 0x53, 0x09, 0xf6, 0x6a, 0x10, 0xdd, 0xab, 0xe2, 0x80, 0x29, + 0x1a, 0xdd, 0x08, 0x8a, 0x98, 0x75, 0x98, 0xfd, 0x43, 0x81, 0x42, 0x21, 0x55, 0xcc, 0x2f, 0xc2, 0x3e, 0x44, 0xd5, + 0x1a, 0xca, 0x39, 0x7e, 0xfb, 0xd2, 0x0c, 0x69, 0x74, 0x23, 0xa9, 0xde, 0xda, 0x78, 0x6c, 0x21, 0x4a, 0x4f, 0x74, + 0xb9, 0xa6, 0xa7, 0xf1, 0x2a, 0x8b, 0x36, 0x80, 0x3f, 0xf1, 0xf6, 0xe5, 0x53, 0x65, 0x61, 0xf2, 0x32, 0x03, 0xc5, + 0xc1, 0xf1, 0xdb, 0x97, 0xaf, 0x64, 0xba, 0xce, 0x79, 0x74, 0x2b, 0x91, 0xb4, 0x1e, 0xbf, 0x7d, 0xf9, 0x33, 0x9a, + 0x7b, 0xbd, 0x2b, 0xe0, 0xfd, 0x0b, 0xe0, 0x2d, 0xa3, 0x64, 0x0d, 0x7d, 0x52, 0xbf, 0xf3, 0x35, 0x76, 0xca, 0xab, + 0xb5, 0x8c, 0x7e, 0x4f, 0x6b, 0x4f, 0x5a, 0xf5, 0x77, 0xe1, 0x53, 0x3b, 0x4f, 0xc0, 0x73, 0x93, 0x67, 0xe2, 0x63, + 0x64, 0x45, 0x3b, 0x41, 0xf4, 0xf5, 0xde, 0xcd, 0x65, 0x2e, 0xca, 0x08, 0x5f, 0x30, 0xb4, 0x0b, 0x8a, 0xf6, 0xf7, + 0xaf, 0xaf, 0xaf, 0x47, 0xd7, 0x8f, 0x46, 0xb2, 0xb8, 0xd8, 0x9f, 0x7c, 0xfb, 0xed, 0xb7, 0xfb, 0xf8, 0x36, 0xf8, + 0xba, 0xed, 0xf6, 0x5e, 0x11, 0x3e, 0x60, 0x01, 0x22, 0x76, 0x7f, 0x0d, 0x57, 0x14, 0xd0, 0xc2, 0x0d, 0xbe, 0x0e, + 0xbe, 0xd6, 0x87, 0xce, 0xd7, 0x87, 0xe5, 0xd5, 0x85, 0x2a, 0xbf, 0xab, 0xe4, 0x83, 0xf1, 0x78, 0xbc, 0x0f, 0x12, + 0xa8, 0xaf, 0x07, 0x7c, 0x10, 0x1c, 0x05, 0x83, 0x0c, 0x2e, 0x34, 0xe5, 0xd5, 0xc5, 0x51, 0xe0, 0x19, 0xd8, 0x36, + 0x58, 0x44, 0x07, 0xe2, 0x12, 0xec, 0x5f, 0xd0, 0xe0, 0xeb, 0x80, 0xb8, 0x94, 0xaf, 0x20, 0xe5, 0xab, 0x83, 0x27, + 0x7e, 0xda, 0xff, 0x52, 0x69, 0x8f, 0xfc, 0xb4, 0x43, 0x4c, 0x7b, 0xf4, 0xd4, 0x4f, 0x3b, 0x52, 0x69, 0xcf, 0xfd, + 0xb4, 0xff, 0x5d, 0x0e, 0x20, 0x75, 0xcf, 0xb7, 0xfe, 0x3b, 0xf5, 0x5a, 0x83, 0xa7, 0x50, 0x94, 0x5d, 0xc6, 0x17, + 0x1c, 0x1a, 0x3d, 0xb8, 0xb9, 0xcc, 0x69, 0x30, 0xc0, 0xf6, 0x7a, 0x46, 0x1e, 0xde, 0x07, 0x5f, 0xaf, 0x8b, 0x3c, + 0x0c, 0xbe, 0x1e, 0x60, 0x21, 0x83, 0xaf, 0x03, 0xf2, 0xb5, 0x3e, 0xd2, 0xae, 0x04, 0xdb, 0x04, 0x2e, 0x34, 0xeb, + 0xd0, 0x06, 0x4c, 0xf3, 0xa5, 0x71, 0x35, 0xfd, 0xad, 0xe8, 0xce, 0x86, 0xb7, 0x44, 0xe5, 0xa6, 0x1b, 0xd4, 0xf4, + 0x2d, 0x78, 0x27, 0x40, 0xa3, 0xa2, 0xe0, 0x2a, 0x2e, 0xc2, 0xe1, 0xb0, 0xbc, 0xba, 0x20, 0x60, 0x97, 0xb9, 0xe2, + 0x71, 0x15, 0x05, 0x42, 0x0e, 0xd5, 0xcf, 0x40, 0x45, 0x02, 0x0b, 0x10, 0xca, 0x08, 0xfe, 0x0b, 0x6a, 0xfa, 0x40, + 0xb2, 0x4d, 0x30, 0xbc, 0xe6, 0x67, 0x1f, 0xb3, 0x6a, 0xa8, 0x44, 0x8b, 0x57, 0x82, 0xc2, 0x0f, 0xf8, 0xeb, 0xaa, + 0x8e, 0x7e, 0x03, 0x37, 0xee, 0xa6, 0x86, 0xfd, 0x81, 0xf4, 0x1c, 0xda, 0xe4, 0x3c, 0x5b, 0x4c, 0x5b, 0x07, 0xfa, + 0x5b, 0x49, 0xaa, 0x79, 0x36, 0x08, 0x86, 0xc1, 0x80, 0x2f, 0xd8, 0x5b, 0x39, 0xe7, 0x9e, 0xf9, 0xd4, 0xb1, 0xf4, + 0xa7, 0x79, 0x96, 0x0d, 0xc0, 0x37, 0x05, 0xf9, 0x91, 0xfd, 0xff, 0x9e, 0x0f, 0x51, 0x78, 0x38, 0x78, 0xb0, 0x4f, + 0x66, 0xc1, 0xea, 0x06, 0x3d, 0x3a, 0xa3, 0x20, 0x13, 0x4b, 0x5e, 0x64, 0x95, 0xb7, 0x54, 0x6e, 0xd6, 0x6d, 0x2f, + 0x8f, 0x3b, 0xcf, 0xe6, 0x55, 0x2c, 0x02, 0x75, 0xce, 0x81, 0xe2, 0x0d, 0x65, 0x4f, 0x65, 0x53, 0x42, 0xaa, 0x0d, + 0x79, 0xc3, 0x72, 0xc0, 0x82, 0xc3, 0xde, 0x70, 0xb8, 0x17, 0x0c, 0x9c, 0x3a, 0x77, 0x10, 0xec, 0x0d, 0x87, 0x47, + 0x81, 0xbb, 0x0f, 0x65, 0x23, 0x77, 0x67, 0xa4, 0x05, 0xfb, 0xbb, 0x08, 0x4b, 0x0a, 0xe2, 0x31, 0xa9, 0xc5, 0x5f, + 0x1a, 0x5c, 0x66, 0x00, 0xd0, 0x47, 0x4a, 0x02, 0x66, 0x60, 0x65, 0x06, 0x10, 0xaa, 0x9c, 0xc6, 0xec, 0x16, 0x98, + 0x47, 0xe0, 0x98, 0x15, 0x4c, 0x16, 0x20, 0x96, 0x04, 0x38, 0x77, 0x41, 0x14, 0xeb, 0x42, 0x8e, 0x21, 0x08, 0x00, + 0xfe, 0x24, 0xa6, 0x14, 0x4c, 0xd2, 0xb1, 0x1b, 0x41, 0x10, 0xc7, 0x67, 0x57, 0xa2, 0x35, 0x39, 0x4b, 0x74, 0x30, + 0x23, 0x09, 0xb0, 0x21, 0x06, 0x86, 0x0f, 0xee, 0xe7, 0xa0, 0xf4, 0xb0, 0x7a, 0x27, 0xe4, 0x82, 0x6f, 0xb9, 0x63, + 0xa1, 0xae, 0xe0, 0xea, 0x09, 0x07, 0xc1, 0x2d, 0xd7, 0x2c, 0xc0, 0xa8, 0x2a, 0xd6, 0x65, 0xc5, 0xd3, 0xf7, 0xb7, + 0x2b, 0x88, 0x05, 0x88, 0x03, 0xfa, 0x56, 0xe6, 0x59, 0x72, 0x1b, 0x3a, 0x7b, 0xae, 0x8d, 0x4a, 0xff, 0xe1, 0xfd, + 0xab, 0x9f, 0x22, 0x10, 0x39, 0xd6, 0x86, 0xd2, 0xdf, 0x72, 0x3c, 0x9b, 0xfc, 0x88, 0x57, 0xfe, 0xc6, 0xbe, 0xe5, + 0xf6, 0xf4, 0xe8, 0xf7, 0xa1, 0x6e, 0x7a, 0xcb, 0x67, 0xb7, 0x7c, 0xe4, 0x8a, 0x43, 0x75, 0x85, 0xfb, 0xfa, 0xe3, + 0xda, 0x37, 0x42, 0xba, 0x7f, 0x9e, 0x29, 0x6f, 0xcc, 0x8f, 0x76, 0x30, 0x0c, 0x82, 0xa9, 0x16, 0x4a, 0x42, 0x14, + 0x12, 0xa6, 0x04, 0x0c, 0xd1, 0x9e, 0x5e, 0x56, 0x53, 0xe4, 0xdc, 0xd4, 0xc8, 0xc2, 0xfb, 0x01, 0xd3, 0x42, 0x87, + 0x46, 0x0e, 0xe5, 0x07, 0x87, 0x13, 0xc6, 0x2c, 0xfc, 0x56, 0x09, 0xd3, 0xaf, 0x16, 0x95, 0x73, 0x10, 0xdd, 0x03, + 0x63, 0x5c, 0xc1, 0x0b, 0xe8, 0x0a, 0xbb, 0x5e, 0xab, 0x28, 0x21, 0x08, 0xa6, 0x87, 0x1c, 0xa0, 0x87, 0x5d, 0xd0, + 0xb2, 0xb2, 0x54, 0xb7, 0x2a, 0x67, 0xa9, 0xa2, 0x2e, 0x43, 0x59, 0x19, 0x2b, 0x0c, 0xfc, 0x92, 0x7d, 0x28, 0xd0, + 0xb3, 0x7c, 0x2a, 0xba, 0xe0, 0x85, 0x50, 0x82, 0xe5, 0xba, 0xde, 0x89, 0x40, 0xd4, 0xf9, 0xa1, 0x77, 0xd5, 0xd7, + 0xb8, 0x7e, 0x3c, 0x7d, 0x25, 0x53, 0xae, 0x4d, 0x28, 0x34, 0x9f, 0x2f, 0x7d, 0xc5, 0x44, 0xc1, 0x3e, 0x42, 0xbf, + 0xda, 0x36, 0xfa, 0xec, 0x66, 0xad, 0x37, 0x83, 0x12, 0x1d, 0xf3, 0x1a, 0x05, 0xd7, 0x4a, 0xa1, 0x60, 0xb4, 0xb7, + 0xf1, 0x67, 0x38, 0x72, 0xab, 0xdb, 0x43, 0xef, 0xb7, 0x2a, 0xbe, 0x78, 0x8d, 0xbe, 0x9d, 0xf6, 0xe7, 0xa8, 0x92, + 0x1f, 0x56, 0x2b, 0xf0, 0xa1, 0x82, 0x48, 0x2b, 0x16, 0xa7, 0x17, 0xea, 0x39, 0x79, 0x7b, 0xfc, 0x1a, 0xfc, 0x28, + 0xf1, 0xf7, 0x2f, 0xdf, 0x07, 0x35, 0x99, 0xc6, 0xb3, 0xc2, 0x7c, 0x68, 0x73, 0x40, 0xa8, 0x16, 0x97, 0x66, 0xdf, + 0xcf, 0xe2, 0x26, 0xfb, 0xae, 0xd9, 0x7a, 0x5a, 0x34, 0x91, 0xa4, 0x0c, 0xb7, 0x0f, 0x06, 0x04, 0xfa, 0x00, 0x51, + 0x9c, 0x7d, 0x41, 0x63, 0x48, 0xf3, 0x99, 0x7d, 0x3f, 0x42, 0xe0, 0xcb, 0x9d, 0x90, 0x6a, 0x5c, 0x61, 0xd1, 0xe8, + 0x21, 0x9f, 0xf1, 0x48, 0x19, 0x16, 0xbd, 0xc3, 0x04, 0xe2, 0x0c, 0xa7, 0xd5, 0x7b, 0xc4, 0x80, 0xc6, 0xbb, 0x81, + 0x96, 0x3d, 0x44, 0x19, 0x75, 0xd9, 0x1b, 0x16, 0xdf, 0x27, 0xeb, 0x30, 0xb3, 0x96, 0x97, 0x43, 0xf8, 0x1b, 0x68, + 0x03, 0x70, 0xca, 0x91, 0xe5, 0xab, 0xcc, 0x46, 0x57, 0x4b, 0x4c, 0x6f, 0x22, 0x88, 0x4d, 0xa4, 0xd3, 0x61, 0xed, + 0xea, 0x54, 0xbd, 0xab, 0x9d, 0xcf, 0x44, 0xaf, 0x02, 0xad, 0x5c, 0xdb, 0x1e, 0x0f, 0xe1, 0x3f, 0xb5, 0xb4, 0xc2, + 0x46, 0xd8, 0x73, 0xf1, 0x85, 0xe7, 0xd8, 0x9c, 0x80, 0x06, 0x97, 0x32, 0x05, 0xe0, 0x2c, 0xad, 0x46, 0xa3, 0x46, + 0xd8, 0x67, 0xe5, 0x7c, 0x0e, 0x5b, 0x0b, 0xf1, 0xb4, 0x00, 0x1c, 0xb8, 0x89, 0xc9, 0xc9, 0xbb, 0x31, 0x39, 0xa7, + 0x1f, 0x15, 0xdc, 0x77, 0x70, 0x5a, 0x2e, 0xe3, 0x54, 0x5e, 0x03, 0x36, 0x65, 0xe0, 0xa7, 0x62, 0xa9, 0x5e, 0x42, + 0xb2, 0xe4, 0xc9, 0x47, 0xb4, 0xda, 0x48, 0x03, 0xe0, 0x2a, 0xa7, 0xc6, 0x72, 0x4f, 0x81, 0xa6, 0xba, 0x52, 0x54, + 0x42, 0x5c, 0x55, 0x71, 0xb2, 0x3c, 0xc1, 0xd4, 0x70, 0x03, 0xbd, 0x88, 0x02, 0xb9, 0xe2, 0x02, 0x48, 0x7a, 0xce, + 0xfe, 0xc8, 0x34, 0xf6, 0xfa, 0x1b, 0x89, 0x02, 0x26, 0x8d, 0xa2, 0x8c, 0x95, 0xb2, 0x97, 0xd2, 0x44, 0xbf, 0x0b, + 0x82, 0xda, 0xbd, 0xfc, 0x1b, 0xea, 0x7e, 0x0a, 0xad, 0x08, 0x1b, 0xe0, 0x85, 0x1a, 0xfc, 0x30, 0xb5, 0x4b, 0xce, + 0x03, 0x32, 0x74, 0xde, 0x67, 0xb5, 0xdd, 0xea, 0x4f, 0x97, 0x80, 0xf5, 0x9a, 0x1a, 0x9f, 0xc2, 0x30, 0x21, 0x26, + 0x56, 0xb2, 0x55, 0x56, 0xda, 0x0d, 0x65, 0xda, 0x49, 0x97, 0xcc, 0x6b, 0xe1, 0x34, 0xef, 0x31, 0xb6, 0x1c, 0xa9, + 0xdc, 0xfd, 0x7e, 0x68, 0x7e, 0xb2, 0x9c, 0xbe, 0xd1, 0x21, 0xac, 0xbd, 0xf1, 0xa0, 0x39, 0xd1, 0xea, 0xaa, 0x8e, + 0x7e, 0x40, 0x07, 0x60, 0xa6, 0x2d, 0x42, 0xa5, 0x0b, 0xbe, 0xed, 0x2b, 0x51, 0x71, 0x49, 0xc2, 0x52, 0x49, 0x60, + 0x67, 0x37, 0x25, 0x3b, 0x9b, 0x80, 0x78, 0x86, 0xbb, 0x9e, 0x16, 0x3b, 0x21, 0x4d, 0x78, 0x8b, 0xbd, 0x04, 0x44, + 0x1d, 0xaa, 0xba, 0x84, 0x6c, 0x8c, 0xa1, 0x8b, 0x7f, 0x51, 0x0a, 0x13, 0xd6, 0x32, 0xa9, 0x4a, 0x4c, 0x50, 0xa8, + 0x72, 0xb7, 0x45, 0x60, 0x89, 0x82, 0x1d, 0xc0, 0xde, 0xbb, 0x51, 0x37, 0xa3, 0xa6, 0xaa, 0x53, 0x2f, 0xc1, 0xc7, + 0x69, 0xd6, 0x55, 0x90, 0x59, 0xd8, 0x55, 0xb1, 0xe6, 0x81, 0x8e, 0xd5, 0xa5, 0x8c, 0x89, 0xbb, 0xb4, 0xc8, 0x10, + 0x1f, 0x19, 0x63, 0x0b, 0x6b, 0x38, 0xd2, 0xf6, 0xb8, 0xe9, 0x09, 0x42, 0x3f, 0x61, 0x43, 0x09, 0xdc, 0x74, 0xb6, + 0xa7, 0xa6, 0x99, 0x0f, 0x88, 0x38, 0x0c, 0x28, 0x90, 0x6c, 0x1c, 0xd2, 0x1c, 0xe9, 0x0b, 0x92, 0x26, 0x0c, 0x94, + 0xad, 0x78, 0x4e, 0x90, 0x15, 0x85, 0x9e, 0xad, 0xab, 0x1a, 0xe2, 0xe7, 0x32, 0xcc, 0xd1, 0x92, 0x53, 0xe1, 0x69, + 0x82, 0x4c, 0xec, 0x8e, 0xb6, 0x99, 0xc9, 0x70, 0x94, 0x2c, 0x30, 0xbf, 0x82, 0x28, 0x71, 0x67, 0x9a, 0x55, 0x39, + 0x18, 0x17, 0xb0, 0x40, 0x2b, 0xdf, 0x83, 0xba, 0xb1, 0x86, 0x36, 0x1a, 0x96, 0xd9, 0xed, 0x4f, 0xb0, 0x5f, 0x6b, + 0xa7, 0x75, 0x99, 0x62, 0x79, 0x99, 0x42, 0xb4, 0x17, 0x32, 0xbf, 0x51, 0x24, 0xba, 0x53, 0x84, 0x21, 0x61, 0x1d, + 0x65, 0x4f, 0xda, 0xd4, 0x00, 0x7a, 0xea, 0x05, 0x80, 0xef, 0x5c, 0xcb, 0xb0, 0x8b, 0x74, 0x7f, 0x55, 0x30, 0x2e, + 0xdd, 0x20, 0x48, 0xd1, 0x9b, 0x14, 0xcc, 0x79, 0x3d, 0x4a, 0xea, 0xcd, 0x69, 0xcb, 0x8c, 0xaa, 0xa3, 0x22, 0xa4, + 0x9c, 0xe0, 0x3f, 0x79, 0x29, 0x35, 0xb1, 0x09, 0x13, 0x3c, 0xf0, 0x61, 0x9e, 0x61, 0x03, 0x6f, 0xb7, 0x0f, 0xd2, + 0x30, 0x69, 0xb3, 0x0d, 0x29, 0x48, 0x2b, 0x4c, 0x9c, 0x10, 0xa8, 0xec, 0x25, 0xee, 0x17, 0x6c, 0x27, 0x4d, 0xc1, + 0x83, 0xb0, 0xd1, 0xc0, 0xc4, 0xad, 0xae, 0x6c, 0x1d, 0x26, 0x34, 0x5c, 0x52, 0xed, 0xec, 0xa4, 0x92, 0xcf, 0xdb, + 0xeb, 0xf2, 0xdc, 0xf6, 0x41, 0xc7, 0x52, 0xeb, 0x1a, 0x1e, 0x68, 0x5e, 0xb3, 0x8b, 0x2b, 0xa6, 0x69, 0xa2, 0xb1, + 0x1e, 0x52, 0x96, 0x1c, 0xeb, 0x7a, 0xba, 0xc2, 0xd5, 0x32, 0xd3, 0x40, 0xf7, 0x12, 0x2f, 0xf4, 0x80, 0x0f, 0x1e, + 0xae, 0x48, 0x74, 0x8e, 0xcd, 0x66, 0xab, 0x9a, 0x4c, 0xf3, 0xbb, 0xb2, 0xe5, 0x26, 0x40, 0x9e, 0xa5, 0xbe, 0xb9, + 0x4f, 0x8e, 0x35, 0x6d, 0xf3, 0x93, 0x00, 0xd7, 0xdc, 0x2b, 0x20, 0xe9, 0x58, 0x82, 0x2e, 0xde, 0xa7, 0x3f, 0x88, + 0xd4, 0x4c, 0x05, 0xbd, 0x73, 0xbe, 0x48, 0xdd, 0xfc, 0x02, 0x6c, 0xa3, 0x36, 0xc6, 0x34, 0x4b, 0xac, 0xc3, 0x44, + 0x59, 0x58, 0x23, 0x0b, 0xb9, 0x04, 0x1f, 0xcc, 0xdd, 0xa6, 0x4e, 0x9f, 0x77, 0x10, 0x61, 0xbf, 0x8b, 0x1e, 0x8f, + 0x30, 0x56, 0xac, 0x41, 0x62, 0x58, 0x85, 0x35, 0x6d, 0x2e, 0x87, 0x28, 0xa7, 0x66, 0xc9, 0x44, 0x4b, 0xea, 0x53, + 0x8a, 0x28, 0x05, 0x73, 0xe3, 0x69, 0xd9, 0x30, 0x25, 0x44, 0xc8, 0x0a, 0xe9, 0x80, 0x6a, 0x2d, 0xb4, 0x54, 0x13, + 0x04, 0x3c, 0xf4, 0xb2, 0xd0, 0x98, 0x82, 0xe8, 0x23, 0x32, 0xdc, 0x88, 0x23, 0xa3, 0xbb, 0x63, 0x14, 0x13, 0x08, + 0xdd, 0xed, 0xe5, 0x85, 0xd5, 0xa7, 0x65, 0x5b, 0x1d, 0xc4, 0x35, 0xa6, 0xc9, 0x1d, 0x04, 0x35, 0x46, 0x41, 0x9b, + 0xd3, 0x8d, 0xfe, 0x5e, 0x84, 0xbe, 0x5d, 0x38, 0x76, 0xa3, 0x20, 0x12, 0x22, 0xd2, 0x7a, 0x4d, 0xc5, 0x00, 0xb5, + 0xf3, 0xd8, 0x45, 0xac, 0xd2, 0xdd, 0x42, 0x94, 0x37, 0x2a, 0xeb, 0x93, 0x75, 0x48, 0xb6, 0x5b, 0x2c, 0x0b, 0x7c, + 0xd9, 0x5f, 0xad, 0xef, 0x80, 0x40, 0x7f, 0xba, 0xfe, 0x2c, 0x04, 0xfa, 0xb3, 0xec, 0x4b, 0x20, 0xd0, 0x9f, 0xae, + 0xff, 0xa7, 0x21, 0xd0, 0x5f, 0xad, 0x3d, 0x08, 0x74, 0x35, 0x18, 0xff, 0x2a, 0x58, 0xf0, 0xe6, 0x75, 0x40, 0x9f, + 0x49, 0x16, 0xbc, 0x79, 0xf1, 0xc2, 0x13, 0xa6, 0xff, 0x20, 0x34, 0x92, 0xbf, 0x91, 0x05, 0x23, 0x6e, 0x0b, 0xbc, + 0x42, 0xad, 0x93, 0x0f, 0x54, 0x94, 0x01, 0x10, 0x7d, 0xf9, 0x6b, 0x56, 0x2d, 0xc3, 0x60, 0x3f, 0x20, 0x33, 0x07, + 0x09, 0x3a, 0x9c, 0x34, 0x6e, 0x6f, 0x1f, 0x44, 0x43, 0xa8, 0x63, 0x23, 0x0f, 0xc0, 0x57, 0x9e, 0xc8, 0xde, 0xbf, + 0x21, 0xe2, 0x27, 0x33, 0x0b, 0x3a, 0xba, 0x1f, 0x10, 0xf0, 0x58, 0xca, 0x3c, 0x04, 0xce, 0xb9, 0x1f, 0x12, 0xfa, + 0xed, 0xda, 0xb3, 0x2d, 0xfa, 0x20, 0xc2, 0x0a, 0x7c, 0xee, 0xfe, 0x5e, 0xf3, 0xd3, 0x2c, 0x25, 0x4e, 0x1e, 0xca, + 0x45, 0x22, 0x53, 0xfe, 0xe1, 0xdd, 0x4b, 0x8b, 0x3c, 0x1e, 0x2a, 0xe8, 0x25, 0x82, 0x21, 0x8d, 0x53, 0x7e, 0x95, + 0x25, 0x7c, 0xf6, 0xe7, 0x83, 0x4d, 0x67, 0x46, 0xf5, 0x9a, 0xd4, 0xfb, 0x7f, 0x46, 0x41, 0xa0, 0xc7, 0xe0, 0xcf, + 0x07, 0x9b, 0xac, 0xde, 0x7f, 0xb0, 0xa9, 0x46, 0xa9, 0x04, 0x78, 0x6f, 0xf8, 0x2d, 0xeb, 0x07, 0x9b, 0x12, 0x7e, + 0xf0, 0xfa, 0x4f, 0x0f, 0x98, 0xcd, 0x36, 0xc8, 0xeb, 0x83, 0x55, 0x5e, 0x39, 0x4c, 0xd0, 0x7b, 0x0a, 0x16, 0xa6, + 0x50, 0x87, 0x47, 0xb5, 0xf6, 0xe4, 0x7e, 0x53, 0xdd, 0x75, 0x42, 0xe0, 0x1a, 0xe9, 0x06, 0x0e, 0xa1, 0xb2, 0x04, + 0x3b, 0xea, 0xe8, 0x94, 0x20, 0xa6, 0xe6, 0xfd, 0x40, 0xd9, 0xfa, 0x7a, 0xc1, 0x8a, 0x5d, 0x33, 0x31, 0xbe, 0xd3, + 0x18, 0xd8, 0x70, 0xd1, 0xd5, 0x62, 0xce, 0xfe, 0x34, 0x3d, 0xde, 0xad, 0x42, 0x12, 0xc4, 0xc8, 0xf6, 0xfb, 0xc4, + 0xeb, 0x59, 0xca, 0xab, 0x38, 0xcb, 0x59, 0x9c, 0xe7, 0x7f, 0xa2, 0x2c, 0xe2, 0xfb, 0x2f, 0x02, 0xdd, 0x1f, 0x8d, + 0x46, 0x71, 0x71, 0x81, 0x57, 0x7f, 0x43, 0x6e, 0x11, 0x16, 0x3b, 0xe3, 0xa5, 0x0d, 0xac, 0xb2, 0x8c, 0xcb, 0x53, + 0x1d, 0xd1, 0xa8, 0xb4, 0x04, 0xbb, 0x5c, 0xca, 0xeb, 0x53, 0x88, 0xee, 0x60, 0x29, 0x78, 0x8c, 0x03, 0xa8, 0xee, + 0x4d, 0x26, 0xec, 0xf2, 0x5a, 0xbf, 0x3b, 0x8b, 0x4b, 0xfe, 0x36, 0xae, 0x96, 0x0c, 0xf6, 0x82, 0xa6, 0xea, 0x85, + 0x5c, 0xaf, 0x5c, 0x25, 0xa7, 0x6b, 0xf1, 0x51, 0xc8, 0x6b, 0xa1, 0x68, 0xef, 0x29, 0xbf, 0x82, 0x16, 0xb1, 0x0d, + 0xea, 0xac, 0x04, 0x4f, 0x2a, 0x8f, 0x13, 0x57, 0xb1, 0x00, 0x32, 0x6a, 0xa2, 0x01, 0x74, 0xe4, 0xa0, 0xa1, 0xdd, + 0x6b, 0xda, 0xb1, 0xdc, 0xa8, 0x2c, 0x32, 0xb0, 0x84, 0x7d, 0x0e, 0xa5, 0x03, 0x62, 0x3b, 0x84, 0x0b, 0x81, 0xab, + 0x27, 0x5e, 0x8d, 0x1a, 0x88, 0x3d, 0xb4, 0xf4, 0xdd, 0x85, 0x14, 0xab, 0x65, 0xd0, 0x2e, 0x1b, 0xc3, 0x84, 0xd7, + 0x6b, 0x74, 0x19, 0x06, 0xc5, 0x7f, 0xe1, 0x16, 0x25, 0xe2, 0x22, 0x65, 0xa9, 0x32, 0x3a, 0xeb, 0xa1, 0x2c, 0x0c, + 0x9f, 0x3d, 0x1d, 0xa5, 0x0e, 0x2b, 0xe7, 0x99, 0xe5, 0x6d, 0x94, 0x26, 0x7e, 0x0e, 0x26, 0x61, 0x7e, 0x2d, 0x73, + 0xa9, 0xe3, 0x92, 0x9f, 0x8a, 0xf5, 0x25, 0x2f, 0xb2, 0xe4, 0x74, 0x99, 0x95, 0x95, 0x2c, 0x6e, 0x17, 0x06, 0xee, + 0x42, 0x97, 0xd5, 0x9a, 0xc4, 0x3b, 0xbf, 0x03, 0x9f, 0x77, 0x15, 0xc0, 0x64, 0xf8, 0x64, 0x4c, 0x6a, 0x6d, 0x2d, + 0x0f, 0x0d, 0xa4, 0xf6, 0xb7, 0xda, 0x27, 0xee, 0xd9, 0x76, 0x8d, 0x36, 0xfd, 0x1c, 0xda, 0x35, 0x52, 0xb3, 0x94, + 0x0a, 0xfe, 0xf7, 0x9a, 0x9b, 0x68, 0x07, 0xa1, 0x43, 0xf2, 0x0e, 0x4b, 0x7d, 0x18, 0x69, 0x12, 0xad, 0x90, 0xa0, + 0x14, 0xf5, 0x6d, 0xbd, 0x50, 0x6d, 0x20, 0x44, 0xdd, 0x16, 0xd3, 0xf4, 0x39, 0x82, 0xb6, 0x83, 0x94, 0x04, 0xf7, + 0x96, 0x8d, 0xf9, 0xd5, 0xb5, 0x7c, 0xe6, 0xd0, 0x9d, 0xc5, 0xec, 0x73, 0x19, 0x06, 0x83, 0xe8, 0x73, 0x59, 0xd8, + 0xe4, 0x9e, 0x55, 0xaa, 0xb2, 0x1c, 0x1a, 0xdb, 0xcb, 0x29, 0x9a, 0xb2, 0x84, 0x0f, 0xd6, 0x61, 0x73, 0xed, 0x53, + 0x9c, 0x7d, 0xba, 0xb9, 0xe4, 0xd5, 0x52, 0xa6, 0x51, 0xf0, 0xfd, 0xf3, 0xf7, 0x81, 0x51, 0x5d, 0x17, 0x1a, 0xb4, + 0x48, 0x6b, 0x73, 0x72, 0x79, 0x01, 0xb2, 0xcc, 0x5e, 0x31, 0x92, 0x1f, 0x77, 0xa2, 0x7c, 0xfe, 0xf9, 0xc3, 0xfb, + 0xf7, 0x6f, 0xf7, 0x50, 0xe1, 0xd3, 0xdb, 0x3b, 0x51, 0xe8, 0x01, 0x7b, 0x0f, 0x36, 0x85, 0x56, 0xb1, 0xd7, 0x7f, + 0xda, 0xb3, 0xaa, 0x68, 0x29, 0xc8, 0x0d, 0x28, 0xa0, 0x57, 0x45, 0x6b, 0x58, 0x0b, 0xa7, 0xc5, 0xf6, 0x33, 0x2b, + 0xed, 0x52, 0x80, 0xba, 0x13, 0x55, 0x73, 0xa4, 0xf4, 0xf2, 0x10, 0x69, 0x21, 0xac, 0xee, 0xd8, 0x6a, 0x55, 0xd7, + 0x56, 0x93, 0x45, 0x95, 0x89, 0x8b, 0x53, 0xdc, 0xfd, 0x5f, 0xb4, 0xe5, 0xcc, 0x0c, 0x2b, 0x7a, 0xd1, 0xde, 0x6d, + 0x0d, 0xa8, 0x32, 0x6d, 0x94, 0xab, 0xf7, 0x10, 0x08, 0xcc, 0xca, 0x7a, 0xea, 0x7f, 0x6c, 0x2c, 0x46, 0xfc, 0x34, + 0x05, 0xe4, 0x06, 0x3c, 0x10, 0x3b, 0x8a, 0x47, 0xa6, 0x7d, 0xd7, 0x28, 0x37, 0x39, 0x4c, 0x5a, 0x09, 0xb3, 0xe1, + 0x24, 0x9a, 0x10, 0x1b, 0x5f, 0x42, 0xd3, 0xb0, 0xef, 0x47, 0xcf, 0x5f, 0xbf, 0x7f, 0xf9, 0xfe, 0xf7, 0xd3, 0xa7, + 0xc7, 0xef, 0x9f, 0x7f, 0xff, 0xe6, 0xdd, 0xcb, 0xe7, 0x27, 0x78, 0x42, 0x68, 0xc0, 0xca, 0x70, 0xa3, 0xad, 0xa2, + 0x9b, 0x65, 0x45, 0xa2, 0x26, 0xcd, 0xa6, 0x28, 0xc4, 0x28, 0xcc, 0x6c, 0x8b, 0xfc, 0xf0, 0xfa, 0xd9, 0xf3, 0x17, + 0x2f, 0x5f, 0x3f, 0x7f, 0xd6, 0xfe, 0x7a, 0x38, 0xa9, 0x49, 0xed, 0x66, 0x4e, 0x47, 0x48, 0xe1, 0x76, 0xbc, 0x3a, + 0xe8, 0x13, 0x6a, 0xe5, 0x7d, 0xfa, 0x94, 0xc1, 0x8a, 0x64, 0x4a, 0x4e, 0x8f, 0xbf, 0x3d, 0xfc, 0x5f, 0xb5, 0xf1, + 0xb6, 0x5b, 0xe0, 0x21, 0x90, 0x8c, 0x29, 0x59, 0x3f, 0x8c, 0x6a, 0x46, 0xd5, 0xcb, 0x48, 0x50, 0x5b, 0x1a, 0xd8, + 0x40, 0xa7, 0x54, 0x85, 0x54, 0x38, 0x4d, 0xe2, 0x8a, 0x5f, 0xc8, 0xe2, 0x36, 0xca, 0x46, 0xad, 0x14, 0xda, 0x58, + 0x00, 0x51, 0x08, 0x82, 0xe5, 0x46, 0x12, 0xe9, 0x29, 0x02, 0xe0, 0x0d, 0x81, 0x1b, 0xd5, 0xb9, 0x8b, 0x16, 0xd0, + 0x2e, 0x98, 0x2c, 0xb6, 0xdb, 0x8e, 0x41, 0xeb, 0xa4, 0x7d, 0xd1, 0x3c, 0x53, 0x44, 0x71, 0x01, 0x8c, 0x39, 0x1c, + 0x6f, 0xea, 0xec, 0x62, 0xe6, 0xb8, 0x3b, 0xd6, 0x51, 0x3f, 0xc1, 0x1a, 0xd1, 0xbd, 0x36, 0x81, 0x65, 0x9a, 0xe7, + 0xe1, 0xb8, 0x45, 0x71, 0x0d, 0xc6, 0x6f, 0x2b, 0x55, 0x2d, 0x33, 0x8d, 0xad, 0x08, 0x33, 0x05, 0xe1, 0xb8, 0x8c, + 0xe8, 0x36, 0xcc, 0xc1, 0x42, 0xa6, 0x31, 0xbf, 0x66, 0x1c, 0xf2, 0x48, 0x1a, 0x98, 0x3c, 0x30, 0x19, 0xbc, 0x23, + 0xd7, 0x32, 0x2a, 0x1a, 0x80, 0x97, 0xb2, 0x39, 0xa8, 0x87, 0xff, 0xa7, 0xb9, 0xa7, 0xdd, 0x6e, 0xdb, 0x46, 0xf6, + 0x7f, 0x9f, 0x82, 0x61, 0xb2, 0x29, 0x99, 0x90, 0x34, 0x29, 0x59, 0xb6, 0x22, 0x59, 0x72, 0x9b, 0xaf, 0x6d, 0x5a, + 0xb7, 0xe9, 0x49, 0xdc, 0xec, 0xdd, 0xf5, 0xfa, 0x58, 0x94, 0x04, 0x49, 0xdc, 0x50, 0xa4, 0x0e, 0x49, 0xf9, 0xa3, + 0x0a, 0xf7, 0x59, 0xf6, 0x11, 0xee, 0x33, 0xf4, 0xc9, 0xee, 0x99, 0x19, 0x80, 0x04, 0xbf, 0x24, 0x79, 0x93, 0xb6, + 0xf7, 0xb4, 0x49, 0x44, 0x10, 0x00, 0x81, 0x01, 0x30, 0x33, 0x98, 0xcf, 0xa8, 0xf8, 0x0c, 0xdb, 0xb8, 0x54, 0x05, + 0x45, 0xb6, 0xc5, 0x4a, 0x20, 0x5a, 0x98, 0x9c, 0xd2, 0xe7, 0xad, 0x24, 0x3c, 0x0b, 0x6f, 0x84, 0x78, 0xf8, 0x24, + 0xaa, 0x29, 0xc4, 0xb3, 0xd1, 0x73, 0x4f, 0x26, 0xf4, 0xc3, 0x49, 0x1b, 0x88, 0x40, 0x9a, 0x03, 0x38, 0x63, 0x4e, + 0x47, 0x74, 0x65, 0xba, 0x7a, 0xb4, 0x11, 0x1b, 0x2f, 0x1d, 0x79, 0x59, 0xf2, 0xd7, 0x02, 0x63, 0x91, 0x72, 0xd0, + 0xcb, 0xb1, 0x46, 0x6b, 0xaa, 0xf1, 0xfd, 0x31, 0xf0, 0x6a, 0xb9, 0x13, 0x8b, 0x1e, 0x19, 0xe5, 0xc2, 0xac, 0xaf, + 0xc2, 0x6e, 0xd9, 0x44, 0xab, 0x1b, 0x18, 0x89, 0x97, 0xc4, 0x14, 0x30, 0xfc, 0x32, 0x62, 0xfc, 0x9f, 0x2b, 0x18, + 0x1f, 0xad, 0xec, 0x32, 0x84, 0xff, 0xf3, 0xdb, 0xf7, 0xe7, 0xa0, 0xbd, 0x72, 0x51, 0xdd, 0xbc, 0x51, 0xb9, 0xa5, + 0x8a, 0x09, 0xfa, 0x20, 0xb5, 0xa7, 0xba, 0x2b, 0xa0, 0xc7, 0x78, 0x2f, 0x38, 0xb8, 0x35, 0x6f, 0x6e, 0x6e, 0x4c, + 0xb0, 0x5b, 0x35, 0xd7, 0x91, 0x4f, 0x3c, 0xe0, 0x54, 0x4d, 0x05, 0x22, 0x67, 0x25, 0x44, 0x0e, 0x41, 0x6f, 0x79, + 0xd6, 0x94, 0xf7, 0x8b, 0xf0, 0xe6, 0x5b, 0xdf, 0x97, 0x85, 0x33, 0x82, 0x55, 0xe3, 0xf2, 0x8a, 0x02, 0x62, 0xd0, + 0x40, 0xc7, 0x64, 0x79, 0xf1, 0x15, 0xb7, 0x0a, 0x98, 0x5e, 0x8d, 0xef, 0xae, 0xb8, 0xe6, 0x21, 0x8b, 0x3a, 0xfc, + 0x62, 0x74, 0x32, 0xf5, 0xae, 0x15, 0xe4, 0x27, 0x07, 0x2a, 0xb8, 0x6c, 0xf9, 0x6c, 0xbc, 0x4e, 0x92, 0x30, 0x30, + 0xa3, 0xf0, 0x46, 0x1d, 0x9e, 0xd0, 0x83, 0xa8, 0xe0, 0xd2, 0xa3, 0xaa, 0x7c, 0x33, 0xf1, 0xbd, 0xc9, 0xc7, 0x81, + 0xfa, 0x68, 0xe3, 0x0d, 0x86, 0x25, 0xae, 0xd1, 0x4e, 0xd5, 0x21, 0x8c, 0x55, 0xf9, 0xd6, 0xf7, 0x4f, 0x0e, 0xa8, + 0xc5, 0xf0, 0xe4, 0x60, 0xea, 0x5d, 0x0f, 0xa5, 0x04, 0x30, 0x5c, 0x3b, 0x3a, 0xe0, 0x81, 0x36, 0x33, 0x7b, 0xb2, + 0x18, 0x23, 0x37, 0x4c, 0x98, 0x96, 0x5f, 0x71, 0x21, 0xa2, 0x0c, 0x8d, 0x57, 0x9b, 0xa0, 0xd0, 0xdc, 0x87, 0x0b, + 0xdd, 0xa7, 0x4f, 0x5a, 0x66, 0x6d, 0xba, 0x90, 0x42, 0xb1, 0xa1, 0x32, 0x0f, 0xab, 0x18, 0x18, 0x4f, 0x46, 0xd7, + 0x44, 0xc0, 0x38, 0x5f, 0x37, 0x26, 0xa9, 0x81, 0x79, 0x74, 0xdc, 0x15, 0xe8, 0x15, 0xf9, 0x4f, 0xe9, 0xde, 0x3b, + 0x81, 0xdc, 0xd9, 0x12, 0xe2, 0xd6, 0x25, 0xcd, 0x0a, 0x9d, 0x42, 0x1e, 0x0d, 0x10, 0x54, 0x22, 0xf8, 0x1d, 0xd2, + 0x76, 0x68, 0xbe, 0x0e, 0xb9, 0xdb, 0xb2, 0x10, 0x3c, 0x6e, 0x2a, 0xb2, 0xa5, 0x09, 0xb8, 0x9c, 0x16, 0x56, 0xa8, + 0x57, 0x5e, 0x2f, 0x11, 0x1b, 0xf2, 0x41, 0xdc, 0xb4, 0x64, 0xa0, 0xa9, 0xd3, 0x12, 0xa3, 0x44, 0x67, 0xc1, 0x77, + 0x4f, 0x52, 0x0f, 0x31, 0x43, 0xbb, 0x88, 0x8d, 0xf0, 0x32, 0xa7, 0x4d, 0x31, 0x21, 0xca, 0x5e, 0x98, 0xe6, 0x61, + 0x9a, 0x69, 0xd5, 0x87, 0x8f, 0x36, 0x01, 0x12, 0xb3, 0x78, 0x30, 0x2c, 0xee, 0x83, 0xc4, 0x1d, 0x9b, 0xb4, 0x99, + 0x55, 0xe5, 0x9b, 0xe9, 0xd8, 0xcf, 0x16, 0x9b, 0x0e, 0xc1, 0xc2, 0x0d, 0xa6, 0x3e, 0x3b, 0x77, 0xc7, 0xdf, 0x61, + 0x9d, 0x97, 0x63, 0xff, 0x05, 0x54, 0x48, 0xd5, 0xe1, 0xa3, 0x0d, 0x91, 0xeb, 0x3a, 0x84, 0x9d, 0xd2, 0x16, 0x28, + 0x7f, 0x87, 0x27, 0x56, 0x62, 0x11, 0xb5, 0xc6, 0xc1, 0x12, 0x89, 0x25, 0x8c, 0x5a, 0x1c, 0x19, 0x4f, 0xec, 0x03, + 0x7b, 0x53, 0xe1, 0xa7, 0x16, 0xc6, 0x15, 0x8a, 0x13, 0x2c, 0xef, 0x4c, 0x79, 0xb0, 0x44, 0x4a, 0xdf, 0x85, 0x37, + 0x62, 0xa4, 0x1c, 0x00, 0x14, 0x88, 0xf2, 0xf4, 0xc5, 0xe8, 0x44, 0x56, 0xfe, 0xa0, 0x84, 0x9c, 0xfa, 0x85, 0x5f, + 0xa9, 0xaa, 0xe4, 0x69, 0x9e, 0x56, 0xb7, 0xea, 0xf0, 0xe4, 0x40, 0xae, 0x3d, 0x1c, 0xf5, 0xce, 0xa4, 0xc9, 0x61, + 0xaf, 0xe2, 0x76, 0x7c, 0x91, 0x3f, 0xa4, 0x97, 0x0a, 0xdc, 0x85, 0x53, 0x28, 0x01, 0x18, 0x15, 0x9b, 0x54, 0xc8, + 0x0f, 0x24, 0x46, 0xcc, 0x09, 0x14, 0xed, 0x1e, 0x81, 0x1f, 0x43, 0xbd, 0x97, 0x2d, 0x21, 0xd9, 0x5f, 0x8a, 0xde, + 0x46, 0xfc, 0xdf, 0x1c, 0x24, 0x28, 0xcf, 0x66, 0x41, 0x1c, 0x46, 0x2a, 0x4c, 0xb3, 0x9c, 0x1d, 0x49, 0x91, 0xb2, + 0xb2, 0xe1, 0x84, 0x6b, 0xc9, 0x2a, 0x00, 0xec, 0xa0, 0xdc, 0x54, 0x9a, 0xf7, 0x48, 0xcf, 0x7f, 0x28, 0x7c, 0x32, + 0x25, 0xa4, 0x95, 0x0d, 0xb0, 0x39, 0xeb, 0xd4, 0xc5, 0x5b, 0xcf, 0xf8, 0x5b, 0x68, 0x2c, 0x5d, 0x63, 0xec, 0x1a, + 0xef, 0x83, 0xcb, 0xb4, 0x76, 0xf1, 0xb2, 0x8c, 0x71, 0x06, 0xeb, 0x6b, 0x10, 0x67, 0xa9, 0x78, 0xaf, 0xf0, 0x2c, + 0x6e, 0x19, 0x72, 0xee, 0x46, 0x73, 0x26, 0x12, 0xb5, 0x89, 0xb7, 0x42, 0x42, 0xa0, 0x4b, 0x60, 0x81, 0x20, 0x64, + 0x0f, 0xb8, 0x01, 0x9d, 0x67, 0x4d, 0x92, 0xc8, 0xff, 0x81, 0xdd, 0xc1, 0x75, 0x32, 0x4e, 0xc2, 0x15, 0x48, 0xa6, + 0xdc, 0x39, 0xd7, 0x34, 0x18, 0xc0, 0xd4, 0xec, 0xf3, 0xb9, 0x4f, 0x9f, 0x98, 0x94, 0x3b, 0x2c, 0x09, 0xe7, 0x73, + 0x9f, 0x69, 0x52, 0x8e, 0xb1, 0xec, 0x33, 0xa7, 0x0f, 0x6c, 0x11, 0x9f, 0x5a, 0x4f, 0x9b, 0x0e, 0x56, 0xce, 0x01, + 0x0a, 0x9d, 0x3e, 0x20, 0x2e, 0x32, 0xa1, 0x42, 0x26, 0x5c, 0x13, 0xe7, 0x22, 0x3f, 0xb8, 0xe6, 0x34, 0x5c, 0x8f, + 0x7d, 0x66, 0xe2, 0x69, 0x80, 0x4f, 0x6e, 0xc6, 0xeb, 0xf1, 0xd8, 0xa7, 0xa4, 0x60, 0x10, 0x65, 0x2d, 0x8c, 0x51, + 0xfa, 0x99, 0xea, 0x7d, 0xe4, 0xd4, 0x92, 0xf2, 0xf0, 0xc1, 0x32, 0x12, 0x6e, 0x0b, 0xf4, 0x81, 0x04, 0x24, 0x9d, + 0xd5, 0x33, 0x3d, 0x50, 0xe1, 0x96, 0xc2, 0x62, 0xb5, 0x5f, 0xc3, 0xd2, 0x0d, 0x2e, 0xd4, 0xf7, 0x08, 0x61, 0xc5, + 0x0d, 0xa6, 0xca, 0x0b, 0xda, 0xbb, 0xaa, 0xa1, 0x92, 0x81, 0x17, 0xcf, 0x21, 0xa7, 0x1a, 0xea, 0x4b, 0xcf, 0x9d, + 0x07, 0x61, 0x9c, 0x78, 0x13, 0xf5, 0xb2, 0xff, 0xd2, 0xd3, 0x2e, 0x96, 0x89, 0xa6, 0x5f, 0x1a, 0x7f, 0x95, 0xb3, + 0x7d, 0x09, 0x4c, 0x89, 0xc9, 0xbe, 0x1a, 0xea, 0xc8, 0xa7, 0x67, 0x5b, 0x3d, 0x81, 0x91, 0xb1, 0xce, 0x5f, 0x07, + 0x50, 0xab, 0x94, 0x37, 0x0c, 0x13, 0x42, 0x42, 0xde, 0xb0, 0xbf, 0xea, 0x7d, 0x12, 0xb5, 0x7c, 0xbb, 0xde, 0x20, + 0xd3, 0x90, 0xe4, 0xc4, 0x17, 0x43, 0xdd, 0x0b, 0xff, 0x50, 0x7a, 0x7e, 0x20, 0xfb, 0x36, 0x14, 0xc8, 0xf8, 0xe8, + 0xdb, 0x22, 0x07, 0xf2, 0x68, 0x93, 0xa4, 0x60, 0x58, 0x18, 0x84, 0x89, 0x02, 0xf1, 0xdb, 0xe0, 0x83, 0xa3, 0xb2, + 0x2d, 0x34, 0xef, 0x55, 0xd3, 0x53, 0x8e, 0x05, 0x9e, 0x23, 0x2d, 0x45, 0xf9, 0x24, 0x84, 0x9b, 0x80, 0x50, 0xa4, + 0x85, 0x68, 0x4d, 0xdc, 0x03, 0x0f, 0x96, 0xaf, 0xc0, 0xbf, 0x49, 0x78, 0xbf, 0x48, 0xcf, 0x1f, 0x6d, 0xe2, 0x53, + 0x41, 0xd4, 0xdf, 0xc4, 0xb8, 0x96, 0xc0, 0xae, 0x70, 0x2a, 0x9f, 0xaa, 0xca, 0xa9, 0xa0, 0x44, 0x58, 0xb7, 0x80, + 0x5e, 0x35, 0xc1, 0xee, 0x46, 0x22, 0x32, 0x3e, 0x4f, 0x3f, 0x2e, 0x18, 0xb0, 0xd2, 0xd1, 0x83, 0x90, 0x4c, 0x19, + 0x6f, 0x95, 0x80, 0x5d, 0x35, 0x12, 0x0c, 0xc0, 0x5c, 0x9c, 0x47, 0x18, 0xa5, 0x57, 0xc0, 0x48, 0x42, 0x9c, 0x32, + 0x31, 0x47, 0x23, 0x94, 0x53, 0xc5, 0x79, 0xc1, 0x6a, 0x9d, 0x60, 0xfc, 0x79, 0x18, 0x00, 0x4b, 0x55, 0x05, 0x2f, + 0x89, 0x80, 0xeb, 0xf3, 0xcb, 0x4f, 0xaa, 0x2a, 0xde, 0xb4, 0x5a, 0xc6, 0xe5, 0x31, 0x80, 0xe3, 0x70, 0x1a, 0xa8, + 0xbd, 0x81, 0xc7, 0x88, 0x4f, 0x63, 0x62, 0xe4, 0xc9, 0x5b, 0xb4, 0x09, 0x5a, 0x39, 0xd4, 0x20, 0x90, 0x09, 0xf5, + 0xd3, 0xd7, 0xfc, 0xda, 0xc9, 0x42, 0x4c, 0xea, 0xc2, 0x34, 0x47, 0x20, 0x89, 0x3c, 0x05, 0xd8, 0x0d, 0x1e, 0x6d, + 0xdc, 0xcc, 0x80, 0x4e, 0x3d, 0x57, 0xc9, 0x7a, 0x6e, 0x84, 0x60, 0x18, 0xa5, 0x57, 0xb9, 0x3b, 0x6b, 0x3e, 0x5f, + 0xd8, 0x92, 0x54, 0xae, 0xa0, 0x3d, 0xdb, 0x80, 0x5b, 0xad, 0xad, 0x22, 0x6f, 0xe9, 0x46, 0x77, 0x64, 0xe4, 0x66, + 0xc8, 0x96, 0x70, 0xba, 0xaa, 0x10, 0x3d, 0x20, 0x00, 0x10, 0x69, 0x50, 0x95, 0x6f, 0xb2, 0x32, 0xc6, 0x67, 0x9b, + 0x59, 0xfa, 0xc0, 0xb7, 0xae, 0xd4, 0xa7, 0xcc, 0x22, 0x29, 0x23, 0x35, 0xe9, 0x6b, 0x71, 0xc3, 0xf4, 0xe2, 0xe2, + 0xf4, 0x82, 0xe2, 0x46, 0xc3, 0xc9, 0x10, 0xa5, 0xa0, 0x71, 0xe3, 0xcc, 0x30, 0xd5, 0x65, 0xfd, 0x8a, 0xd2, 0xbb, + 0x3f, 0x74, 0x39, 0x18, 0x2c, 0x47, 0x00, 0xcb, 0x51, 0x23, 0x80, 0x75, 0xc5, 0x8a, 0x00, 0x2f, 0x02, 0x5c, 0x48, + 0x84, 0x1c, 0x08, 0x65, 0xc1, 0x54, 0xb2, 0x2d, 0x14, 0xc1, 0xd1, 0xa0, 0xb1, 0xd3, 0xd1, 0x88, 0x06, 0x83, 0x10, + 0x5b, 0x45, 0xe9, 0xc9, 0x01, 0xd5, 0x26, 0xa2, 0x48, 0x95, 0x00, 0x0c, 0x11, 0xcc, 0x30, 0x87, 0x02, 0xa4, 0x01, + 0x1f, 0x38, 0xf9, 0x45, 0xc7, 0x5a, 0xa2, 0xf2, 0xd9, 0x39, 0x2d, 0x32, 0x3c, 0xd8, 0x4a, 0x1d, 0x9e, 0x60, 0x62, + 0x4f, 0x20, 0xeb, 0x10, 0xfa, 0xea, 0xe4, 0x80, 0x1e, 0x95, 0xd2, 0x89, 0xc8, 0x3b, 0x11, 0x52, 0xc7, 0x1e, 0xef, + 0xe0, 0x5e, 0x47, 0x25, 0x4e, 0xd8, 0x0a, 0x4a, 0xdd, 0x54, 0x55, 0x96, 0x9c, 0xc1, 0xe2, 0x31, 0xf6, 0x20, 0x00, + 0x8f, 0x0d, 0x8e, 0x0f, 0xaa, 0xb2, 0x74, 0x6f, 0x71, 0xe6, 0xe2, 0x8d, 0x7b, 0xab, 0x39, 0xfc, 0x55, 0x7e, 0xd6, + 0xe2, 0xe2, 0x59, 0x9b, 0xf0, 0xc5, 0x05, 0xef, 0x3a, 0xc1, 0x58, 0x6b, 0x0b, 0xb4, 0x5a, 0xaa, 0x59, 0xdc, 0x85, + 0x58, 0xdc, 0x69, 0xc3, 0xe2, 0x4e, 0xb7, 0x2c, 0xae, 0xcf, 0x17, 0x52, 0xc9, 0x40, 0x17, 0xa1, 0xc7, 0x74, 0x06, + 0x3c, 0xce, 0x8f, 0xf4, 0xf8, 0x39, 0x43, 0x38, 0x99, 0xb1, 0x0f, 0x16, 0xc3, 0x0d, 0xb0, 0xaa, 0x83, 0x8b, 0x04, + 0x88, 0xea, 0xc4, 0xb3, 0x53, 0x37, 0x91, 0x24, 0x03, 0x9a, 0x5f, 0x9e, 0x2f, 0xec, 0x52, 0x6c, 0x68, 0x68, 0x8b, + 0x86, 0x99, 0x2e, 0xb6, 0xcc, 0x74, 0x52, 0x38, 0xba, 0x7c, 0xda, 0x74, 0x08, 0xe5, 0x49, 0xc1, 0x1e, 0x04, 0x2f, + 0x0a, 0xdc, 0x32, 0xc5, 0x7d, 0xd8, 0x8c, 0x63, 0xa5, 0x1d, 0xb5, 0x72, 0xe3, 0xf8, 0x26, 0x8c, 0xc0, 0x0c, 0x01, + 0xba, 0xb9, 0xdf, 0x96, 0x5a, 0x7a, 0x01, 0x8f, 0x70, 0xd6, 0xb8, 0x99, 0xf2, 0xf7, 0xf2, 0x96, 0x6a, 0x75, 0x3a, + 0x54, 0x63, 0xe5, 0x26, 0x09, 0x8b, 0x10, 0xe8, 0x2e, 0xa4, 0xc2, 0xf8, 0x7f, 0xb2, 0xcd, 0x6a, 0x70, 0x88, 0x2f, + 0x61, 0x75, 0xc4, 0xd0, 0x2b, 0x60, 0xc1, 0x48, 0xef, 0x18, 0xe8, 0x1b, 0x29, 0x5a, 0x6a, 0x94, 0x01, 0xfe, 0x27, + 0x3c, 0xae, 0x5a, 0x24, 0xf9, 0xf3, 0x3a, 0x47, 0xba, 0xb5, 0x72, 0xa7, 0xef, 0xc1, 0xda, 0x45, 0x6b, 0x19, 0xe0, + 0xb9, 0x22, 0xc7, 0x46, 0x8d, 0x88, 0x27, 0x9c, 0xe4, 0x48, 0x12, 0xb1, 0x24, 0xb7, 0x0b, 0x86, 0x90, 0x02, 0xae, + 0x39, 0xbb, 0xdc, 0xb4, 0xd2, 0x83, 0xb9, 0xa7, 0x57, 0xb0, 0x26, 0xa0, 0x36, 0x7f, 0x30, 0xcc, 0x84, 0x6e, 0xbe, + 0xe1, 0x1c, 0xe9, 0xa0, 0x0e, 0xbd, 0x80, 0xa4, 0xe7, 0xb6, 0xb8, 0x4c, 0x8f, 0x22, 0xa0, 0x5a, 0xa0, 0x3c, 0x7c, + 0x3c, 0xc7, 0x5f, 0xce, 0x65, 0xfa, 0x78, 0x8c, 0xbf, 0x5a, 0x97, 0x99, 0xaa, 0xaa, 0x24, 0x45, 0x90, 0xe6, 0xac, + 0x0e, 0x0b, 0xfb, 0x89, 0x8c, 0xb2, 0xef, 0xb1, 0x6d, 0xf8, 0x02, 0x3f, 0x7c, 0xb4, 0x89, 0x21, 0x0c, 0x81, 0x3c, + 0x87, 0xc0, 0x8a, 0xf4, 0xb4, 0xb6, 0x7c, 0xde, 0x50, 0x3e, 0xd6, 0xff, 0x60, 0xc2, 0x8f, 0xbb, 0x24, 0xcc, 0x69, + 0x4a, 0x51, 0x06, 0x72, 0x35, 0xf6, 0x02, 0x37, 0xba, 0xbb, 0xa2, 0x5b, 0x88, 0x26, 0x09, 0x79, 0x1f, 0xe4, 0xc2, + 0x81, 0xbb, 0xa2, 0x0d, 0x48, 0x22, 0x29, 0xa8, 0xee, 0x38, 0xa1, 0x1f, 0xfc, 0x10, 0x49, 0xfc, 0x5d, 0xe1, 0x1a, + 0xcb, 0x17, 0xa4, 0xf0, 0xa1, 0xab, 0x47, 0x1b, 0x8d, 0x55, 0xbb, 0x29, 0xcd, 0xb6, 0xc4, 0x40, 0xc2, 0xf2, 0xe0, + 0x95, 0x78, 0x39, 0xf5, 0x7a, 0x68, 0xe4, 0x31, 0x0e, 0x6f, 0xcd, 0x47, 0x9b, 0xe4, 0x54, 0x5d, 0xba, 0xd1, 0x47, + 0x36, 0x35, 0x27, 0x5e, 0x34, 0xf1, 0x81, 0x79, 0x1c, 0xfb, 0x6e, 0xf0, 0x91, 0x3f, 0x9a, 0xe1, 0x3a, 0x41, 0xb3, + 0xad, 0x9d, 0x37, 0x68, 0x01, 0x13, 0x12, 0x24, 0x22, 0x57, 0x5b, 0x03, 0x05, 0xe5, 0xc5, 0x48, 0x5c, 0xeb, 0x73, + 0x46, 0x31, 0xaf, 0x65, 0x80, 0xd7, 0x01, 0x58, 0x92, 0x41, 0x18, 0x07, 0x43, 0xc5, 0xf5, 0x52, 0x0d, 0x79, 0xaa, + 0xa4, 0x47, 0xcb, 0xf2, 0x10, 0x5f, 0x61, 0x0f, 0xff, 0xfd, 0xe7, 0xa0, 0xe4, 0x3e, 0x9f, 0xcb, 0x7a, 0xf9, 0xbc, + 0x19, 0x42, 0xa9, 0x49, 0xee, 0x83, 0xf7, 0xf8, 0x38, 0x67, 0x30, 0x9b, 0x3f, 0x2d, 0x37, 0x76, 0xe3, 0x78, 0xbd, + 0x64, 0x53, 0x52, 0x86, 0x9d, 0xe6, 0x83, 0x2a, 0xde, 0x43, 0xe4, 0x81, 0xfd, 0x73, 0xdd, 0x3a, 0x3e, 0x7c, 0x01, + 0x66, 0x7c, 0xc0, 0x50, 0x86, 0xb3, 0x99, 0x9a, 0x8b, 0x02, 0x76, 0x34, 0x73, 0x0e, 0xff, 0xb9, 0x7e, 0xfd, 0xca, + 0x7e, 0x9d, 0x35, 0x0e, 0x80, 0x31, 0x16, 0x36, 0x49, 0x9c, 0x2f, 0x96, 0xc6, 0x2b, 0x66, 0x34, 0x73, 0x83, 0xe6, + 0xe9, 0x5c, 0x14, 0xb6, 0xf8, 0x8a, 0xb1, 0x29, 0x30, 0xdc, 0x46, 0xa5, 0xf4, 0xca, 0x67, 0xd7, 0x2c, 0xb3, 0x77, + 0xaa, 0x7e, 0xac, 0xa6, 0x05, 0x06, 0x64, 0xe5, 0xba, 0x47, 0xce, 0xd5, 0x49, 0x53, 0x1a, 0xe1, 0x1c, 0xf8, 0xcc, + 0xe5, 0x23, 0x56, 0x3a, 0x52, 0x23, 0x43, 0x95, 0x06, 0xd0, 0x38, 0xb2, 0xd3, 0x86, 0xf2, 0x1e, 0x20, 0xea, 0x86, + 0xb1, 0x19, 0x8e, 0xde, 0x83, 0x04, 0x16, 0x1c, 0x4e, 0x3e, 0x9c, 0x3c, 0x2d, 0x97, 0x9a, 0x34, 0x41, 0xac, 0x4e, + 0xd4, 0xa6, 0x92, 0x90, 0x46, 0xb8, 0x00, 0xa0, 0x2f, 0x8c, 0x10, 0x57, 0xd5, 0xae, 0x8d, 0x52, 0x9c, 0xf9, 0x18, + 0xd3, 0xbb, 0x07, 0x2c, 0x8e, 0x1b, 0x01, 0x96, 0x2d, 0xba, 0xa1, 0xe6, 0xb5, 0x8b, 0xf0, 0xc8, 0xcb, 0x0d, 0xdb, + 0x00, 0x96, 0x00, 0x27, 0x58, 0xfe, 0x16, 0x92, 0x97, 0xab, 0x25, 0x37, 0xe2, 0x8c, 0xe6, 0x63, 0x95, 0x1b, 0xd8, + 0x35, 0xbd, 0xbf, 0x51, 0xf9, 0xa0, 0x0a, 0x64, 0xba, 0x76, 0x68, 0x5a, 0x01, 0xf5, 0x56, 0xa4, 0x4a, 0xd8, 0x81, + 0x18, 0x53, 0x09, 0xbf, 0xb2, 0xd9, 0x8c, 0x4d, 0x92, 0x58, 0x17, 0x32, 0xa6, 0x2c, 0xa4, 0x3a, 0x28, 0xed, 0x1e, + 0x0c, 0xd4, 0x9f, 0x20, 0xb0, 0x8c, 0x88, 0x3c, 0xc8, 0x07, 0x24, 0xee, 0x4c, 0xf5, 0x60, 0xa2, 0x1e, 0x8b, 0x20, + 0xe2, 0x5f, 0x01, 0x29, 0x74, 0x4d, 0x39, 0x0e, 0x8d, 0xd3, 0x9f, 0x7c, 0x5f, 0x84, 0x99, 0xa9, 0xe7, 0x76, 0x54, + 0xb4, 0xed, 0xf8, 0x6e, 0x9c, 0xd7, 0x1d, 0xc7, 0x4e, 0x55, 0x03, 0x1c, 0x9a, 0x3f, 0x96, 0xb6, 0x31, 0x11, 0xa8, + 0x81, 0x7a, 0xf6, 0xf6, 0xc5, 0x0f, 0xaf, 0x5e, 0xee, 0x8b, 0x11, 0xb0, 0xcb, 0x36, 0x74, 0xb9, 0x0e, 0xb6, 0x74, + 0xfa, 0xcb, 0x4f, 0xf7, 0xeb, 0xb6, 0xe5, 0x3c, 0x73, 0x54, 0x83, 0x6c, 0xd0, 0x25, 0xbc, 0x38, 0x09, 0xaf, 0x59, + 0xf4, 0xd9, 0x60, 0x90, 0x3b, 0xaf, 0x1f, 0xee, 0xdb, 0x9f, 0x5f, 0xfd, 0xb4, 0xf7, 0x50, 0x8f, 0x1c, 0x1b, 0x70, + 0x7b, 0x12, 0xae, 0xee, 0x31, 0xbb, 0xb6, 0x6a, 0xa8, 0x13, 0x3f, 0x8c, 0x59, 0xc3, 0x08, 0x5e, 0x9c, 0xbd, 0x7d, + 0x8f, 0xe0, 0xca, 0x59, 0x10, 0xea, 0xea, 0xf3, 0x26, 0xff, 0xf3, 0xbb, 0x57, 0xef, 0xdf, 0xab, 0x06, 0xa6, 0xe4, + 0x8e, 0xe5, 0xde, 0xf9, 0x26, 0xde, 0x41, 0x71, 0x6a, 0xf7, 0x3a, 0x51, 0x35, 0xba, 0x48, 0x17, 0x67, 0x43, 0x65, + 0x95, 0x6d, 0xce, 0xa9, 0x1d, 0xff, 0x32, 0xdd, 0x7e, 0xf7, 0x9a, 0x57, 0x0d, 0x3e, 0xda, 0x4e, 0x52, 0x0b, 0x25, + 0x4b, 0x2f, 0xb8, 0xaa, 0x29, 0x75, 0x6f, 0x6b, 0x4a, 0xe1, 0xfa, 0x58, 0xc1, 0x8f, 0xeb, 0x70, 0x29, 0xb1, 0x23, + 0xec, 0x76, 0x37, 0xb8, 0xa4, 0x3b, 0xdc, 0x67, 0x0c, 0x9a, 0xa7, 0x54, 0x29, 0x8f, 0xba, 0xa6, 0x98, 0x5f, 0xbc, + 0x32, 0xd8, 0x4e, 0x7c, 0xb0, 0xbc, 0x67, 0xb2, 0x1a, 0xb2, 0xc8, 0xaa, 0x72, 0xbf, 0x99, 0x41, 0xe9, 0x56, 0x40, + 0xcd, 0x48, 0x75, 0xc3, 0x69, 0xca, 0xca, 0x9d, 0x82, 0x39, 0xbb, 0x39, 0x0e, 0x93, 0x24, 0x5c, 0xf6, 0x1c, 0x7b, + 0x75, 0xab, 0x2a, 0x7d, 0x21, 0xec, 0xe0, 0xd6, 0xf6, 0xbd, 0xdf, 0xfe, 0x53, 0x42, 0xf3, 0x54, 0x7e, 0x95, 0xb0, + 0xe5, 0x8a, 0x45, 0x6e, 0xb2, 0x8e, 0x58, 0xaa, 0xfc, 0xf6, 0xbf, 0x2f, 0x4a, 0x17, 0xfb, 0xbe, 0xdc, 0x86, 0x58, + 0x7a, 0xb9, 0xc9, 0x95, 0x1f, 0xde, 0x3c, 0xc8, 0xfd, 0xea, 0x76, 0x54, 0x5e, 0x78, 0xf3, 0x45, 0x56, 0xfb, 0x34, + 0xd9, 0x32, 0x37, 0x31, 0x7a, 0xd2, 0x07, 0x28, 0x67, 0xe1, 0x4d, 0xef, 0xb7, 0xff, 0x64, 0x02, 0x9b, 0x9d, 0xbb, + 0xae, 0x7e, 0xa0, 0xc5, 0x15, 0xad, 0xaf, 0x53, 0x59, 0x62, 0x78, 0x5f, 0x59, 0xe0, 0x4a, 0x21, 0xed, 0xca, 0xaa, + 0x6e, 0x6e, 0xcb, 0x9c, 0xbe, 0xf3, 0xe6, 0x8b, 0xcf, 0x9d, 0x14, 0x00, 0x74, 0xe7, 0xac, 0xa0, 0xd2, 0x17, 0x98, + 0xd6, 0xa8, 0xb7, 0xff, 0x82, 0x7d, 0xe6, 0xbc, 0x76, 0x4d, 0xe9, 0x4b, 0xcc, 0x86, 0x4b, 0x6e, 0x5f, 0x8c, 0x46, + 0x59, 0x4a, 0x5a, 0xb9, 0x3d, 0x78, 0x06, 0x9e, 0x56, 0x4a, 0x38, 0x7b, 0xd1, 0xb3, 0x75, 0x0a, 0xd9, 0xb3, 0x07, + 0x40, 0xd0, 0xc6, 0xbd, 0x06, 0x1c, 0xcd, 0xf8, 0x9a, 0x5c, 0xd5, 0x2a, 0xdf, 0xae, 0x20, 0x6b, 0x28, 0xc5, 0x74, + 0xa6, 0x99, 0xd6, 0xd0, 0xa8, 0x1f, 0xce, 0x4d, 0xe4, 0xae, 0x48, 0x49, 0xa0, 0xa0, 0xc6, 0x04, 0x84, 0x2e, 0xa5, + 0x5b, 0xf4, 0xb5, 0xeb, 0x5f, 0xef, 0x77, 0xa1, 0x6a, 0xa6, 0x60, 0x48, 0x9a, 0xff, 0x3c, 0xe2, 0x8d, 0x74, 0x79, + 0x7f, 0xda, 0x8d, 0x69, 0xe2, 0x5e, 0x35, 0x99, 0xd6, 0xbf, 0xd9, 0x6d, 0x5a, 0x7f, 0xbe, 0x97, 0x69, 0xfd, 0x9b, + 0x2f, 0x6e, 0x5a, 0xff, 0x4a, 0x36, 0xad, 0x87, 0x4d, 0xfc, 0x8a, 0xed, 0x65, 0xc9, 0x2c, 0xac, 0x8d, 0xc2, 0x9b, + 0x78, 0xe0, 0xf0, 0x4b, 0x4f, 0x3c, 0x59, 0x30, 0x90, 0x22, 0x71, 0x70, 0xf9, 0xe1, 0x1c, 0x0c, 0x8e, 0x9b, 0x4d, + 0x8a, 0xbf, 0x94, 0x41, 0xb1, 0x1f, 0xce, 0x55, 0x29, 0x50, 0x7e, 0x20, 0x02, 0xe5, 0x43, 0x70, 0x80, 0x7f, 0xde, + 0x3a, 0xcf, 0x2f, 0x9c, 0x7e, 0xdb, 0x81, 0x40, 0x33, 0x20, 0x18, 0xc0, 0x02, 0xbb, 0xdf, 0x6e, 0x43, 0xc1, 0x8d, + 0x54, 0xd0, 0x82, 0x02, 0x4f, 0x2a, 0xe8, 0x40, 0xc1, 0x44, 0x2a, 0x38, 0x82, 0x82, 0xa9, 0x54, 0x70, 0x0c, 0x05, + 0xd7, 0x6a, 0x7a, 0x11, 0x64, 0x8e, 0x03, 0xc7, 0xfa, 0x65, 0x21, 0x47, 0x4a, 0x26, 0xc5, 0x12, 0x55, 0x8e, 0x0d, + 0x11, 0xb0, 0xd3, 0x3c, 0xd4, 0xb9, 0x89, 0xfa, 0xe8, 0xab, 0x11, 0xb8, 0xd2, 0x83, 0x50, 0xcf, 0x00, 0x91, 0x28, + 0xd5, 0x6c, 0x8b, 0xd7, 0x6a, 0x2f, 0x33, 0xb4, 0xb7, 0x8d, 0x96, 0x30, 0x5c, 0xef, 0xa1, 0x1b, 0x95, 0xa8, 0xdc, + 0x79, 0xba, 0xc8, 0xa2, 0x77, 0xad, 0x07, 0xb9, 0x37, 0x62, 0x1b, 0x62, 0x18, 0x83, 0x6a, 0xfa, 0x25, 0xf2, 0x07, + 0x56, 0x12, 0x82, 0xb3, 0x99, 0x88, 0x5a, 0x25, 0x3e, 0xa0, 0xa0, 0x37, 0x42, 0xdf, 0xcd, 0x03, 0x8c, 0xf1, 0x58, + 0x77, 0x34, 0xfa, 0x65, 0x16, 0x42, 0x8c, 0xae, 0xb9, 0x6b, 0x23, 0x71, 0xe7, 0xbd, 0x85, 0x41, 0x32, 0xee, 0xde, + 0x1c, 0x62, 0xc2, 0x9e, 0x4e, 0x7b, 0x2b, 0xe3, 0x66, 0xc1, 0x82, 0xde, 0x8c, 0x5b, 0x81, 0xc2, 0xfa, 0x93, 0x91, + 0xcf, 0x52, 0x17, 0xd6, 0x69, 0xb8, 0x27, 0xf2, 0xb7, 0x34, 0x4a, 0x33, 0xdb, 0x4a, 0xb9, 0x61, 0x95, 0x26, 0xcb, + 0xbf, 0xbf, 0x84, 0x19, 0xcc, 0x4b, 0x36, 0x5e, 0xcf, 0x95, 0xb3, 0x70, 0xbe, 0xd3, 0xe4, 0x45, 0x7e, 0x05, 0xa3, + 0x54, 0x49, 0xd1, 0x67, 0x8a, 0xed, 0xcd, 0xbf, 0x45, 0x8f, 0x69, 0xb1, 0x7e, 0x02, 0x63, 0x53, 0x12, 0x42, 0xd9, + 0xf0, 0x1d, 0x80, 0xb6, 0x64, 0x54, 0x72, 0x06, 0xf0, 0x93, 0x9e, 0xcf, 0x5d, 0x69, 0x3c, 0xc3, 0x1f, 0x59, 0x1c, + 0xbb, 0x73, 0x51, 0xbf, 0x3a, 0x4e, 0xf0, 0xaf, 0xca, 0x6e, 0xfa, 0x08, 0x40, 0x90, 0x19, 0x7b, 0x15, 0x53, 0x21, + 0xb0, 0x60, 0x06, 0x13, 0x3a, 0x58, 0xb4, 0xdc, 0xae, 0xc6, 0xb3, 0x60, 0x79, 0x8a, 0x26, 0x2e, 0x80, 0x44, 0xae, + 0x99, 0x5f, 0x2e, 0x4c, 0xdc, 0x79, 0xb9, 0x88, 0xd6, 0x3a, 0x95, 0xc7, 0x96, 0x59, 0x98, 0x14, 0x0a, 0x3f, 0xc7, + 0x64, 0xc2, 0x0f, 0xe7, 0xbf, 0xab, 0xbd, 0xc4, 0x16, 0x3b, 0x97, 0xf7, 0x81, 0x11, 0x24, 0x23, 0x0b, 0x61, 0xac, + 0x58, 0x00, 0xc2, 0x5e, 0x90, 0x2c, 0x4c, 0xf4, 0xec, 0xd7, 0x5a, 0x81, 0x6e, 0x58, 0xb8, 0xb6, 0x9b, 0x72, 0x3c, + 0x93, 0x5e, 0x34, 0x1f, 0xbb, 0x9a, 0xd3, 0x3a, 0x36, 0xc4, 0x1f, 0xcb, 0xee, 0xe8, 0x29, 0xf6, 0xa0, 0x4c, 0xbd, + 0xeb, 0xcd, 0x2c, 0x0c, 0x12, 0x73, 0xe6, 0x2e, 0x3d, 0xff, 0xae, 0xb7, 0x0c, 0x83, 0x30, 0x5e, 0xb9, 0x13, 0xd6, + 0xcf, 0x45, 0x37, 0x7d, 0x8c, 0x94, 0xc5, 0x83, 0x35, 0x38, 0x56, 0x2b, 0x62, 0x4b, 0x6a, 0x9d, 0x05, 0xc2, 0x9a, + 0xf9, 0xec, 0x36, 0xe5, 0x9f, 0x2f, 0x54, 0xa6, 0xaa, 0xb8, 0xe5, 0xa8, 0x05, 0xdc, 0x43, 0x78, 0x94, 0x2d, 0x88, + 0x2d, 0xd9, 0xe7, 0xcc, 0x7c, 0xcf, 0x6a, 0x75, 0x22, 0xb6, 0x54, 0xac, 0x4e, 0x63, 0xe7, 0x51, 0x78, 0x33, 0x84, + 0xd1, 0x62, 0x63, 0x33, 0x66, 0xfe, 0x0c, 0xdf, 0x98, 0xe8, 0xd8, 0x2b, 0xfa, 0x31, 0x51, 0xe4, 0x03, 0xbd, 0xb1, + 0x65, 0x1f, 0x5e, 0xf7, 0x5a, 0x8a, 0xdd, 0x5f, 0x7a, 0x81, 0x49, 0xd3, 0x39, 0xb6, 0x57, 0x52, 0x5f, 0x32, 0xfc, + 0xf4, 0x0d, 0x56, 0x77, 0x14, 0xbb, 0x0f, 0x57, 0xfb, 0x99, 0x1f, 0xde, 0xf4, 0x16, 0xde, 0x74, 0xca, 0x82, 0x3e, + 0x8e, 0x39, 0x2b, 0x64, 0xbe, 0xef, 0xad, 0x62, 0x2f, 0xee, 0x2f, 0xdd, 0x5b, 0xde, 0xeb, 0x61, 0x53, 0xaf, 0x6d, + 0xde, 0x6b, 0x7b, 0xef, 0x5e, 0xa5, 0x6e, 0xc0, 0x89, 0x98, 0xfa, 0xe1, 0x43, 0xeb, 0x28, 0x76, 0x69, 0x9e, 0x7b, + 0xf7, 0xba, 0x8a, 0xd8, 0x66, 0xe9, 0x46, 0x73, 0x2f, 0xe8, 0xd9, 0xa9, 0x75, 0xbd, 0xa1, 0x8d, 0xf1, 0xb0, 0xdb, + 0xed, 0xa6, 0xd6, 0x54, 0x3c, 0xd9, 0xd3, 0x69, 0x6a, 0x4d, 0xc4, 0xd3, 0x6c, 0x66, 0xdb, 0xb3, 0x59, 0x6a, 0x79, + 0xa2, 0xa0, 0xdd, 0x9a, 0x4c, 0xdb, 0xad, 0xd4, 0xba, 0x91, 0x6a, 0xa4, 0x16, 0xe3, 0x4f, 0x11, 0x9b, 0xf6, 0x71, + 0x23, 0x71, 0x73, 0xf4, 0x63, 0xdb, 0x4e, 0x11, 0x03, 0x5c, 0x14, 0x70, 0x13, 0x4a, 0x15, 0x2f, 0x37, 0x7b, 0xd7, + 0x54, 0xf2, 0xcf, 0x4d, 0x26, 0xb5, 0xf5, 0xa6, 0x6e, 0xf4, 0xf1, 0x52, 0x91, 0x66, 0xe1, 0xba, 0x54, 0x6d, 0x23, + 0xc0, 0x60, 0xde, 0xf6, 0x20, 0x62, 0x6a, 0x7f, 0x1c, 0x46, 0x70, 0x66, 0x23, 0x77, 0xea, 0xad, 0xe3, 0x9e, 0xd3, + 0x5a, 0xdd, 0x8a, 0x22, 0xbe, 0xd7, 0xf3, 0x02, 0x3c, 0x7b, 0xbd, 0x38, 0xf4, 0xbd, 0xa9, 0x28, 0x6a, 0x3a, 0x4b, + 0x4e, 0x4b, 0xef, 0x63, 0xbc, 0x20, 0x0f, 0xa3, 0x5e, 0xb9, 0xbe, 0xaf, 0x58, 0xed, 0x58, 0x61, 0x6e, 0x8c, 0x9a, + 0x0c, 0xc5, 0x8e, 0x09, 0x2e, 0x18, 0x1b, 0xc8, 0x39, 0x5c, 0xdd, 0x66, 0x7b, 0xde, 0x39, 0x5a, 0xdd, 0xa6, 0xdf, + 0x2c, 0xd9, 0xd4, 0x73, 0x15, 0x2d, 0xdf, 0x4d, 0x8e, 0x0d, 0xda, 0x0e, 0x7d, 0xd3, 0xb0, 0x4d, 0xc5, 0xb1, 0x80, + 0xc8, 0xd2, 0x0f, 0xbc, 0xe5, 0x2a, 0x8c, 0x12, 0x37, 0x48, 0xd2, 0x74, 0x74, 0x99, 0xa6, 0xfd, 0x73, 0x4f, 0xbb, + 0xf8, 0xbb, 0x46, 0xb4, 0x90, 0xb4, 0x83, 0xa9, 0x7e, 0x69, 0xbc, 0x62, 0xb2, 0x25, 0x13, 0x90, 0x31, 0xb4, 0x62, + 0x92, 0x2b, 0x13, 0xbd, 0xad, 0x56, 0x26, 0x20, 0x67, 0xd5, 0xc9, 0x30, 0xaa, 0x58, 0x05, 0x29, 0x10, 0x54, 0x78, + 0xc5, 0x06, 0xe7, 0x92, 0x59, 0x14, 0x30, 0x3d, 0x58, 0x99, 0xdc, 0x3a, 0x5f, 0x36, 0xf1, 0x9e, 0xe7, 0xbb, 0x79, + 0xcf, 0x7f, 0x24, 0xfb, 0xf0, 0x9e, 0xe7, 0x5f, 0x9c, 0xf7, 0x7c, 0x59, 0x75, 0xeb, 0x3c, 0x0f, 0x07, 0x6a, 0xa6, + 0xcb, 0x02, 0xd2, 0x14, 0x51, 0xc0, 0xc4, 0x97, 0xc9, 0x7f, 0xeb, 0x5f, 0x27, 0x7a, 0xa3, 0x14, 0xc0, 0x44, 0xb9, + 0x81, 0x81, 0x7f, 0x1b, 0x0c, 0x7e, 0x88, 0xe4, 0xe7, 0xd9, 0x6c, 0xf0, 0x32, 0x94, 0x0a, 0xb2, 0x27, 0x6e, 0xe6, + 0x53, 0x08, 0x6e, 0x45, 0x6f, 0x32, 0x43, 0x2c, 0x48, 0xff, 0x05, 0xb1, 0x71, 0xc8, 0xea, 0x7e, 0x9a, 0x99, 0x43, + 0xf6, 0x8b, 0x43, 0xd0, 0x32, 0xfb, 0x63, 0xe1, 0x01, 0x5d, 0x11, 0x5a, 0xcf, 0x59, 0xc2, 0x43, 0x96, 0x3c, 0xbf, + 0x7b, 0x33, 0xd5, 0xce, 0x43, 0x3d, 0xf5, 0xe2, 0xb7, 0x65, 0xff, 0x63, 0x71, 0x05, 0x91, 0xa7, 0x93, 0x72, 0x93, + 0x46, 0x29, 0xcc, 0x10, 0xbe, 0xa6, 0xe6, 0xa7, 0x85, 0x99, 0xf6, 0xe4, 0x86, 0x3c, 0xcf, 0x68, 0x85, 0x18, 0x73, + 0x3f, 0xbd, 0x0d, 0xe7, 0xf2, 0x30, 0x75, 0x2a, 0x86, 0x6d, 0x99, 0x52, 0x73, 0x6f, 0x9a, 0xa6, 0x7a, 0x5f, 0x00, + 0x42, 0x22, 0xb4, 0x6c, 0x17, 0x13, 0x17, 0xe7, 0x17, 0x5a, 0xae, 0x8b, 0x26, 0x45, 0xf3, 0x39, 0x98, 0x6e, 0x70, + 0xb5, 0x34, 0x87, 0x99, 0xaa, 0x10, 0xf8, 0xc8, 0xa4, 0x47, 0x9a, 0x10, 0xd8, 0x1a, 0xc8, 0x86, 0x70, 0x85, 0x05, + 0xa9, 0xda, 0x1c, 0x13, 0x70, 0xd0, 0xf6, 0x04, 0x82, 0x2c, 0x09, 0x69, 0x17, 0xa1, 0x1d, 0x5e, 0x07, 0x1f, 0x52, + 0x35, 0xe3, 0xfd, 0x70, 0xfb, 0x0d, 0x4f, 0x0e, 0xa0, 0xc1, 0xb0, 0x24, 0xc9, 0xda, 0x61, 0x32, 0x0b, 0xac, 0x44, + 0x7c, 0x63, 0x58, 0xf1, 0x8d, 0xf2, 0x64, 0x23, 0x02, 0x94, 0x25, 0xee, 0xca, 0x04, 0xf1, 0x09, 0xe2, 0x5e, 0x8e, + 0xf1, 0xa4, 0x58, 0x68, 0xfd, 0x75, 0x0c, 0xb8, 0x11, 0x6f, 0xf2, 0x88, 0x7f, 0xfa, 0x93, 0x75, 0x14, 0x87, 0x51, + 0x6f, 0x15, 0x7a, 0x41, 0xc2, 0xa2, 0x14, 0x41, 0x75, 0x81, 0xf0, 0x11, 0xe0, 0xb9, 0xdc, 0x84, 0x2b, 0x77, 0xe2, + 0x25, 0x77, 0x3d, 0x9b, 0xb3, 0x14, 0x76, 0x9f, 0x73, 0x07, 0x76, 0x6d, 0xfd, 0x1e, 0x87, 0xe6, 0x53, 0x64, 0xfc, + 0xa2, 0x2a, 0x3b, 0x23, 0x6f, 0xf3, 0xbe, 0xf4, 0x96, 0x42, 0xb4, 0x01, 0xfb, 0xe1, 0x46, 0xe6, 0x1c, 0xb0, 0x3c, + 0x2c, 0xb5, 0x3d, 0x65, 0x73, 0x03, 0xb1, 0x36, 0x68, 0x80, 0xc4, 0x1f, 0xab, 0xa3, 0x2b, 0x76, 0x7d, 0x31, 0x70, + 0x3c, 0xfa, 0x3e, 0x23, 0xeb, 0xb9, 0x90, 0xd0, 0xd4, 0xd8, 0xa7, 0xe6, 0x98, 0xcd, 0xc2, 0x88, 0x51, 0x38, 0x7f, + 0xa7, 0xbb, 0xba, 0xdd, 0xbf, 0xfb, 0xed, 0xd3, 0xaf, 0xef, 0x27, 0x08, 0x13, 0x4d, 0x74, 0xa6, 0xef, 0xe8, 0xad, + 0x4a, 0xcf, 0x80, 0x35, 0x24, 0xc8, 0x4f, 0xc8, 0x1f, 0x05, 0x5c, 0xb1, 0x6b, 0xa3, 0xa6, 0xae, 0x42, 0x4e, 0xf3, + 0x22, 0xe6, 0xbb, 0x89, 0x77, 0x2d, 0x78, 0xc6, 0xf6, 0xd1, 0xea, 0x56, 0xac, 0x31, 0x12, 0xbc, 0x7b, 0x2c, 0x52, + 0x69, 0x28, 0x62, 0x91, 0xca, 0xc5, 0xb8, 0x48, 0xfd, 0xca, 0x6c, 0x44, 0x20, 0xb1, 0x12, 0xa5, 0xef, 0xac, 0x6e, + 0x65, 0x12, 0x9d, 0x37, 0xcb, 0x28, 0x75, 0x39, 0x02, 0xec, 0xd2, 0x9b, 0x4e, 0x7d, 0x96, 0x16, 0x16, 0xba, 0xb8, + 0x96, 0x12, 0x70, 0x32, 0x38, 0xb8, 0xe3, 0x38, 0xf4, 0xd7, 0x09, 0xab, 0x07, 0x17, 0x01, 0xa7, 0x65, 0xe7, 0xc0, + 0xc1, 0xdf, 0xc5, 0xb1, 0x76, 0x80, 0xdd, 0x86, 0x6d, 0x62, 0xf7, 0x21, 0xe1, 0x83, 0xd9, 0x2e, 0x0e, 0x1d, 0x5e, + 0x65, 0x83, 0x36, 0x6a, 0x26, 0x62, 0x00, 0x59, 0x22, 0xec, 0xad, 0x58, 0x0e, 0x2f, 0xcb, 0x82, 0xde, 0x67, 0x45, + 0x69, 0x71, 0x32, 0xbf, 0xcf, 0x19, 0x7b, 0x56, 0x7f, 0xc6, 0x9e, 0x89, 0x33, 0xb6, 0x7d, 0x67, 0x3e, 0x9c, 0x39, + 0xf0, 0x5f, 0x3f, 0x9f, 0x50, 0xcf, 0x56, 0xda, 0xab, 0x5b, 0xc5, 0x59, 0xdd, 0x2a, 0x66, 0x6b, 0x75, 0xab, 0x60, + 0xd7, 0x68, 0x79, 0x64, 0x58, 0x2d, 0xdd, 0xb0, 0x15, 0x28, 0x84, 0x3f, 0x76, 0xe1, 0x95, 0x73, 0x08, 0xef, 0xa0, + 0x55, 0xa7, 0xfa, 0xae, 0xb5, 0xfd, 0xa8, 0xd3, 0x59, 0x12, 0x48, 0x5b, 0xb7, 0x12, 0x77, 0x3c, 0x66, 0xd3, 0xde, + 0x2c, 0x9c, 0xac, 0xe3, 0x7f, 0xf3, 0xf1, 0x73, 0x20, 0x6e, 0x45, 0x04, 0xa5, 0x7e, 0x44, 0x53, 0x90, 0xee, 0x5d, + 0x33, 0xd1, 0xc3, 0x26, 0x5b, 0xa7, 0x1e, 0x65, 0xa7, 0x68, 0x59, 0x87, 0x35, 0x9b, 0xbc, 0x1e, 0xd0, 0xbf, 0xdb, + 0x2a, 0x35, 0xa3, 0x98, 0xcf, 0x00, 0xcb, 0x56, 0x70, 0xdc, 0x1f, 0x1a, 0x7c, 0x35, 0xed, 0x6e, 0xfd, 0x70, 0x2f, + 0xc4, 0x97, 0x2e, 0x05, 0x51, 0xe1, 0x74, 0x8b, 0x7b, 0x49, 0x6d, 0xef, 0xb5, 0x69, 0x8f, 0x54, 0x7a, 0xdd, 0x42, + 0x10, 0xf2, 0xba, 0x7b, 0x62, 0xf9, 0x87, 0xcf, 0x0e, 0xe1, 0x3f, 0xe2, 0xea, 0xff, 0x91, 0xd4, 0x31, 0xea, 0x2f, + 0x93, 0x02, 0xa3, 0x4e, 0xac, 0x12, 0x32, 0xe2, 0xfb, 0xd7, 0x9f, 0xcd, 0xee, 0xd7, 0x60, 0xef, 0xda, 0x64, 0xb4, + 0x57, 0xae, 0xfd, 0x3c, 0x0c, 0x21, 0x73, 0x7a, 0xb5, 0xba, 0x00, 0x0f, 0x79, 0x60, 0x24, 0x03, 0x68, 0x24, 0xee, + 0x11, 0x64, 0x2f, 0xa2, 0x62, 0x1b, 0xba, 0x4a, 0x9c, 0x35, 0x5d, 0x25, 0xde, 0xed, 0xbe, 0x4a, 0x7c, 0xbf, 0xd7, + 0x55, 0xe2, 0xdd, 0x17, 0xbf, 0x4a, 0x9c, 0x55, 0xaf, 0x12, 0x67, 0xa1, 0xb0, 0xd4, 0x36, 0x5e, 0xaf, 0xf9, 0xcf, + 0x0f, 0xa4, 0x8a, 0x7d, 0x17, 0x0e, 0x3a, 0x36, 0x65, 0x9c, 0x38, 0xff, 0xaf, 0x2f, 0x16, 0xb8, 0x11, 0xdf, 0xa1, + 0xe1, 0x62, 0x7e, 0xb5, 0xe0, 0x98, 0x1d, 0xbf, 0x23, 0x15, 0xfb, 0x61, 0x30, 0xff, 0x19, 0x54, 0xf1, 0x20, 0x0e, + 0x8c, 0xa4, 0x17, 0x5e, 0xfc, 0x73, 0xb8, 0x5a, 0xaf, 0xde, 0x40, 0x5f, 0x1f, 0xbc, 0xd8, 0x1b, 0xfb, 0x2c, 0x0b, + 0xf1, 0x41, 0x86, 0x96, 0x5c, 0xb6, 0x0e, 0xb6, 0xcd, 0xe2, 0xa7, 0x7b, 0x2b, 0x7e, 0xa2, 0xf5, 0x33, 0xff, 0x4d, + 0x16, 0x9c, 0x6a, 0xfd, 0x45, 0x04, 0x42, 0x26, 0x96, 0x06, 0x7d, 0xff, 0xcb, 0xc8, 0x59, 0xa8, 0xd7, 0xcc, 0x52, + 0x58, 0xd6, 0x34, 0xf6, 0xc3, 0xca, 0xfd, 0xbc, 0x5e, 0xeb, 0x46, 0x16, 0x01, 0xb5, 0x2a, 0xce, 0x5f, 0x86, 0xeb, + 0x98, 0x4d, 0xc3, 0x9b, 0x40, 0x35, 0x02, 0x6e, 0x0e, 0x4a, 0x49, 0x24, 0xb3, 0x36, 0x98, 0xbb, 0xfb, 0x3d, 0x32, + 0xca, 0x10, 0x28, 0x01, 0x52, 0xc7, 0xaf, 0x57, 0x26, 0x19, 0x18, 0x98, 0x38, 0x45, 0x35, 0x4b, 0x32, 0xf9, 0x40, + 0xd3, 0xc2, 0xc1, 0xfd, 0x5a, 0x0a, 0xa3, 0xa0, 0xd0, 0xe2, 0x52, 0xe1, 0x58, 0x0b, 0x84, 0x70, 0x51, 0x84, 0x21, + 0xab, 0x59, 0x38, 0xfe, 0x86, 0xe2, 0x77, 0xe4, 0x6f, 0x21, 0x20, 0x44, 0xba, 0xe6, 0xeb, 0xc1, 0x83, 0x72, 0xd1, + 0xe3, 0x0b, 0x09, 0x8c, 0x6f, 0xaf, 0x59, 0xe4, 0xbb, 0x77, 0x9a, 0x9e, 0x86, 0xc1, 0x8f, 0x00, 0x80, 0x97, 0xe1, + 0x4d, 0x20, 0x57, 0xc0, 0x5c, 0x79, 0x35, 0x7b, 0xa9, 0x36, 0x7c, 0x1c, 0xb8, 0x53, 0x49, 0x23, 0xf0, 0xac, 0x95, + 0x3b, 0x67, 0xff, 0x63, 0xd0, 0xbf, 0x7f, 0xd7, 0x53, 0xe3, 0x5d, 0x98, 0x7d, 0xe8, 0x97, 0xd5, 0x1e, 0x9f, 0x79, + 0xfc, 0xf8, 0x41, 0xf3, 0xb4, 0xb5, 0x89, 0xcf, 0xdc, 0x48, 0x8c, 0xa2, 0xa6, 0xb5, 0xde, 0x78, 0x0a, 0x60, 0x14, + 0xe7, 0xe1, 0x7a, 0xb2, 0x40, 0x93, 0xea, 0x2f, 0x37, 0xdf, 0x04, 0xfa, 0xc4, 0x24, 0xf1, 0xd9, 0xd4, 0x4b, 0x45, + 0x39, 0x14, 0xf0, 0xfb, 0xaf, 0x20, 0xfe, 0xf9, 0x9f, 0x08, 0x86, 0xea, 0xae, 0xc9, 0xbc, 0xb1, 0xef, 0xb5, 0x79, + 0xfb, 0x90, 0xcb, 0x9c, 0x47, 0x16, 0x13, 0x4a, 0xba, 0x7a, 0x24, 0x93, 0x96, 0x81, 0x26, 0x47, 0xf1, 0x6d, 0x0a, + 0x50, 0x2c, 0xbe, 0xc2, 0x2c, 0xba, 0xa6, 0x73, 0x97, 0x16, 0x83, 0x71, 0x6c, 0x55, 0x42, 0x32, 0xdc, 0xd0, 0x85, + 0x21, 0xfa, 0x2a, 0xbf, 0x5b, 0x7a, 0x81, 0x81, 0x49, 0x78, 0xaa, 0x6f, 0xdc, 0x5b, 0x48, 0x43, 0x01, 0xc8, 0xad, + 0xfc, 0x0a, 0x0a, 0x0d, 0xd9, 0x91, 0x13, 0x32, 0x6d, 0xaa, 0xb5, 0x90, 0x10, 0xda, 0xc0, 0xd1, 0x57, 0x8a, 0xa2, + 0x28, 0xd9, 0x35, 0x42, 0xc9, 0xee, 0x11, 0x58, 0x8e, 0xd7, 0x01, 0xd0, 0x96, 0xa4, 0xab, 0x5b, 0x2a, 0x81, 0x9b, + 0x01, 0xaa, 0xb6, 0x45, 0x01, 0x8f, 0xb4, 0xdc, 0xb1, 0x45, 0x81, 0xb8, 0xd0, 0x43, 0x94, 0x5c, 0x37, 0x82, 0x84, + 0x0c, 0x3d, 0x05, 0x2f, 0xec, 0xf8, 0x96, 0x4b, 0x82, 0x15, 0x9b, 0x1e, 0x47, 0x7d, 0x56, 0x1f, 0x92, 0x37, 0x90, + 0xb0, 0x20, 0x68, 0x1d, 0x4a, 0x19, 0x36, 0x0c, 0x56, 0x83, 0x1b, 0xf1, 0x5e, 0x74, 0x9b, 0x2c, 0x59, 0xb0, 0x56, + 0x31, 0x25, 0x27, 0x86, 0x48, 0x86, 0x3a, 0x2f, 0x89, 0xd9, 0x02, 0x6c, 0x53, 0xdf, 0x72, 0x41, 0xb4, 0x30, 0xe6, + 0x28, 0xd5, 0x35, 0x26, 0xdc, 0x37, 0x31, 0xe6, 0xb8, 0xad, 0x4c, 0x21, 0xf8, 0x92, 0x86, 0x45, 0x6c, 0xce, 0xbd, + 0x91, 0x91, 0x53, 0xa0, 0xb0, 0x53, 0x5c, 0x5c, 0x24, 0xc0, 0xae, 0xb9, 0xe5, 0x45, 0xcb, 0x34, 0x32, 0x6e, 0x49, + 0x50, 0x14, 0xe9, 0xd5, 0x6e, 0xf8, 0x38, 0x21, 0x2e, 0x64, 0x63, 0x3f, 0x93, 0x4a, 0x3f, 0x0d, 0x93, 0xfe, 0xc8, + 0xee, 0x88, 0x90, 0x10, 0xa8, 0x3e, 0xb2, 0x3b, 0xd0, 0xdb, 0xbf, 0x02, 0x69, 0x8a, 0xba, 0x05, 0x5d, 0x1b, 0x90, + 0x69, 0x69, 0x02, 0xb1, 0x42, 0xb7, 0x1c, 0x20, 0x3b, 0xdd, 0x82, 0xc5, 0x11, 0xc4, 0x81, 0x11, 0xf7, 0xc5, 0x21, + 0xe6, 0xce, 0x24, 0x5a, 0x2d, 0x8c, 0xcd, 0x9a, 0xa3, 0xa1, 0x3f, 0x71, 0x6c, 0xfb, 0xa0, 0x52, 0x1f, 0x04, 0xd9, + 0x75, 0xb5, 0x75, 0x23, 0x19, 0x38, 0xb6, 0xe9, 0x3d, 0xb1, 0x5a, 0xfd, 0x0a, 0x8d, 0x96, 0x42, 0x79, 0x8f, 0x50, + 0xfc, 0x35, 0x7c, 0xb4, 0xd1, 0x2a, 0x07, 0x52, 0x2f, 0x3b, 0x67, 0xe0, 0xd8, 0x52, 0x2e, 0xff, 0x1a, 0x55, 0x49, + 0x3f, 0x05, 0x12, 0xa7, 0xb4, 0x72, 0x23, 0x48, 0x46, 0xa1, 0xc1, 0x31, 0xfa, 0x8b, 0xf2, 0x54, 0xd1, 0xe8, 0xf8, + 0xe8, 0xfa, 0xa8, 0x2f, 0x30, 0x8a, 0xf0, 0x5e, 0x94, 0x3b, 0x28, 0x7d, 0x31, 0x2e, 0x63, 0x38, 0x1e, 0xf6, 0x9e, + 0xe5, 0x1a, 0xbd, 0xad, 0xdc, 0x02, 0xf6, 0xdf, 0x40, 0x3e, 0xad, 0x31, 0x84, 0xdf, 0x80, 0x1a, 0x90, 0xba, 0x66, + 0x67, 0x87, 0x10, 0x2d, 0x49, 0xee, 0xae, 0x48, 0x24, 0xf7, 0xef, 0x0c, 0x89, 0x0e, 0xea, 0xd0, 0xb2, 0xfe, 0xea, + 0xc9, 0xdd, 0x3d, 0xbb, 0x64, 0xc1, 0xb4, 0xd8, 0x61, 0x89, 0x7e, 0xed, 0xdf, 0x5d, 0x01, 0xa3, 0x40, 0x4e, 0xa7, + 0xb0, 0x06, 0xa3, 0xa4, 0x61, 0x80, 0x9b, 0x9f, 0x8e, 0x9b, 0xb7, 0x17, 0x17, 0x83, 0x0d, 0x28, 0x20, 0x6b, 0xd6, + 0x4c, 0x12, 0x8a, 0x43, 0xe2, 0x10, 0x74, 0x6e, 0xd6, 0x04, 0x23, 0xda, 0xb8, 0x13, 0x13, 0x61, 0x49, 0x9a, 0xb7, + 0xf1, 0x78, 0x28, 0xf0, 0x7d, 0xa5, 0xd6, 0xde, 0x6e, 0xa9, 0x75, 0xb2, 0x4b, 0x6a, 0x4d, 0x8e, 0x7b, 0x64, 0xfe, + 0x94, 0x39, 0x30, 0x0a, 0xe6, 0x5c, 0x76, 0x01, 0x2d, 0x88, 0xba, 0xd1, 0xcf, 0x4f, 0xb4, 0xaa, 0xf4, 0x46, 0xb6, + 0xa1, 0x28, 0xfe, 0x96, 0x2e, 0x28, 0x42, 0xa1, 0x2e, 0xcb, 0xc6, 0xcf, 0x72, 0xd9, 0x38, 0xdd, 0x6a, 0x72, 0x97, + 0x2d, 0xc1, 0xfd, 0x4b, 0xee, 0x90, 0xd9, 0xed, 0x20, 0x77, 0x8b, 0xcc, 0x47, 0x2a, 0x39, 0xfa, 0xe5, 0x17, 0x0d, + 0xc9, 0x7d, 0x54, 0xdc, 0x32, 0x8a, 0x5e, 0xa4, 0xc5, 0xaa, 0xb9, 0x9f, 0x5f, 0x5e, 0x0e, 0x52, 0x77, 0x1c, 0x72, + 0x56, 0x2c, 0x6f, 0x9b, 0xa2, 0xa3, 0x97, 0xfc, 0x5a, 0xda, 0x24, 0x99, 0x47, 0x16, 0x01, 0x58, 0x88, 0xe9, 0x4b, + 0x7a, 0xed, 0xcc, 0x06, 0x02, 0x07, 0x59, 0xe3, 0x40, 0xba, 0x5b, 0x3a, 0x4f, 0xe9, 0xaa, 0x72, 0xd5, 0xb5, 0x83, + 0xd4, 0x9d, 0x34, 0xc1, 0xb2, 0x3c, 0x02, 0x61, 0x7d, 0x29, 0x49, 0x10, 0x7a, 0xb6, 0x62, 0xf7, 0x6b, 0x18, 0x00, + 0xa4, 0xff, 0xe5, 0x67, 0xce, 0x0a, 0x80, 0x24, 0x52, 0xb1, 0x65, 0x9d, 0x3f, 0x1e, 0x62, 0x93, 0xcc, 0xcf, 0xb0, + 0x6a, 0xf5, 0x9b, 0x24, 0xef, 0xd9, 0x70, 0x77, 0xad, 0xa2, 0x38, 0x9f, 0xd7, 0xe8, 0x89, 0x71, 0xf0, 0x5d, 0x16, + 0xad, 0x03, 0xcc, 0x42, 0x64, 0x26, 0x91, 0x3b, 0xf9, 0xb8, 0x91, 0xbe, 0xc7, 0x45, 0xa2, 0x20, 0x2e, 0x2e, 0x2a, + 0x15, 0xfa, 0x2e, 0x06, 0xed, 0x66, 0x3d, 0xab, 0x15, 0x4b, 0x82, 0x9a, 0xde, 0x43, 0xbb, 0xed, 0x3e, 0x9b, 0x1d, + 0x96, 0xe4, 0xa7, 0xad, 0x4e, 0x51, 0xba, 0x9e, 0x8d, 0x63, 0x19, 0xfe, 0xca, 0x1d, 0x5b, 0xff, 0xf8, 0x4f, 0xc7, + 0xfc, 0x9b, 0xa5, 0x35, 0xfa, 0x9c, 0x21, 0x40, 0xfb, 0x82, 0x62, 0x5a, 0x56, 0xd3, 0x54, 0x4a, 0x9a, 0x86, 0x35, + 0xf3, 0x7c, 0xdf, 0xf4, 0xc1, 0xbd, 0x68, 0xf3, 0x59, 0xd3, 0xc3, 0x7e, 0xd6, 0x90, 0x2e, 0xe2, 0x33, 0xfa, 0x29, + 0xee, 0x94, 0x64, 0xb1, 0x5e, 0x8e, 0x37, 0xb2, 0xa0, 0x5c, 0x92, 0x9f, 0x57, 0x65, 0xe6, 0xf2, 0x67, 0x67, 0xb3, + 0x59, 0x51, 0x6a, 0x6c, 0x2b, 0x87, 0x28, 0xf9, 0x7d, 0x68, 0xdb, 0x76, 0x19, 0xbe, 0x4d, 0x07, 0x85, 0x0e, 0x86, + 0x89, 0x42, 0xf8, 0xee, 0xee, 0x3d, 0xf5, 0x07, 0x8d, 0x96, 0xba, 0x6a, 0x3a, 0x8f, 0xb4, 0xd5, 0xfe, 0x5f, 0x0c, + 0x05, 0x51, 0xc3, 0xae, 0xe3, 0x5f, 0xdd, 0x2b, 0x5b, 0x7a, 0x2a, 0x1f, 0xe0, 0xfb, 0x35, 0xde, 0xb1, 0xd7, 0xf7, + 0x68, 0xda, 0xb4, 0xbd, 0x53, 0x2b, 0x27, 0xbb, 0x05, 0x9b, 0xa5, 0x3e, 0x59, 0x2a, 0x79, 0x09, 0x5b, 0xc6, 0xbd, + 0x09, 0x43, 0x05, 0xa9, 0x25, 0x51, 0x5b, 0xb4, 0xea, 0x31, 0xe7, 0x60, 0xc7, 0xe5, 0x08, 0x3c, 0x6c, 0x2b, 0xa8, + 0xac, 0xaa, 0x68, 0xd6, 0xc4, 0x47, 0x90, 0x8a, 0x6d, 0xaa, 0x0a, 0x27, 0xdc, 0xa6, 0x1d, 0xfb, 0x2f, 0x85, 0x7a, + 0x0a, 0x70, 0xa7, 0x1b, 0x61, 0x6d, 0x42, 0xca, 0x13, 0xfc, 0x3b, 0x53, 0xce, 0x3d, 0x5b, 0xdd, 0x16, 0x8d, 0xbb, + 0xba, 0xa0, 0x6e, 0xca, 0x49, 0x19, 0x8d, 0xba, 0x0e, 0xf5, 0x65, 0x26, 0x40, 0x33, 0xd9, 0xba, 0x05, 0x2c, 0x68, + 0x0a, 0xc9, 0x11, 0x6b, 0x74, 0x63, 0x78, 0x9d, 0x85, 0x9d, 0x97, 0xcb, 0xf7, 0xf3, 0xd4, 0xde, 0x30, 0x07, 0xe3, + 0x69, 0x17, 0x95, 0x7b, 0x85, 0xad, 0x8a, 0xa6, 0x32, 0xb8, 0x07, 0xc4, 0x8d, 0x54, 0x59, 0x47, 0xbe, 0x49, 0x99, + 0x03, 0x35, 0x7d, 0x53, 0x9d, 0x77, 0x73, 0xf7, 0x4e, 0x07, 0xf4, 0x1a, 0x55, 0x50, 0xed, 0xa5, 0xda, 0x2b, 0xeb, + 0xb0, 0xc5, 0x38, 0x61, 0x05, 0xc0, 0x15, 0x45, 0x41, 0xa3, 0x21, 0xa5, 0x84, 0xfb, 0x68, 0xd2, 0xd9, 0x5b, 0x19, + 0x59, 0x8b, 0x79, 0x62, 0x77, 0xf5, 0x55, 0xa8, 0x6f, 0xa1, 0x19, 0x04, 0xd8, 0x71, 0xec, 0x84, 0xcf, 0x26, 0xec, + 0x18, 0x19, 0x5d, 0x39, 0xb8, 0x83, 0xf0, 0x94, 0x9a, 0x14, 0x91, 0x96, 0x4e, 0x29, 0xea, 0x12, 0xbe, 0xaf, 0x15, + 0xde, 0x9f, 0x17, 0xa4, 0xf1, 0xdc, 0x1f, 0xa8, 0xa5, 0xef, 0x55, 0x7b, 0xe9, 0x05, 0xfb, 0xd7, 0x75, 0x6f, 0xf7, + 0xae, 0x0b, 0xcc, 0xe1, 0xde, 0x95, 0x81, 0xbb, 0x24, 0x2b, 0xa5, 0x64, 0xf0, 0xbd, 0xa4, 0x3c, 0x90, 0x63, 0x59, + 0xa8, 0xd8, 0x8a, 0x6e, 0xf4, 0x3f, 0xad, 0x07, 0xa3, 0x93, 0xd3, 0xdb, 0xa5, 0xaf, 0x5c, 0xb3, 0x08, 0xb2, 0xa8, + 0x0e, 0x54, 0xc7, 0xb2, 0x55, 0x05, 0x23, 0x33, 0x78, 0xc1, 0x7c, 0xa0, 0xfe, 0x72, 0xfe, 0xda, 0xec, 0xaa, 0xa7, + 0x60, 0x8e, 0x71, 0x3d, 0x47, 0x16, 0xf7, 0xcc, 0xbd, 0x63, 0xd1, 0x55, 0x4b, 0x55, 0x30, 0x59, 0x2a, 0x31, 0xb7, + 0x58, 0xa6, 0xb4, 0xd4, 0x3d, 0x72, 0xf2, 0x29, 0x22, 0xad, 0xb6, 0x0a, 0x88, 0xd5, 0x69, 0x75, 0x15, 0xa7, 0x75, + 0x68, 0x1d, 0x75, 0xd5, 0xe1, 0x57, 0x8a, 0x72, 0x32, 0x65, 0xb3, 0x78, 0x88, 0xe2, 0x98, 0x13, 0xe4, 0x07, 0xe9, + 0xb7, 0xa2, 0x58, 0x13, 0x3f, 0x36, 0x1d, 0x65, 0xc3, 0x1f, 0x15, 0x05, 0x90, 0x51, 0x4f, 0x79, 0x38, 0x6b, 0xcd, + 0x0e, 0x67, 0xcf, 0xfa, 0xbc, 0x38, 0xfd, 0xaa, 0x50, 0xdd, 0xa0, 0x7f, 0x5b, 0x52, 0xb3, 0x38, 0x89, 0xc2, 0x8f, + 0x8c, 0xf3, 0x92, 0x4a, 0x26, 0x28, 0x2a, 0x37, 0x6d, 0x55, 0xbf, 0xe4, 0x74, 0xc7, 0x93, 0x59, 0x2b, 0xaf, 0x8e, + 0x63, 0x3c, 0xc8, 0x06, 0x79, 0x72, 0x20, 0x86, 0x7e, 0x22, 0x83, 0xc9, 0x31, 0xeb, 0x00, 0xe5, 0xa8, 0x7c, 0x8e, + 0x73, 0x31, 0xbf, 0x13, 0x08, 0x79, 0x9f, 0x7b, 0x70, 0xc4, 0xd8, 0x6c, 0xa0, 0xfe, 0xe8, 0xb4, 0xba, 0x86, 0xe3, + 0x1c, 0x59, 0x47, 0xdd, 0x89, 0x6d, 0x1c, 0x5a, 0x87, 0x66, 0xdb, 0x3a, 0x32, 0xba, 0x66, 0xd7, 0xe8, 0x7e, 0xd7, + 0x9d, 0x98, 0x87, 0xd6, 0xa1, 0x61, 0x9b, 0x5d, 0x28, 0x34, 0xbb, 0x66, 0xf7, 0xda, 0x3c, 0xec, 0x4e, 0x6c, 0x2c, + 0x6d, 0x59, 0x9d, 0x8e, 0xe9, 0xd8, 0x56, 0xa7, 0x63, 0x74, 0xac, 0xa3, 0x23, 0xd3, 0x69, 0x5b, 0x47, 0x47, 0x67, + 0x9d, 0xae, 0xd5, 0x86, 0x77, 0xed, 0xf6, 0xa4, 0x6d, 0x39, 0x8e, 0x09, 0x7f, 0x19, 0x5d, 0xab, 0x45, 0x3f, 0x1c, + 0xc7, 0x6a, 0x3b, 0x86, 0xed, 0x77, 0x5a, 0xd6, 0xd1, 0x33, 0x03, 0xff, 0xc6, 0x6a, 0x06, 0xfe, 0x05, 0xdd, 0x18, + 0xcf, 0xac, 0xd6, 0x11, 0xfd, 0xc2, 0x0e, 0xaf, 0x0f, 0xbb, 0xff, 0x50, 0x0f, 0x1a, 0xe7, 0xe0, 0xd0, 0x1c, 0xba, + 0x1d, 0xab, 0xdd, 0x36, 0x0e, 0x1d, 0xab, 0xdb, 0x5e, 0x98, 0x87, 0x2d, 0xeb, 0xe8, 0x78, 0x62, 0x3a, 0xd6, 0xf1, + 0xb1, 0x61, 0x9b, 0x6d, 0xab, 0x65, 0x38, 0xd6, 0x61, 0x1b, 0x7f, 0xb4, 0xad, 0xd6, 0xf5, 0xf1, 0x33, 0xeb, 0xa8, + 0xb3, 0x38, 0xb2, 0x0e, 0x3f, 0x1c, 0x76, 0xad, 0x56, 0x7b, 0xd1, 0x3e, 0xb2, 0x5a, 0xc7, 0xd7, 0x47, 0xd6, 0xe1, + 0xc2, 0x6c, 0x1d, 0x6d, 0x6d, 0xe9, 0xb4, 0x2c, 0x80, 0x11, 0xbe, 0x86, 0x17, 0x06, 0x7f, 0x01, 0x7f, 0x16, 0xd8, + 0xf6, 0x0f, 0xec, 0x26, 0xae, 0x36, 0x7d, 0x66, 0x75, 0x8f, 0x27, 0x54, 0x1d, 0x0a, 0x4c, 0x51, 0x03, 0x9a, 0x5c, + 0x9b, 0xf4, 0x59, 0xec, 0xce, 0x14, 0x1d, 0x89, 0x3f, 0xfc, 0x63, 0xd7, 0x26, 0x7c, 0x98, 0xbe, 0xfb, 0xa7, 0xf6, + 0x93, 0x2d, 0xf9, 0xc9, 0xc1, 0x9c, 0xb6, 0xfe, 0x7c, 0xf8, 0x15, 0xe5, 0xd7, 0x1c, 0x19, 0xbf, 0x36, 0x29, 0x25, + 0xff, 0xb5, 0x5b, 0x29, 0xf9, 0x7c, 0xbd, 0x8f, 0x52, 0xf2, 0x5f, 0x5f, 0x5c, 0x29, 0xf9, 0x6b, 0xd9, 0xb7, 0xe6, + 0x75, 0x39, 0x0d, 0xd8, 0xf7, 0x9b, 0xb2, 0xc8, 0x21, 0x70, 0xb5, 0x8b, 0x9f, 0xd6, 0x97, 0x10, 0xda, 0xef, 0x75, + 0x38, 0x78, 0xbe, 0x2e, 0x18, 0x7c, 0x86, 0x80, 0x63, 0x5f, 0x87, 0x84, 0x63, 0x3f, 0xac, 0x07, 0x60, 0x65, 0xc6, + 0xd9, 0x1c, 0x6f, 0x6a, 0x2e, 0x5c, 0x7f, 0x96, 0xb1, 0x48, 0x50, 0xd2, 0xc7, 0x62, 0x70, 0x5c, 0x03, 0xf2, 0x0c, + 0x37, 0x99, 0xf5, 0x32, 0x88, 0xc1, 0x22, 0x18, 0x2c, 0x39, 0x66, 0x51, 0x5a, 0x6a, 0x6c, 0x89, 0x60, 0x88, 0x57, + 0xdc, 0x0b, 0xaa, 0xf1, 0x3d, 0x1a, 0x00, 0xd7, 0xf7, 0xee, 0x54, 0xfb, 0x55, 0xc0, 0xb2, 0x4e, 0x18, 0x48, 0x03, + 0xb7, 0x5f, 0xf7, 0xbe, 0x68, 0x86, 0x5b, 0x32, 0xbc, 0x6e, 0x1e, 0x29, 0x8c, 0xa4, 0xdc, 0xde, 0x29, 0x9a, 0xf1, + 0xee, 0x9a, 0x66, 0xcd, 0xe7, 0x0b, 0xcd, 0xb7, 0xd8, 0x10, 0x67, 0x1d, 0x97, 0x41, 0x55, 0x4a, 0x62, 0x5d, 0x0b, + 0x90, 0xfc, 0x82, 0x9a, 0x1b, 0x1a, 0xe7, 0x9c, 0xaa, 0xad, 0x20, 0xbf, 0x63, 0x4b, 0xef, 0x0a, 0x7d, 0xca, 0xc6, + 0xc9, 0x4f, 0x36, 0x78, 0xaf, 0xf0, 0x7e, 0x05, 0x4e, 0x94, 0x73, 0x3c, 0xe3, 0x50, 0x86, 0xf3, 0x46, 0xea, 0x97, + 0xa4, 0x11, 0xe9, 0xc2, 0xd9, 0x54, 0x79, 0xd1, 0x46, 0xb7, 0x04, 0x87, 0x2d, 0x05, 0x17, 0x84, 0x9f, 0x27, 0x27, + 0x80, 0x94, 0x1c, 0x35, 0xd0, 0xcf, 0x61, 0x5b, 0x67, 0xa2, 0xde, 0x43, 0xd8, 0xc4, 0x3c, 0x26, 0xb3, 0x22, 0x47, + 0x9b, 0xd9, 0xcc, 0xfc, 0xd0, 0x4d, 0x7a, 0xc8, 0xa6, 0x49, 0x2c, 0x6f, 0x0b, 0x3d, 0x16, 0xfa, 0x5b, 0x8c, 0xe9, + 0xe4, 0x8e, 0x79, 0x27, 0xe8, 0xf9, 0xb0, 0xcd, 0xfe, 0x2e, 0x73, 0x38, 0xdb, 0x14, 0xcc, 0x51, 0x9c, 0xce, 0xb1, + 0xe1, 0x1c, 0x19, 0xd6, 0x71, 0x47, 0x4f, 0xc5, 0x81, 0x93, 0xbb, 0x2c, 0x00, 0x04, 0x1c, 0x20, 0xb2, 0x61, 0x7a, + 0x81, 0x97, 0x78, 0xae, 0x9f, 0x02, 0x3f, 0x5c, 0xbc, 0xa4, 0xfc, 0x6b, 0x1d, 0x27, 0x30, 0x47, 0xc1, 0xf4, 0xa2, + 0xf3, 0x87, 0x39, 0x66, 0xc9, 0x0d, 0x63, 0x41, 0x83, 0x61, 0x4c, 0xd9, 0x97, 0xe4, 0xf7, 0xb3, 0xac, 0x4f, 0xc9, + 0x6a, 0x6d, 0x9c, 0x04, 0x7c, 0x7f, 0x08, 0xc7, 0x87, 0x74, 0x64, 0x7c, 0xd7, 0x84, 0x70, 0x7f, 0xd9, 0x8d, 0x70, + 0x13, 0xb6, 0x0f, 0xc2, 0xfd, 0xe5, 0x8b, 0x23, 0xdc, 0xef, 0x64, 0x84, 0x5b, 0xf0, 0x1f, 0xcc, 0x35, 0x4c, 0xef, + 0xf1, 0x59, 0x83, 0xcc, 0x28, 0x4f, 0xd5, 0x03, 0x62, 0xe0, 0x55, 0x3d, 0x4f, 0x1f, 0xf4, 0xb7, 0x42, 0xa2, 0x56, + 0x14, 0x80, 0x62, 0xd6, 0x0d, 0x4a, 0x0a, 0xe9, 0x81, 0xab, 0x5b, 0x96, 0x18, 0x92, 0xdd, 0x28, 0x6f, 0x82, 0xc4, + 0xb7, 0xde, 0xf1, 0x7b, 0x24, 0x28, 0x74, 0x5f, 0x87, 0xd1, 0xd2, 0xc5, 0xe8, 0xaf, 0x2a, 0x26, 0x78, 0x87, 0x07, + 0x1b, 0x9c, 0x71, 0x27, 0x61, 0x30, 0xcd, 0xb4, 0x92, 0x6c, 0x70, 0x41, 0x1c, 0xb7, 0x7a, 0xc7, 0xdc, 0x48, 0x35, + 0xe8, 0x35, 0x2c, 0xee, 0x93, 0xb6, 0xfd, 0xa4, 0x75, 0xf8, 0xe4, 0xc8, 0x86, 0xff, 0x1d, 0xd6, 0x4e, 0x0d, 0x5e, + 0x71, 0x19, 0x06, 0x90, 0x63, 0x52, 0xd4, 0x6c, 0xaa, 0x76, 0xc3, 0xd8, 0xc7, 0xbc, 0xd6, 0x71, 0x7d, 0xa5, 0xa9, + 0x7b, 0x97, 0xd7, 0xa9, 0xad, 0xb1, 0x08, 0xd7, 0xd2, 0xb0, 0x6a, 0x46, 0xe3, 0x05, 0x6b, 0x90, 0xb3, 0x4b, 0x35, + 0xe4, 0xd7, 0x7c, 0xba, 0xf9, 0xbc, 0x58, 0x3b, 0xbd, 0xcc, 0x13, 0xd9, 0x8a, 0x7c, 0x46, 0x3b, 0x21, 0xc8, 0x55, + 0x94, 0x36, 0x86, 0x03, 0xc7, 0x44, 0x13, 0x10, 0x0c, 0x3c, 0x4b, 0x3f, 0xea, 0xd2, 0x02, 0x25, 0xd1, 0x3a, 0x98, + 0x68, 0xf8, 0xd3, 0x1d, 0xc7, 0x9a, 0x77, 0x10, 0x59, 0xfc, 0xc3, 0x3a, 0xae, 0x9a, 0x3b, 0xb4, 0xf3, 0xac, 0x7f, + 0xb1, 0x58, 0x15, 0xf7, 0x49, 0x62, 0x44, 0xa8, 0xc7, 0xa6, 0xa5, 0x35, 0x07, 0xee, 0x93, 0xac, 0xe1, 0x93, 0xc4, + 0x08, 0x9e, 0x82, 0xee, 0x73, 0x60, 0x3f, 0x7e, 0x4c, 0xb5, 0x1e, 0x0c, 0xc4, 0xb4, 0x4e, 0x27, 0x79, 0xd0, 0x50, + 0xc5, 0x9d, 0x87, 0x14, 0x37, 0xb4, 0x37, 0x31, 0xc2, 0xa7, 0x4f, 0x87, 0x03, 0x47, 0xc7, 0x8c, 0xb2, 0x22, 0x33, + 0x3c, 0x4f, 0x56, 0x7c, 0xb6, 0x9f, 0xa1, 0x91, 0x5e, 0xeb, 0x4a, 0xbb, 0x82, 0x3b, 0x93, 0x2d, 0xdc, 0x11, 0x38, + 0xf6, 0x82, 0xe4, 0x81, 0x64, 0x50, 0xe0, 0x0a, 0x83, 0x1f, 0x51, 0x27, 0xbb, 0x75, 0xb5, 0x2d, 0xdb, 0xb2, 0xd5, + 0xac, 0xe1, 0xcc, 0x9b, 0x0f, 0x36, 0x61, 0xe2, 0x42, 0x1a, 0x56, 0x3f, 0x9c, 0x83, 0x1f, 0x5d, 0xe2, 0x25, 0x3e, + 0xe4, 0xf4, 0x04, 0x87, 0xba, 0x25, 0xdd, 0xcb, 0x53, 0xee, 0xdd, 0xe0, 0x46, 0x1f, 0x31, 0xaf, 0xbb, 0x70, 0xc5, + 0xc5, 0x38, 0x76, 0x3f, 0x02, 0x31, 0xd4, 0x54, 0x0d, 0x64, 0x03, 0x2c, 0x8a, 0x4d, 0xd9, 0x5b, 0xa8, 0xa7, 0x40, + 0x1b, 0x5d, 0xe5, 0x93, 0x98, 0x45, 0xee, 0x12, 0x12, 0x1b, 0x6d, 0x52, 0x83, 0x63, 0x5a, 0x95, 0xa3, 0x5a, 0xc5, + 0x79, 0x76, 0x64, 0x28, 0x2d, 0xc7, 0x50, 0x6c, 0x40, 0xb7, 0x6a, 0x6a, 0x6c, 0xd2, 0xcb, 0xfe, 0x2e, 0x83, 0x07, + 0xc2, 0x2f, 0x0f, 0x69, 0x1e, 0x64, 0xea, 0xc0, 0x55, 0x49, 0x09, 0xc5, 0x2f, 0xd6, 0xa4, 0x84, 0x26, 0x1e, 0x29, + 0x3d, 0xcf, 0xd9, 0x6d, 0xa2, 0x63, 0xce, 0x4b, 0x5e, 0xc5, 0xd3, 0x37, 0xe8, 0x30, 0xec, 0x05, 0x8a, 0xf7, 0xe9, + 0x93, 0xe6, 0x81, 0x33, 0xd3, 0x40, 0x82, 0x0f, 0x3c, 0xeb, 0x05, 0x80, 0x79, 0xb9, 0x9a, 0x1e, 0x81, 0x05, 0x9e, + 0x86, 0xf0, 0x6f, 0x5e, 0x2c, 0x7e, 0x70, 0x33, 0x09, 0xcb, 0x77, 0x83, 0x39, 0xa0, 0x34, 0x37, 0x98, 0x57, 0xcc, + 0xb1, 0xc8, 0xe7, 0xb9, 0x54, 0x9a, 0x77, 0x95, 0x9b, 0x4a, 0xc5, 0xcf, 0xef, 0xce, 0x29, 0xa7, 0xaf, 0xa6, 0x02, + 0x95, 0x43, 0x17, 0xdd, 0x5c, 0x93, 0xfb, 0x74, 0xf0, 0xf5, 0xc9, 0x92, 0x25, 0x2e, 0xa9, 0x81, 0xe0, 0xf2, 0x0b, + 0xec, 0x80, 0xc2, 0x09, 0x0d, 0x8f, 0x0d, 0x35, 0xa0, 0x30, 0xe7, 0x44, 0x27, 0x0c, 0x85, 0xd3, 0x29, 0x13, 0x2d, + 0x3e, 0x07, 0x8e, 0x41, 0x0e, 0x07, 0x13, 0x17, 0x43, 0x1d, 0x0f, 0x82, 0x50, 0x1d, 0x7e, 0x9d, 0xf9, 0x66, 0x36, + 0x2d, 0x82, 0xef, 0x05, 0x1f, 0x2f, 0x22, 0xe6, 0xff, 0x7b, 0xf0, 0x35, 0x10, 0xee, 0xaf, 0x2f, 0x55, 0xbd, 0x9f, + 0x58, 0x8b, 0x88, 0xcd, 0x06, 0x5f, 0xd7, 0x24, 0x98, 0xc7, 0xeb, 0x3d, 0x8d, 0x45, 0x6d, 0xb7, 0xf2, 0x90, 0x73, + 0xed, 0xbd, 0x2e, 0xf5, 0x43, 0x7e, 0x5b, 0x87, 0x1b, 0xe0, 0xa6, 0x70, 0xc7, 0x76, 0xfa, 0x78, 0x7f, 0x1e, 0xfb, + 0xee, 0xe4, 0x63, 0x9f, 0xde, 0x14, 0x1e, 0x4c, 0xa0, 0xd6, 0x13, 0x77, 0xd5, 0x43, 0xf2, 0x2a, 0x17, 0x82, 0xf7, + 0x34, 0x95, 0x66, 0x9c, 0x5d, 0xed, 0x5e, 0xc6, 0xad, 0xbc, 0xc1, 0x2f, 0xe3, 0xa7, 0x6e, 0x16, 0x5e, 0xc2, 0xc4, + 0xa7, 0xf0, 0x21, 0x4d, 0xc5, 0x45, 0x9d, 0xae, 0xa8, 0x78, 0xb1, 0xb6, 0xda, 0x8a, 0xd3, 0xfd, 0xae, 0x73, 0xed, + 0xd8, 0x8b, 0x96, 0x63, 0x75, 0x3f, 0x38, 0xdd, 0x45, 0xdb, 0x3a, 0xf6, 0xcd, 0xb6, 0x75, 0x0c, 0x7f, 0x3e, 0x1c, + 0x5b, 0xdd, 0x85, 0xd9, 0xb2, 0x0e, 0x3f, 0x38, 0x2d, 0xdf, 0xec, 0x5a, 0xc7, 0xf0, 0xe7, 0x8c, 0x5a, 0xc1, 0x05, + 0x88, 0xee, 0x3b, 0x5f, 0x17, 0xb0, 0x80, 0xf4, 0x3b, 0xd3, 0xc9, 0x1a, 0x05, 0xf2, 0x56, 0xa3, 0xd7, 0x05, 0x94, + 0x41, 0x19, 0x7f, 0xd0, 0x14, 0xa1, 0xaf, 0x05, 0x03, 0x46, 0x39, 0x7e, 0x84, 0x79, 0x9b, 0xf0, 0x43, 0x17, 0x89, + 0x56, 0x6a, 0x8f, 0x11, 0x6f, 0x53, 0x9f, 0x5c, 0x44, 0x24, 0x01, 0x26, 0x45, 0xf0, 0x2f, 0x2b, 0x0c, 0x8d, 0x27, + 0x72, 0x62, 0x49, 0x58, 0x29, 0x4f, 0x44, 0x9f, 0xee, 0x1e, 0x38, 0x7a, 0xf3, 0xb3, 0x2c, 0x11, 0xea, 0x17, 0xed, + 0x5b, 0x4a, 0x3d, 0xf6, 0x59, 0xfd, 0x60, 0x52, 0xa6, 0x3c, 0x9f, 0x12, 0x44, 0x14, 0x9f, 0x7a, 0x51, 0x36, 0x3c, + 0x09, 0x45, 0x3b, 0xf5, 0x59, 0x59, 0x74, 0xc8, 0x18, 0xf9, 0x06, 0xb8, 0xe4, 0x6b, 0xd7, 0x97, 0x0c, 0xd9, 0xa4, + 0x96, 0x0f, 0x32, 0xcc, 0xff, 0xf8, 0x71, 0x3e, 0x38, 0xb3, 0x34, 0xee, 0x13, 0xa7, 0x03, 0x64, 0xb7, 0xc3, 0xda, + 0x5b, 0x6d, 0x2a, 0x77, 0xc7, 0xa2, 0xcf, 0x83, 0x50, 0x0b, 0xbb, 0x29, 0x61, 0xb1, 0xd1, 0x68, 0xd8, 0x59, 0xb1, + 0xd7, 0x80, 0x28, 0xfe, 0xa5, 0xab, 0x8e, 0xaa, 0xf7, 0x03, 0x61, 0x7e, 0x10, 0x6c, 0x89, 0xbf, 0xcf, 0xef, 0x62, + 0x2a, 0x80, 0x66, 0xcb, 0x3c, 0x76, 0x38, 0x88, 0xff, 0xd9, 0x93, 0x40, 0x67, 0x4d, 0xb0, 0x97, 0x28, 0x9d, 0xd6, + 0x82, 0xf3, 0x5e, 0x46, 0x57, 0x89, 0xa0, 0xb2, 0xf8, 0x54, 0x85, 0x22, 0x48, 0x25, 0x8c, 0xd9, 0xc3, 0x33, 0x63, + 0xd1, 0x8c, 0x5a, 0xe4, 0x05, 0x86, 0x87, 0xb9, 0x4e, 0x84, 0xe3, 0xa8, 0xfe, 0xf8, 0x71, 0x23, 0x11, 0x22, 0xe3, + 0x9c, 0x98, 0x25, 0x59, 0x7e, 0x53, 0x55, 0xc6, 0x6f, 0xaa, 0x8c, 0x62, 0xb2, 0x7e, 0x11, 0x6b, 0x08, 0x1b, 0x57, + 0xda, 0x7b, 0xf8, 0x73, 0xcc, 0xdc, 0xc4, 0xe2, 0xca, 0x52, 0x4d, 0x22, 0xee, 0x86, 0xc3, 0xda, 0x60, 0xdd, 0xca, + 0x23, 0x68, 0x66, 0x69, 0x12, 0xff, 0xb6, 0xe6, 0x51, 0x1d, 0xa0, 0x8f, 0x4f, 0x76, 0x1e, 0x80, 0xec, 0x6d, 0xe2, + 0x52, 0x60, 0x18, 0x99, 0xe4, 0x86, 0x89, 0x2b, 0x52, 0x76, 0x02, 0x5f, 0xde, 0xaf, 0x35, 0xbf, 0x90, 0x22, 0x3f, + 0x0c, 0xdf, 0x9e, 0x7f, 0xab, 0xf0, 0xfd, 0x4f, 0xd6, 0x02, 0x78, 0x91, 0xa1, 0xcc, 0x3f, 0x03, 0xca, 0xfc, 0xa3, + 0xf0, 0x24, 0x53, 0x2a, 0xe6, 0x6c, 0x24, 0x08, 0xa2, 0x00, 0x9a, 0x6c, 0x28, 0x96, 0x6b, 0x3f, 0xf1, 0x56, 0x6e, + 0x94, 0x1c, 0x60, 0xda, 0x1f, 0x40, 0x72, 0x6a, 0x53, 0x3c, 0x08, 0x32, 0xc3, 0x10, 0x81, 0x5b, 0x93, 0x40, 0xd8, + 0x61, 0xcc, 0x3c, 0x3f, 0x33, 0xc3, 0x10, 0x1f, 0x70, 0x27, 0x13, 0xb6, 0x4a, 0x06, 0x85, 0xf4, 0x42, 0xe1, 0x24, + 0x61, 0x89, 0x19, 0x27, 0x11, 0x73, 0x97, 0x6a, 0x16, 0x20, 0xbc, 0xda, 0x5f, 0xbc, 0x1e, 0x2f, 0xbd, 0x24, 0x8b, + 0xb0, 0x4b, 0x13, 0x04, 0x83, 0x08, 0x18, 0xe2, 0x70, 0x94, 0x72, 0x10, 0x9e, 0x85, 0xf3, 0xd2, 0x8e, 0xca, 0x39, + 0x97, 0x53, 0x8c, 0xdf, 0x4e, 0x37, 0x19, 0x90, 0x16, 0x4f, 0x42, 0xff, 0x8a, 0xc7, 0xb0, 0xc8, 0x02, 0x01, 0xab, + 0xc3, 0x13, 0x7e, 0xbd, 0x55, 0x30, 0x7c, 0x8b, 0xda, 0xb1, 0x21, 0x42, 0x7d, 0x53, 0x74, 0x8b, 0x03, 0x5e, 0x19, + 0x48, 0x13, 0xf5, 0x8c, 0x49, 0x46, 0x68, 0x2c, 0xe7, 0xc0, 0x08, 0x15, 0x0c, 0x66, 0x16, 0xce, 0x30, 0x73, 0xa7, + 0xc4, 0x51, 0x21, 0xaf, 0xf4, 0xe9, 0xd3, 0x8b, 0xd1, 0x6f, 0xff, 0x81, 0x4c, 0x28, 0x0b, 0x47, 0xc4, 0x94, 0xb8, + 0x90, 0x6b, 0x71, 0xee, 0xd3, 0x18, 0xa1, 0xb1, 0x14, 0x9b, 0x8a, 0x10, 0x3d, 0x62, 0x6b, 0xa5, 0xa3, 0x4b, 0x11, + 0xa2, 0x11, 0x72, 0x28, 0xe9, 0x22, 0xf2, 0x05, 0xa6, 0xe4, 0x1c, 0x89, 0x98, 0x28, 0xca, 0x3f, 0x6f, 0x9f, 0x1f, + 0x2b, 0x79, 0x0c, 0xa3, 0x3a, 0x8b, 0x1e, 0xda, 0x43, 0xc3, 0x13, 0x57, 0x41, 0xa6, 0x05, 0xd9, 0x8f, 0xb8, 0x77, + 0x00, 0xd3, 0x5c, 0x84, 0x4b, 0x66, 0x79, 0xe1, 0xc1, 0x0d, 0x1b, 0x9b, 0xee, 0xca, 0x23, 0xbb, 0x1c, 0x94, 0xbb, + 0x29, 0xc4, 0xf9, 0x65, 0xe6, 0x2e, 0xc4, 0x5f, 0xa7, 0x39, 0x28, 0xc3, 0x62, 0x4c, 0xce, 0x4e, 0x2b, 0xd7, 0x03, + 0x42, 0xfc, 0x02, 0x09, 0x8e, 0xe1, 0xf0, 0xe4, 0xc0, 0x1d, 0x16, 0x83, 0x02, 0x5b, 0x22, 0xb9, 0x4d, 0x91, 0x08, + 0x9c, 0x52, 0x6c, 0x5f, 0x11, 0xc6, 0x37, 0x7f, 0x30, 0xc3, 0xd9, 0x4c, 0x0e, 0xe4, 0x6b, 0x15, 0x87, 0x97, 0x01, + 0x2d, 0xdf, 0xd2, 0xe1, 0x8a, 0xbe, 0x54, 0xfd, 0x44, 0xf6, 0x53, 0xed, 0x61, 0x04, 0x6f, 0x98, 0x33, 0x1c, 0xf7, + 0x4a, 0x40, 0xe0, 0x0c, 0x62, 0x0f, 0xa9, 0x12, 0xc7, 0x23, 0xe5, 0xf4, 0x13, 0x0d, 0x9c, 0xcb, 0x83, 0xc1, 0x80, + 0xd0, 0x5c, 0x19, 0xdb, 0x01, 0x10, 0x6b, 0x12, 0xfd, 0xc0, 0x64, 0x13, 0x68, 0x68, 0x92, 0xbb, 0x2c, 0x36, 0x2a, + 0x4f, 0xa7, 0x3a, 0xc6, 0x03, 0x57, 0x6c, 0xbf, 0xc2, 0x06, 0x85, 0x8d, 0xc7, 0xd7, 0x1d, 0xf0, 0xbb, 0xe8, 0xa7, + 0x84, 0xe6, 0x95, 0x6f, 0x08, 0xa3, 0x9b, 0xbe, 0x7b, 0x17, 0x4a, 0x66, 0x4c, 0x3c, 0xa2, 0xc9, 0x19, 0x96, 0x9e, + 0x0b, 0x4f, 0xe2, 0xca, 0x41, 0xcb, 0x12, 0xa2, 0x54, 0x0f, 0x9b, 0x9c, 0xc4, 0x64, 0xd7, 0x59, 0x93, 0xeb, 0x16, + 0x27, 0x83, 0xc8, 0x33, 0xcd, 0xcf, 0x61, 0xe1, 0x25, 0xa2, 0x85, 0xf4, 0xe4, 0x00, 0xe6, 0x07, 0x51, 0x58, 0x0a, + 0x8c, 0x93, 0xa7, 0x43, 0xa8, 0x17, 0x37, 0x26, 0x53, 0xac, 0x37, 0x53, 0xc1, 0xf3, 0xe1, 0xc5, 0x52, 0x4a, 0xf3, + 0x27, 0x55, 0xa9, 0xf2, 0x32, 0x76, 0x3d, 0x13, 0xb8, 0x3b, 0x7b, 0xd0, 0x87, 0x35, 0xa6, 0x0e, 0x4a, 0xfb, 0x09, + 0x13, 0x41, 0x0e, 0xce, 0x92, 0x86, 0x38, 0x08, 0x4d, 0x55, 0x88, 0x9f, 0xdd, 0x52, 0x21, 0xdf, 0xc7, 0xdb, 0x6a, + 0xe5, 0x9c, 0x53, 0x56, 0x6d, 0xee, 0x6a, 0xea, 0x43, 0xdc, 0xf1, 0x95, 0xda, 0x58, 0x0a, 0xf5, 0xce, 0x92, 0x01, + 0x54, 0x15, 0xb2, 0x78, 0x77, 0xb5, 0xa2, 0xca, 0x7a, 0xff, 0xe4, 0x80, 0xae, 0xa5, 0x43, 0xda, 0x61, 0xc3, 0x13, + 0x30, 0xe5, 0xa6, 0x45, 0x77, 0x57, 0x2b, 0xbe, 0xa4, 0xf4, 0x8b, 0xde, 0x1c, 0x2c, 0x92, 0xa5, 0x3f, 0xfc, 0x3f, + 0x04, 0xdf, 0xdf, 0xf4, 0x2c, 0x6c, 0x03, 0x00}; } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 5a8128ba43..7c015adcf7 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -36,75 +36,153 @@ #endif #endif -namespace esphome { -namespace web_server { +namespace esphome::web_server { static const char *const TAG = "web_server"; -#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS -static const char *const HEADER_PNA_NAME = "Private-Network-Access-Name"; -static const char *const HEADER_PNA_ID = "Private-Network-Access-ID"; -static const char *const HEADER_CORS_REQ_PNA = "Access-Control-Request-Private-Network"; -static const char *const HEADER_CORS_ALLOW_PNA = "Access-Control-Allow-Private-Network"; -#endif +// Longest: UPDATE AVAILABLE (16 chars + null terminator, rounded up) +static constexpr size_t PSTR_LOCAL_SIZE = 18; +#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), PSTR_LOCAL_SIZE - 1) // Parse URL and return match info -static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain) { - UrlMatch match{}; +// URL formats (disambiguated by HTTP method for 3-segment case): +// GET /{domain}/{entity_name} - main device state +// POST /{domain}/{entity_name}/{action} - main device action +// GET /{domain}/{device_name}/{entity_name} - sub-device state (USE_DEVICES only) +// POST /{domain}/{device_name}/{entity_name}/{action} - sub-device action (USE_DEVICES only) +static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain, bool is_post = false) { + // URL must start with '/' and have content after it + if (url_len < 2 || url_ptr[0] != '/') + return UrlMatch{}; - // URL must start with '/' - if (url_len < 2 || url_ptr[0] != '/') { - return match; - } - - // Skip leading '/' - const char *start = url_ptr + 1; + const char *p = url_ptr + 1; const char *end = url_ptr + url_len; - // Find domain (everything up to next '/' or end) - const char *domain_end = (const char *) memchr(start, '/', end - start); - if (!domain_end) { - // No second slash found - original behavior returns invalid - return match; - } + // Helper to find next segment: returns pointer after '/' or nullptr if no more slashes + auto next_segment = [&end](const char *start) -> const char * { + const char *slash = (const char *) memchr(start, '/', end - start); + return slash ? slash + 1 : nullptr; + }; - // Set domain - match.domain = start; - match.domain_len = domain_end - start; + // Helper to make StringRef from segment start to next segment (or end) + auto make_ref = [&end](const char *start, const char *next_start) -> StringRef { + return StringRef(start, (next_start ? next_start - 1 : end) - start); + }; + + // Parse domain segment + const char *s1 = p; + const char *s2 = next_segment(s1); + + // Must have domain with trailing slash + if (!s2) + return UrlMatch{}; + + UrlMatch match{}; + match.domain = make_ref(s1, s2); match.valid = true; - if (only_domain) { + if (only_domain || s2 >= end) return match; - } - // Parse ID if present - if (domain_end + 1 >= end) { - return match; // Nothing after domain slash - } + // Parse remaining segments only when needed + const char *s3 = next_segment(s2); + const char *s4 = s3 ? next_segment(s3) : nullptr; - const char *id_start = domain_end + 1; - const char *id_end = (const char *) memchr(id_start, '/', end - id_start); + StringRef seg2 = make_ref(s2, s3); + StringRef seg3 = s3 ? make_ref(s3, s4) : StringRef(); + StringRef seg4 = s4 ? make_ref(s4, nullptr) : StringRef(); - if (!id_end) { - // No more slashes, entire remaining string is ID - match.id = id_start; - match.id_len = end - id_start; - return match; - } + // Reject empty segments + if (seg2.empty() || (s3 && seg3.empty()) || (s4 && seg4.empty())) + return UrlMatch{}; - // Set ID - match.id = id_start; - match.id_len = id_end - id_start; - - // Parse method if present - if (id_end + 1 < end) { - match.method = id_end + 1; - match.method_len = end - (id_end + 1); + // Interpret based on segment count + if (!s3) { + // 1 segment after domain: /{domain}/{entity} + match.id = seg2; + } else if (!s4) { + // 2 segments after domain: /{domain}/{X}/{Y} + // HTTP method disambiguates: GET = device/entity, POST = entity/action + if (is_post) { + match.id = seg2; + match.method = seg3; + return match; + } +#ifdef USE_DEVICES + match.device_name = seg2; + match.id = seg3; +#else + return UrlMatch{}; // 3-segment GET not supported without USE_DEVICES +#endif + } else { + // 3 segments after domain: /{domain}/{device}/{entity}/{action} +#ifdef USE_DEVICES + if (!is_post) { + return UrlMatch{}; // 4-segment GET not supported (action requires POST) + } + match.device_name = seg2; + match.id = seg3; + match.method = seg4; +#else + return UrlMatch{}; // Not supported without USE_DEVICES +#endif } return match; } +EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const { + EntityMatchResult result{false, this->method.empty()}; + +#ifdef USE_DEVICES + Device *entity_device = entity->get_device(); + bool url_has_device = !this->device_name.empty(); + bool entity_has_device = (entity_device != nullptr); + + // Device matching: URL device segment must match entity's device + if (url_has_device != entity_has_device) { + return result; // Mismatch: one has device, other doesn't + } + if (url_has_device && this->device_name != entity_device->get_name()) { + return result; // Device name doesn't match + } +#endif + + // Try matching by entity name (new format) + if (this->id == entity->get_name()) { + result.matched = true; + return result; + } + + // Fall back to object_id (deprecated format) + char object_id_buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = entity->get_object_id_to(object_id_buf); + if (this->id == object_id) { + result.matched = true; + // Log deprecation warning +#ifdef USE_DEVICES + Device *device = entity->get_device(); + if (device != nullptr) { + ESP_LOGW(TAG, + "Deprecated URL format: /%.*s/%.*s/%.*s - use entity name '/%.*s/%s/%s' instead. " + "Object ID URLs will be removed in 2026.7.0.", + (int) this->domain.size(), this->domain.c_str(), (int) this->device_name.size(), + this->device_name.c_str(), (int) this->id.size(), this->id.c_str(), (int) this->domain.size(), + this->domain.c_str(), device->get_name(), entity->get_name().c_str()); + } else +#endif + { + ESP_LOGW(TAG, + "Deprecated URL format: /%.*s/%.*s - use entity name '/%.*s/%s' instead. " + "Object ID URLs will be removed in 2026.7.0.", + (int) this->domain.size(), this->domain.c_str(), (int) this->id.size(), this->id.c_str(), + (int) this->domain.size(), this->domain.c_str(), entity->get_name().c_str()); + } + } + + return result; +} + #if !defined(USE_ESP32) && defined(USE_ARDUINO) // helper for allowing only unique entries in the queue void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) { @@ -240,8 +318,8 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource for (auto &group : ws->sorting_groups_) { json::JsonBuilder builder; JsonObject root = builder.root(); - root["name"] = group.second.name; - root["sorting_weight"] = group.second.weight; + root[ESPHOME_F("name")] = group.second.name; + root[ESPHOME_F("sorting_weight")] = group.second.weight; message = builder.serialize(); // up to 31 groups should be able to be queued initially without defer @@ -282,15 +360,17 @@ std::string WebServer::get_config_json() { json::JsonBuilder builder; JsonObject root = builder.root(); - root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); - root["comment"] = App.get_comment(); + root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); + char comment_buffer[ESPHOME_COMMENT_SIZE]; + App.get_comment_string(comment_buffer); + root[ESPHOME_F("comment")] = comment_buffer; #if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA) - root["ota"] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal + root[ESPHOME_F("ota")] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal #else - root["ota"] = true; + root[ESPHOME_F("ota")] = true; #endif - root["log"] = this->expose_log_; - root["lang"] = "en"; + root[ESPHOME_F("log")] = this->expose_log_; + root[ESPHOME_F("lang")] = "en"; return builder.serialize(); } @@ -301,12 +381,7 @@ void WebServer::setup() { #ifdef USE_LOGGER if (logger::global_logger != nullptr && this->expose_log_) { - logger::global_logger->add_on_log_callback( - // logs are not deferred, the memory overhead would be too large - [this](int level, const char *tag, const char *message, size_t message_len) { - (void) message_len; - this->events_.try_send_nodefer(message, "log", millis()); - }); + logger::global_logger->add_log_listener(this); } #endif @@ -322,6 +397,16 @@ void WebServer::setup() { this->set_interval(10000, [this]() { this->events_.try_send_nodefer("", "ping", millis(), 30000); }); } void WebServer::loop() { this->events_.loop(); } + +#ifdef USE_LOGGER +void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) { + (void) level; + (void) tag; + (void) message_len; + this->events_.try_send_nodefer(message, "log", millis()); +} +#endif + void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:\n" @@ -337,7 +422,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { #else AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); #endif - response->addHeader("Content-Encoding", "gzip"); + response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip")); request->send(response); } #elif USE_WEBSERVER_VERSION >= 2 @@ -357,10 +442,10 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse(200, ""); - response->addHeader(HEADER_CORS_ALLOW_PNA, "true"); - response->addHeader(HEADER_PNA_NAME, App.get_name().c_str()); - std::string mac = get_mac_address_pretty(); - response->addHeader(HEADER_PNA_ID, mac.c_str()); + response->addHeader(ESPHOME_F("Access-Control-Allow-Private-Network"), ESPHOME_F("true")); + response->addHeader(ESPHOME_F("Private-Network-Access-Name"), App.get_name().c_str()); + char mac_s[18]; + response->addHeader(ESPHOME_F("Private-Network-Access-ID"), get_mac_address_pretty_into_buffer(mac_s)); request->send(response); } #endif @@ -374,7 +459,7 @@ void WebServer::handle_css_request(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE); #endif - response->addHeader("Content-Encoding", "gzip"); + response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip")); request->send(response); } #endif @@ -388,24 +473,64 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { AsyncWebServerResponse *response = request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE); #endif - response->addHeader("Content-Encoding", "gzip"); + response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip")); request->send(response); } #endif // Helper functions to reduce code size by avoiding macro expansion +// Build unique id as: {domain}/{device_name}/{entity_name} or {domain}/{entity_name} +// Uses names (not object_id) to avoid UTF-8 collision issues static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) { - char id_buf[160]; // object_id can be up to 128 chars + prefix + dash + null - const auto &object_id = obj->get_object_id(); - snprintf(id_buf, sizeof(id_buf), "%s-%s", prefix, object_id.c_str()); - root["id"] = id_buf; + const StringRef &name = obj->get_name(); + size_t prefix_len = strlen(prefix); + size_t name_len = name.size(); + +#ifdef USE_DEVICES + Device *device = obj->get_device(); + const char *device_name = device ? device->get_name() : nullptr; + size_t device_len = device_name ? strlen(device_name) : 0; +#endif + + // Build id into stack buffer - ArduinoJson copies the string + // Format: {prefix}/{device?}/{name} + // Buffer size guaranteed by schema validation (NAME_MAX_LENGTH=120): + // With devices: domain(20) + "/" + device(120) + "/" + name(120) + null = 263, rounded up to 280 for safety margin + // Without devices: domain(20) + "/" + name(120) + null = 142, rounded up to 150 for safety margin +#ifdef USE_DEVICES + char id_buf[280]; +#else + char id_buf[150]; +#endif + char *p = id_buf; + memcpy(p, prefix, prefix_len); + p += prefix_len; + *p++ = '/'; +#ifdef USE_DEVICES + if (device_name) { + memcpy(p, device_name, device_len); + p += device_len; + *p++ = '/'; + } +#endif + memcpy(p, name.c_str(), name_len); + p[name_len] = '\0'; + + root[ESPHOME_F("id")] = id_buf; + if (start_config == DETAIL_ALL) { - root["name"] = obj->get_name(); - root["icon"] = obj->get_icon_ref(); - root["entity_category"] = obj->get_entity_category(); + root[ESPHOME_F("domain")] = prefix; + root[ESPHOME_F("name")] = name; +#ifdef USE_DEVICES + if (device_name) { + root[ESPHOME_F("device")] = device_name; + } +#endif + root[ESPHOME_F("icon")] = obj->get_icon_ref(); + root[ESPHOME_F("entity_category")] = obj->get_entity_category(); bool is_disabled = obj->is_disabled_by_default(); if (is_disabled) - root["is_disabled_by_default"] = is_disabled; + root[ESPHOME_F("is_disabled_by_default")] = is_disabled; } } @@ -415,14 +540,14 @@ template static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value, JsonDetail start_config) { set_json_id(root, obj, prefix, start_config); - root["value"] = value; + root[ESPHOME_F("value")] = value; } template -static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state, +static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const char *state, const T &value, JsonDetail start_config) { set_json_value(root, obj, prefix, value, start_config); - root["state"] = state; + root[ESPHOME_F("state")] = state; } // Helper to get request detail parameter @@ -439,12 +564,13 @@ void WebServer::on_sensor_update(sensor::Sensor *obj) { } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (sensor::Sensor *obj : App.get_sensors()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; // Note: request->method() is always HTTP_GET here (canHandle ensures this) - if (match.method_empty()) { + if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->sensor_json(obj, obj->state, detail); + std::string data = this->sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -452,24 +578,25 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::sensor_state_json_generator(WebServer *web_server, void *source) { - return web_server->sensor_json((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE); + return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE); } std::string WebServer::sensor_all_json_generator(WebServer *web_server, void *source) { - return web_server->sensor_json((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL); + return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) { +std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); const auto uom_ref = obj->get_unit_of_measurement_ref(); - - std::string state = - std::isnan(value) ? "NA" : value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref); + char buf[VALUE_ACCURACY_MAX_LEN]; + const char *state = std::isnan(value) + ? "NA" + : (value_accuracy_with_uom_to_buf(buf, value, obj->get_accuracy_decimals(), uom_ref), buf); set_json_icon_state_value(root, obj, "sensor", state, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); if (!uom_ref.empty()) - root["uom"] = uom_ref; + root[ESPHOME_F("uom")] = uom_ref; } return builder.serialize(); @@ -484,12 +611,13 @@ void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj) { } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (text_sensor::TextSensor *obj : App.get_text_sensors()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; // Note: request->method() is always HTTP_GET here (canHandle ensures this) - if (match.method_empty()) { + if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->text_sensor_json(obj, obj->state, detail); + std::string data = this->text_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -497,19 +625,19 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const request->send(404); } std::string WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) { - return web_server->text_sensor_json((text_sensor::TextSensor *) (source), - ((text_sensor::TextSensor *) (source))->state, DETAIL_STATE); + return web_server->text_sensor_json_((text_sensor::TextSensor *) (source), + ((text_sensor::TextSensor *) (source))->state, DETAIL_STATE); } std::string WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) { - return web_server->text_sensor_json((text_sensor::TextSensor *) (source), - ((text_sensor::TextSensor *) (source))->state, DETAIL_ALL); + return web_server->text_sensor_json_((text_sensor::TextSensor *) (source), + ((text_sensor::TextSensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, - JsonDetail start_config) { +std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config); + set_json_icon_state_value(root, obj, "text_sensor", value.c_str(), value.c_str(), start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -526,12 +654,13 @@ void WebServer::on_switch_update(switch_::Switch *obj) { } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (switch_::Switch *obj : App.get_switches()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->switch_json(obj, obj->state, detail); + std::string data = this->switch_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -573,18 +702,18 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::switch_state_json_generator(WebServer *web_server, void *source) { - return web_server->switch_json((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE); + return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE); } std::string WebServer::switch_all_json_generator(WebServer *web_server, void *source) { - return web_server->switch_json((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL); + return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL); } -std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) { +std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); set_json_icon_state_value(root, obj, "switch", value ? "ON" : "OFF", value, start_config); if (start_config == DETAIL_ALL) { - root["assumed_state"] = obj->assumed_state(); + root[ESPHOME_F("assumed_state")] = obj->assumed_state(); this->add_sorting_info_(root, obj); } @@ -595,11 +724,12 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail #ifdef USE_BUTTON void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (button::Button *obj : App.get_buttons()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->button_json(obj, detail); + std::string data = this->button_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals("press")) { this->defer([obj]() { obj->press(); }); @@ -613,12 +743,12 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM request->send(404); } std::string WebServer::button_state_json_generator(WebServer *web_server, void *source) { - return web_server->button_json((button::Button *) (source), DETAIL_STATE); + return web_server->button_json_((button::Button *) (source), DETAIL_STATE); } std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) { - return web_server->button_json((button::Button *) (source), DETAIL_ALL); + return web_server->button_json_((button::Button *) (source), DETAIL_ALL); } -std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) { +std::string WebServer::button_json_(button::Button *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -639,12 +769,13 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; // Note: request->method() is always HTTP_GET here (canHandle ensures this) - if (match.method_empty()) { + if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->binary_sensor_json(obj, obj->state, detail); + std::string data = this->binary_sensor_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -652,14 +783,14 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con request->send(404); } std::string WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) { - return web_server->binary_sensor_json((binary_sensor::BinarySensor *) (source), - ((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE); + return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source), + ((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE); } std::string WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) { - return web_server->binary_sensor_json((binary_sensor::BinarySensor *) (source), - ((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL); + return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source), + ((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL); } -std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { +std::string WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -680,18 +811,25 @@ void WebServer::on_fan_update(fan::Fan *obj) { } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (fan::Fan *obj : App.get_fans()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->fan_json(obj, detail); + std::string data = this->fan_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals("toggle")) { this->defer([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method_equals("turn_on") || match.method_equals("turn_off")) { - auto call = match.method_equals("turn_on") ? obj->turn_on() : obj->turn_off(); + } else { + bool is_on = match.method_equals("turn_on"); + bool is_off = match.method_equals("turn_off"); + if (!is_on && !is_off) { + request->send(404); + return; + } + auto call = is_on ? obj->turn_on() : obj->turn_off(); parse_int_param_(request, "speed_level", call, &decltype(call)::set_speed); @@ -715,31 +853,29 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } this->defer([call]() mutable { call.perform(); }); request->send(200); - } else { - request->send(404); } return; } request->send(404); } std::string WebServer::fan_state_json_generator(WebServer *web_server, void *source) { - return web_server->fan_json((fan::Fan *) (source), DETAIL_STATE); + return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE); } std::string WebServer::fan_all_json_generator(WebServer *web_server, void *source) { - return web_server->fan_json((fan::Fan *) (source), DETAIL_ALL); + return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL); } -std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { +std::string WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); set_json_icon_state_value(root, obj, "fan", obj->state ? "ON" : "OFF", obj->state, start_config); const auto traits = obj->get_traits(); if (traits.supports_speed()) { - root["speed_level"] = obj->speed; - root["speed_count"] = traits.supported_speed_count(); + root[ESPHOME_F("speed_level")] = obj->speed; + root[ESPHOME_F("speed_count")] = traits.supported_speed_count(); } if (obj->get_traits().supports_oscillation()) - root["oscillation"] = obj->oscillating; + root[ESPHOME_F("oscillation")] = obj->oscillating; if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -756,54 +892,58 @@ void WebServer::on_light_update(light::LightState *obj) { } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (light::LightState *obj : App.get_lights()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->light_json(obj, detail); + std::string data = this->light_json_(obj, detail); request->send(200, "application/json", data.c_str()); } else if (match.method_equals("toggle")) { this->defer([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method_equals("turn_on")) { - auto call = obj->turn_on(); - - // Parse color parameters - parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f); - parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f); - parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f); - parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f); - parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f); - parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature); - - // Parse timing parameters - parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000); - parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); - - parse_string_param_(request, "effect", call, &decltype(call)::set_effect); - - this->defer([call]() mutable { call.perform(); }); - request->send(200); - } else if (match.method_equals("turn_off")) { - auto call = obj->turn_off(); - parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); - this->defer([call]() mutable { call.perform(); }); - request->send(200); } else { - request->send(404); + bool is_on = match.method_equals("turn_on"); + bool is_off = match.method_equals("turn_off"); + if (!is_on && !is_off) { + request->send(404); + return; + } + auto call = is_on ? obj->turn_on() : obj->turn_off(); + + if (is_on) { + // Parse color parameters + parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f); + parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f); + parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f); + parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f); + parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f); + parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature); + + // Parse timing parameters + parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000); + } + parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); + + if (is_on) { + parse_string_param_(request, "effect", call, &decltype(call)::set_effect); + } + + this->defer([call]() mutable { call.perform(); }); + request->send(200); } return; } request->send(404); } std::string WebServer::light_state_json_generator(WebServer *web_server, void *source) { - return web_server->light_json((light::LightState *) (source), DETAIL_STATE); + return web_server->light_json_((light::LightState *) (source), DETAIL_STATE); } std::string WebServer::light_all_json_generator(WebServer *web_server, void *source) { - return web_server->light_json((light::LightState *) (source), DETAIL_ALL); + return web_server->light_json_((light::LightState *) (source), DETAIL_ALL); } -std::string WebServer::light_json(light::LightState *obj, JsonDetail start_config) { +std::string WebServer::light_json_(light::LightState *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -811,7 +951,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi light::LightJSONSchema::dump_json(*obj, root); if (start_config == DETAIL_ALL) { - JsonArray opt = root["effects"].to(); + JsonArray opt = root[ESPHOME_F("effects")].to(); opt.add("None"); for (auto const &option : obj->get_effects()) { opt.add(option->get_name()); @@ -831,12 +971,13 @@ void WebServer::on_cover_update(cover::Cover *obj) { } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (cover::Cover *obj : App.get_covers()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->cover_json(obj, detail); + std::string data = this->cover_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -885,23 +1026,24 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } std::string WebServer::cover_state_json_generator(WebServer *web_server, void *source) { - return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE); + return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE); } std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) { - return web_server->cover_json((cover::Cover *) (source), DETAIL_ALL); + return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL); } -std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { +std::string WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, start_config); - root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); + char buf[PSTR_LOCAL_SIZE]; + root[ESPHOME_F("current_operation")] = PSTR_LOCAL(cover::cover_operation_to_str(obj->current_operation)); if (obj->get_traits().get_supports_position()) - root["position"] = obj->position; + root[ESPHOME_F("position")] = obj->position; if (obj->get_traits().get_supports_tilt()) - root["tilt"] = obj->tilt; + root[ESPHOME_F("tilt")] = obj->tilt; if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -918,12 +1060,13 @@ void WebServer::on_number_update(number::Number *obj) { } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_numbers()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->number_json(obj, obj->state, detail); + std::string data = this->number_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -943,33 +1086,33 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::number_state_json_generator(WebServer *web_server, void *source) { - return web_server->number_json((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE); + return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE); } std::string WebServer::number_all_json_generator(WebServer *web_server, void *source) { - return web_server->number_json((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL); + return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL); } -std::string WebServer::number_json(number::Number *obj, float value, JsonDetail start_config) { +std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); const auto uom_ref = obj->traits.get_unit_of_measurement_ref(); + const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step()); - std::string val_str = std::isnan(value) - ? "\"NaN\"" - : value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - std::string state_str = std::isnan(value) ? "NA" - : value_accuracy_with_uom_to_string( - value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref); + // Need two buffers: one for value, one for state with UOM + char val_buf[VALUE_ACCURACY_MAX_LEN]; + char state_buf[VALUE_ACCURACY_MAX_LEN]; + const char *val_str = std::isnan(value) ? "\"NaN\"" : (value_accuracy_to_buf(val_buf, value, accuracy), val_buf); + const char *state_str = + std::isnan(value) ? "NA" : (value_accuracy_with_uom_to_buf(state_buf, value, accuracy, uom_ref), state_buf); set_json_icon_state_value(root, obj, "number", state_str, val_str, start_config); if (start_config == DETAIL_ALL) { - root["min_value"] = - value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step())); - root["max_value"] = - value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step())); - root["step"] = value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step())); - root["mode"] = (int) obj->traits.get_mode(); + // ArduinoJson copies the string immediately, so we can reuse val_buf + root[ESPHOME_F("min_value")] = (value_accuracy_to_buf(val_buf, obj->traits.get_min_value(), accuracy), val_buf); + root[ESPHOME_F("max_value")] = (value_accuracy_to_buf(val_buf, obj->traits.get_max_value(), accuracy), val_buf); + root[ESPHOME_F("step")] = (value_accuracy_to_buf(val_buf, obj->traits.get_step(), accuracy), val_buf); + root[ESPHOME_F("mode")] = (int) obj->traits.get_mode(); if (!uom_ref.empty()) - root["uom"] = uom_ref; + root[ESPHOME_F("uom")] = uom_ref; this->add_sorting_info_(root, obj); } @@ -985,11 +1128,12 @@ void WebServer::on_date_update(datetime::DateEntity *obj) { } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_dates()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->date_json(obj, detail); + std::string data = this->date_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1015,16 +1159,22 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat } std::string WebServer::date_state_json_generator(WebServer *web_server, void *source) { - return web_server->date_json((datetime::DateEntity *) (source), DETAIL_STATE); + return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE); } std::string WebServer::date_all_json_generator(WebServer *web_server, void *source) { - return web_server->date_json((datetime::DateEntity *) (source), DETAIL_ALL); + return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL); } -std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_config) { +std::string WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day); + // Format: YYYY-MM-DD (max 10 chars + null) + char value[12]; +#ifdef USE_ESP8266 + snprintf_P(value, sizeof(value), PSTR("%d-%02d-%02d"), obj->year, obj->month, obj->day); +#else + snprintf(value, sizeof(value), "%d-%02d-%02d", obj->year, obj->month, obj->day); +#endif set_json_icon_state_value(root, obj, "date", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); @@ -1042,11 +1192,12 @@ void WebServer::on_time_update(datetime::TimeEntity *obj) { } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_times()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->time_json(obj, detail); + std::string data = this->time_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1071,16 +1222,22 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } std::string WebServer::time_state_json_generator(WebServer *web_server, void *source) { - return web_server->time_json((datetime::TimeEntity *) (source), DETAIL_STATE); + return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE); } std::string WebServer::time_all_json_generator(WebServer *web_server, void *source) { - return web_server->time_json((datetime::TimeEntity *) (source), DETAIL_ALL); + return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL); } -std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_config) { +std::string WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second); + // Format: HH:MM:SS (8 chars + null) + char value[12]; +#ifdef USE_ESP8266 + snprintf_P(value, sizeof(value), PSTR("%02d:%02d:%02d"), obj->hour, obj->minute, obj->second); +#else + snprintf(value, sizeof(value), "%02d:%02d:%02d", obj->hour, obj->minute, obj->second); +#endif set_json_icon_state_value(root, obj, "time", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); @@ -1098,11 +1255,12 @@ void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_datetimes()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->datetime_json(obj, detail); + std::string data = this->datetime_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1127,17 +1285,24 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur request->send(404); } std::string WebServer::datetime_state_json_generator(WebServer *web_server, void *source) { - return web_server->datetime_json((datetime::DateTimeEntity *) (source), DETAIL_STATE); + return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE); } std::string WebServer::datetime_all_json_generator(WebServer *web_server, void *source) { - return web_server->datetime_json((datetime::DateTimeEntity *) (source), DETAIL_ALL); + return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL); } -std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail start_config) { +std::string WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - std::string value = - str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second); + // Format: YYYY-MM-DD HH:MM:SS (max 19 chars + null) + char value[24]; +#ifdef USE_ESP8266 + snprintf_P(value, sizeof(value), PSTR("%d-%02d-%02d %02d:%02d:%02d"), obj->year, obj->month, obj->day, obj->hour, + obj->minute, obj->second); +#else + snprintf(value, sizeof(value), "%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, + obj->second); +#endif set_json_icon_state_value(root, obj, "datetime", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); @@ -1155,12 +1320,13 @@ void WebServer::on_text_update(text::Text *obj) { } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_texts()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->text_json(obj, obj->state, detail); + std::string data = this->text_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1180,22 +1346,22 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat } std::string WebServer::text_state_json_generator(WebServer *web_server, void *source) { - return web_server->text_json((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE); + return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE); } std::string WebServer::text_all_json_generator(WebServer *web_server, void *source) { - return web_server->text_json((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL); + return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL); } -std::string WebServer::text_json(text::Text *obj, const std::string &value, JsonDetail start_config) { +std::string WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - std::string state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value; - set_json_icon_state_value(root, obj, "text", state, value, start_config); - root["min_length"] = obj->traits.get_min_length(); - root["max_length"] = obj->traits.get_max_length(); - root["pattern"] = obj->traits.get_pattern(); + const char *state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value.c_str(); + set_json_icon_state_value(root, obj, "text", state, value.c_str(), start_config); + root[ESPHOME_F("min_length")] = obj->traits.get_min_length(); + root[ESPHOME_F("max_length")] = obj->traits.get_max_length(); + root[ESPHOME_F("pattern")] = obj->traits.get_pattern_c_str(); if (start_config == DETAIL_ALL) { - root["mode"] = (int) obj->traits.get_mode(); + root[ESPHOME_F("mode")] = (int) obj->traits.get_mode(); this->add_sorting_info_(root, obj); } @@ -1211,12 +1377,13 @@ void WebServer::on_select_update(select::Select *obj) { } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_selects()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->select_json(obj, obj->has_state() ? obj->current_option() : "", detail); + std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : "", detail); request->send(200, "application/json", data.c_str()); return; } @@ -1237,19 +1404,19 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); - return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE); + return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE); } std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) { auto *obj = (select::Select *) (source); - return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL); + return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL); } -std::string WebServer::select_json(select::Select *obj, const char *value, JsonDetail start_config) { +std::string WebServer::select_json_(select::Select *obj, const char *value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); set_json_icon_state_value(root, obj, "select", value, value, start_config); if (start_config == DETAIL_ALL) { - JsonArray opt = root["option"].to(); + JsonArray opt = root[ESPHOME_F("option")].to(); for (auto &option : obj->traits.get_options()) { opt.add(option); } @@ -1260,9 +1427,6 @@ std::string WebServer::select_json(select::Select *obj, const char *value, JsonD } #endif -// Longest: HORIZONTAL -#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), 15) - #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { if (!this->include_internal_ && obj->is_internal()) @@ -1271,12 +1435,13 @@ void WebServer::on_climate_update(climate::Climate *obj) { } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_climates()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->climate_json(obj, detail); + std::string data = this->climate_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1306,13 +1471,13 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url } std::string WebServer::climate_state_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->climate_json((climate::Climate *) (source), DETAIL_STATE); + return web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE); } std::string WebServer::climate_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL); + return web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL); } -std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { +std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); @@ -1320,35 +1485,36 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf const auto traits = obj->get_traits(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); - char buf[16]; + char buf[PSTR_LOCAL_SIZE]; + char temp_buf[VALUE_ACCURACY_MAX_LEN]; if (start_config == DETAIL_ALL) { - JsonArray opt = root["modes"].to(); + JsonArray opt = root[ESPHOME_F("modes")].to(); for (climate::ClimateMode m : traits.get_supported_modes()) opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); if (!traits.get_supported_custom_fan_modes().empty()) { - JsonArray opt = root["fan_modes"].to(); + JsonArray opt = root[ESPHOME_F("fan_modes")].to(); for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); } if (!traits.get_supported_custom_fan_modes().empty()) { - JsonArray opt = root["custom_fan_modes"].to(); + JsonArray opt = root[ESPHOME_F("custom_fan_modes")].to(); for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) opt.add(custom_fan_mode); } if (traits.get_supports_swing_modes()) { - JsonArray opt = root["swing_modes"].to(); + JsonArray opt = root[ESPHOME_F("swing_modes")].to(); for (auto swing_mode : traits.get_supported_swing_modes()) opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); } if (traits.get_supports_presets() && obj->preset.has_value()) { - JsonArray opt = root["presets"].to(); + JsonArray opt = root[ESPHOME_F("presets")].to(); for (climate::ClimatePreset m : traits.get_supported_presets()) opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); } if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { - JsonArray opt = root["custom_presets"].to(); + JsonArray opt = root[ESPHOME_F("custom_presets")].to(); for (auto const &custom_preset : traits.get_supported_custom_presets()) opt.add(custom_preset); } @@ -1356,49 +1522,55 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf } bool has_state = false; - root["mode"] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy); - root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); - root["step"] = traits.get_visual_target_temperature_step(); + root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); + root[ESPHOME_F("max_temp")] = + (value_accuracy_to_buf(temp_buf, traits.get_visual_max_temperature(), target_accuracy), temp_buf); + root[ESPHOME_F("min_temp")] = + (value_accuracy_to_buf(temp_buf, traits.get_visual_min_temperature(), target_accuracy), temp_buf); + root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step(); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { - root["action"] = PSTR_LOCAL(climate_action_to_string(obj->action)); - root["state"] = root["action"]; + root[ESPHOME_F("action")] = PSTR_LOCAL(climate_action_to_string(obj->action)); + root[ESPHOME_F("state")] = root[ESPHOME_F("action")]; has_state = true; } if (traits.get_supports_fan_modes() && obj->fan_mode.has_value()) { - root["fan_mode"] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); + root[ESPHOME_F("fan_mode")] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); } if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) { - root["custom_fan_mode"] = obj->get_custom_fan_mode(); + root[ESPHOME_F("custom_fan_mode")] = obj->get_custom_fan_mode(); } if (traits.get_supports_presets() && obj->preset.has_value()) { - root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); + root[ESPHOME_F("preset")] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); } if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) { - root["custom_preset"] = obj->get_custom_preset(); + root[ESPHOME_F("custom_preset")] = obj->get_custom_preset(); } if (traits.get_supports_swing_modes()) { - root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); + root[ESPHOME_F("swing_mode")] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { - if (!std::isnan(obj->current_temperature)) { - root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, current_accuracy); - } else { - root["current_temperature"] = "NA"; - } + root[ESPHOME_F("current_temperature")] = + std::isnan(obj->current_temperature) + ? "NA" + : (value_accuracy_to_buf(temp_buf, obj->current_temperature, current_accuracy), temp_buf); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { - root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); - root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, target_accuracy); + root[ESPHOME_F("target_temperature_low")] = + (value_accuracy_to_buf(temp_buf, obj->target_temperature_low, target_accuracy), temp_buf); + root[ESPHOME_F("target_temperature_high")] = + (value_accuracy_to_buf(temp_buf, obj->target_temperature_high, target_accuracy), temp_buf); if (!has_state) { - root["state"] = value_accuracy_to_string((obj->target_temperature_high + obj->target_temperature_low) / 2.0f, - target_accuracy); + root[ESPHOME_F("state")] = + (value_accuracy_to_buf(temp_buf, (obj->target_temperature_high + obj->target_temperature_low) / 2.0f, + target_accuracy), + temp_buf); } } else { - root["target_temperature"] = value_accuracy_to_string(obj->target_temperature, target_accuracy); + root[ESPHOME_F("target_temperature")] = + (value_accuracy_to_buf(temp_buf, obj->target_temperature, target_accuracy), temp_buf); if (!has_state) - root["state"] = root["target_temperature"]; + root[ESPHOME_F("state")] = root[ESPHOME_F("target_temperature")]; } return builder.serialize(); @@ -1414,12 +1586,13 @@ void WebServer::on_lock_update(lock::Lock *obj) { } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (lock::Lock *obj : App.get_locks()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->lock_json(obj, obj->state, detail); + std::string data = this->lock_json_(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1461,16 +1634,17 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat request->send(404); } std::string WebServer::lock_state_json_generator(WebServer *web_server, void *source) { - return web_server->lock_json((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE); + return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE); } std::string WebServer::lock_all_json_generator(WebServer *web_server, void *source) { - return web_server->lock_json((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL); + return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL); } -std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { +std::string WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "lock", lock::lock_state_to_string(value), value, start_config); + char buf[PSTR_LOCAL_SIZE]; + set_json_icon_state_value(root, obj, "lock", PSTR_LOCAL(lock::lock_state_to_string(value)), value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1487,12 +1661,13 @@ void WebServer::on_valve_update(valve::Valve *obj) { } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (valve::Valve *obj : App.get_valves()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->valve_json(obj, detail); + std::string data = this->valve_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1539,21 +1714,22 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa request->send(404); } std::string WebServer::valve_state_json_generator(WebServer *web_server, void *source) { - return web_server->valve_json((valve::Valve *) (source), DETAIL_STATE); + return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE); } std::string WebServer::valve_all_json_generator(WebServer *web_server, void *source) { - return web_server->valve_json((valve::Valve *) (source), DETAIL_ALL); + return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL); } -std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { +std::string WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); set_json_icon_state_value(root, obj, "valve", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position, start_config); - root["current_operation"] = valve::valve_operation_to_str(obj->current_operation); + char buf[PSTR_LOCAL_SIZE]; + root[ESPHOME_F("current_operation")] = PSTR_LOCAL(valve::valve_operation_to_str(obj->current_operation)); if (obj->get_traits().get_supports_position()) - root["position"] = obj->position; + root[ESPHOME_F("position")] = obj->position; if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1570,12 +1746,13 @@ void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->alarm_control_panel_json(obj, obj->get_state(), detail); + std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail); request->send(200, "application/json", data.c_str()); return; } @@ -1616,22 +1793,22 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques request->send(404); } std::string WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) { - return web_server->alarm_control_panel_json((alarm_control_panel::AlarmControlPanel *) (source), - ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), - DETAIL_STATE); + return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source), + ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), + DETAIL_STATE); } std::string WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) { - return web_server->alarm_control_panel_json((alarm_control_panel::AlarmControlPanel *) (source), - ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), - DETAIL_ALL); + return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source), + ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), + DETAIL_ALL); } -std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, - alarm_control_panel::AlarmControlPanelState value, - JsonDetail start_config) { +std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, + JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); - char buf[16]; + char buf[PSTR_LOCAL_SIZE]; set_json_icon_state_value(root, obj, "alarm-control-panel", PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); if (start_config == DETAIL_ALL) { @@ -1651,13 +1828,14 @@ void WebServer::on_event(event::Event *obj) { void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (event::Event *obj : App.get_events()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; // Note: request->method() is always HTTP_GET here (canHandle ensures this) - if (match.method_empty()) { + if (entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->event_json(obj, "", detail); + std::string data = this->event_json_(obj, "", detail); request->send(200, "application/json", data.c_str()); return; } @@ -1672,44 +1850,46 @@ static std::string get_event_type(event::Event *event) { std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { auto *event = static_cast(source); - return web_server->event_json(event, get_event_type(event), DETAIL_STATE); + return web_server->event_json_(event, get_event_type(event), DETAIL_STATE); } +// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) { auto *event = static_cast(source); - return web_server->event_json(event, get_event_type(event), DETAIL_ALL); + return web_server->event_json_(event, get_event_type(event), DETAIL_ALL); } -std::string WebServer::event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config) { +std::string WebServer::event_json_(event::Event *obj, const std::string &event_type, JsonDetail start_config) { json::JsonBuilder builder; JsonObject root = builder.root(); set_json_id(root, obj, "event", start_config); if (!event_type.empty()) { - root["event_type"] = event_type; + root[ESPHOME_F("event_type")] = event_type; } if (start_config == DETAIL_ALL) { - JsonArray event_types = root["event_types"].to(); + JsonArray event_types = root[ESPHOME_F("event_types")].to(); for (const char *event_type : obj->get_event_types()) { event_types.add(event_type); } - root["device_class"] = obj->get_device_class_ref(); + root[ESPHOME_F("device_class")] = obj->get_device_class_ref(); this->add_sorting_info_(root, obj); } return builder.serialize(); } +// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) #endif #ifdef USE_UPDATE -static const char *update_state_to_string(update::UpdateState state) { +static const LogString *update_state_to_string(update::UpdateState state) { switch (state) { case update::UPDATE_STATE_NO_UPDATE: - return "NO UPDATE"; + return LOG_STR("NO UPDATE"); case update::UPDATE_STATE_AVAILABLE: - return "UPDATE AVAILABLE"; + return LOG_STR("UPDATE AVAILABLE"); case update::UPDATE_STATE_INSTALLING: - return "INSTALLING"; + return LOG_STR("INSTALLING"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } @@ -1718,12 +1898,13 @@ void WebServer::on_update(update::UpdateEntity *obj) { } void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (update::UpdateEntity *obj : App.get_updates()) { - if (!match.id_equals_entity(obj)) + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) continue; - if (request->method() == HTTP_GET && match.method_empty()) { + if (request->method() == HTTP_GET && entity_match.action_is_empty) { auto detail = get_request_detail(request); - std::string data = this->update_json(obj, detail); + std::string data = this->update_json_(obj, detail); request->send(200, "application/json", data.c_str()); return; } @@ -1741,24 +1922,25 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM } std::string WebServer::update_state_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); + return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); } std::string WebServer::update_all_json_generator(WebServer *web_server, void *source) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); + return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE); } -std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { +std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "update", update_state_to_string(obj->state), obj->update_info.latest_version, - start_config); + char buf[PSTR_LOCAL_SIZE]; + set_json_icon_state_value(root, obj, "update", PSTR_LOCAL(update_state_to_string(obj->state)), + obj->update_info.latest_version, start_config); if (start_config == DETAIL_ALL) { - root["current_version"] = obj->update_info.current_version; - root["title"] = obj->update_info.title; - root["summary"] = obj->update_info.summary; - root["release_url"] = obj->update_info.release_url; + root[ESPHOME_F("current_version")] = obj->update_info.current_version; + root[ESPHOME_F("title")] = obj->update_info.title; + root[ESPHOME_F("summary")] = obj->update_info.summary; + root[ESPHOME_F("release_url")] = obj->update_info.release_url; this->add_sorting_info_(root, obj); } @@ -1791,7 +1973,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { } #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS - if (method == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) + if (method == HTTP_OPTIONS && request->hasHeader(ESPHOME_F("Access-Control-Request-Private-Network"))) return true; #endif @@ -1924,92 +2106,120 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { #endif #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS - if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) { + if (request->method() == HTTP_OPTIONS && request->hasHeader(ESPHOME_F("Access-Control-Request-Private-Network"))) { this->handle_pna_cors_request(request); return; } #endif // Parse URL for component routing - UrlMatch match = match_url(url.c_str(), url.length(), false); + // Pass HTTP method to disambiguate 3-segment URLs (GET=sub-device state, POST=main device action) + UrlMatch match = match_url(url.c_str(), url.length(), false, request->method() == HTTP_POST); - // Component routing using minimal code repetition - struct ComponentRoute { - const char *domain; - void (WebServer::*handler)(AsyncWebServerRequest *, const UrlMatch &); - }; - - static const ComponentRoute ROUTES[] = { + // Route to appropriate handler based on domain + // NOLINTNEXTLINE(readability-simplify-boolean-expr) + if (false) { // Start chain for else-if macro pattern + } #ifdef USE_SENSOR - {"sensor", &WebServer::handle_sensor_request}, + else if (match.domain_equals("sensor")) { + this->handle_sensor_request(request, match); + } #endif #ifdef USE_SWITCH - {"switch", &WebServer::handle_switch_request}, + else if (match.domain_equals("switch")) { + this->handle_switch_request(request, match); + } #endif #ifdef USE_BUTTON - {"button", &WebServer::handle_button_request}, + else if (match.domain_equals("button")) { + this->handle_button_request(request, match); + } #endif #ifdef USE_BINARY_SENSOR - {"binary_sensor", &WebServer::handle_binary_sensor_request}, + else if (match.domain_equals("binary_sensor")) { + this->handle_binary_sensor_request(request, match); + } #endif #ifdef USE_FAN - {"fan", &WebServer::handle_fan_request}, + else if (match.domain_equals("fan")) { + this->handle_fan_request(request, match); + } #endif #ifdef USE_LIGHT - {"light", &WebServer::handle_light_request}, + else if (match.domain_equals("light")) { + this->handle_light_request(request, match); + } #endif #ifdef USE_TEXT_SENSOR - {"text_sensor", &WebServer::handle_text_sensor_request}, + else if (match.domain_equals("text_sensor")) { + this->handle_text_sensor_request(request, match); + } #endif #ifdef USE_COVER - {"cover", &WebServer::handle_cover_request}, + else if (match.domain_equals("cover")) { + this->handle_cover_request(request, match); + } #endif #ifdef USE_NUMBER - {"number", &WebServer::handle_number_request}, + else if (match.domain_equals("number")) { + this->handle_number_request(request, match); + } #endif #ifdef USE_DATETIME_DATE - {"date", &WebServer::handle_date_request}, + else if (match.domain_equals("date")) { + this->handle_date_request(request, match); + } #endif #ifdef USE_DATETIME_TIME - {"time", &WebServer::handle_time_request}, + else if (match.domain_equals("time")) { + this->handle_time_request(request, match); + } #endif #ifdef USE_DATETIME_DATETIME - {"datetime", &WebServer::handle_datetime_request}, + else if (match.domain_equals("datetime")) { + this->handle_datetime_request(request, match); + } #endif #ifdef USE_TEXT - {"text", &WebServer::handle_text_request}, + else if (match.domain_equals("text")) { + this->handle_text_request(request, match); + } #endif #ifdef USE_SELECT - {"select", &WebServer::handle_select_request}, + else if (match.domain_equals("select")) { + this->handle_select_request(request, match); + } #endif #ifdef USE_CLIMATE - {"climate", &WebServer::handle_climate_request}, + else if (match.domain_equals("climate")) { + this->handle_climate_request(request, match); + } #endif #ifdef USE_LOCK - {"lock", &WebServer::handle_lock_request}, + else if (match.domain_equals("lock")) { + this->handle_lock_request(request, match); + } #endif #ifdef USE_VALVE - {"valve", &WebServer::handle_valve_request}, + else if (match.domain_equals("valve")) { + this->handle_valve_request(request, match); + } #endif #ifdef USE_ALARM_CONTROL_PANEL - {"alarm_control_panel", &WebServer::handle_alarm_control_panel_request}, + else if (match.domain_equals("alarm_control_panel")) { + this->handle_alarm_control_panel_request(request, match); + } #endif #ifdef USE_UPDATE - {"update", &WebServer::handle_update_request}, -#endif - }; - - // Check each route - for (const auto &route : ROUTES) { - if (match.domain_equals(route.domain)) { - (this->*route.handler)(request, match); - return; - } + else if (match.domain_equals("update")) { + this->handle_update_request(request, match); + } +#endif + else { + // No matching handler found - send 404 + ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str()); + request->send(404, "text/plain", "Not Found"); } - - // No matching handler found - send 404 - ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str()); - request->send(404, "text/plain", "Not Found"); } bool WebServer::isRequestHandlerTrivial() const { return false; } @@ -2017,9 +2227,9 @@ bool WebServer::isRequestHandlerTrivial() const { return false; } void WebServer::add_sorting_info_(JsonObject &root, EntityBase *entity) { #ifdef USE_WEBSERVER_SORTING if (this->sorting_entitys_.find(entity) != this->sorting_entitys_.end()) { - root["sorting_weight"] = this->sorting_entitys_[entity].weight; + root[ESPHOME_F("sorting_weight")] = this->sorting_entitys_[entity].weight; if (this->sorting_groups_.find(this->sorting_entitys_[entity].group_id) != this->sorting_groups_.end()) { - root["sorting_group"] = this->sorting_groups_[this->sorting_entitys_[entity].group_id].name; + root[ESPHOME_F("sorting_group")] = this->sorting_groups_[this->sorting_entitys_[entity].group_id].name; } } #endif @@ -2035,6 +2245,5 @@ void WebServer::add_sorting_group(uint64_t group_id, const std::string &group_na } #endif -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 7e1af88645..3e1dd867c6 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -7,6 +7,9 @@ #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/core/entity_base.h" +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif #include #include @@ -30,40 +33,31 @@ extern const uint8_t ESPHOME_WEBSERVER_JS_INCLUDE[] PROGMEM; extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE; #endif -namespace esphome { -namespace web_server { +namespace esphome::web_server { + +/// Result of matching a URL against an entity +struct EntityMatchResult { + bool matched; ///< True if entity matched the URL + bool action_is_empty; ///< True if no action/method segment in URL +}; /// Internal helper struct that is used to parse incoming URLs struct UrlMatch { - const char *domain; ///< Pointer to domain within URL, for example "sensor" - const char *id; ///< Pointer to id within URL, for example "living_room_fan" - const char *method; ///< Pointer to method within URL, for example "turn_on" - uint8_t domain_len; ///< Length of domain string - uint8_t id_len; ///< Length of id string - uint8_t method_len; ///< Length of method string - bool valid; ///< Whether this match is valid + StringRef domain; ///< Domain within URL, for example "sensor" + StringRef id; ///< Entity name/id within URL, for example "Temperature" + StringRef method; ///< Method within URL, for example "turn_on" +#ifdef USE_DEVICES + StringRef device_name; ///< Device name within URL, empty for main device +#endif + bool valid{false}; ///< Whether this match is valid // Helper methods for string comparisons - bool domain_equals(const char *str) const { - return domain && domain_len == strlen(str) && memcmp(domain, str, domain_len) == 0; - } + bool domain_equals(const char *str) const { return this->domain == str; } + bool method_equals(const char *str) const { return this->method == str; } - bool id_equals_entity(EntityBase *entity) const { - // Zero-copy comparison using StringRef - StringRef static_ref = entity->get_object_id_ref_for_api_(); - if (!static_ref.empty()) { - return id && id_len == static_ref.size() && memcmp(id, static_ref.c_str(), id_len) == 0; - } - // Fallback to allocation (rare) - const auto &obj_id = entity->get_object_id(); - return id && id_len == obj_id.length() && memcmp(id, obj_id.c_str(), id_len) == 0; - } - - bool method_equals(const char *str) const { - return method && method_len == strlen(str) && memcmp(method, str, method_len) == 0; - } - - bool method_empty() const { return method_len == 0; } + /// Match entity by name first, then fall back to object_id with deprecation warning + /// Returns EntityMatchResult with match status and whether action segment is empty + EntityMatchResult match_entity(EntityBase *entity) const; }; #ifdef USE_WEBSERVER_SORTING @@ -168,9 +162,16 @@ class DeferredUpdateEventSourceList : public std::list'. void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match); - - /// Dump the event details with its value as a JSON string. - std::string event_json(event::Event *obj, const std::string &event_type, JsonDetail start_config); #endif #ifdef USE_UPDATE @@ -482,8 +447,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { static std::string update_state_json_generator(WebServer *web_server, void *source); static std::string update_all_json_generator(WebServer *web_server, void *source); - /// Dump the update state with its value as a JSON string. - std::string update_json(update::UpdateEntity *obj, JsonDetail start_config); #endif /// Override the web handler's canHandle method. @@ -583,8 +546,70 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { const char *js_include_{nullptr}; #endif bool expose_log_{true}; + + private: +#ifdef USE_SENSOR + std::string sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config); +#endif +#ifdef USE_SWITCH + std::string switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config); +#endif +#ifdef USE_BUTTON + std::string button_json_(button::Button *obj, JsonDetail start_config); +#endif +#ifdef USE_BINARY_SENSOR + std::string binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config); +#endif +#ifdef USE_FAN + std::string fan_json_(fan::Fan *obj, JsonDetail start_config); +#endif +#ifdef USE_LIGHT + std::string light_json_(light::LightState *obj, JsonDetail start_config); +#endif +#ifdef USE_TEXT_SENSOR + std::string text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config); +#endif +#ifdef USE_COVER + std::string cover_json_(cover::Cover *obj, JsonDetail start_config); +#endif +#ifdef USE_NUMBER + std::string number_json_(number::Number *obj, float value, JsonDetail start_config); +#endif +#ifdef USE_DATETIME_DATE + std::string date_json_(datetime::DateEntity *obj, JsonDetail start_config); +#endif +#ifdef USE_DATETIME_TIME + std::string time_json_(datetime::TimeEntity *obj, JsonDetail start_config); +#endif +#ifdef USE_DATETIME_DATETIME + std::string datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config); +#endif +#ifdef USE_TEXT + std::string text_json_(text::Text *obj, const std::string &value, JsonDetail start_config); +#endif +#ifdef USE_SELECT + std::string select_json_(select::Select *obj, const char *value, JsonDetail start_config); +#endif +#ifdef USE_CLIMATE + std::string climate_json_(climate::Climate *obj, JsonDetail start_config); +#endif +#ifdef USE_LOCK + std::string lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config); +#endif +#ifdef USE_VALVE + std::string valve_json_(valve::Valve *obj, JsonDetail start_config); +#endif +#ifdef USE_ALARM_CONTROL_PANEL + std::string alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); +#endif +#ifdef USE_EVENT + std::string event_json_(event::Event *obj, const std::string &event_type, JsonDetail start_config); +#endif +#ifdef USE_UPDATE + std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config); +#endif }; -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index 870a338620..a21e9cb9ff 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -3,8 +3,30 @@ #if USE_WEBSERVER_VERSION == 1 -namespace esphome { -namespace web_server { +namespace esphome::web_server { + +// Write HTML-escaped text to stream (escapes ", &, <, >) +static void write_html_escaped(AsyncResponseStream *stream, const char *text) { + for (const char *p = text; *p; ++p) { + switch (*p) { + case '"': + stream->print("""); + break; + case '&': + stream->print("&"); + break; + case '<': + stream->print("<"); + break; + case '>': + stream->print(">"); + break; + default: + stream->write(*p); + break; + } + } +} void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action, const std::function &action_func = nullptr) { @@ -15,9 +37,29 @@ void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string & stream->print("\" id=\""); stream->print(klass.c_str()); stream->print("-"); - stream->print(obj->get_object_id().c_str()); + char object_id_buf[OBJECT_ID_MAX_LEN]; + stream->print(obj->get_object_id_to(object_id_buf).c_str()); + // Add data attributes for hierarchical URL support + stream->print("\" data-domain=\""); + stream->print(klass.c_str()); + stream->print("\" data-name=\""); + write_html_escaped(stream, obj->get_name().c_str()); +#ifdef USE_DEVICES + Device *device = obj->get_device(); + if (device != nullptr) { + stream->print("\" data-device=\""); + write_html_escaped(stream, device->get_name()); + } +#endif stream->print("\">"); - stream->print(obj->get_name().c_str()); +#ifdef USE_DEVICES + if (device != nullptr) { + stream->print("["); + write_html_escaped(stream, device->get_name()); + stream->print("] "); + } +#endif + write_html_escaped(stream, obj->get_name().c_str()); stream->print(""); stream->print(action.c_str()); if (action_func) { @@ -142,7 +184,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream.print(R"(" maxlength=")"); stream.print(text->traits.get_max_length()); stream.print(R"(" pattern=")"); - stream.print(text->traits.get_pattern().c_str()); + stream.print(text->traits.get_pattern_c_str()); stream.print(R"(" value=")"); stream.print(text->state.c_str()); stream.print(R"("/>)"); @@ -190,9 +232,8 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { } #endif - stream->print( - ESPHOME_F("

See ESPHome Web API for " - "REST API documentation.

")); + stream->print(ESPHOME_F("

See ESPHome Web API for " + "REST API documentation.

")); #if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED) // Show OTA form only if web_server OTA is not explicitly disabled // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal @@ -215,6 +256,5 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { request->send(stream); } -} // namespace web_server -} // namespace esphome +} // namespace esphome::web_server #endif diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 039a452d64..7e95e00f29 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -6,20 +6,7 @@ #include #include "esphome/core/component.h" - -// Platform-agnostic macros for web server components -// On ESP32 (both Arduino and IDF): Use plain strings (no PROGMEM) -// On ESP8266: Use Arduino's F() macro for PROGMEM strings -#ifdef USE_ESP32 -#define ESPHOME_F(string_literal) (string_literal) -#define ESPHOME_PGM_P const char * -#define ESPHOME_strncpy_P strncpy -#else -// ESP8266 uses Arduino macros -#define ESPHOME_F(string_literal) F(string_literal) -#define ESPHOME_PGM_P PGM_P -#define ESPHOME_strncpy_P strncpy_P -#endif +#include "esphome/core/progmem.h" #if USE_ESP32 #include "esphome/core/hal.h" @@ -111,9 +98,9 @@ class WebServerBase : public Component { this->initialized_++; return; } - this->server_ = std::make_shared(this->port_); + this->server_ = std::make_unique(this->port_); // All content is controlled and created by user - so allowing all origins is fine here. - DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); + DefaultHeaders::Instance().addHeader(ESPHOME_F("Access-Control-Allow-Origin"), ESPHOME_F("*")); this->server_->begin(); for (auto *handler : this->handlers_) @@ -127,7 +114,7 @@ class WebServerBase : public Component { this->server_ = nullptr; } } - std::shared_ptr get_server() const { return server_; } + AsyncWebServer *get_server() const { return this->server_.get(); } float get_setup_priority() const override; #ifdef USE_WEBSERVER_AUTH @@ -143,7 +130,7 @@ class WebServerBase : public Component { protected: int initialized_{0}; uint16_t port_{80}; - std::shared_ptr server_{nullptr}; + std::unique_ptr server_{nullptr}; std::vector handlers_; #ifdef USE_WEBSERVER_AUTH internal::Credentials credentials_; diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index d5d34b520b..f27814062c 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -13,7 +13,8 @@ namespace web_server_idf { static const char *const TAG = "web_server_idf_utils"; -void url_decode(char *str) { +size_t url_decode(char *str) { + char *start = str; char *ptr = str, buf; for (; *str; str++, ptr++) { if (*str == '%') { @@ -31,7 +32,8 @@ void url_decode(char *str) { *ptr = *str; } } - *ptr = *str; + *ptr = '\0'; + return ptr - start; } bool request_has_header(httpd_req_t *req, const char *name) { return httpd_req_get_hdr_value_len(req, name); } diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index f70a5f0760..3a86aec7ac 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -8,6 +8,10 @@ namespace esphome { namespace web_server_idf { +/// Decode URL-encoded string in-place (e.g., %20 -> space, + -> space) +/// Returns the new length of the decoded string +size_t url_decode(char *str); + bool request_has_header(httpd_req_t *req, const char *name); optional request_get_header(httpd_req_t *req, const char *name); optional request_get_url_query(httpd_req_t *req); diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index ce91569de2..5062aa1e6c 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -87,6 +87,29 @@ int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_ } } // namespace +void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) { + // CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions + // + // The race condition occurs because close() initiates lwIP teardown while + // the TCP/IP thread can still receive packets, causing assertions when + // recv_tcp() sees partially-torn-down state. + // + // By shutting down receive first, we tell lwIP to stop accepting new data BEFORE + // the teardown begins, eliminating the race window. We only shutdown RD (not RDWR) + // to allow the FIN packet to be sent cleanly during close(). + // + // Note: This function may be called with an already-closed socket if the network + // stack closed it. In that case, shutdown() will fail but close() is safe to call. + // + // See: https://github.com/esphome/esphome-webserver/issues/163 + + // Attempt shutdown - ignore errors as socket may already be closed + shutdown(sockfd, SHUT_RD); + + // Always close - safe even if socket is already closed by network stack + close(sockfd); +} + void AsyncWebServer::end() { if (this->server_) { httpd_stop(this->server_); @@ -101,6 +124,13 @@ void AsyncWebServer::begin() { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = this->port_; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; + // Always enable LRU purging to handle socket exhaustion gracefully. + // When max sockets is reached, the oldest connection is closed to make room for new ones. + // This prevents "httpd_accept_conn: error in accept (23)" errors. + // See: https://github.com/esphome/esphome/issues/12464 + config.lru_purge_enable = true; + // Use custom close function that shuts down before closing to prevent lwIP race conditions + config.close_fn = AsyncWebServer::safe_close_with_shutdown; if (httpd_start(&this->server_, &config) == ESP_OK) { const httpd_uri_t handler_get = { .uri = "", @@ -217,11 +247,20 @@ optional AsyncWebServerRequest::get_header(const char *name) const } std::string AsyncWebServerRequest::url() const { - auto *str = strchr(this->req_->uri, '?'); - if (str == nullptr) { - return this->req_->uri; + auto *query_start = strchr(this->req_->uri, '?'); + std::string result; + if (query_start == nullptr) { + result = this->req_->uri; + } else { + result = std::string(this->req_->uri, query_start - this->req_->uri); } - return std::string(this->req_->uri, str - this->req_->uri); + // Decode URL-encoded characters in-place (e.g., %20 -> space) + // This matches AsyncWebServer behavior on Arduino + if (!result.empty()) { + size_t new_len = url_decode(&result[0]); + result.resize(new_len); + } + return result; } std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); } @@ -242,6 +281,7 @@ void AsyncWebServerRequest::send(int code, const char *content_type, const char void AsyncWebServerRequest::redirect(const std::string &url) { httpd_resp_set_status(*this, "302 Found"); httpd_resp_set_hdr(*this, "Location", url.c_str()); + httpd_resp_set_hdr(*this, "Connection", "close"); httpd_resp_send(*this, nullptr, 0); } @@ -312,8 +352,9 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw void AsyncWebServerRequest::requestAuthentication(const char *realm) const { httpd_resp_set_hdr(*this, "Connection", "keep-alive"); - auto auth_val = str_sprintf("Basic realm=\"%s\"", realm ? realm : "Login Required"); - httpd_resp_set_hdr(*this, "WWW-Authenticate", auth_val.c_str()); + // Note: realm is never configured in ESPHome, always nullptr -> "Login Required" + (void) realm; // Unused - always use default + httpd_resp_set_hdr(*this, "WWW-Authenticate", "Basic realm=\"Login Required\""); httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr); } #endif @@ -490,17 +531,11 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * void AsyncEventSourceResponse::destroy(void *ptr) { auto *rsp = static_cast(ptr); int fd = rsp->fd_.exchange(0); // Atomically get and clear fd - - if (fd > 0) { - ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); - // Immediately shut down the socket to prevent lwIP from delivering more data - // This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack - // tries to deliver queued data after the session is marked as dead - // See: https://github.com/esphome/esphome/issues/11936 - shutdown(fd, SHUT_RDWR); - // Note: We don't close() the socket - httpd owns it and will close it - } - // Session will be cleaned up in the main loop to avoid race conditions + ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd); + // Mark as dead - will be cleaned up in the main loop + // Note: We don't delete or remove from set here to avoid race conditions + // httpd will call our custom close_fn (safe_close_with_shutdown) which handles + // shutdown() before close() to prevent lwIP race conditions } // helper for allowing only unique entries in the queue @@ -630,17 +665,92 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char event_buffer_.append(CRLF_STR, CRLF_LEN); } - if (message && *message) { - event_buffer_.append("data: ", sizeof("data: ") - 1); - event_buffer_.append(message); - event_buffer_.append(CRLF_STR, CRLF_LEN); + // Match ESPAsyncWebServer: null message means no data lines and no terminating blank line + if (message) { + // SSE spec requires each line of a multi-line message to have its own "data:" prefix + // Handle \n, \r, and \r\n line endings (matching ESPAsyncWebServer behavior) + + // Fast path: check if message contains any newlines at all + // Most SSE messages (JSON state updates) have no newlines + const char *first_n = strchr(message, '\n'); + const char *first_r = strchr(message, '\r'); + + if (first_n == nullptr && first_r == nullptr) { + // No newlines - fast path (most common case) + event_buffer_.append("data: ", sizeof("data: ") - 1); + event_buffer_.append(message); + event_buffer_.append(CRLF_STR CRLF_STR, CRLF_LEN * 2); // data line + blank line terminator + } else { + // Has newlines - handle multi-line message + const char *line_start = message; + size_t msg_len = strlen(message); + const char *msg_end = message + msg_len; + + // Reuse the first search results + const char *next_n = first_n; + const char *next_r = first_r; + + while (line_start <= msg_end) { + const char *line_end; + const char *next_line; + + if (next_n == nullptr && next_r == nullptr) { + // No more line breaks - output remaining text as final line + event_buffer_.append("data: ", sizeof("data: ") - 1); + event_buffer_.append(line_start); + event_buffer_.append(CRLF_STR, CRLF_LEN); + break; + } + + // Determine line ending type and next line start + if (next_n != nullptr && next_r != nullptr) { + if (next_r + 1 == next_n) { + // \r\n sequence + line_end = next_r; + next_line = next_n + 1; + } else { + // Mixed \n and \r - use whichever comes first + line_end = (next_r < next_n) ? next_r : next_n; + next_line = line_end + 1; + } + } else if (next_n != nullptr) { + // Unix LF + line_end = next_n; + next_line = next_n + 1; + } else { + // Old Mac CR + line_end = next_r; + next_line = next_r + 1; + } + + // Output this line + event_buffer_.append("data: ", sizeof("data: ") - 1); + event_buffer_.append(line_start, line_end - line_start); + event_buffer_.append(CRLF_STR, CRLF_LEN); + + line_start = next_line; + + // Check if we've consumed all content + if (line_start >= msg_end) { + break; + } + + // Search for next newlines only in remaining string + next_n = strchr(line_start, '\n'); + next_r = strchr(line_start, '\r'); + } + + // Terminate message with blank line + event_buffer_.append(CRLF_STR, CRLF_LEN); + } } - if (event_buffer_.empty()) { + if (event_buffer_.size() == static_cast(chunk_len_header_len)) { + // Nothing was added, reset buffer + event_buffer_.resize(0); return true; } - event_buffer_.append(CRLF_STR, CRLF_LEN); event_buffer_.append(CRLF_STR, CRLF_LEN); // chunk length header itself and the final chunk terminating CRLF are not counted as part of the chunk diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 5ec6fec009..5f9f598388 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -199,12 +199,15 @@ class AsyncWebServer { return *handler; } + httpd_handle_t get_server() { return this->server_; } + protected: uint16_t port_{}; httpd_handle_t server_{}; static esp_err_t request_handler(httpd_req_t *r); static esp_err_t request_post_handler(httpd_req_t *r); esp_err_t request_handler_(AsyncWebServerRequest *request) const; + static void safe_close_with_shutdown(httpd_handle_t hd, int sockfd); #ifdef USE_WEBSERVER_OTA esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type); #endif diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index ebe987cc65..3384a0572f 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -245,10 +245,8 @@ void WeikaiGPIOPin::setup() { this->pin_mode(this->flags_); } -std::string WeikaiGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "%u via WeiKai %s", this->pin_, this->parent_->get_name()); - return buffer; +size_t WeikaiGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via WeiKai %s", this->pin_, this->parent_->get_name()); } /////////////////////////////////////////////////////////////////////////////// diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 987278213a..a27c14106d 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -278,7 +278,7 @@ class WeikaiGPIOPin : public GPIOPin { gpio::Flags get_flags() const override { return this->flags_; } void setup() override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void pin_mode(gpio::Flags flags) override { this->parent_->set_pin_direction_(this->pin_, flags); } bool digital_read() override { return this->parent_->read_pin_val_(this->pin_) != this->inverted_; } void digital_write(bool value) override { this->parent_->write_pin_val_(this->pin_, value != this->inverted_); } diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index 1ac32f30da..6fe735362d 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -163,6 +163,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { } uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0}; + bool skip_footer = false; // Read all bytes. for (int i = 0; i < WHIRLPOOL_STATE_LENGTH; i++) { // Read bit @@ -170,6 +171,13 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_GAP)) return false; } + if (i == 14 && !data.is_valid()) { + // Remote control only sent 14 bytes, nothing more to read, not even the footer + ESP_LOGV(TAG, "Remote control only sent %d bytes", i); + skip_footer = true; + break; + } + for (int j = 0; j < 8; j++) { if (data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_ONE_SPACE)) { remote_state[i] |= 1 << j; @@ -183,7 +191,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { ESP_LOGVV(TAG, "Byte %d %02X", i, remote_state[i]); } // Validate footer - if (!data.expect_mark(WHIRLPOOL_BIT_MARK)) { + if (!data.expect_mark(WHIRLPOOL_BIT_MARK) && !skip_footer) { ESP_LOGV(TAG, "Footer fail"); return false; } @@ -196,7 +204,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { for (uint8_t i = 14; i < 20; i++) checksum20 ^= remote_state[i]; - if (checksum13 != remote_state[13] || checksum20 != remote_state[20]) { + if (checksum13 != remote_state[13] || (!skip_footer && checksum20 != remote_state[20])) { ESP_LOGVV(TAG, "Checksum fail"); return false; } diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 11bd7798e2..824944d4a2 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -64,11 +64,18 @@ _LOGGER = logging.getLogger(__name__) NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" CONF_MIN_AUTH_MODE = "min_auth_mode" +CONF_POST_CONNECT_ROAMING = "post_connect_roaming" # Maximum number of WiFi networks that can be configured # Limited to 127 because selected_sta_index_ is int8_t in C++ MAX_WIFI_NETWORKS = 127 +# Default AP timeout - allows sufficient time to try all BSSIDs during initial connection +# After AP starts, WiFi scanning is skipped to avoid disrupting the AP, so we only +# get best-effort connection attempts. Longer timeout ensures we exhaust all options +# before falling back to AP mode. Aligned with improv wifi_timeout default. +DEFAULT_AP_TIMEOUT = "90s" + wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") ManualIP = wifi_ns.struct("ManualIP") @@ -91,6 +98,7 @@ WIFI_MIN_AUTH_MODES = { VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True) WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) +WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition) WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action) WiFiConfigureAction = wifi_ns.class_( @@ -177,7 +185,7 @@ CONF_AP_TIMEOUT = "ap_timeout" WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend( { cv.Optional( - CONF_AP_TIMEOUT, default="1min" + CONF_AP_TIMEOUT, default=DEFAULT_AP_TIMEOUT ): cv.positive_time_period_milliseconds, } ) @@ -341,11 +349,8 @@ CONFIG_SCHEMA = cv.All( cv.boolean, cv.only_on_esp32 ), cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean, - cv.Optional("enable_mdns"): cv.invalid( - "This option has been removed. Please use the [disabled] option under the " - "new mdns component instead." - ), cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, + cv.Optional(CONF_POST_CONNECT_ROAMING, default=True): cv.boolean, cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True), cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation( single=True @@ -461,7 +466,7 @@ async def to_code(config): ) cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) cg.add_define("USE_WIFI_AP") - elif CORE.is_esp32 and CORE.using_esp_idf: + elif CORE.is_esp32 and not CORE.using_arduino: add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False) add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) @@ -479,11 +484,23 @@ async def to_code(config): cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE])) if config[CONF_FAST_CONNECT]: cg.add_define("USE_WIFI_FAST_CONNECT") - cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN])) + # passive_scan defaults to false in C++ - only set if true + if config[CONF_PASSIVE_SCAN]: + cg.add(var.set_passive_scan(True)) if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) + # enable_on_boot defaults to true in C++ - only set if false + if not config[CONF_ENABLE_ON_BOOT]: + cg.add(var.set_enable_on_boot(False)) - cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) + # post_connect_roaming defaults to true in C++ - disable if user disabled it + # or if 802.11k/v is enabled (driver handles roaming natively) + if ( + not config[CONF_POST_CONNECT_ROAMING] + or config.get(CONF_ENABLE_BTM) + or config.get(CONF_ENABLE_RRM) + ): + cg.add(var.set_post_connect_roaming(False)) if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) @@ -503,7 +520,7 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) # Apply high performance WiFi settings if high performance networking is enabled - if CORE.is_esp32 and CORE.using_esp_idf and has_high_performance_networking(): + if CORE.is_esp32 and has_high_performance_networking(): # Check if PSRAM is guaranteed (set by psram component during final validation) psram_guaranteed = psram_is_guaranteed() @@ -581,6 +598,11 @@ async def wifi_enabled_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) +@automation.register_condition("wifi.ap_active", WiFiAPActiveCondition, cv.Schema({})) +async def wifi_ap_active_to_code(config, condition_id, template_arg, args): + return cg.new_Pvariable(condition_id, template_arg) + + @automation.register_action("wifi.enable", WiFiEnableAction, cv.Schema({})) async def wifi_enable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) @@ -592,6 +614,8 @@ async def wifi_disable_to_code(config, action_id, template_arg, args): KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" +RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" +WIFI_LISTENERS_KEY = "wifi_listeners" def request_wifi_scan_results(): @@ -604,13 +628,41 @@ def request_wifi_scan_results(): CORE.data[KEEP_SCAN_RESULTS_KEY] = True +def enable_runtime_power_save_control(): + """Enable runtime WiFi power save control. + + Components that need to dynamically switch WiFi power saving on/off for latency + performance (e.g., audio streaming, large data transfers) should call this + function during their code generation. This enables the request_high_performance() + and release_high_performance() APIs. + + Only supported on ESP32. + """ + CORE.data[RUNTIME_POWER_SAVE_KEY] = True + + +def request_wifi_listeners() -> None: + """Request that WiFi state listeners be compiled in. + + Components that need to be notified about WiFi state changes (IP address changes, + scan results, connection state) should call this function during their code generation. + This enables the add_ip_state_listener(), add_scan_results_listener(), + and add_connect_state_listener() APIs. + """ + CORE.data[WIFI_LISTENERS_KEY] = True + + @coroutine_with_priority(CoroPriority.FINAL) async def final_step(): - """Final code generation step to configure scan result retention.""" + """Final code generation step to configure optional WiFi features.""" if CORE.data.get(KEEP_SCAN_RESULTS_KEY, False): cg.add( cg.RawExpression("wifi::global_wifi_component->set_keep_scan_results(true)") ) + if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False): + cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE") + if CORE.data.get(WIFI_LISTENERS_KEY, False): + cg.add_define("USE_WIFI_LISTENERS") @automation.register_action( diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h new file mode 100644 index 0000000000..fb0e71bcf6 --- /dev/null +++ b/esphome/components/wifi/automation.h @@ -0,0 +1,118 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_WIFI +#include "wifi_component.h" + +namespace esphome::wifi { + +template class WiFiConnectedCondition : public Condition { + public: + bool check(const Ts &...x) override { return global_wifi_component->is_connected(); } +}; + +template class WiFiEnabledCondition : public Condition { + public: + bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); } +}; + +template class WiFiAPActiveCondition : public Condition { + public: + bool check(const Ts &...x) override { return global_wifi_component->is_ap_active(); } +}; + +template class WiFiEnableAction : public Action { + public: + void play(const Ts &...x) override { global_wifi_component->enable(); } +}; + +template class WiFiDisableAction : public Action { + public: + void play(const Ts &...x) override { global_wifi_component->disable(); } +}; + +template class WiFiConfigureAction : public Action, public Component { + public: + TEMPLATABLE_VALUE(std::string, ssid) + TEMPLATABLE_VALUE(std::string, password) + TEMPLATABLE_VALUE(bool, save) + TEMPLATABLE_VALUE(uint32_t, connection_timeout) + + void play(const Ts &...x) override { + auto ssid = this->ssid_.value(x...); + auto password = this->password_.value(x...); + // Avoid multiple calls + if (this->connecting_) + return; + // If already connected to the same AP, do nothing + char ssid_buf[SSID_BUFFER_SIZE]; + if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), ssid.c_str()) == 0) { + // Callback to notify the user that the connection was successful + this->connect_trigger_->trigger(); + return; + } + // Create a new WiFiAP object with the new SSID and password + this->new_sta_.set_ssid(ssid); + this->new_sta_.set_password(password); + // Save the current STA + this->old_sta_ = global_wifi_component->get_sta(); + // Disable WiFi + global_wifi_component->disable(); + // Set the state to connecting + this->connecting_ = true; + // Store the new STA so once the WiFi is enabled, it will connect to it + // This is necessary because the WiFiComponent will raise an error and fallback to the saved STA + // if trying to connect to a new STA while already connected to another one + if (this->save_.value(x...)) { + global_wifi_component->save_wifi_sta(new_sta_.get_ssid(), new_sta_.get_password()); + } else { + global_wifi_component->set_sta(new_sta_); + } + // Enable WiFi + global_wifi_component->enable(); + // Set timeout for the connection + this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() { + // If the timeout is reached, stop connecting and revert to the old AP + global_wifi_component->disable(); + global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password()); + global_wifi_component->enable(); + // Start a timeout for the fallback if the connection to the old AP fails + this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() { + this->connecting_ = false; + this->error_trigger_->trigger(); + }); + }); + } + + Trigger<> *get_connect_trigger() const { return this->connect_trigger_; } + Trigger<> *get_error_trigger() const { return this->error_trigger_; } + + void loop() override { + if (!this->connecting_) + return; + if (global_wifi_component->is_connected()) { + // The WiFi is connected, stop the timeout and reset the connecting flag + this->cancel_timeout("wifi-connect-timeout"); + this->cancel_timeout("wifi-fallback-timeout"); + this->connecting_ = false; + char ssid_buf[SSID_BUFFER_SIZE]; + if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), this->new_sta_.get_ssid().c_str()) == 0) { + // Callback to notify the user that the connection was successful + this->connect_trigger_->trigger(); + } else { + // Callback to notify the user that the connection failed + this->error_trigger_->trigger(); + } + } + } + + protected: + bool connecting_{false}; + WiFiAP new_sta_; + WiFiAP old_sta_; + Trigger<> *connect_trigger_{new Trigger<>()}; + Trigger<> *error_trigger_{new Trigger<>()}; +}; + +} // namespace esphome::wifi +#endif diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 51a5a47323..6654474329 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -2,6 +2,7 @@ #ifdef USE_WIFI #include #include +#include #ifdef USE_ESP32 #if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1) @@ -27,6 +28,7 @@ #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/string_ref.h" #include "esphome/core/util.h" #ifdef USE_CAPTIVE_PORTAL @@ -37,8 +39,7 @@ #include "esphome/components/esp32_improv/esp32_improv_component.h" #endif -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi"; @@ -143,6 +144,56 @@ static const char *const TAG = "wifi"; /// - Networks not in scan results → Tried in RETRY_HIDDEN phase /// - Networks visible in scan + not marked hidden → Skipped in RETRY_HIDDEN phase /// - Networks marked 'hidden: true' always use hidden mode, even if broadcasting SSID +/// +/// ┌──────────────────────────────────────────────────────────────────────┐ +/// │ Post-Connect Roaming (for stationary devices) │ +/// ├──────────────────────────────────────────────────────────────────────┤ +/// │ Purpose: Handle AP reboot or power loss scenarios where device │ +/// │ connects to suboptimal AP and never switches back │ +/// │ │ +/// │ Loop call site: roaming enabled && attempts < 3 && 5 min elapsed │ +/// │ ↓ │ +/// │ ┌─────────────────┐ Hidden? ┌──────────────────────────┐ │ +/// │ │ check_roaming_ ├───────────→│ attempts = MAX, stop │ │ +/// │ └────────┬────────┘ └──────────────────────────┘ │ +/// │ ↓ │ +/// │ attempts++, update last_check │ +/// │ ↓ │ +/// │ RSSI > -49 dBm? ────Yes────→ Skip scan (excellent signal)─┐ │ +/// │ ↓ No │ │ +/// │ ┌─────────────────┐ │ │ +/// │ │ Start scan │ │ │ +/// │ └────────┬────────┘ │ │ +/// │ ↓ │ │ +/// │ ┌────────────────────────┐ │ │ +/// │ │ process_roaming_scan_ │ │ │ +/// │ └────────┬───────────────┘ │ │ +/// │ ↓ │ │ +/// │ ┌─────────────────┐ No ┌───────────────┐ │ │ +/// │ │ +10 dB better AP├────────→│ Stay connected│───────────────┤ │ +/// │ └────────┬────────┘ └───────────────┘ │ │ +/// │ │ Yes │ │ +/// │ ↓ │ │ +/// │ ┌─────────────────┐ │ │ +/// │ │ start_connecting│ (roaming_connect_active_ = true) │ │ +/// │ └────────┬────────┘ │ │ +/// │ ↓ │ │ +/// │ ┌────┴────┐ │ │ +/// │ ↓ ↓ │ │ +/// │ ┌───────┐ ┌───────┐ │ │ +/// │ │SUCCESS│ │FAILED │ │ │ +/// │ └───┬───┘ └───┬───┘ │ │ +/// │ ↓ ↓ │ │ +/// │ Keep counter retry_connect() → normal reconnect flow │ │ +/// │ (no reset) (keeps counter, handles retries) │ │ +/// │ │ │ │ │ +/// │ └──────────────┴────────────────────────────────────────┘ │ +/// │ │ +/// │ After 3 checks: attempts >= 3, stop checking │ +/// │ Non-roaming disconnect: clear_roaming_state_() resets counter │ +/// │ Roaming success: counter preserved (prevents ping-pong) │ +/// │ Roaming fail: normal flow handles reconnection, counter preserved │ +/// └──────────────────────────────────────────────────────────────────────┘ static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) { switch (phase) { @@ -199,7 +250,27 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1; /// Cooldown duration in milliseconds after adapter restart or repeated failures /// Allows WiFi hardware to stabilize before next connection attempt -static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 1000; +static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500; + +/// Cooldown duration when fallback AP is active and captive portal may be running +/// Longer interval gives users time to configure WiFi without constant connection attempts +/// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown +static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000; + +/// Timeout for WiFi scan operations +/// This is a fallback in case we don't receive a scan done callback from the WiFi driver. +/// Normal scans complete via callback; this only triggers if something goes wrong. +static constexpr uint32_t WIFI_SCAN_TIMEOUT_MS = 31000; + +/// Timeout for WiFi connection attempts +/// This is a fallback in case we don't receive connection success/failure callbacks. +/// Some platforms (especially LibreTiny/Beken) can take 30-60 seconds to connect, +/// particularly with fast_connect enabled where no prior scan provides channel info. +/// Do not lower this value - connection failures are detected via callbacks, not timeout. +/// If this timeout fires prematurely while a connection is still in progress, it causes +/// cascading failures: the subsequent scan will also fail because the WiFi driver is +/// still busy with the previous connection attempt. +static constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 46000; static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { switch (phase) { @@ -275,7 +346,9 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { } } - if (!this->ssid_was_seen_in_scan_(sta.get_ssid())) { + // If we didn't scan this cycle, treat all networks as potentially hidden + // Otherwise, only retry networks that weren't seen in the scan + if (!this->did_scan_this_cycle_ || !this->ssid_was_seen_in_scan_(sta.get_ssid())) { ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); return static_cast(i); } @@ -295,7 +368,6 @@ void WiFiComponent::start_initial_connection_() { WiFiAP params = this->build_params_for_current_phase_(); this->start_connecting(params); } else { - ESP_LOGI(TAG, "Starting scan"); this->start_scanning(); } } @@ -323,6 +395,19 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { this->wifi_pre_setup_(); + +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + // Create semaphore for high-performance mode requests + // Start at 0, increment on request, decrement on release + this->high_performance_semaphore_ = xSemaphoreCreateCounting(UINT32_MAX, 0); + if (this->high_performance_semaphore_ == nullptr) { + ESP_LOGE(TAG, "Failed semaphore"); + } + + // Store the configured power save mode as baseline + this->configured_power_save_ = this->power_save_; +#endif + if (this->enable_on_boot_) { this->start(); } else { @@ -334,13 +419,10 @@ void WiFiComponent::setup() { } void WiFiComponent::start() { - ESP_LOGCONFIG(TAG, - "Starting\n" - " Local MAC: %s", - get_mac_address_pretty().c_str()); + ESP_LOGCONFIG(TAG, "Starting"); this->last_connected_ = millis(); - uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL; + uint32_t hash = this->has_sta() ? App.get_config_version_hash() : 88491487UL; this->pref_ = global_preferences->make_preference(hash, true); #ifdef USE_WIFI_FAST_CONNECT @@ -359,10 +441,23 @@ void WiFiComponent::start() { if (this->has_sta()) { this->wifi_sta_pre_setup_(); - if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { + if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) { ESP_LOGV(TAG, "Setting Output Power Option failed"); } +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + // Synchronize power_save_ with semaphore state before applying + if (this->high_performance_semaphore_ != nullptr) { + UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_); + if (semaphore_count > 0) { + this->power_save_ = WIFI_POWER_SAVE_NONE; + this->is_high_performance_mode_ = true; + } else { + this->power_save_ = this->configured_power_save_; + this->is_high_performance_mode_ = false; + } + } +#endif if (!this->wifi_apply_power_save_()) { ESP_LOGV(TAG, "Setting Power Save Option failed"); } @@ -393,7 +488,7 @@ void WiFiComponent::start() { #ifdef USE_WIFI_AP } else if (this->has_ap()) { this->setup_ap_config_(); - if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { + if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) { ESP_LOGV(TAG, "Setting Output Power Option failed"); } #ifdef USE_CAPTIVE_PORTAL @@ -417,10 +512,6 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); - // Enter cooldown state to allow WiFi hardware to stabilize after restart - // Don't set retry_phase_ or num_retried_ here - state machine handles transitions - this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; - this->action_started_ = millis(); this->error_from_callback_ = false; } @@ -441,12 +532,21 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); - if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) { + // Skip cooldown if new credentials were provided while connecting + if (this->skip_cooldown_next_cycle_) { + this->skip_cooldown_next_cycle_ = false; + this->check_connecting_finished(now); + break; + } + // Use longer cooldown when captive portal/improv is active to avoid disrupting user config + bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_(); + uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS; + if (now - this->action_started_ > cooldown_duration) { // After cooldown we either restarted the adapter because of // a failure, or something tried to connect over and over // so we entered cooldown. In both cases we call // check_connecting_finished to continue the state machine. - this->check_connecting_finished(); + this->check_connecting_finished(now); } break; } @@ -457,7 +557,7 @@ void WiFiComponent::loop() { } case WIFI_COMPONENT_STATE_STA_CONNECTING: { this->status_set_warning(LOG_STR("associating to network")); - this->check_connecting_finished(); + this->check_connecting_finished(now); break; } @@ -471,6 +571,19 @@ void WiFiComponent::loop() { } else { this->status_clear_warning(); this->last_connected_ = now; + + // Post-connect roaming: check for better AP + if (this->post_connect_roaming_) { + if (this->roaming_scan_active_) { + if (this->scan_done_) { + this->process_roaming_scan_(); + } + // else: scan in progress, wait + } else if (this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS && + now - this->roaming_last_check_ >= ROAMING_CHECK_INTERVAL) { + this->check_roaming_(now); + } + } } break; } @@ -495,7 +608,8 @@ void WiFiComponent::loop() { #endif // USE_WIFI_AP #ifdef USE_IMPROV - if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { + if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active() && + !esp32_improv::global_improv_component->should_start()) { if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) { if (this->wifi_mode_(true, {})) esp32_improv::global_improv_component->start(); @@ -511,11 +625,37 @@ void WiFiComponent::loop() { } } } + +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + // Check if power save mode needs to be updated based on high-performance requests + if (this->high_performance_semaphore_ != nullptr) { + // Semaphore count directly represents active requests (starts at 0, increments on request) + UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_); + + if (semaphore_count > 0 && !this->is_high_performance_mode_) { + // Transition to high-performance mode (no power save) + ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count, + semaphore_count == 1 ? "request" : "requests"); + this->power_save_ = WIFI_POWER_SAVE_NONE; + if (this->wifi_apply_power_save_()) { + this->is_high_performance_mode_ = true; + } + } else if (semaphore_count == 0 && this->is_high_performance_mode_) { + // Restore to configured power save mode + ESP_LOGV(TAG, "Restoring power save mode to configured setting"); + this->power_save_ = this->configured_power_save_; + if (this->wifi_apply_power_save_()) { + this->is_high_performance_mode_ = false; + } + } + } +#endif } WiFiComponent::WiFiComponent() { global_wifi_component = this; } bool WiFiComponent::has_ap() const { return this->has_ap_; } +bool WiFiComponent::is_ap_active() const { return this->ap_started_; } bool WiFiComponent::has_sta() const { return !this->sta_.empty(); } #ifdef USE_WIFI_11KV_SUPPORT void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } @@ -563,23 +703,26 @@ void WiFiComponent::setup_ap_config_() { } this->ap_setup_ = this->wifi_start_ap_(this->ap_); - auto ip_address = this->wifi_soft_ap_ip().str(); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, "Setting up AP:\n" " AP SSID: '%s'\n" " AP Password: '%s'\n" " IP Address: %s", - this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str()); + this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), this->wifi_soft_ap_ip().str_to(ip_buf)); #ifdef USE_WIFI_MANUAL_IP auto manual_ip = this->ap_.get_manual_ip(); if (manual_ip.has_value()) { + char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " AP Static IP: '%s'\n" " AP Gateway: '%s'\n" " AP Subnet: '%s'", - manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(), - manual_ip->subnet.str().c_str()); + manual_ip->static_ip.str_to(static_ip_buf), manual_ip->gateway.str_to(gateway_buf), + manual_ip->subnet.str_to(subnet_buf)); } #endif @@ -600,11 +743,19 @@ float WiFiComponent::get_loop_priority() const { void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); } void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } +void WiFiComponent::clear_sta() { + // Clear roaming state - no more configured networks + this->clear_roaming_state_(); + this->sta_.clear(); + this->selected_sta_index_ = -1; +} void WiFiComponent::set_sta(const WiFiAP &ap) { - this->clear_sta(); + this->clear_sta(); // Also clears roaming state this->init_sta(1); this->add_sta(ap); this->selected_sta_index_ = 0; + // When new credentials are set (e.g., from improv), skip cooldown to retry immediately + this->skip_cooldown_next_cycle_ = true; } WiFiAP WiFiComponent::build_params_for_current_phase_() { @@ -631,8 +782,8 @@ WiFiAP WiFiComponent::build_params_for_current_phase_() { case WiFiRetryPhase::RETRY_HIDDEN: // Hidden network mode: clear BSSID/channel to trigger probe request // (both explicit hidden and retry hidden use same behavior) - params.set_bssid(optional{}); - params.set_channel(optional{}); + params.clear_bssid(); + params.clear_channel(); break; case WiFiRetryPhase::SCAN_CONNECTING: @@ -666,6 +817,17 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa sta.set_ssid(ssid); sta.set_password(password); this->set_sta(sta); + + // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected) + this->connect_soon_(); +} + +void WiFiComponent::connect_soon_() { + // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing + if (this->state_ == WIFI_COMPONENT_STATE_COOLDOWN) { + ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials"); + this->retry_connect(); + } } void WiFiComponent::start_connecting(const WiFiAP &ap) { @@ -673,21 +835,22 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { char bssid_s[18]; int8_t priority = 0; - if (ap.get_bssid().has_value()) { - format_mac_addr_upper(ap.get_bssid().value().data(), bssid_s); - priority = this->get_sta_priority(ap.get_bssid().value()); + if (ap.has_bssid()) { + format_mac_addr_upper(ap.get_bssid().data(), bssid_s); + priority = this->get_sta_priority(ap.get_bssid()); } ESP_LOGI(TAG, "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...", - ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_s : LOG_STR_LITERAL("any"), priority, - this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), - LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); + ap.get_ssid().c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1, + get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); #ifdef ESPHOME_LOG_HAS_VERBOSE - ESP_LOGV(TAG, "Connection Params:"); - ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str()); - if (ap.get_bssid().has_value()) { + ESP_LOGV(TAG, + "Connection Params:\n" + " SSID: '%s'", + ap.get_ssid().c_str()); + if (ap.has_bssid()) { ESP_LOGV(TAG, " BSSID: %s", bssid_s); } else { ESP_LOGV(TAG, " BSSID: Not Set"); @@ -695,36 +858,50 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { #ifdef USE_WIFI_WPA2_EAP if (ap.get_eap().has_value()) { - ESP_LOGV(TAG, " WPA2 Enterprise authentication configured:"); EAPAuth eap_config = ap.get_eap().value(); - ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str()); - ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str()); - ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str()); + // clang-format off + ESP_LOGV( + TAG, + " WPA2 Enterprise authentication configured:\n" + " Identity: " LOG_SECRET("'%s'") "\n" + " Username: " LOG_SECRET("'%s'") "\n" + " Password: " LOG_SECRET("'%s'"), + eap_config.identity.c_str(), eap_config.username.c_str(), eap_config.password.c_str()); + // clang-format on #if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2)); #endif bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert); bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert); bool client_key_present = eap_config.client_key != nullptr && strlen(eap_config.client_key); - ESP_LOGV(TAG, " CA Cert: %s", ca_cert_present ? "present" : "not present"); - ESP_LOGV(TAG, " Client Cert: %s", client_cert_present ? "present" : "not present"); - ESP_LOGV(TAG, " Client Key: %s", client_key_present ? "present" : "not present"); + ESP_LOGV(TAG, + " CA Cert: %s\n" + " Client Cert: %s\n" + " Client Key: %s", + ca_cert_present ? "present" : "not present", client_cert_present ? "present" : "not present", + client_key_present ? "present" : "not present"); } else { #endif ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); #ifdef USE_WIFI_WPA2_EAP } #endif - if (ap.get_channel().has_value()) { - ESP_LOGV(TAG, " Channel: %u", *ap.get_channel()); + if (ap.has_channel()) { + ESP_LOGV(TAG, " Channel: %u", ap.get_channel()); } else { ESP_LOGV(TAG, " Channel not set"); } #ifdef USE_WIFI_MANUAL_IP if (ap.get_manual_ip().has_value()) { ManualIP m = *ap.get_manual_ip(); - ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str().c_str(), - m.gateway.str().c_str(), m.subnet.str().c_str(), m.dns1.str().c_str(), m.dns2.str().c_str()); + char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str_to(static_ip_buf), + m.gateway.str_to(gateway_buf), m.subnet.str_to(subnet_buf), m.dns1.str_to(dns1_buf), + m.dns2.str_to(dns2_buf)); } else #endif { @@ -745,14 +922,6 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { } const LogString *get_signal_bars(int8_t rssi) { - // Check for disconnected sentinel value first - if (rssi == WIFI_RSSI_DISCONNECTED) { - // MULTIPLICATION SIGN - // Unicode: U+00D7, UTF-8: C3 97 - return LOG_STR("\033[0;31m" // red - "\xc3\x97\xc3\x97\xc3\x97\xc3\x97" - "\033[0m"); - } // LOWER ONE QUARTER BLOCK // Unicode: U+2582, UTF-8: E2 96 82 // LOWER HALF BLOCK @@ -797,36 +966,40 @@ const LogString *get_signal_bars(int8_t rssi) { void WiFiComponent::print_connect_params_() { bssid_t bssid = wifi_bssid(); - char bssid_s[18]; + char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; format_mac_addr_upper(bssid.data(), bssid_s); - - ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); - if (this->is_disabled()) { - ESP_LOGCONFIG(TAG, " Disabled"); - return; - } + // Use stack buffers for IP address formatting to avoid heap allocations + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; for (auto &ip : wifi_sta_ip_addresses()) { if (ip.is_set()) { - ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str().c_str()); + ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str_to(ip_buf)); } } int8_t rssi = wifi_rssi(); + // Use stack buffers for SSID and all IP addresses to avoid heap allocations + char ssid_buf[SSID_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; + // clang-format off ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'") "\n" - " BSSID: " LOG_SECRET("%s") "\n" - " Hostname: '%s'\n" - " Signal strength: %d dB %s\n" - " Channel: %" PRId32 "\n" - " Subnet: %s\n" - " Gateway: %s\n" - " DNS1: %s\n" - " DNS2: %s", - wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), - get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), - wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); + " BSSID: " LOG_SECRET("%s") "\n" + " Hostname: '%s'\n" + " Signal strength: %d dB %s\n" + " Channel: %" PRId32 "\n" + " Subnet: %s\n" + " Gateway: %s\n" + " DNS1: %s\n" + " DNS2: %s", + wifi_ssid_to(ssid_buf), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), + get_wifi_channel(), wifi_subnet_mask_().str_to(subnet_buf), wifi_gateway_ip_().str_to(gateway_buf), + wifi_dns_ip_(0).str_to(dns1_buf), wifi_dns_ip_(1).str_to(dns2_buf)); + // clang-format on #ifdef ESPHOME_LOG_HAS_VERBOSE - if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) { - ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid())); + if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) { + ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(config->get_bssid())); } #endif #ifdef USE_WIFI_11KV_SUPPORT @@ -937,32 +1110,52 @@ template static void insertion_sort_scan_results(VectorType } } -// Helper function to log scan results - marked noinline to prevent re-inlining into loop +// Helper function to log matching scan results - marked noinline to prevent re-inlining into loop +// +// IMPORTANT: This function deliberately uses a SINGLE log call to minimize blocking. +// In environments with many matching networks (e.g., 18+ mesh APs), multiple log calls +// per network would block the main loop for an unacceptable duration. Each log call +// has overhead from UART transmission, so combining INFO+DEBUG into one line halves +// the blocking time. Do NOT split this into separate ESP_LOGI/ESP_LOGD calls. __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) { char bssid_s[18]; auto bssid = res.get_bssid(); format_mac_addr_upper(bssid.data(), bssid_s); - if (res.get_matches()) { - ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), - res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s, - LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority()); - } else { - ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, - LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + // Single combined log line with all details when DEBUG enabled + ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s Ch:%2u %3ddB P:%d", res.get_ssid().c_str(), + res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s, + LOG_STR_ARG(get_signal_bars(res.get_rssi())), res.get_channel(), res.get_rssi(), res.get_priority()); +#else + ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), + res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s, + LOG_STR_ARG(get_signal_bars(res.get_rssi()))); +#endif } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE +// Helper function to log non-matching scan results at verbose level +__attribute__((noinline)) static void log_scan_result_non_matching(const WiFiScanResult &res) { + char bssid_s[18]; + auto bssid = res.get_bssid(); + format_mac_addr_upper(bssid.data(), bssid_s); + + ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, + LOG_STR_ARG(get_signal_bars(res.get_rssi()))); +} +#endif + void WiFiComponent::check_scanning_finished() { if (!this->scan_done_) { - if (millis() - this->action_started_ > 30000) { + if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) { ESP_LOGE(TAG, "Scan timeout"); this->retry_connect(); } return; } this->scan_done_ = false; + this->did_scan_this_cycle_ = true; if (this->scan_result_.empty()) { ESP_LOGW(TAG, "No networks found"); @@ -989,8 +1182,20 @@ void WiFiComponent::check_scanning_finished() { // Sort scan results using insertion sort for better memory efficiency insertion_sort_scan_results(this->scan_result_); + size_t non_matching_count = 0; for (auto &res : this->scan_result_) { - log_scan_result(res); + if (res.get_matches()) { + log_scan_result(res); + } else { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + log_scan_result_non_matching(res); +#else + non_matching_count++; +#endif + } + } + if (non_matching_count > 0) { + ESP_LOGD(TAG, "- %zu non-matching (VERBOSE to show)", non_matching_count); } // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ @@ -1034,18 +1239,27 @@ void WiFiComponent::check_scanning_finished() { } void WiFiComponent::dump_config() { + char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, "WiFi:\n" + " Local MAC: %s\n" " Connected: %s", - YESNO(this->is_connected())); - this->print_connect_params_(); + get_mac_address_pretty_into_buffer(mac_s), YESNO(this->is_connected())); + if (this->is_disabled()) { + ESP_LOGCONFIG(TAG, " Disabled"); + return; + } + if (this->is_connected()) { + this->print_connect_params_(); + } } -void WiFiComponent::check_connecting_finished() { +void WiFiComponent::check_connecting_finished(uint32_t now) { auto status = this->wifi_sta_connect_status_(); if (status == WiFiSTAConnectStatus::CONNECTED) { - if (wifi_ssid().empty()) { + char ssid_buf[SSID_BUFFER_SIZE]; + if (wifi_ssid_to(ssid_buf)[0] == '\0') { ESP_LOGW(TAG, "Connection incomplete"); this->retry_connect(); return; @@ -1067,8 +1281,6 @@ void WiFiComponent::check_connecting_finished() { // the first connection as a failure. this->error_from_callback_ = false; - this->print_connect_params_(); - if (this->has_ap()) { #ifdef USE_CAPTIVE_PORTAL if (this->is_captive_portal_active_()) { @@ -1086,26 +1298,33 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; + this->print_connect_params_(); - // Clear priority tracking if all priorities are at minimum - this->clear_priorities_if_all_min_(); + // Reset roaming state on successful connection + this->roaming_last_check_ = now; + // Only reset attempts if this wasn't a roaming-triggered connection + // (prevents ping-pong between APs) + if (!this->roaming_connect_active_) { + this->roaming_attempts_ = 0; + } + this->roaming_connect_active_ = false; + + // Clear all priority penalties - the next reconnect will happen when an AP disconnects, + // which means the landscape has likely changed and previous tracked failures are stale + this->clear_all_bssid_priorities_(); #ifdef USE_WIFI_FAST_CONNECT this->save_fast_connect_settings_(); #endif - // Free scan results memory unless a component needs them - if (!this->keep_scan_results_) { - this->scan_result_.clear(); - this->scan_result_.shrink_to_fit(); - } + this->release_scan_results_(); return; } - uint32_t now = millis(); - if (now - this->action_started_ > 30000) { - ESP_LOGW(TAG, "Connection timeout"); + if (now - this->action_started_ > WIFI_CONNECT_TIMEOUT_MS) { + ESP_LOGW(TAG, "Connection timeout, aborting connection attempt"); + this->wifi_disconnect_(); this->retry_connect(); return; } @@ -1184,8 +1403,8 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { } case WiFiRetryPhase::SCAN_CONNECTING: - // If scan found no matching networks, skip to hidden network mode - if (!this->scan_result_.empty() && !this->scan_result_[0].get_matches()) { + // If scan found no networks or no matching networks, skip to hidden network mode + if (this->scan_result_.empty() || !this->scan_result_[0].get_matches()) { return WiFiRetryPhase::RETRY_HIDDEN; } @@ -1229,9 +1448,16 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::RESTARTING_ADAPTER; case WiFiRetryPhase::RESTARTING_ADAPTER: - // After restart, go back to explicit hidden if we went through it initially, otherwise scan - return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN - : WiFiRetryPhase::SCAN_CONNECTING; + // After restart, go back to explicit hidden if we went through it initially + if (this->went_through_explicit_hidden_phase_()) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } + // Skip scanning when captive portal/improv is active to avoid disrupting AP + // Even passive scans can cause brief AP disconnections on ESP32 + if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { + return WiFiRetryPhase::RETRY_HIDDEN; + } + return WiFiRetryPhase::SCAN_CONNECTING; } // Should never reach here @@ -1318,7 +1544,17 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { // without disrupting the captive portal/improv connection if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { this->restart_adapter(); + } else { + // Even when skipping full restart, disconnect to clear driver state + // Without this, platforms like LibreTiny may think we're still connecting + this->wifi_disconnect_(); } + // Clear scan flag - we're starting a new retry cycle + this->did_scan_this_cycle_ = false; + // Always enter cooldown after restart (or skip-restart) to allow stabilization + // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting return true; @@ -1329,9 +1565,15 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { return false; // Did not start scan, can proceed with connection } +void WiFiComponent::clear_all_bssid_priorities_() { + if (!this->sta_priorities_.empty()) { + decltype(this->sta_priorities_)().swap(this->sta_priorities_); + } +} + /// Clear BSSID priority tracking if all priorities are at minimum (saves memory) /// At minimum priority, all BSSIDs are equally bad, so priority tracking is useless -/// Called after successful connection or after failed connection attempts +/// Called after failed connection attempts void WiFiComponent::clear_priorities_if_all_min_() { if (this->sta_priorities_.empty()) { return; @@ -1353,8 +1595,7 @@ void WiFiComponent::clear_priorities_if_all_min_() { // All priorities are at minimum - clear the vector to save memory and reset ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)"); - this->sta_priorities_.clear(); - this->sta_priorities_.shrink_to_fit(); + this->clear_all_bssid_priorities_(); } /// Log failed connection attempt and decrease BSSID priority to avoid repeated failures @@ -1382,21 +1623,21 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { // Scan-based phase: always use best result (index 0) failed_bssid = this->scan_result_[0].get_bssid(); - } else if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) { + } else if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) { // Config has specific BSSID (fast_connect or user-specified) - failed_bssid = *config->get_bssid(); + failed_bssid = config->get_bssid(); } if (!failed_bssid.has_value()) { return; // No BSSID to penalize } - // Get SSID for logging - std::string ssid; + // Get SSID for logging (use pointer to avoid copy) + const std::string *ssid = nullptr; if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { - ssid = this->scan_result_[0].get_ssid(); + ssid = &this->scan_result_[0].get_ssid(); } else if (const WiFiAP *config = this->get_selected_sta_()) { - ssid = config->get_ssid(); + ssid = &config->get_ssid(); } // Only decrease priority on the last attempt for this phase @@ -1416,8 +1657,8 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { } char bssid_s[18]; format_mac_addr_upper(failed_bssid.value().data(), bssid_s); - ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s, - old_priority, new_priority); + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", + ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority); // After adjusting priority, check if all priorities are now at minimum // If so, clear the vector to save memory and reset for fresh start @@ -1492,6 +1733,17 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() { } void WiFiComponent::retry_connect() { + // If this was a roaming attempt, preserve roaming_attempts_ count + // (so we stop roaming after ROAMING_MAX_ATTEMPTS failures) + // Otherwise reset all roaming state + if (this->roaming_connect_active_) { + this->roaming_connect_active_ = false; + this->roaming_scan_active_ = false; + // Keep roaming_attempts_ - will prevent further roaming after max failures + } else { + this->clear_roaming_state_(); + } + this->log_and_adjust_priority_for_failed_connect_(); // Determine next retry phase based on current state @@ -1519,12 +1771,31 @@ void WiFiComponent::retry_connect() { } } +#ifdef USE_RP2040 +// RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart +// mDNS when the network interface reconnects. However, this callback is disabled +// in the arduino-pico framework. As a workaround, we block component setup until +// WiFi is connected, ensuring mDNS.begin() is called with an active connection. + +bool WiFiComponent::can_proceed() { + if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { + return true; + } + return this->is_connected(); +} +#endif + void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool WiFiComponent::is_connected() { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; } -void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; } +void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { + this->power_save_ = power_save; +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + this->configured_power_save_ = power_save; +#endif +} void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; } @@ -1543,6 +1814,38 @@ bool WiFiComponent::is_esp32_improv_active_() { #endif } +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) +bool WiFiComponent::request_high_performance() { + // Already configured for high performance - request satisfied + if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) { + return true; + } + + // Semaphore initialization failed + if (this->high_performance_semaphore_ == nullptr) { + return false; + } + + // Give the semaphore (non-blocking). This increments the count. + return xSemaphoreGive(this->high_performance_semaphore_) == pdTRUE; +} + +bool WiFiComponent::release_high_performance() { + // Already configured for high performance - nothing to release + if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) { + return true; + } + + // Semaphore initialization failed + if (this->high_performance_semaphore_ == nullptr) { + return false; + } + + // Take the semaphore (non-blocking). This decrements the count. + return xSemaphoreTake(this->high_performance_semaphore_, 0) == pdTRUE; +} +#endif // USE_ESP32 && USE_WIFI_RUNTIME_POWER_SAVE + #ifdef USE_WIFI_FAST_CONNECT bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { SavedWifiFastConnectSettings fast_connect_save{}; @@ -1601,24 +1904,27 @@ void WiFiComponent::save_fast_connect_settings_() { #endif void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } -void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } -void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } +void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; } +void WiFiAP::clear_bssid() { this->bssid_ = {}; } void WiFiAP::set_password(const std::string &password) { this->password_ = password; } #ifdef USE_WIFI_WPA2_EAP void WiFiAP::set_eap(optional eap_auth) { this->eap_ = std::move(eap_auth); } #endif -void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } +void WiFiAP::set_channel(uint8_t channel) { this->channel_ = channel; } +void WiFiAP::clear_channel() { this->channel_ = 0; } #ifdef USE_WIFI_MANUAL_IP void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } #endif void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } -const optional &WiFiAP::get_bssid() const { return this->bssid_; } +const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; } +bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; } const std::string &WiFiAP::get_password() const { return this->password_; } #ifdef USE_WIFI_WPA2_EAP const optional &WiFiAP::get_eap() const { return this->eap_; } #endif -const optional &WiFiAP::get_channel() const { return this->channel_; } +uint8_t WiFiAP::get_channel() const { return this->channel_; } +bool WiFiAP::has_channel() const { return this->channel_ != 0; } #ifdef USE_WIFI_MANUAL_IP const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } #endif @@ -1646,7 +1952,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { // network is configured without SSID - match other settings } // If BSSID configured, only match for correct BSSIDs - if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_) + if (config.has_bssid() && config.get_bssid() != this->bssid_) return false; #ifdef USE_WIFI_WPA2_EAP @@ -1664,7 +1970,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) const { #endif // If channel configured, only match networks on that channel. - if (config.get_channel().has_value() && *config.get_channel() != this->channel_) { + if (config.has_channel() && config.get_channel() != this->channel_) { return false; } return true; @@ -1680,8 +1986,107 @@ bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; } bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->bssid_ == rhs.bssid_; } +void WiFiComponent::clear_roaming_state_() { + this->roaming_attempts_ = 0; + this->roaming_last_check_ = 0; + this->roaming_scan_active_ = false; + this->roaming_connect_active_ = false; +} + +void WiFiComponent::release_scan_results_() { + if (!this->keep_scan_results_) { +#ifdef USE_RP2040 + // std::vector - use swap trick since shrink_to_fit is non-binding + decltype(this->scan_result_)().swap(this->scan_result_); +#else + // FixedVector::shrink_to_fit() actually frees all memory + this->scan_result_.shrink_to_fit(); +#endif + } +} + +void WiFiComponent::check_roaming_(uint32_t now) { + // Guard: not for hidden networks (may not appear in scan) + const WiFiAP *selected = this->get_selected_sta_(); + if (selected == nullptr || selected->get_hidden()) { + this->roaming_attempts_ = ROAMING_MAX_ATTEMPTS; // Stop checking forever + return; + } + + this->roaming_last_check_ = now; + this->roaming_attempts_++; + + // Guard: skip scan if signal is already good (no meaningful improvement possible) + int8_t rssi = this->wifi_rssi(); + if (rssi > ROAMING_GOOD_RSSI) + return; + + ESP_LOGD(TAG, "Roam scan (%d dBm)", rssi); + this->roaming_scan_active_ = true; + this->wifi_scan_start_(this->passive_scan_); +} + +void WiFiComponent::process_roaming_scan_() { + this->scan_done_ = false; + this->roaming_scan_active_ = false; + + // Get current connection info + int8_t current_rssi = this->wifi_rssi(); + // Guard: must still be connected (RSSI may have become invalid during scan) + if (current_rssi == WIFI_RSSI_DISCONNECTED) { + this->release_scan_results_(); + return; + } + + char ssid_buf[SSID_BUFFER_SIZE]; + StringRef current_ssid(this->wifi_ssid_to(ssid_buf)); + bssid_t current_bssid = this->wifi_bssid(); + + // Find best candidate: same SSID, different BSSID + const WiFiScanResult *best = nullptr; + char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + + for (const auto &result : this->scan_result_) { + // Must be same SSID, different BSSID + if (current_ssid != result.get_ssid() || result.get_bssid() == current_bssid) + continue; + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + format_mac_addr_upper(result.get_bssid().data(), bssid_buf); + ESP_LOGV(TAG, "Roam candidate %s %d dBm", bssid_buf, result.get_rssi()); +#endif + + // Track the best candidate + if (best == nullptr || result.get_rssi() > best->get_rssi()) { + best = &result; + } + } + + // Check if best candidate meets minimum improvement threshold + const WiFiAP *selected = this->get_selected_sta_(); + int8_t improvement = (best == nullptr) ? 0 : best->get_rssi() - current_rssi; + if (selected == nullptr || improvement < ROAMING_MIN_IMPROVEMENT) { + ESP_LOGV(TAG, "Roam best %+d dB (need +%d)", improvement, ROAMING_MIN_IMPROVEMENT); + this->release_scan_results_(); + return; + } + + format_mac_addr_upper(best->get_bssid().data(), bssid_buf); + ESP_LOGI(TAG, "Roaming to %s (%+d dB)", bssid_buf, improvement); + + WiFiAP roam_params = *selected; + apply_scan_result_to_params(roam_params, *best); + this->release_scan_results_(); + + // Mark as roaming attempt - affects retry behavior if connection fails + this->roaming_connect_active_ = true; + + // Connect directly - wifi_sta_connect_ handles disconnect internally + this->error_from_callback_ = false; + this->start_connecting(roam_params); +} + WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace wifi -} // namespace esphome +} // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 2fd7fa6cd4..09af384725 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -6,16 +6,12 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" +#include #include #include -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include -#include -#include -#endif - #ifdef USE_LIBRETINY #include #endif @@ -49,12 +45,19 @@ extern "C" { #include #endif -namespace esphome { -namespace wifi { +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) +#include +#include +#endif + +namespace esphome::wifi { /// Sentinel value for RSSI when WiFi is not connected static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; +/// Buffer size for SSID (IEEE 802.11 max 32 bytes + null terminator) +static constexpr size_t SSID_BUFFER_SIZE = 33; + struct SavedWifiSettings { char ssid[33]; char password[65]; @@ -147,25 +150,28 @@ template using wifi_scan_vector_t = FixedVector; class WiFiAP { public: void set_ssid(const std::string &ssid); - void set_bssid(bssid_t bssid); - void set_bssid(optional bssid); + void set_bssid(const bssid_t &bssid); + void clear_bssid(); void set_password(const std::string &password); #ifdef USE_WIFI_WPA2_EAP void set_eap(optional eap_auth); #endif // USE_WIFI_WPA2_EAP - void set_channel(optional channel); + void set_channel(uint8_t channel); + void clear_channel(); void set_priority(int8_t priority) { priority_ = priority; } #ifdef USE_WIFI_MANUAL_IP void set_manual_ip(optional manual_ip); #endif void set_hidden(bool hidden); const std::string &get_ssid() const; - const optional &get_bssid() const; + const bssid_t &get_bssid() const; + bool has_bssid() const; const std::string &get_password() const; #ifdef USE_WIFI_WPA2_EAP const optional &get_eap() const; #endif // USE_WIFI_WPA2_EAP - const optional &get_channel() const; + uint8_t get_channel() const; + bool has_channel() const; int8_t get_priority() const { return priority_; } #ifdef USE_WIFI_MANUAL_IP const optional &get_manual_ip() const; @@ -175,16 +181,17 @@ class WiFiAP { protected: std::string ssid_; std::string password_; - optional bssid_; #ifdef USE_WIFI_WPA2_EAP optional eap_; #endif // USE_WIFI_WPA2_EAP #ifdef USE_WIFI_MANUAL_IP optional manual_ip_; #endif - optional channel_; - int8_t priority_{0}; - bool hidden_{false}; + // Group small types together to minimize padding + bssid_t bssid_{}; // 6 bytes, all zeros = any/not set + uint8_t channel_{0}; // 1 byte, 0 = auto/not set + int8_t priority_{0}; // 1 byte + bool hidden_{false}; // 1 byte (+ 3 bytes end padding to 4-byte align) }; class WiFiScanResult { @@ -238,6 +245,51 @@ enum WifiMinAuthMode : uint8_t { struct IDFWiFiEvent; #endif +#ifdef USE_LIBRETINY +struct LTWiFiEvent; +#endif + +/** Listener interface for WiFi IP state changes. + * + * Components can implement this interface to receive IP address updates + * without the overhead of std::function callbacks. + */ +class WiFiIPStateListener { + public: + virtual void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) = 0; +}; + +/** Listener interface for WiFi scan results. + * + * Components can implement this interface to receive scan results + * without the overhead of std::function callbacks. + */ +class WiFiScanResultsListener { + public: + virtual void on_wifi_scan_results(const wifi_scan_vector_t &results) = 0; +}; + +/** Listener interface for WiFi connection state changes. + * + * Components can implement this interface to receive connection updates + * without the overhead of std::function callbacks. + */ +class WiFiConnectStateListener { + public: + virtual void on_wifi_connect_state(StringRef ssid, std::span bssid) = 0; +}; + +/** Listener interface for WiFi power save mode changes. + * + * Components can implement this interface to receive power save mode updates + * without the overhead of std::function callbacks. + */ +class WiFiPowerSaveListener { + public: + virtual void on_wifi_power_save(WiFiPowerSaveMode mode) = 0; +}; + /// This component is responsible for managing the ESP WiFi interface. class WiFiComponent : public Component { public: @@ -249,10 +301,7 @@ class WiFiComponent : public Component { WiFiAP get_sta() const; void init_sta(size_t count); void add_sta(const WiFiAP &ap); - void clear_sta() { - this->sta_.clear(); - this->selected_sta_index_ = -1; - } + void clear_sta(); #ifdef USE_WIFI_AP /** Setup an Access Point that should be created if no connection to a station can be made. @@ -276,10 +325,14 @@ class WiFiComponent : public Component { // Backward compatibility overload - ignores 'two' parameter void start_connecting(const WiFiAP &ap, bool /* two */) { this->start_connecting(ap); } - void check_connecting_finished(); + void check_connecting_finished(uint32_t now); void retry_connect(); +#ifdef USE_RP2040 + bool can_proceed() override; +#endif + void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected(); @@ -291,6 +344,7 @@ class WiFiComponent : public Component { void set_passive_scan(bool passive); void save_wifi_sta(const std::string &ssid, const std::string &password); + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup WiFi interface. @@ -307,6 +361,7 @@ class WiFiComponent : public Component { bool has_sta() const; bool has_ap() const; + bool is_ap_active() const; #ifdef USE_WIFI_11KV_SUPPORT void set_btm(bool btm); @@ -351,18 +406,74 @@ class WiFiComponent : public Component { network::IPAddresses wifi_sta_ip_addresses(); std::string wifi_ssid(); + /// Write SSID to buffer without heap allocation. + /// Returns pointer to buffer, or empty string if not connected. + const char *wifi_ssid_to(std::span buffer); bssid_t wifi_bssid(); int8_t wifi_rssi(); void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; } + void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; } Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }; Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; }; int32_t get_wifi_channel(); +#ifdef USE_WIFI_LISTENERS + /** Add a listener for IP state changes. + * Listener receives: IP addresses, DNS address 1, DNS address 2 + */ + void add_ip_state_listener(WiFiIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); } + /// Add a listener for WiFi scan results + void add_scan_results_listener(WiFiScanResultsListener *listener) { + this->scan_results_listeners_.push_back(listener); + } + /** Add a listener for WiFi connection state changes. + * Listener receives: SSID, BSSID + */ + void add_connect_state_listener(WiFiConnectStateListener *listener) { + this->connect_state_listeners_.push_back(listener); + } + /** Add a listener for WiFi power save mode changes. + * Listener receives: WiFiPowerSaveMode + */ + void add_power_save_listener(WiFiPowerSaveListener *listener) { this->power_save_listeners_.push_back(listener); } +#endif // USE_WIFI_LISTENERS + +#ifdef USE_WIFI_RUNTIME_POWER_SAVE + /** Request high-performance mode (no power saving) for improved WiFi latency. + * + * Components that need maximum WiFi performance (e.g., audio streaming, large data transfers) + * can call this method to temporarily disable WiFi power saving. Multiple components can + * request high performance simultaneously using a counting semaphore. + * + * Power saving will be restored to the YAML-configured mode when all components have + * called release_high_performance(). + * + * Note: Only supported on ESP32. + * + * @return true if request was satisfied (high-performance mode active or already configured), + * false if operation failed (semaphore error) + */ + bool request_high_performance(); + + /** Release a high-performance mode request. + * + * Should be called when a component no longer needs maximum WiFi latency. + * When all requests are released (semaphore count reaches zero), WiFi power saving + * is restored to the YAML-configured mode. + * + * Note: Only supported on ESP32. + * + * @return true if release was successful (or already in high-performance config), + * false if operation failed (semaphore error) + */ + bool release_high_performance(); +#endif // USE_WIFI_RUNTIME_POWER_SAVE + protected: #ifdef USE_WIFI_AP void setup_ap_config_(); @@ -394,6 +505,8 @@ class WiFiComponent : public Component { int8_t find_next_hidden_sta_(int8_t start_index); /// Log failed connection and decrease BSSID priority to avoid repeated attempts void log_and_adjust_priority_for_failed_connect_(); + /// Clear all BSSID priority penalties after successful connection (stale after disconnect) + void clear_all_bssid_priorities_(); /// Clear BSSID priority tracking if all priorities are at minimum (saves memory) void clear_priorities_if_all_min_(); /// Advance to next target (AP/SSID) within current phase, or increment retry counter @@ -424,6 +537,8 @@ class WiFiComponent : public Component { return true; } + void connect_soon_(); + void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); @@ -455,16 +570,20 @@ class WiFiComponent : public Component { void save_fast_connect_settings_(); #endif + // Post-connect roaming methods + void check_roaming_(uint32_t now); + void process_roaming_scan_(); + void clear_roaming_state_(); + + /// Free scan results memory unless a component needs them + void release_scan_results_(); + #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); void wifi_scan_done_callback_(void *arg, STATUS status); static void s_wifi_scan_done_callback(void *arg, STATUS status); #endif -#ifdef USE_ESP32_FRAMEWORK_ARDUINO - void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info); - void wifi_scan_done_callback_(); -#endif #ifdef USE_ESP32 void wifi_process_event_(IDFWiFiEvent *data); #endif @@ -476,6 +595,7 @@ class WiFiComponent : public Component { #ifdef USE_LIBRETINY void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info); + void wifi_process_event_(LTWiFiEvent *event); void wifi_scan_done_callback_(); #endif @@ -485,16 +605,29 @@ class WiFiComponent : public Component { #ifdef USE_WIFI_AP WiFiAP ap_; #endif - optional output_power_; + float output_power_{NAN}; +#ifdef USE_WIFI_LISTENERS + std::vector ip_state_listeners_; + std::vector scan_results_listeners_; + std::vector connect_state_listeners_; + std::vector power_save_listeners_; +#endif // USE_WIFI_LISTENERS ESPPreferenceObject pref_; #ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; #endif + // Post-connect roaming constants + static constexpr uint32_t ROAMING_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes + static constexpr int8_t ROAMING_MIN_IMPROVEMENT = 10; // dB + static constexpr int8_t ROAMING_GOOD_RSSI = -49; // Skip scan if signal is excellent + static constexpr uint8_t ROAMING_MAX_ATTEMPTS = 3; + // Group all 32-bit integers together uint32_t action_started_; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; + uint32_t roaming_last_check_{0}; #ifdef USE_WIFI_AP uint32_t ap_timeout_{}; #endif @@ -509,6 +642,7 @@ class WiFiComponent : public Component { // Used to access password, manual_ip, priority, EAP settings, and hidden flag // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) int8_t selected_sta_index_{-1}; + uint8_t roaming_attempts_{0}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; @@ -520,15 +654,27 @@ class WiFiComponent : public Component { bool error_from_callback_{false}; bool scan_done_{false}; bool ap_setup_{false}; + bool ap_started_{false}; bool passive_scan_{false}; bool has_saved_wifi_settings_{false}; #ifdef USE_WIFI_11KV_SUPPORT bool btm_{false}; bool rrm_{false}; #endif - bool enable_on_boot_; + bool enable_on_boot_{true}; bool got_ipv4_address_{false}; bool keep_scan_results_{false}; + bool did_scan_this_cycle_{false}; + bool skip_cooldown_next_cycle_{false}; + bool post_connect_roaming_{true}; // Enabled by default + bool roaming_scan_active_{false}; + bool roaming_connect_active_{false}; // True during roaming connection attempt (preserves roaming_attempts_) +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; + bool is_high_performance_mode_{false}; + + SemaphoreHandle_t high_performance_semaphore_{nullptr}; +#endif // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; @@ -542,107 +688,5 @@ class WiFiComponent : public Component { extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -template class WiFiConnectedCondition : public Condition { - public: - bool check(const Ts &...x) override { return global_wifi_component->is_connected(); } -}; - -template class WiFiEnabledCondition : public Condition { - public: - bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); } -}; - -template class WiFiEnableAction : public Action { - public: - void play(const Ts &...x) override { global_wifi_component->enable(); } -}; - -template class WiFiDisableAction : public Action { - public: - void play(const Ts &...x) override { global_wifi_component->disable(); } -}; - -template class WiFiConfigureAction : public Action, public Component { - public: - TEMPLATABLE_VALUE(std::string, ssid) - TEMPLATABLE_VALUE(std::string, password) - TEMPLATABLE_VALUE(bool, save) - TEMPLATABLE_VALUE(uint32_t, connection_timeout) - - void play(const Ts &...x) override { - auto ssid = this->ssid_.value(x...); - auto password = this->password_.value(x...); - // Avoid multiple calls - if (this->connecting_) - return; - // If already connected to the same AP, do nothing - if (global_wifi_component->wifi_ssid() == ssid) { - // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); - return; - } - // Create a new WiFiAP object with the new SSID and password - this->new_sta_.set_ssid(ssid); - this->new_sta_.set_password(password); - // Save the current STA - this->old_sta_ = global_wifi_component->get_sta(); - // Disable WiFi - global_wifi_component->disable(); - // Set the state to connecting - this->connecting_ = true; - // Store the new STA so once the WiFi is enabled, it will connect to it - // This is necessary because the WiFiComponent will raise an error and fallback to the saved STA - // if trying to connect to a new STA while already connected to another one - if (this->save_.value(x...)) { - global_wifi_component->save_wifi_sta(new_sta_.get_ssid(), new_sta_.get_password()); - } else { - global_wifi_component->set_sta(new_sta_); - } - // Enable WiFi - global_wifi_component->enable(); - // Set timeout for the connection - this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() { - // If the timeout is reached, stop connecting and revert to the old AP - global_wifi_component->disable(); - global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password()); - global_wifi_component->enable(); - // Start a timeout for the fallback if the connection to the old AP fails - this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() { - this->connecting_ = false; - this->error_trigger_->trigger(); - }); - }); - } - - Trigger<> *get_connect_trigger() const { return this->connect_trigger_; } - Trigger<> *get_error_trigger() const { return this->error_trigger_; } - - void loop() override { - if (!this->connecting_) - return; - if (global_wifi_component->is_connected()) { - // The WiFi is connected, stop the timeout and reset the connecting flag - this->cancel_timeout("wifi-connect-timeout"); - this->cancel_timeout("wifi-fallback-timeout"); - this->connecting_ = false; - if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) { - // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); - } else { - // Callback to notify the user that the connection failed - this->error_trigger_->trigger(); - } - } - } - - protected: - bool connecting_{false}; - WiFiAP new_sta_; - WiFiAP old_sta_; - Trigger<> *connect_trigger_{new Trigger<>()}; - Trigger<> *error_trigger_{new Trigger<>()}; -}; - -} // namespace wifi -} // namespace esphome +} // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index a543628e27..b7d820413c 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -38,8 +38,7 @@ extern "C" { #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_esp8266"; @@ -83,8 +82,11 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { if (!ret) { ESP_LOGW(TAG, "Set mode failed"); + return false; } + this->ap_started_ = target_ap; + return ret; } bool WiFiComponent::wifi_apply_power_save_() { @@ -102,7 +104,15 @@ bool WiFiComponent::wifi_apply_power_save_() { break; } wifi_fpm_auto_sleep_set_in_null_mode(1); - return wifi_set_sleep_type(power_save); + bool success = wifi_set_sleep_type(power_save); +#ifdef USE_WIFI_LISTENERS + if (success) { + for (auto *listener : this->power_save_listeners_) { + listener->on_wifi_power_save(this->power_save_); + } + } +#endif + return success; } #if LWIP_VERSION_MAJOR != 1 @@ -247,9 +257,9 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { memcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); memcpy(reinterpret_cast(conf.password), ap.get_password().c_str(), ap.get_password().size()); - if (ap.get_bssid().has_value()) { + if (ap.has_bssid()) { conf.bssid_set = 1; - memcpy(conf.bssid, ap.get_bssid()->data(), 6); + memcpy(conf.bssid, ap.get_bssid().data(), 6); } else { conf.bssid_set = 0; } @@ -361,7 +371,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { while (!connected) { uint8_t ipv6_addr_count = 0; for (auto addr : addrList) { - ESP_LOGV(TAG, "Address %s", addr.toString().c_str()); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGV(TAG, "Address %s", network::IPAddress(addr.ipFromNetifNum()).str_to(ip_buf)); if (addr.isV6()) { ipv6_addr_count++; } @@ -371,8 +382,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } #endif /* USE_NETWORK_IPV6 */ - if (ap.get_channel().has_value()) { - ret = wifi_set_channel(*ap.get_channel()); + if (ap.has_channel()) { + ret = wifi_set_channel(ap.get_channel()); if (!ret) { ESP_LOGV(TAG, "wifi_set_channel failed"); return false; @@ -403,21 +414,6 @@ const LogString *get_auth_mode_str(uint8_t mode) { return LOG_STR("UNKNOWN"); } } -#ifdef ipv4_addr -std::string format_ip_addr(struct ipv4_addr ip) { - char buf[20]; - sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), - uint8_t(ip.addr >> 24)); - return buf; -} -#else -std::string format_ip_addr(struct ip_addr ip) { - char buf[20]; - sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), - uint8_t(ip.addr >> 24)); - return buf; -} -#endif const LogString *get_op_mode_str(uint8_t mode) { switch (mode) { case WIFI_OFF: @@ -508,31 +504,51 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { auto it = event->event_info.connected; - char buf[33]; - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(), +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.bssid, bssid_buf); + ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=%s channel=%u", it.ssid_len, (const char *) it.ssid, bssid_buf, it.channel); +#endif s_sta_connected = true; +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); + } + // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here +#ifdef USE_WIFI_MANUAL_IP + if (const WiFiAP *config = global_wifi_component->get_selected_sta_(); + config && config->get_manual_ip().has_value()) { + for (auto *listener : global_wifi_component->ip_state_listeners_) { + listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), + global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1)); + } + } +#endif +#endif break; } case EVENT_STAMODE_DISCONNECTED: { auto it = event->event_info.disconnected; - char buf[33]; - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; if (it.reason == REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len, + (const char *) it.ssid); s_sta_connect_not_found = true; } else { char bssid_s[18]; format_mac_addr_upper(it.bssid, bssid_s); - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, - LOG_STR_ARG(get_disconnect_reason_str(it.reason))); + ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len, + (const char *) it.ssid, bssid_s, LOG_STR_ARG(get_disconnect_reason_str(it.reason))); s_sta_connect_error = true; } s_sta_connected = false; s_sta_connecting = false; +#ifdef USE_WIFI_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; + for (auto *listener : global_wifi_component->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); + } +#endif break; } case EVENT_STAMODE_AUTHMODE_CHANGE: { @@ -552,9 +568,17 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } case EVENT_STAMODE_GOT_IP: { auto it = event->event_info.got_ip; - ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), format_ip_addr(it.gw).c_str(), - format_ip_addr(it.mask).c_str()); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE], gw_buf[network::IP_ADDRESS_BUFFER_SIZE], + mask_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", network::IPAddress(&it.ip).str_to(ip_buf), + network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf)); s_sta_got_ip = true; +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->ip_state_listeners_) { + listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0), + global_wifi_component->get_dns_address(1)); + } +#endif break; } case EVENT_STAMODE_DHCP_TIMEOUT: { @@ -562,18 +586,30 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { break; } case EVENT_SOFTAPMODE_STACONNECTED: { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE auto it = event->event_info.sta_connected; - ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), it.aid); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", mac_buf, it.aid); +#endif break; } case EVENT_SOFTAPMODE_STADISCONNECTED: { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE auto it = event->event_info.sta_disconnected; - ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), it.aid); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", mac_buf, it.aid); +#endif break; } case EVENT_SOFTAPMODE_PROBEREQRECVED: { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE auto it = event->event_info.ap_probereqrecved; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", mac_buf, it.rssi); +#endif break; } #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) @@ -584,9 +620,14 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { break; } case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE auto it = event->event_info.distribute_sta_ip; - ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), - format_ip_addr(it.ip).c_str(), it.aid); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", mac_buf, network::IPAddress(&it.ip).str_to(ip_buf), + it.aid); +#endif break; } #endif @@ -729,6 +770,11 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { it->is_hidden != 0); } this->scan_done_ = true; +#ifdef USE_WIFI_LISTENERS + for (auto *listener : global_wifi_component->scan_results_listeners_) { + listener->on_wifi_scan_results(global_wifi_component->scan_result_); + } +#endif } #ifdef USE_WIFI_AP @@ -768,10 +814,13 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { network::IPAddress start_address = network::IPAddress(&info.ip); start_address += 99; lease.start_ip = start_address; - ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str().c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; +#endif + ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str_to(ip_buf)); start_address += 10; lease.end_ip = start_address; - ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str()); + ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str_to(ip_buf)); if (!wifi_softap_set_dhcps_lease(&lease)) { ESP_LOGE(TAG, "Set SoftAP DHCP lease failed"); return false; @@ -814,7 +863,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } memcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); conf.ssid_len = static_cast(ap.get_ssid().size()); - conf.channel = ap.get_channel().value_or(1); + conf.channel = ap.has_channel() ? ap.get_channel() : 1; conf.ssid_hidden = ap.get_hidden(); conf.max_connection = 5; conf.beacon_interval = 100; @@ -864,23 +913,38 @@ network::IPAddress WiFiComponent::wifi_soft_ap_ip() { bssid_t WiFiComponent::wifi_bssid() { bssid_t bssid{}; - uint8_t *raw_bssid = WiFi.BSSID(); - if (raw_bssid != nullptr) { - for (size_t i = 0; i < bssid.size(); i++) - bssid[i] = raw_bssid[i]; + struct station_config conf {}; + if (wifi_station_get_config(&conf)) { + std::copy_n(conf.bssid, bssid.size(), bssid.begin()); } return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + struct station_config conf {}; + if (!wifi_station_get_config(&conf)) { + buffer[0] = '\0'; + return buffer.data(); + } + // conf.ssid is uint8[32], not null-terminated if full + size_t len = strnlen(reinterpret_cast(conf.ssid), sizeof(conf.ssid)); + memcpy(buffer.data(), conf.ssid, len); + buffer[len] = '\0'; + return buffer.data(); +} +int8_t WiFiComponent::wifi_rssi() { + if (WiFi.status() != WL_CONNECTED) + return WIFI_RSSI_DISCONNECTED; + int8_t rssi = WiFi.RSSI(); + // Values >= 31 are error codes per NONOS SDK API, not valid RSSI readings + return rssi >= 31 ? WIFI_RSSI_DISCONNECTED : rssi; +} int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {(const ip_addr_t *) WiFi.dnsIP(num)}; } void WiFiComponent::wifi_loop_() {} -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 4aac03885a..820725ed31 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -41,8 +41,7 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_esp32"; @@ -54,7 +53,6 @@ static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid- #endif // USE_WIFI_AP static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_ap_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -282,7 +280,15 @@ bool WiFiComponent::wifi_apply_power_save_() { power_save = WIFI_PS_NONE; break; } - return esp_wifi_set_ps(power_save) == ESP_OK; + bool success = esp_wifi_set_ps(power_save) == ESP_OK; +#ifdef USE_WIFI_LISTENERS + if (success) { + for (auto *listener : this->power_save_listeners_) { + listener->on_wifi_power_save(this->power_save_); + } + } +#endif + return success; } bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { @@ -333,14 +339,14 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { conf.sta.rm_enabled = this->rrm_; #endif - if (ap.get_bssid().has_value()) { + if (ap.has_bssid()) { conf.sta.bssid_set = true; - memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6); + memcpy(conf.sta.bssid, ap.get_bssid().data(), 6); } else { conf.sta.bssid_set = false; } - if (ap.get_channel().has_value()) { - conf.sta.channel = *ap.get_channel(); + if (ap.has_channel()) { + conf.sta.channel = ap.get_channel(); conf.sta.scan_method = WIFI_FAST_SCAN; } else { conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; @@ -477,6 +483,12 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { s_sta_connected = false; s_sta_connect_error = false; s_sta_connect_not_found = false; + // Reset IP address flags - ensures we don't report connected before DHCP completes + // (IP_EVENT_STA_LOST_IP doesn't always fire on disconnect) + this->got_ipv4_address_ = false; +#if USE_NETWORK_IPV6 + this->num_ipv6_addresses_ = 0; +#endif err = esp_wifi_connect(); if (err != ESP_OK) { @@ -603,10 +615,6 @@ const char *get_auth_mode_str(uint8_t mode) { } } -std::string format_ip4_addr(const esp_ip4_addr_t &ip) { return str_snprintf(IPSTR, 15, IP2STR(&ip)); } -#if LWIP_IPV6 -std::string format_ip6_addr(const esp_ip6_addr_t &ip) { return str_snprintf(IPV6STR, 39, IPV62STR(ip)); } -#endif /* LWIP_IPV6 */ const char *get_disconnect_reason_str(uint8_t reason) { switch (reason) { case WIFI_REASON_AUTH_EXPIRE: @@ -718,6 +726,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { ESP_LOGV(TAG, "STA stop"); s_sta_started = false; + s_sta_connecting = false; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) { const auto &it = data->data.sta_authmode_change; @@ -725,51 +734,76 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_CONNECTED) { const auto &it = data->data.sta_connected; - char buf[33]; - assert(it.ssid_len <= 32); - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, - format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.bssid, bssid_buf); + ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", it.ssid_len, + (const char *) it.ssid, bssid_buf, it.channel, get_auth_mode_str(it.authmode)); +#endif s_sta_connected = true; +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); + } + // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here +#ifdef USE_WIFI_MANUAL_IP + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } + } +#endif +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) { const auto &it = data->data.sta_disconnected; - char buf[33]; - assert(it.ssid_len <= 32); - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; if (it.reason == WIFI_REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); + ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len, + (const char *) it.ssid); s_sta_connect_not_found = true; } else if (it.reason == WIFI_REASON_ROAMING) { - ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf); + ESP_LOGI(TAG, "Disconnected ssid='%.*s' reason='Station Roaming'", it.ssid_len, (const char *) it.ssid); return; } else { char bssid_s[18]; format_mac_addr_upper(it.bssid, bssid_s); - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, - get_disconnect_reason_str(it.reason)); + ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len, + (const char *) it.ssid, bssid_s, get_disconnect_reason_str(it.reason)); s_sta_connect_error = true; } s_sta_connected = false; s_sta_connecting = false; error_from_callback_ = true; +#ifdef USE_WIFI_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); + } +#endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { const auto &it = data->data.ip_got_ip; #if USE_NETWORK_IPV6 esp_netif_create_ip6_linklocal(s_sta_netif); #endif /* USE_NETWORK_IPV6 */ - ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), - format_ip4_addr(it.ip_info.gw).c_str()); + ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } +#endif #if USE_NETWORK_IPV6 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) { const auto &it = data->data.ip_got_ip6; - ESP_LOGV(TAG, "IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str()); + ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } +#endif #endif /* USE_NETWORK_IPV6 */ } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) { @@ -809,30 +843,47 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid.empty()); } +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { ESP_LOGV(TAG, "AP start"); - s_ap_started = true; + this->ap_started_ = true; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STOP) { ESP_LOGV(TAG, "AP stop"); - s_ap_started = false; + this->ap_started_ = false; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE const auto &it = data->data.ap_probe_req_rx; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", mac_buf, it.rssi); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STACONNECTED) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE const auto &it = data->data.ap_staconnected; - ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(it.mac).c_str()); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGV(TAG, "AP client connected MAC=%s", mac_buf); +#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STADISCONNECTED) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE const auto &it = data->data.ap_stadisconnected; - ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(it.mac).c_str()); + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", mac_buf); +#endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) { const auto &it = data->data.ip_ap_staipassigned; - ESP_LOGV(TAG, "AP client assigned IP %s", format_ip4_addr(it.ip).c_str()); + ESP_LOGV(TAG, "AP client assigned IP " IPSTR, IP2STR(&it.ip)); } } @@ -927,10 +978,13 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { network::IPAddress start_address = network::IPAddress(&info.ip); start_address += 99; lease.start_ip = start_address; - ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str().c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; +#endif + ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str_to(ip_buf)); start_address += 10; lease.end_ip = start_address; - ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str()); + ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str_to(ip_buf)); err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); if (err != ESP_OK) { @@ -942,8 +996,10 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { // Configure DHCP Option 114 (Captive Portal URI) if captive portal is enabled // This provides a standards-compliant way for clients to discover the captive portal if (captive_portal::global_captive_portal != nullptr) { - static char captive_portal_uri[32]; - snprintf(captive_portal_uri, sizeof(captive_portal_uri), "http://%s", network::IPAddress(&info.ip).str().c_str()); + // Buffer must be static - dhcps_set_option_info stores pointer, doesn't copy + static char captive_portal_uri[24]; // "http://" (7) + IPv4 max (15) + null + memcpy(captive_portal_uri, "http://", 7); // NOLINT(bugprone-not-null-terminated-result) - str_to null-terminates + network::IPAddress(&info.ip).str_to(captive_portal_uri + 7); err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_CAPTIVEPORTAL_URI, captive_portal_uri, strlen(captive_portal_uri)); if (err != ESP_OK) { @@ -976,7 +1032,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; } memcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); - conf.ap.channel = ap.get_channel().value_or(1); + conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1; conf.ap.ssid_hidden = ap.get_ssid().size(); conf.ap.max_connection = 5; conf.ap.beacon_interval = 100; @@ -1050,6 +1106,19 @@ std::string WiFiComponent::wifi_ssid() { size_t len = strnlen(ssid_s, sizeof(info.ssid)); return {ssid_s, len}; } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + wifi_ap_record_t info{}; + esp_err_t err = esp_wifi_sta_get_ap_info(&info); + if (err != ESP_OK) { + buffer[0] = '\0'; + return buffer.data(); + } + // info.ssid is uint8[33], but only 32 bytes are SSID data + size_t len = strnlen(reinterpret_cast(info.ssid), 32); + memcpy(buffer.data(), info.ssid, len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); @@ -1093,8 +1162,6 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_ip); } -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif // USE_ESP32 #endif diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 98cbfddb1d..68fcc3577d 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -3,24 +3,88 @@ #ifdef USE_WIFI #ifdef USE_LIBRETINY +#include #include #include #include "lwip/ip_addr.h" #include "lwip/err.h" #include "lwip/dns.h" +#include +#include + #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_lt"; -static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +// Thread-safe event handling for LibreTiny WiFi +// +// LibreTiny's WiFi.onEvent() callback runs in the WiFi driver's thread context, +// not the main ESPHome loop. Without synchronization, modifying shared state +// (like connection status flags) from the callback causes race conditions: +// - The main loop may never see state changes (values cached in registers) +// - State changes may be visible in inconsistent order +// - LibreTiny targets (BK7231, RTL8720) lack atomic instructions (no LDREX/STREX) +// +// Solution: Queue events in the callback and process them in the main loop. +// This is the same approach used by ESP32 IDF's wifi_process_event_(). +// All state modifications happen in the main loop context, eliminating races. + +static constexpr size_t EVENT_QUEUE_SIZE = 16; // Max pending WiFi events before overflow +static QueueHandle_t s_event_queue = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static volatile uint32_t s_event_queue_overflow_count = + 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +// Event structure for queued WiFi events - contains a copy of event data +// to avoid lifetime issues with the original event data from the callback +struct LTWiFiEvent { + arduino_event_id_t event_id; + union { + struct { + uint8_t ssid[33]; + uint8_t ssid_len; + uint8_t bssid[6]; + uint8_t channel; + uint8_t authmode; + } sta_connected; + struct { + uint8_t ssid[33]; + uint8_t ssid_len; + uint8_t bssid[6]; + uint8_t reason; + } sta_disconnected; + struct { + uint8_t old_mode; + uint8_t new_mode; + } sta_authmode_change; + struct { + uint32_t status; + uint8_t number; + uint8_t scan_id; + } scan_done; + struct { + uint8_t mac[6]; + int rssi; + } ap_probe_req; + } data; +}; + +// Connection state machine - only modified from main loop after queue processing +enum class LTWiFiSTAState : uint8_t { + IDLE, // Not connecting + CONNECTING, // Connection in progress + CONNECTED, // Successfully connected with IP + ERROR_NOT_FOUND, // AP not found (probe failed) + ERROR_FAILED, // Connection failed (auth, timeout, etc.) +}; + +static LTWiFiSTAState s_sta_state = LTWiFiSTAState::IDLE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) bool WiFiComponent::wifi_mode_(optional sta, optional ap) { uint8_t current_mode = WiFi.getMode(); @@ -51,8 +115,11 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { if (!ret) { ESP_LOGW(TAG, "Setting mode failed"); + return false; } + this->ap_started_ = enable_ap; + return ret; } bool WiFiComponent::wifi_apply_output_power_(float output_power) { @@ -67,7 +134,17 @@ bool WiFiComponent::wifi_sta_pre_setup_() { delay(10); return true; } -bool WiFiComponent::wifi_apply_power_save_() { return WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); } +bool WiFiComponent::wifi_apply_power_save_() { + bool success = WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); +#ifdef USE_WIFI_LISTENERS + if (success) { + for (auto *listener : this->power_save_listeners_) { + listener->on_wifi_power_save(this->power_save_); + } + } +#endif + return success; +} bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) @@ -124,11 +201,12 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { this->wifi_apply_hostname_(); - s_sta_connecting = true; + // Reset state machine before connecting + s_sta_state = LTWiFiSTAState::CONNECTING; WiFiStatus status = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), - ap.get_channel().has_value() ? *ap.get_channel() : 0, - ap.get_bssid().has_value() ? ap.get_bssid()->data() : NULL); + ap.get_channel(), // 0 = auto + ap.has_bssid() ? ap.get_bssid().data() : NULL); if (status != WL_CONNECTED) { ESP_LOGW(TAG, "esp_wifi_connect failed: %d", status); return false; @@ -153,14 +231,6 @@ const char *get_auth_mode_str(uint8_t mode) { } } -using esphome_ip4_addr_t = IPAddress; - -std::string format_ip4_addr(const esphome_ip4_addr_t &ip) { - char buf[20]; - uint32_t addr = ip; - sprintf(buf, "%u.%u.%u.%u", uint8_t(addr >> 0), uint8_t(addr >> 8), uint8_t(addr >> 16), uint8_t(addr >> 24)); - return buf; -} const char *get_op_mode_str(uint8_t mode) { switch (mode) { case WIFI_OFF: @@ -259,16 +329,101 @@ const char *get_disconnect_reason_str(uint8_t reason) { using esphome_wifi_event_id_t = arduino_event_id_t; using esphome_wifi_event_info_t = arduino_event_info_t; +// Event callback - runs in WiFi driver thread context +// Only queues events for processing in main loop, no logging or state changes here void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) { + if (s_event_queue == nullptr) { + return; + } + + // Allocate on heap and fill directly to avoid extra memcpy + auto *to_send = new LTWiFiEvent{}; // NOLINT(cppcoreguidelines-owning-memory) + to_send->event_id = event; + + // Copy event-specific data switch (event) { + case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { + auto &it = info.wifi_sta_connected; + to_send->data.sta_connected.ssid_len = it.ssid_len; + memcpy(to_send->data.sta_connected.ssid, it.ssid, + std::min(static_cast(it.ssid_len), sizeof(to_send->data.sta_connected.ssid) - 1)); + memcpy(to_send->data.sta_connected.bssid, it.bssid, 6); + to_send->data.sta_connected.channel = it.channel; + to_send->data.sta_connected.authmode = it.authmode; + break; + } + case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: { + auto &it = info.wifi_sta_disconnected; + to_send->data.sta_disconnected.ssid_len = it.ssid_len; + memcpy(to_send->data.sta_disconnected.ssid, it.ssid, + std::min(static_cast(it.ssid_len), sizeof(to_send->data.sta_disconnected.ssid) - 1)); + memcpy(to_send->data.sta_disconnected.bssid, it.bssid, 6); + to_send->data.sta_disconnected.reason = it.reason; + break; + } + case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { + auto &it = info.wifi_sta_authmode_change; + to_send->data.sta_authmode_change.old_mode = it.old_mode; + to_send->data.sta_authmode_change.new_mode = it.new_mode; + break; + } + case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: { + auto &it = info.wifi_scan_done; + to_send->data.scan_done.status = it.status; + to_send->data.scan_done.number = it.number; + to_send->data.scan_done.scan_id = it.scan_id; + break; + } + case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { + auto &it = info.wifi_ap_probereqrecved; + memcpy(to_send->data.ap_probe_req.mac, it.mac, 6); + to_send->data.ap_probe_req.rssi = it.rssi; + break; + } + case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { + auto &it = info.wifi_sta_connected; + memcpy(to_send->data.sta_connected.bssid, it.bssid, 6); + break; + } + case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { + auto &it = info.wifi_sta_disconnected; + memcpy(to_send->data.sta_disconnected.bssid, it.bssid, 6); + break; + } + case ESPHOME_EVENT_ID_WIFI_READY: + case ESPHOME_EVENT_ID_WIFI_STA_START: + case ESPHOME_EVENT_ID_WIFI_STA_STOP: + case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: + case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: + case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: + case ESPHOME_EVENT_ID_WIFI_AP_START: + case ESPHOME_EVENT_ID_WIFI_AP_STOP: + case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: + // No additional data needed + break; + default: + // Unknown event, don't queue + delete to_send; // NOLINT(cppcoreguidelines-owning-memory) + return; + } + + // Queue event (don't block if queue is full) + if (xQueueSend(s_event_queue, &to_send, 0) != pdPASS) { + delete to_send; // NOLINT(cppcoreguidelines-owning-memory) + s_event_queue_overflow_count++; + } +} + +// Process a single event from the queue - runs in main loop context +void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { + switch (event->event_id) { case ESPHOME_EVENT_ID_WIFI_READY: { ESP_LOGV(TAG, "Ready"); break; } case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: { - auto it = info.wifi_scan_done; - ESP_LOGV(TAG, "Scan done: status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); - + auto &it = event->data.scan_done; + ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); this->wifi_scan_done_callback_(); break; } @@ -279,30 +434,62 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_STOP: { ESP_LOGV(TAG, "STA stop"); + s_sta_state = LTWiFiSTAState::IDLE; break; } case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { - auto it = info.wifi_sta_connected; - char buf[33]; - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; - ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, - format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); - + auto &it = event->data.sta_connected; + char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.bssid, bssid_buf); + ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", it.ssid_len, + (const char *) it.ssid, bssid_buf, it.channel, get_auth_mode_str(it.authmode)); + // Note: We don't set CONNECTED state here yet - wait for GOT_IP + // This matches ESP32 IDF behavior where s_sta_connected is set but + // wifi_sta_connect_status_() also checks got_ipv4_address_ +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); + } + // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here +#ifdef USE_WIFI_MANUAL_IP + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { + s_sta_state = LTWiFiSTAState::CONNECTED; + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } + } +#endif +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: { - auto it = info.wifi_sta_disconnected; - char buf[33]; - memcpy(buf, it.ssid, it.ssid_len); - buf[it.ssid_len] = '\0'; - if (it.reason == WIFI_REASON_NO_AP_FOUND) { - ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); - } else { - char bssid_s[18]; - format_mac_addr_upper(it.bssid, bssid_s); - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + auto &it = event->data.sta_disconnected; + + // LibreTiny can send spurious disconnect events with empty ssid/bssid during connection. + // These are typically "Association Leave" events that don't indicate actual failures: + // [W][wifi_lt]: Disconnected ssid='' bssid=00:00:00:00:00:00 reason='Association Leave' + // [W][wifi_lt]: Disconnected ssid='' bssid=00:00:00:00:00:00 reason='Association Leave' + // [V][wifi_lt]: Connected ssid='WIFI' bssid=... channel=3, authmode=WPA2 PSK + // Without this check, the spurious events would transition state to ERROR_FAILED, + // causing wifi_sta_connect_status_() to return an error. The main loop would then + // call retry_connect(), aborting a connection that may succeed moments later. + // Only ignore benign reasons - real failures like NO_AP_FOUND should still be processed. + if (it.ssid_len == 0 && s_sta_state == LTWiFiSTAState::CONNECTING && it.reason != WIFI_REASON_NO_AP_FOUND) { + ESP_LOGV(TAG, "Ignoring disconnect event with empty ssid while connecting (reason=%s)", get_disconnect_reason_str(it.reason)); + break; + } + + if (it.reason == WIFI_REASON_NO_AP_FOUND) { + ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len, + (const char *) it.ssid); + s_sta_state = LTWiFiSTAState::ERROR_NOT_FOUND; + } else { + char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len, + (const char *) it.ssid, bssid_s, get_disconnect_reason_str(it.reason)); + s_sta_state = LTWiFiSTAState::ERROR_FAILED; } uint8_t reason = it.reason; @@ -313,37 +500,51 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ this->error_from_callback_ = true; } - s_sta_connecting = false; +#ifdef USE_WIFI_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); + } +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: { - auto it = info.wifi_sta_authmode_change; + auto &it = event->data.sta_authmode_change; ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); // Mitigate CVE-2020-12638 // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) { ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting"); - // we can't call retry_connect() from this context, so disconnect immediately - // and notify main thread with error_from_callback_ WiFi.disconnect(); this->error_from_callback_ = true; + s_sta_state = LTWiFiSTAState::ERROR_FAILED; } break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: { - // auto it = info.got_ip.ip_info; - ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(), - format_ip4_addr(WiFi.gatewayIP()).c_str()); - s_sta_connecting = false; + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE], gw_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ESP_LOGV(TAG, "static_ip=%s gateway=%s", network::IPAddress(WiFi.localIP()).str_to(ip_buf), + network::IPAddress(WiFi.gatewayIP()).str_to(gw_buf)); + s_sta_state = LTWiFiSTAState::CONNECTED; +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { - // auto it = info.got_ip.ip_info; ESP_LOGV(TAG, "Got IPv6"); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } +#endif break; } case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { ESP_LOGV(TAG, "Lost IP"); + // Don't change state to IDLE - let the disconnect event handle that break; } case ESPHOME_EVENT_ID_WIFI_AP_START: { @@ -355,15 +556,21 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ break; } case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { - auto it = info.wifi_sta_connected; - auto &mac = it.bssid; - ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + auto &it = event->data.sta_connected; + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.bssid, mac_buf); + ESP_LOGV(TAG, "AP client connected MAC=%s", mac_buf); +#endif break; } case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: { - auto it = info.wifi_sta_disconnected; - auto &mac = it.bssid; - ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + auto &it = event->data.sta_disconnected; + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.bssid, mac_buf); + ESP_LOGV(TAG, "AP client disconnected MAC=%s", mac_buf); +#endif break; } case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: { @@ -371,8 +578,12 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ break; } case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: { - auto it = info.wifi_ap_probereqrecved; - ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + auto &it = event->data.ap_probe_req; + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(it.mac, mac_buf); + ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", mac_buf, it.rssi); +#endif break; } default: @@ -380,23 +591,35 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } } void WiFiComponent::wifi_pre_setup_() { + // Create event queue for thread-safe event handling + // Events are pushed from WiFi callback thread and processed in main loop + s_event_queue = xQueueCreate(EVENT_QUEUE_SIZE, sizeof(LTWiFiEvent *)); + if (s_event_queue == nullptr) { + ESP_LOGE(TAG, "Failed to create event queue"); + return; + } + auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2); WiFi.onEvent(f); // Make sure WiFi is in clean state before anything starts this->wifi_mode_(false, false); } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { - auto status = WiFi.status(); - if (status == WL_CONNECTED) { - return WiFiSTAConnectStatus::CONNECTED; - } else if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) { - return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; - } else if (status == WL_NO_SSID_AVAIL) { - return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; - } else if (s_sta_connecting) { - return WiFiSTAConnectStatus::CONNECTING; + // Use state machine instead of querying WiFi.status() directly + // State is updated in main loop from queued events, ensuring thread safety + switch (s_sta_state) { + case LTWiFiSTAState::CONNECTED: + return WiFiSTAConnectStatus::CONNECTED; + case LTWiFiSTAState::ERROR_NOT_FOUND: + return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; + case LTWiFiSTAState::ERROR_FAILED: + return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; + case LTWiFiSTAState::CONNECTING: + return WiFiSTAConnectStatus::CONNECTING; + case LTWiFiSTAState::IDLE: + default: + return WiFiSTAConnectStatus::IDLE; } - return WiFiSTAConnectStatus::IDLE; } bool WiFiComponent::wifi_scan_start_(bool passive) { // enable STA @@ -414,6 +637,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } void WiFiComponent::wifi_scan_done_callback_() { this->scan_result_.clear(); + this->scan_done_ = true; int16_t num = WiFi.scanComplete(); if (num < 0) @@ -432,7 +656,11 @@ void WiFiComponent::wifi_scan_done_callback_() { ssid.length() == 0); } WiFi.scanDelete(); - this->scan_done_ = true; +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } +#endif } #ifdef USE_WIFI_AP @@ -468,13 +696,18 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { yield(); return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), - ap.get_channel().value_or(1), ap.get_hidden()); + ap.has_channel() ? ap.get_channel() : 1, ap.get_hidden()); } network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; } #endif // USE_WIFI_AP -bool WiFiComponent::wifi_disconnect_() { return WiFi.disconnect(); } +bool WiFiComponent::wifi_disconnect_() { + // Reset state first so disconnect events aren't ignored + // and wifi_sta_connect_status_() returns IDLE instead of CONNECTING + s_sta_state = LTWiFiSTAState::IDLE; + return WiFi.disconnect(); +} bssid_t WiFiComponent::wifi_bssid() { bssid_t bssid{}; @@ -486,15 +719,43 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + // TODO: Find direct LibreTiny API to avoid Arduino String allocation + String ssid = WiFi.SSID(); + size_t len = std::min(static_cast(ssid.length()), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), ssid.c_str(), len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; } -void WiFiComponent::wifi_loop_() {} +void WiFiComponent::wifi_loop_() { + // Process all pending events from the queue + if (s_event_queue == nullptr) { + return; + } -} // namespace wifi -} // namespace esphome + // Check for dropped events due to queue overflow + if (s_event_queue_overflow_count > 0) { + ESP_LOGW(TAG, "Event queue overflow, %" PRIu32 " events dropped", s_event_queue_overflow_count); + s_event_queue_overflow_count = 0; + } + while (true) { + LTWiFiEvent *event; + if (xQueueReceive(s_event_queue, &event, 0) != pdTRUE) { + // No more events + break; + } + + wifi_process_event_(event); + delete event; // NOLINT(cppcoreguidelines-owning-memory) + } +} + +} // namespace esphome::wifi #endif // USE_LIBRETINY #endif diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 91766e8ab5..1aa737ff4a 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -1,4 +1,3 @@ - #include "wifi_component.h" #ifdef USE_WIFI @@ -15,22 +14,29 @@ #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace wifi { +namespace esphome::wifi { static const char *const TAG = "wifi_pico_w"; +// Track previous state for detecting changes +static bool s_sta_was_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_sta_had_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + bool WiFiComponent::wifi_mode_(optional sta, optional ap) { if (sta.has_value()) { if (sta.value()) { cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_STA, true, CYW43_COUNTRY_WORLDWIDE); } } + + bool ap_state = false; if (ap.has_value()) { if (ap.value()) { cyw43_wifi_set_up(&cyw43_state, CYW43_ITF_AP, true, CYW43_COUNTRY_WORLDWIDE); + ap_state = true; } } + this->ap_started_ = ap_state; return true; } @@ -48,10 +54,18 @@ bool WiFiComponent::wifi_apply_power_save_() { break; } int ret = cyw43_wifi_pm(&cyw43_state, pm); - return ret == 0; + bool success = ret == 0; +#ifdef USE_WIFI_LISTENERS + if (success) { + for (auto *listener : this->power_save_listeners_) { + listener->on_wifi_power_save(this->power_save_); + } + } +#endif + return success; } -// TODO: The driver doesnt seem to have an API for this +// TODO: The driver doesn't seem to have an API for this bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; } bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { @@ -178,7 +192,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { } #endif - WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1)); + WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.has_channel() ? ap.get_channel() : 1); return true; } @@ -200,6 +214,14 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + // TODO: Find direct CYW43 API to avoid Arduino String allocation + String ssid = WiFi.SSID(); + size_t len = std::min(static_cast(ssid.length()), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), ssid.c_str(), len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } @@ -219,16 +241,81 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { } void WiFiComponent::wifi_loop_() { + // Handle scan completion if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { this->scan_done_ = true; ESP_LOGV(TAG, "Scan done"); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } +#endif + } + + // Poll for connection state changes + // The arduino-pico WiFi library doesn't have event callbacks like ESP8266/ESP32, + // so we need to poll the link status to detect state changes + auto status = cyw43_tcpip_link_status(&cyw43_state, CYW43_ITF_STA); + bool is_connected = (status == CYW43_LINK_UP); + + // Detect connection state change + if (is_connected && !s_sta_was_connected) { + // Just connected + s_sta_was_connected = true; + ESP_LOGV(TAG, "Connected"); +#ifdef USE_WIFI_LISTENERS + String ssid = WiFi.SSID(); + bssid_t bssid = this->wifi_bssid(); + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(ssid.c_str(), ssid.length()), bssid); + } + // For static IP configurations, notify IP listeners immediately as the IP is already configured +#ifdef USE_WIFI_MANUAL_IP + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { + s_sta_had_ip = true; + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } + } +#endif +#endif + } else if (!is_connected && s_sta_was_connected) { + // Just disconnected + s_sta_was_connected = false; + s_sta_had_ip = false; + ESP_LOGV(TAG, "Disconnected"); +#ifdef USE_WIFI_LISTENERS + static constexpr uint8_t EMPTY_BSSID[6] = {}; + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); + } +#endif + } + + // Detect IP address changes (only when connected) + if (is_connected) { + bool has_ip = false; + // Check for any IP address (IPv4 or IPv6) + for (auto addr : addrList) { + has_ip = true; + break; + } + + if (has_ip && !s_sta_had_ip) { + // Just got IP address + s_sta_had_ip = true; + ESP_LOGV(TAG, "Got IP address"); +#ifdef USE_WIFI_LISTENERS + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } +#endif + } } } void WiFiComponent::wifi_pre_setup_() {} -} // namespace wifi -} // namespace esphome - +} // namespace esphome::wifi #endif #endif diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index a4da582c55..8a7f192367 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_DNS_ADDRESS, CONF_IP_ADDRESS, CONF_MAC_ADDRESS, + CONF_POWER_SAVE_MODE, CONF_SCAN_RESULTS, CONF_SSID, ENTITY_CATEGORY_DIAGNOSTIC, @@ -15,31 +16,30 @@ DEPENDENCIES = ["wifi"] wifi_info_ns = cg.esphome_ns.namespace("wifi_info") IPAddressWiFiInfo = wifi_info_ns.class_( - "IPAddressWiFiInfo", text_sensor.TextSensor, cg.PollingComponent + "IPAddressWiFiInfo", text_sensor.TextSensor, cg.Component ) ScanResultsWiFiInfo = wifi_info_ns.class_( - "ScanResultsWiFiInfo", text_sensor.TextSensor, cg.PollingComponent -) -SSIDWiFiInfo = wifi_info_ns.class_( - "SSIDWiFiInfo", text_sensor.TextSensor, cg.PollingComponent + "ScanResultsWiFiInfo", text_sensor.TextSensor, cg.Component ) +SSIDWiFiInfo = wifi_info_ns.class_("SSIDWiFiInfo", text_sensor.TextSensor, cg.Component) BSSIDWiFiInfo = wifi_info_ns.class_( - "BSSIDWiFiInfo", text_sensor.TextSensor, cg.PollingComponent + "BSSIDWiFiInfo", text_sensor.TextSensor, cg.Component ) MacAddressWifiInfo = wifi_info_ns.class_( "MacAddressWifiInfo", text_sensor.TextSensor, cg.Component ) DNSAddressWifiInfo = wifi_info_ns.class_( - "DNSAddressWifiInfo", text_sensor.TextSensor, cg.PollingComponent + "DNSAddressWifiInfo", text_sensor.TextSensor, cg.Component +) +PowerSaveModeWiFiInfo = wifi_info_ns.class_( + "PowerSaveModeWiFiInfo", text_sensor.TextSensor, cg.Component ) CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( IPAddressWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ) - .extend(cv.polling_component_schema("1s")) - .extend( + ).extend( { cv.Optional(f"address_{x}"): text_sensor.text_sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, @@ -49,22 +49,36 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_SCAN_RESULTS): text_sensor.text_sensor_schema( ScanResultsWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("60s")), + ), cv.Optional(CONF_SSID): text_sensor.text_sensor_schema( SSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ), cv.Optional(CONF_BSSID): text_sensor.text_sensor_schema( BSSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ), cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ), cv.Optional(CONF_DNS_ADDRESS): text_sensor.text_sensor_schema( DNSAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ).extend(cv.polling_component_schema("1s")), + ), + cv.Optional(CONF_POWER_SAVE_MODE): text_sensor.text_sensor_schema( + PowerSaveModeWiFiInfo, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), } ) +# Keys that require WiFi listeners +_NETWORK_INFO_KEYS = { + CONF_SSID, + CONF_BSSID, + CONF_IP_ADDRESS, + CONF_DNS_ADDRESS, + CONF_SCAN_RESULTS, + CONF_POWER_SAVE_MODE, +} + async def setup_conf(config, key): if key in config: @@ -74,6 +88,10 @@ async def setup_conf(config, key): async def to_code(config): + # Request WiFi listeners for any sensor that needs them + if _NETWORK_INFO_KEYS.intersection(config): + wifi.request_wifi_listeners() + await setup_conf(config, CONF_SSID) await setup_conf(config, CONF_BSSID) await setup_conf(config, CONF_MAC_ADDRESS) @@ -81,6 +99,7 @@ async def to_code(config): await setup_conf(config, CONF_SCAN_RESULTS) wifi.request_wifi_scan_results() await setup_conf(config, CONF_DNS_ADDRESS) + await setup_conf(config, CONF_POWER_SAVE_MODE) if conf := config.get(CONF_IP_ADDRESS): wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS]) await cg.register_component(wifi_info, config[CONF_IP_ADDRESS]) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 2612e4af8d..2c0e66eeaf 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -2,18 +2,193 @@ #ifdef USE_WIFI #include "esphome/core/log.h" -namespace esphome { -namespace wifi_info { +#ifdef USE_ESP8266 +#include +#endif + +namespace esphome::wifi_info { static const char *const TAG = "wifi_info"; +#ifdef USE_WIFI_LISTENERS + +static constexpr size_t MAX_STATE_LENGTH = 255; + +/******************** + * IPAddressWiFiInfo + *******************/ + +void IPAddressWiFiInfo::setup() { wifi::global_wifi_component->add_ip_state_listener(this); } + void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "IP Address", this); } -void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } -void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } -void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } -void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); } + +void IPAddressWiFiInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) { + char buf[network::IP_ADDRESS_BUFFER_SIZE]; + ips[0].str_to(buf); + this->publish_state(buf); + uint8_t sensor = 0; + for (const auto &ip : ips) { + if (ip.is_set()) { + if (this->ip_sensors_[sensor] != nullptr) { + ip.str_to(buf); + this->ip_sensors_[sensor]->publish_state(buf); + } + sensor++; + } + } +} + +/********************* + * DNSAddressWifiInfo + ********************/ + +void DNSAddressWifiInfo::setup() { wifi::global_wifi_component->add_ip_state_listener(this); } + void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "DNS Address", this); } -} // namespace wifi_info -} // namespace esphome +void DNSAddressWifiInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) { + // IP_ADDRESS_BUFFER_SIZE (40) = max IP (39) + null; space reuses first null's slot + char buf[network::IP_ADDRESS_BUFFER_SIZE * 2]; + dns1.str_to(buf); + size_t len1 = strlen(buf); + buf[len1] = ' '; + dns2.str_to(buf + len1 + 1); + this->publish_state(buf); +} + +/********************** + * ScanResultsWiFiInfo + *********************/ + +void ScanResultsWiFiInfo::setup() { wifi::global_wifi_component->add_scan_results_listener(this); } + +void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "Scan Results", this); } + +// Format: "SSID: -XXdB\n" - caller must ensure ssid_len + 9 bytes available in buffer +static char *format_scan_entry(char *buf, const char *ssid, size_t ssid_len, int8_t rssi) { + memcpy(buf, ssid, ssid_len); + buf += ssid_len; + *buf++ = ':'; + *buf++ = ' '; + buf = int8_to_str(buf, rssi); + *buf++ = 'd'; + *buf++ = 'B'; + *buf++ = '\n'; + return buf; +} + +void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t &results) { + char buf[MAX_STATE_LENGTH + 1]; + char *ptr = buf; + const char *end = buf + MAX_STATE_LENGTH; + + for (const auto &scan : results) { + if (scan.get_is_hidden()) + continue; + const std::string &ssid = scan.get_ssid(); + // Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9 + if (ptr + ssid.size() + 9 > end) + break; + ptr = format_scan_entry(ptr, ssid.c_str(), ssid.size(), scan.get_rssi()); + } + + *ptr = '\0'; + this->publish_state(buf); +} + +/*************** + * SSIDWiFiInfo + **************/ + +void SSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_listener(this); } + +void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "SSID", this); } + +void SSIDWiFiInfo::on_wifi_connect_state(StringRef ssid, std::span bssid) { + this->publish_state(ssid.c_str(), ssid.size()); +} + +/**************** + * BSSIDWiFiInfo + ***************/ + +void BSSIDWiFiInfo::setup() { wifi::global_wifi_component->add_connect_state_listener(this); } + +void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "BSSID", this); } + +void BSSIDWiFiInfo::on_wifi_connect_state(StringRef ssid, std::span bssid) { + char buf[18] = "unknown"; + if (mac_address_is_valid(bssid.data())) { + format_mac_addr_upper(bssid.data(), buf); + } + this->publish_state(buf); +} + +/************************ + * PowerSaveModeWiFiInfo + ***********************/ + +void PowerSaveModeWiFiInfo::setup() { wifi::global_wifi_component->add_power_save_listener(this); } + +void PowerSaveModeWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WiFi Power Save Mode", this); } + +void PowerSaveModeWiFiInfo::on_wifi_power_save(wifi::WiFiPowerSaveMode mode) { +#ifdef USE_ESP8266 +#define MODE_STR(s) static const char MODE_##s[] PROGMEM = #s + MODE_STR(NONE); + MODE_STR(LIGHT); + MODE_STR(HIGH); + MODE_STR(UNKNOWN); + + const char *mode_str_p; + switch (mode) { + case wifi::WIFI_POWER_SAVE_NONE: + mode_str_p = MODE_NONE; + break; + case wifi::WIFI_POWER_SAVE_LIGHT: + mode_str_p = MODE_LIGHT; + break; + case wifi::WIFI_POWER_SAVE_HIGH: + mode_str_p = MODE_HIGH; + break; + default: + mode_str_p = MODE_UNKNOWN; + break; + } + + char mode_str[8]; + strncpy_P(mode_str, mode_str_p, sizeof(mode_str)); + mode_str[sizeof(mode_str) - 1] = '\0'; +#undef MODE_STR +#else + const char *mode_str; + switch (mode) { + case wifi::WIFI_POWER_SAVE_NONE: + mode_str = "NONE"; + break; + case wifi::WIFI_POWER_SAVE_LIGHT: + mode_str = "LIGHT"; + break; + case wifi::WIFI_POWER_SAVE_HIGH: + mode_str = "HIGH"; + break; + default: + mode_str = "UNKNOWN"; + break; + } +#endif + this->publish_state(mode_str); +} + +#endif + +/********************* + * MacAddressWifiInfo + ********************/ + +void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "MAC Address", this); } + +} // namespace esphome::wifi_info #endif diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 04889d6bb3..6beb1372f5 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -2,134 +2,90 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/wifi/wifi_component.h" #ifdef USE_WIFI #include +#include -namespace esphome { -namespace wifi_info { +namespace esphome::wifi_info { -static constexpr size_t MAX_STATE_LENGTH = 255; - -class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +#ifdef USE_WIFI_LISTENERS +class IPAddressWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { public: - void update() override { - auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses(); - if (ips != this->last_ips_) { - this->last_ips_ = ips; - this->publish_state(ips[0].str()); - uint8_t sensor = 0; - for (auto &ip : ips) { - if (ip.is_set()) { - if (this->ip_sensors_[sensor] != nullptr) { - this->ip_sensors_[sensor]->publish_state(ip.str()); - } - sensor++; - } - } - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } + // WiFiIPStateListener interface + void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) override; + protected: - network::IPAddresses last_ips_; std::array ip_sensors_; }; -class DNSAddressWifiInfo : public PollingComponent, public text_sensor::TextSensor { +class DNSAddressWifiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener { public: - void update() override { - auto dns_one = wifi::global_wifi_component->get_dns_address(0); - auto dns_two = wifi::global_wifi_component->get_dns_address(1); + void setup() override; + void dump_config() override; - std::string dns_results = dns_one.str() + " " + dns_two.str(); + // WiFiIPStateListener interface + void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, + const network::IPAddress &dns2) override; +}; - if (dns_results != this->last_results_) { - this->last_results_ = dns_results; - this->publish_state(dns_results); - } - } +class ScanResultsWiFiInfo final : public Component, + public text_sensor::TextSensor, + public wifi::WiFiScanResultsListener { + public: + void setup() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } void dump_config() override; - protected: - std::string last_results_; + // WiFiScanResultsListener interface + void on_wifi_scan_results(const wifi::wifi_scan_vector_t &results) override; }; -class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class SSIDWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { public: - void update() override { - std::string scan_results; - for (auto &scan : wifi::global_wifi_component->get_scan_result()) { - if (scan.get_is_hidden()) - continue; - - scan_results += scan.get_ssid(); - scan_results += ": "; - scan_results += esphome::to_string(scan.get_rssi()); - scan_results += "dB\n"; - } - - // There's a limit of 255 characters per state. - // Longer states just don't get sent so we truncate it. - if (scan_results.length() > MAX_STATE_LENGTH) { - scan_results.resize(MAX_STATE_LENGTH); - } - if (this->last_scan_results_ != scan_results) { - this->last_scan_results_ = scan_results; - this->publish_state(scan_results); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; - protected: - std::string last_scan_results_; + // WiFiConnectStateListener interface + void on_wifi_connect_state(StringRef ssid, std::span bssid) override; }; -class SSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class BSSIDWiFiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiConnectStateListener { public: - void update() override { - std::string ssid = wifi::global_wifi_component->wifi_ssid(); - if (this->last_ssid_ != ssid) { - this->last_ssid_ = ssid; - this->publish_state(this->last_ssid_); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; - protected: - std::string last_ssid_; + // WiFiConnectStateListener interface + void on_wifi_connect_state(StringRef ssid, std::span bssid) override; }; -class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { +class PowerSaveModeWiFiInfo final : public Component, + public text_sensor::TextSensor, + public wifi::WiFiPowerSaveListener { public: - void update() override { - wifi::bssid_t bssid = wifi::global_wifi_component->wifi_bssid(); - if (memcmp(bssid.data(), last_bssid_.data(), 6) != 0) { - std::copy(bssid.begin(), bssid.end(), last_bssid_.begin()); - char buf[18]; - format_mac_addr_upper(bssid.data(), buf); - this->publish_state(buf); - } - } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void setup() override; void dump_config() override; - protected: - wifi::bssid_t last_bssid_; + // WiFiPowerSaveListener interface + void on_wifi_power_save(wifi::WiFiPowerSaveMode mode) override; }; - -class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { - public: - void setup() override { this->publish_state(get_mac_address_pretty()); } - void dump_config() override; -}; - -} // namespace wifi_info -} // namespace esphome +#endif + +class MacAddressWifiInfo final : public Component, public text_sensor::TextSensor { + public: + void setup() override { + char mac_s[18]; + this->publish_state(get_mac_address_pretty_into_buffer(mac_s)); + } + void dump_config() override; +}; + +} // namespace esphome::wifi_info #endif diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py index 99b51adea0..82cb90c745 100644 --- a/esphome/components/wifi_signal/sensor.py +++ b/esphome/components/wifi_signal/sensor.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import sensor +from esphome.components import sensor, wifi import esphome.config_validation as cv from esphome.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, @@ -25,5 +25,6 @@ CONFIG_SCHEMA = sensor.sensor_schema( async def to_code(config): + wifi.request_wifi_listeners() var = await sensor.new_sensor(config) await cg.register_component(var, config) diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.cpp b/esphome/components/wifi_signal/wifi_signal_sensor.cpp index 4347295421..11d816a909 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.cpp +++ b/esphome/components/wifi_signal/wifi_signal_sensor.cpp @@ -2,13 +2,11 @@ #ifdef USE_WIFI #include "esphome/core/log.h" -namespace esphome { -namespace wifi_signal { +namespace esphome::wifi_signal { static const char *const TAG = "wifi_signal.sensor"; void WiFiSignalSensor::dump_config() { LOG_SENSOR("", "WiFi Signal", this); } -} // namespace wifi_signal -} // namespace esphome +} // namespace esphome::wifi_signal #endif diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.h b/esphome/components/wifi_signal/wifi_signal_sensor.h index 5cfd19b523..2e1f8cbb2b 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.h +++ b/esphome/components/wifi_signal/wifi_signal_sensor.h @@ -2,20 +2,37 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/wifi/wifi_component.h" #ifdef USE_WIFI -namespace esphome { -namespace wifi_signal { +#include +namespace esphome::wifi_signal { +#ifdef USE_WIFI_LISTENERS +class WiFiSignalSensor : public sensor::Sensor, public PollingComponent, public wifi::WiFiConnectStateListener { +#else class WiFiSignalSensor : public sensor::Sensor, public PollingComponent { +#endif public: - void update() override { this->publish_state(wifi::global_wifi_component->wifi_rssi()); } +#ifdef USE_WIFI_LISTENERS + void setup() override { wifi::global_wifi_component->add_connect_state_listener(this); } +#endif + void update() override { + int8_t rssi = wifi::global_wifi_component->wifi_rssi(); + if (rssi != wifi::WIFI_RSSI_DISCONNECTED) { + this->publish_state(rssi); + } + } void dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + +#ifdef USE_WIFI_LISTENERS + // WiFiConnectStateListener interface - update RSSI immediately on connect + void on_wifi_connect_state(StringRef ssid, std::span bssid) override { this->update(); } +#endif }; -} // namespace wifi_signal -} // namespace esphome +} // namespace esphome::wifi_signal #endif diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp index 2de6f0d2e3..7810a40ae1 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -131,15 +131,21 @@ void Wireguard::update() { } void Wireguard::dump_config() { - ESP_LOGCONFIG(TAG, "WireGuard:"); - ESP_LOGCONFIG(TAG, " Address: %s", this->address_.c_str()); - ESP_LOGCONFIG(TAG, " Netmask: %s", this->netmask_.c_str()); - ESP_LOGCONFIG(TAG, " Private Key: " LOG_SECRET("%s"), mask_key(this->private_key_).c_str()); - ESP_LOGCONFIG(TAG, " Peer Endpoint: " LOG_SECRET("%s"), this->peer_endpoint_.c_str()); - ESP_LOGCONFIG(TAG, " Peer Port: " LOG_SECRET("%d"), this->peer_port_); - ESP_LOGCONFIG(TAG, " Peer Public Key: " LOG_SECRET("%s"), this->peer_public_key_.c_str()); - ESP_LOGCONFIG(TAG, " Peer Pre-shared Key: " LOG_SECRET("%s"), - (!this->preshared_key_.empty() ? mask_key(this->preshared_key_).c_str() : "NOT IN USE")); + // clang-format off + ESP_LOGCONFIG( + TAG, + "WireGuard:\n" + " Address: %s\n" + " Netmask: %s\n" + " Private Key: " LOG_SECRET("%s") "\n" + " Peer Endpoint: " LOG_SECRET("%s") "\n" + " Peer Port: " LOG_SECRET("%d") "\n" + " Peer Public Key: " LOG_SECRET("%s") "\n" + " Peer Pre-shared Key: " LOG_SECRET("%s"), + this->address_.c_str(), this->netmask_.c_str(), mask_key(this->private_key_).c_str(), + this->peer_endpoint_.c_str(), this->peer_port_, this->peer_public_key_.c_str(), + (!this->preshared_key_.empty() ? mask_key(this->preshared_key_).c_str() : "NOT IN USE")); + // clang-format on ESP_LOGCONFIG(TAG, " Peer Allowed IPs:"); for (auto &allowed_ip : this->allowed_ips_) { ESP_LOGCONFIG(TAG, " - %s/%s", std::get<0>(allowed_ip).c_str(), std::get<1>(allowed_ip).c_str()); diff --git a/esphome/components/wl_134/wl_134.cpp b/esphome/components/wl_134/wl_134.cpp index 403f8bd1b3..20a145d183 100644 --- a/esphome/components/wl_134/wl_134.cpp +++ b/esphome/components/wl_134/wl_134.cpp @@ -68,12 +68,15 @@ Wl134Component::Rfid134Error Wl134Component::read_packet_() { reading.reserved1 = this->hex_lsb_ascii_to_uint64_(&(packet[RFID134_PACKET_RESERVED1]), RFID134_PACKET_CHECKSUM - RFID134_PACKET_RESERVED1); - ESP_LOGV(TAG, "Tag id: %012lld", reading.id); - ESP_LOGV(TAG, "Country: %03d", reading.country); - ESP_LOGV(TAG, "isData: %s", reading.isData ? "true" : "false"); - ESP_LOGV(TAG, "isAnimal: %s", reading.isAnimal ? "true" : "false"); - ESP_LOGV(TAG, "Reserved0: %d", reading.reserved0); - ESP_LOGV(TAG, "Reserved1: %" PRId32, reading.reserved1); + ESP_LOGV(TAG, + "Tag id: %012lld\n" + "Country: %03d\n" + "isData: %s\n" + "isAnimal: %s\n" + "Reserved0: %d\n" + "Reserved1: %" PRId32, + reading.id, reading.country, reading.isData ? "true" : "false", reading.isAnimal ? "true" : "false", + reading.reserved0, reading.reserved1); char buf[20]; sprintf(buf, "%03d%012lld", reading.country, reading.id); diff --git a/esphome/components/wts01/wts01.cpp b/esphome/components/wts01/wts01.cpp index cb910d89cf..a7948c805a 100644 --- a/esphome/components/wts01/wts01.cpp +++ b/esphome/components/wts01/wts01.cpp @@ -71,17 +71,20 @@ void WTS01Sensor::process_packet_() { } // Extract temperature value - int8_t temp = this->buffer_[6]; - int32_t sign = 1; + const uint8_t raw = this->buffer_[6]; - // Handle negative temperatures - if (temp < 0) { - sign = -1; + // WTS01 encodes sign in bit 7, magnitude in bits 0-6 + const bool negative = (raw & 0x80) != 0; + const uint8_t magnitude = raw & 0x7F; + + const float decimal = static_cast(this->buffer_[7]) / 100.0f; + + float temperature = static_cast(magnitude) + decimal; + + if (negative) { + temperature = -temperature; } - // Calculate temperature (temp + decimal/100) - float temperature = static_cast(temp) + (sign * static_cast(this->buffer_[7]) / 100.0f); - ESP_LOGV(TAG, "Received new temperature: %.2f°C", temperature); this->publish_state(temperature); diff --git a/esphome/components/x9c/x9c.cpp b/esphome/components/x9c/x9c.cpp index 5cd4fba8c0..8f66c46015 100644 --- a/esphome/components/x9c/x9c.cpp +++ b/esphome/components/x9c/x9c.cpp @@ -62,14 +62,14 @@ void X9cOutput::write_state(float state) { } void X9cOutput::dump_config() { - ESP_LOGCONFIG(TAG, "X9C Potentiometer Output:"); - LOG_PIN(" Chip Select Pin: ", this->cs_pin_); - LOG_PIN(" Increment Pin: ", this->inc_pin_); - LOG_PIN(" Up/Down Pin: ", this->ud_pin_); ESP_LOGCONFIG(TAG, + "X9C Potentiometer Output:\n" " Initial Value: %f\n" " Step Delay: %d", this->initial_value_, this->step_delay_); + LOG_PIN(" Chip Select Pin: ", this->cs_pin_); + LOG_PIN(" Increment Pin: ", this->inc_pin_); + LOG_PIN(" Up/Down Pin: ", this->ud_pin_); LOG_FLOAT_OUTPUT(this); } diff --git a/esphome/components/xgzp68xx/xgzp68xx.cpp b/esphome/components/xgzp68xx/xgzp68xx.cpp index 2b0824de0a..b5b786c105 100644 --- a/esphome/components/xgzp68xx/xgzp68xx.cpp +++ b/esphome/components/xgzp68xx/xgzp68xx.cpp @@ -72,8 +72,10 @@ void XGZP68XXComponent::update() { temperature_raw = encode_uint16(data[3], data[4]); // Convert the pressure data to hPa - ESP_LOGV(TAG, "Got raw pressure=%" PRIu32 ", raw temperature=%u", pressure_raw, temperature_raw); - ESP_LOGV(TAG, "K value is %u", this->k_value_); + ESP_LOGV(TAG, + "Got raw pressure=%" PRIu32 ", raw temperature=%u\n" + "K value is %u", + pressure_raw, temperature_raw, this->k_value_); // Sign extend the pressure float pressure_in_pa = (float) (((int32_t) pressure_raw << 8) >> 8); diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 564870d74e..0018d35f1f 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -12,6 +12,9 @@ namespace xiaomi_ble { static const char *const TAG = "xiaomi_ble"; +// Maximum bytes to log in very verbose hex output (covers largest packet of ~24 bytes) +static constexpr size_t XIAOMI_MAX_LOG_BYTES = 32; + bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { // button pressed, 3 bytes, only byte 3 is used for supported devices so far if ((value_type == 0x1001) && (value_length == 3)) { @@ -263,7 +266,10 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address) { if ((raw.size() != 19) && ((raw.size() < 22) || (raw.size() > 24))) { ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size()); - ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(XIAOMI_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty_to(hex_buf, raw.data(), raw.size())); return false; } @@ -320,12 +326,17 @@ bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, c memcpy(mac_address + 4, mac_reverse + 1, 1); memcpy(mac_address + 5, mac_reverse, 1); ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed."); - ESP_LOGVV(TAG, " MAC address : %s", format_mac_address_pretty(mac_address).c_str()); - ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str()); - ESP_LOGVV(TAG, " Key : %s", format_hex_pretty(vector.key, vector.keysize).c_str()); - ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty(vector.iv, vector.ivsize).c_str()); - ESP_LOGVV(TAG, " Cipher : %s", format_hex_pretty(vector.ciphertext, vector.datasize).c_str()); - ESP_LOGVV(TAG, " Tag : %s", format_hex_pretty(vector.tag, vector.tagsize).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + format_mac_addr_upper(mac_address, mac_buf); + char hex_buf[format_hex_pretty_size(XIAOMI_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, " MAC address : %s", mac_buf); + ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty_to(hex_buf, raw.data(), raw.size())); + ESP_LOGVV(TAG, " Key : %s", format_hex_pretty_to(hex_buf, vector.key, vector.keysize)); + ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty_to(hex_buf, vector.iv, vector.ivsize)); + ESP_LOGVV(TAG, " Cipher : %s", format_hex_pretty_to(hex_buf, vector.ciphertext, vector.datasize)); + ESP_LOGVV(TAG, " Tag : %s", format_hex_pretty_to(hex_buf, vector.tag, vector.tagsize)); mbedtls_ccm_free(&ctx); return false; } @@ -341,20 +352,23 @@ bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, c raw[0] &= ~0x08; ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption passed."); - ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", format_hex_pretty(raw.data() + cipher_pos, vector.datasize).c_str(), - static_cast(raw[4])); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(XIAOMI_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", + format_hex_pretty_to(hex_buf, raw.data() + cipher_pos, vector.datasize), static_cast(raw[4])); mbedtls_ccm_free(&ctx); return true; } -bool report_xiaomi_results(const optional &result, const std::string &address) { +bool report_xiaomi_results(const optional &result, const char *address) { if (!result.has_value()) { ESP_LOGVV(TAG, "report_xiaomi_results(): no results available."); return false; } - ESP_LOGD(TAG, "Got Xiaomi %s (%s):", result->name.c_str(), address.c_str()); + ESP_LOGD(TAG, "Got Xiaomi %s (%s):", result->name.c_str(), address); if (result->temperature.has_value()) { ESP_LOGD(TAG, " Temperature: %.1f°C", *result->temperature); diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index 77fb04fd78..42609a998b 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -71,7 +71,7 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result); optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data); bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address); -bool report_xiaomi_results(const optional &result, const std::string &address); +bool report_xiaomi_results(const optional &result, const char *address); class XiaomiListener : public esp32_ble_tracker::ESPBTDeviceListener { public: diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp index 4642768f90..82a04f0d6e 100644 --- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp @@ -1,4 +1,5 @@ #include "xiaomi_cgd1.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,11 +9,14 @@ namespace xiaomi_cgd1 { static const char *const TAG = "xiaomi_cgd1"; +static constexpr size_t CGD1_BINDKEY_SIZE = 16; + void XiaomiCGD1::dump_config() { + char bindkey_hex[format_hex_pretty_size(CGD1_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi CGD1\n" " Bindkey: %s", - format_hex_pretty(this->bindkey_, 16).c_str()); + format_hex_pretty_to(bindkey_hex, this->bindkey_, CGD1_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -23,7 +27,9 @@ bool XiaomiCGD1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -42,7 +48,7 @@ bool XiaomiCGD1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) @@ -57,17 +63,7 @@ bool XiaomiCGD1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { return success; } -void XiaomiCGD1::set_bindkey(const std::string &bindkey) { - memset(bindkey_, 0, 16); - if (bindkey.size() != 32) { - return; - } - char temp[3] = {0}; - for (int i = 0; i < 16; i++) { - strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, nullptr, 16); - } -} +void XiaomiCGD1::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } } // namespace xiaomi_cgd1 } // namespace esphome diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h index 393795439b..4a34eea32a 100644 --- a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h @@ -13,7 +13,7 @@ namespace xiaomi_cgd1 { class XiaomiCGD1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; }; - void set_bindkey(const std::string &bindkey); + void set_bindkey(const char *bindkey); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp index 0dcbcbd05c..39ece3e091 100644 --- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp +++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.cpp @@ -1,4 +1,5 @@ #include "xiaomi_cgdk2.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,11 +9,14 @@ namespace xiaomi_cgdk2 { static const char *const TAG = "xiaomi_cgdk2"; +static constexpr size_t CGDK2_BINDKEY_SIZE = 16; + void XiaomiCGDK2::dump_config() { + char bindkey_hex[format_hex_pretty_size(CGDK2_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi CGDK2\n" " Bindkey: %s", - format_hex_pretty(this->bindkey_, 16).c_str()); + format_hex_pretty_to(bindkey_hex, this->bindkey_, CGDK2_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -23,7 +27,9 @@ bool XiaomiCGDK2::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -42,7 +48,7 @@ bool XiaomiCGDK2::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) @@ -57,17 +63,7 @@ bool XiaomiCGDK2::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { return success; } -void XiaomiCGDK2::set_bindkey(const std::string &bindkey) { - memset(bindkey_, 0, 16); - if (bindkey.size() != 32) { - return; - } - char temp[3] = {0}; - for (int i = 0; i < 16; i++) { - strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, nullptr, 16); - } -} +void XiaomiCGDK2::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } } // namespace xiaomi_cgdk2 } // namespace esphome diff --git a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h index 1f5ef89869..ed917e2bbd 100644 --- a/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h +++ b/esphome/components/xiaomi_cgdk2/xiaomi_cgdk2.h @@ -13,7 +13,7 @@ namespace xiaomi_cgdk2 { class XiaomiCGDK2 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; }; - void set_bindkey(const std::string &bindkey); + void set_bindkey(const char *bindkey); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp index f9fffa3f20..448592db16 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp @@ -1,4 +1,5 @@ #include "xiaomi_cgg1.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,11 +9,14 @@ namespace xiaomi_cgg1 { static const char *const TAG = "xiaomi_cgg1"; +static constexpr size_t CGG1_BINDKEY_SIZE = 16; + void XiaomiCGG1::dump_config() { + char bindkey_hex[format_hex_pretty_size(CGG1_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi CGG1\n" " Bindkey: %s", - format_hex_pretty(this->bindkey_, 16).c_str()); + format_hex_pretty_to(bindkey_hex, this->bindkey_, CGG1_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -23,7 +27,9 @@ bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -42,7 +48,7 @@ bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) @@ -57,17 +63,7 @@ bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { return success; } -void XiaomiCGG1::set_bindkey(const std::string &bindkey) { - memset(bindkey_, 0, 16); - if (bindkey.size() != 32) { - return; - } - char temp[3] = {0}; - for (int i = 0; i < 16; i++) { - strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, nullptr, 16); - } -} +void XiaomiCGG1::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } } // namespace xiaomi_cgg1 } // namespace esphome diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index 52904fd75e..c560bddd69 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -13,7 +13,7 @@ namespace xiaomi_cgg1 { class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } - void set_bindkey(const std::string &bindkey); + void set_bindkey(const char *bindkey); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp index db63beea89..8813f6479b 100644 --- a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp +++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.cpp @@ -1,4 +1,5 @@ #include "xiaomi_cgpr1.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -21,7 +22,9 @@ bool XiaomiCGPR1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -40,7 +43,7 @@ bool XiaomiCGPR1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->idle_time.has_value() && this->idle_time_ != nullptr) @@ -57,17 +60,7 @@ bool XiaomiCGPR1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { return success; } -void XiaomiCGPR1::set_bindkey(const std::string &bindkey) { - memset(bindkey_, 0, 16); - if (bindkey.size() != 32) { - return; - } - char temp[3] = {0}; - for (int i = 0; i < 16; i++) { - strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, nullptr, 16); - } -} +void XiaomiCGPR1::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } } // namespace xiaomi_cgpr1 } // namespace esphome diff --git a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h index 124f9411a1..82bbbfa58d 100644 --- a/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h +++ b/esphome/components/xiaomi_cgpr1/xiaomi_cgpr1.h @@ -16,7 +16,7 @@ class XiaomiCGPR1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } - void set_bindkey(const std::string &bindkey); + void set_bindkey(const char *bindkey); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp index 990346e01e..159b6df80b 100644 --- a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp @@ -21,7 +21,9 @@ bool XiaomiGCLS002::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -39,7 +41,7 @@ bool XiaomiGCLS002::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp index 30990b121d..e10754d832 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp @@ -22,7 +22,9 @@ bool XiaomiHHCCJCY01::parse_device(const esp32_ble_tracker::ESPBTDevice &device) ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -40,7 +42,7 @@ bool XiaomiHHCCJCY01::parse_device(const esp32_ble_tracker::ESPBTDevice &device) if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) diff --git a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.cpp b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.cpp index 2bc52b8085..028d797ac1 100644 --- a/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.cpp +++ b/esphome/components/xiaomi_hhccjcy10/xiaomi_hhccjcy10.cpp @@ -23,7 +23,8 @@ bool XiaomiHHCCJCY10::parse_device(const esp32_ble_tracker::ESPBTDevice &device) ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str_to(addr_buf)); bool success = false; for (auto &service_data : device.get_service_datas()) { diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp index 3ae29088bb..2d2447db27 100644 --- a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp @@ -19,7 +19,9 @@ bool XiaomiHHCCPOT002::parse_device(const esp32_ble_tracker::ESPBTDevice &device ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -37,7 +39,7 @@ bool XiaomiHHCCPOT002::parse_device(const esp32_ble_tracker::ESPBTDevice &device if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->moisture.has_value() && this->moisture_ != nullptr) diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp index 1efebc2849..8216a92e54 100644 --- a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp @@ -21,7 +21,9 @@ bool XiaomiJQJCY01YM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -39,7 +41,7 @@ bool XiaomiJQJCY01YM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp index a6f27c58b9..e140835d03 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp @@ -20,7 +20,9 @@ bool XiaomiLYWSD02::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -38,7 +40,7 @@ bool XiaomiLYWSD02::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp index dff1228f64..2dd60d4ecb 100644 --- a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp @@ -1,4 +1,5 @@ #include "xiaomi_lywsd02mmc.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,11 +9,14 @@ namespace xiaomi_lywsd02mmc { static const char *const TAG = "xiaomi_lywsd02mmc"; +static constexpr size_t LYWSD02MMC_BINDKEY_SIZE = 16; + void XiaomiLYWSD02MMC::dump_config() { + char bindkey_hex[format_hex_pretty_size(LYWSD02MMC_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi LYWSD02MMC\n" " Bindkey: %s", - format_hex_pretty(this->bindkey_, 16).c_str()); + format_hex_pretty_to(bindkey_hex, this->bindkey_, LYWSD02MMC_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -23,7 +27,9 @@ bool XiaomiLYWSD02MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -42,7 +48,7 @@ bool XiaomiLYWSD02MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) @@ -57,17 +63,7 @@ bool XiaomiLYWSD02MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device return success; } -void XiaomiLYWSD02MMC::set_bindkey(const std::string &bindkey) { - memset(this->bindkey_, 0, 16); - if (bindkey.size() != 32) { - return; - } - char temp[3] = {0}; - for (int i = 0; i < 16; i++) { - strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - this->bindkey_[i] = std::strtoul(temp, nullptr, 16); - } -} +void XiaomiLYWSD02MMC::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } } // namespace xiaomi_lywsd02mmc } // namespace esphome diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h index e1e0fcae40..968604fee6 100644 --- a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h @@ -13,7 +13,7 @@ namespace xiaomi_lywsd02mmc { class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { this->address_ = address; } - void set_bindkey(const std::string &bindkey); + void set_bindkey(const char *bindkey); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp index fb0165a21f..b11bbdc40c 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp @@ -1,4 +1,5 @@ #include "xiaomi_lywsd03mmc.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,11 +9,14 @@ namespace xiaomi_lywsd03mmc { static const char *const TAG = "xiaomi_lywsd03mmc"; +static constexpr size_t LYWSD03MMC_BINDKEY_SIZE = 16; + void XiaomiLYWSD03MMC::dump_config() { + char bindkey_hex[format_hex_pretty_size(LYWSD03MMC_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi LYWSD03MMC\n" " Bindkey: %s", - format_hex_pretty(this->bindkey_, 16).c_str()); + format_hex_pretty_to(bindkey_hex, this->bindkey_, LYWSD03MMC_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -23,7 +27,9 @@ bool XiaomiLYWSD03MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -46,7 +52,7 @@ bool XiaomiLYWSD03MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 *res->humidity = trunc(*res->humidity); } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) @@ -61,17 +67,7 @@ bool XiaomiLYWSD03MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device return success; } -void XiaomiLYWSD03MMC::set_bindkey(const std::string &bindkey) { - memset(bindkey_, 0, 16); - if (bindkey.size() != 32) { - return; - } - char temp[3] = {0}; - for (int i = 0; i < 16; i++) { - strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, nullptr, 16); - } -} +void XiaomiLYWSD03MMC::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } } // namespace xiaomi_lywsd03mmc } // namespace esphome diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h index 3c7907479a..d890e5ed12 100644 --- a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h @@ -13,7 +13,7 @@ namespace xiaomi_lywsd03mmc { class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; }; - void set_bindkey(const std::string &bindkey); + void set_bindkey(const char *bindkey); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp index 749ca83afb..65991ffa0e 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp @@ -20,7 +20,9 @@ bool XiaomiLYWSDCGQ::parse_device(const esp32_ble_tracker::ESPBTDevice &device) ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -38,7 +40,7 @@ bool XiaomiLYWSDCGQ::parse_device(const esp32_ble_tracker::ESPBTDevice &device) if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) diff --git a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.cpp b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.cpp index e613faec7e..1097b9c1e8 100644 --- a/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.cpp +++ b/esphome/components/xiaomi_mhoc303/xiaomi_mhoc303.cpp @@ -20,7 +20,9 @@ bool XiaomiMHOC303::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -38,7 +40,7 @@ bool XiaomiMHOC303::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp index 90b654873b..10cd15ddbd 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp @@ -1,4 +1,5 @@ #include "xiaomi_mhoc401.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,11 +9,14 @@ namespace xiaomi_mhoc401 { static const char *const TAG = "xiaomi_mhoc401"; +static constexpr size_t MHOC401_BINDKEY_SIZE = 16; + void XiaomiMHOC401::dump_config() { + char bindkey_hex[format_hex_pretty_size(MHOC401_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi MHOC401\n" " Bindkey: %s", - format_hex_pretty(this->bindkey_, 16).c_str()); + format_hex_pretty_to(bindkey_hex, this->bindkey_, MHOC401_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -23,7 +27,9 @@ bool XiaomiMHOC401::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -46,7 +52,7 @@ bool XiaomiMHOC401::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 *res->humidity = trunc(*res->humidity); } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) @@ -61,17 +67,7 @@ bool XiaomiMHOC401::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { return success; } -void XiaomiMHOC401::set_bindkey(const std::string &bindkey) { - memset(bindkey_, 0, 16); - if (bindkey.size() != 32) { - return; - } - char temp[3] = {0}; - for (int i = 0; i < 16; i++) { - strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, nullptr, 16); - } -} +void XiaomiMHOC401::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } } // namespace xiaomi_mhoc401 } // namespace esphome diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h index 1acdaa88af..13547e45d9 100644 --- a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h @@ -13,7 +13,7 @@ namespace xiaomi_mhoc401 { class XiaomiMHOC401 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; }; - void set_bindkey(const std::string &bindkey); + void set_bindkey(const char *bindkey); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp index 29c9de1652..e4f77fb915 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp @@ -1,4 +1,5 @@ #include "xiaomi_miscale.h" +#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -19,7 +20,9 @@ bool XiaomiMiscale::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -30,7 +33,7 @@ bool XiaomiMiscale::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (!parse_message_(service_data.data, *res)) continue; - if (!report_results_(res, device.address_str())) + if (!report_results_(res, addr_str)) continue; if (res->weight.has_value() && this->weight_ != nullptr) @@ -61,9 +64,10 @@ optional XiaomiMiscale::parse_header_(const esp32_ble_tracker::Serv } else if (service_data.uuid == esp32_ble_tracker::ESPBTUUID::from_uint16(0x181B) && service_data.data.size() == 13) { result.version = 2; } else { + char uuid_buf[esp32_ble::UUID_STR_LEN]; ESP_LOGVV(TAG, "parse_header(): Couldn't identify scale version or data size was not correct. UUID: %s, data_size: %d", - service_data.uuid.to_string().c_str(), service_data.data.size()); + service_data.uuid.to_str(uuid_buf), service_data.data.size()); return {}; } @@ -145,13 +149,13 @@ bool XiaomiMiscale::parse_message_v2_(const std::vector &message, Parse return true; } -bool XiaomiMiscale::report_results_(const optional &result, const std::string &address) { +bool XiaomiMiscale::report_results_(const optional &result, const char *address) { if (!result.has_value()) { ESP_LOGVV(TAG, "report_results(): no results available."); return false; } - ESP_LOGD(TAG, "Got Xiaomi Miscale v%d (%s):", result->version, address.c_str()); + ESP_LOGD(TAG, "Got Xiaomi Miscale v%d (%s):", result->version, address); if (result->weight.has_value()) { ESP_LOGD(TAG, " Weight: %.2fkg", *result->weight); diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.h b/esphome/components/xiaomi_miscale/xiaomi_miscale.h index 10d308ef6c..3d793e07ac 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.h +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.h @@ -37,7 +37,7 @@ class XiaomiMiscale : public Component, public esp32_ble_tracker::ESPBTDeviceLis bool parse_message_(const std::vector &message, ParseResult &result); bool parse_message_v1_(const std::vector &message, ParseResult &result); bool parse_message_v2_(const std::vector &message, ParseResult &result); - bool report_results_(const optional &result, const std::string &address); + bool report_results_(const optional &result, const char *address); }; } // namespace xiaomi_miscale diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp index 16c0b42279..ec03c851cd 100644 --- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp @@ -1,4 +1,5 @@ #include "xiaomi_mjyd02yla.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -22,7 +23,9 @@ bool XiaomiMJYD02YLA::parse_device(const esp32_ble_tracker::ESPBTDevice &device) ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -41,7 +44,7 @@ bool XiaomiMJYD02YLA::parse_device(const esp32_ble_tracker::ESPBTDevice &device) if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->idle_time.has_value() && this->idle_time_ != nullptr) @@ -60,17 +63,7 @@ bool XiaomiMJYD02YLA::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return success; } -void XiaomiMJYD02YLA::set_bindkey(const std::string &bindkey) { - memset(bindkey_, 0, 16); - if (bindkey.size() != 32) { - return; - } - char temp[3] = {0}; - for (int i = 0; i < 16; i++) { - strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, nullptr, 16); - } -} +void XiaomiMJYD02YLA::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } } // namespace xiaomi_mjyd02yla } // namespace esphome diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h index e1b4055696..bf9dcaf844 100644 --- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h @@ -16,7 +16,7 @@ class XiaomiMJYD02YLA : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } - void set_bindkey(const std::string &bindkey); + void set_bindkey(const char *bindkey); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp index 1a8e72bd2c..a3f9325946 100644 --- a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp @@ -18,7 +18,9 @@ bool XiaomiMUE4094RT::parse_device(const esp32_ble_tracker::ESPBTDevice &device) ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -36,7 +38,7 @@ bool XiaomiMUE4094RT::parse_device(const esp32_ble_tracker::ESPBTDevice &device) if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->has_motion.has_value()) { diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp index 498e724368..ee3ad316e1 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp @@ -1,4 +1,5 @@ #include "xiaomi_rtcgq02lm.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,9 +9,12 @@ namespace xiaomi_rtcgq02lm { static const char *const TAG = "xiaomi_rtcgq02lm"; +static constexpr size_t RTCGQ02LM_BINDKEY_SIZE = 16; + void XiaomiRTCGQ02LM::dump_config() { + char bindkey_hex[format_hex_pretty_size(RTCGQ02LM_BINDKEY_SIZE)]; ESP_LOGCONFIG(TAG, "Xiaomi RTCGQ02LM"); - ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, RTCGQ02LM_BINDKEY_SIZE, '.')); #ifdef USE_BINARY_SENSOR LOG_BINARY_SENSOR(" ", "Motion", this->motion_); LOG_BINARY_SENSOR(" ", "Light", this->light_); @@ -26,7 +30,9 @@ bool XiaomiRTCGQ02LM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -46,7 +52,7 @@ bool XiaomiRTCGQ02LM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } #ifdef USE_BINARY_SENSOR @@ -73,17 +79,7 @@ bool XiaomiRTCGQ02LM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) return success; } -void XiaomiRTCGQ02LM::set_bindkey(const std::string &bindkey) { - memset(bindkey_, 0, 16); - if (bindkey.size() != 32) { - return; - } - char temp[3] = {0}; - for (int i = 0; i < 16; i++) { - strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - bindkey_[i] = std::strtoul(temp, nullptr, 16); - } -} +void XiaomiRTCGQ02LM::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } } // namespace xiaomi_rtcgq02lm } // namespace esphome diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h index ae00a28ac9..87dfc0b62b 100644 --- a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h @@ -19,7 +19,7 @@ namespace xiaomi_rtcgq02lm { class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; }; - void set_bindkey(const std::string &bindkey); + void set_bindkey(const char *bindkey); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp index b57bf5cd05..b0e02e2372 100644 --- a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp @@ -20,7 +20,9 @@ bool XiaomiWX08ZM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -38,7 +40,7 @@ bool XiaomiWX08ZM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { continue; } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->is_active.has_value()) { diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp index f8712e7fd4..50cf5f2d76 100644 --- a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.cpp @@ -1,4 +1,5 @@ #include "xiaomi_xmwsdj04mmc.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP32 @@ -8,9 +9,14 @@ namespace xiaomi_xmwsdj04mmc { static const char *const TAG = "xiaomi_xmwsdj04mmc"; +static constexpr size_t XMWSDJ04MMC_BINDKEY_SIZE = 16; + void XiaomiXMWSDJ04MMC::dump_config() { - ESP_LOGCONFIG(TAG, "Xiaomi XMWSDJ04MMC"); - ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); + char bindkey_hex[format_hex_pretty_size(XMWSDJ04MMC_BINDKEY_SIZE)]; + ESP_LOGCONFIG(TAG, + "Xiaomi XMWSDJ04MMC\n" + " Bindkey: %s", + format_hex_pretty_to(bindkey_hex, this->bindkey_, XMWSDJ04MMC_BINDKEY_SIZE, '.')); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -21,7 +27,9 @@ bool XiaomiXMWSDJ04MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &devic ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); return false; } - ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + const char *addr_str = device.address_str_to(addr_buf); + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str); bool success = false; for (auto &service_data : device.get_service_datas()) { @@ -44,7 +52,7 @@ bool XiaomiXMWSDJ04MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &devic // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 *res->humidity = trunc(*res->humidity); } - if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + if (!(xiaomi_ble::report_xiaomi_results(res, addr_str))) { continue; } if (res->temperature.has_value() && this->temperature_ != nullptr) @@ -59,17 +67,7 @@ bool XiaomiXMWSDJ04MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &devic return success; } -void XiaomiXMWSDJ04MMC::set_bindkey(const std::string &bindkey) { - memset(this->bindkey_, 0, 16); - if (bindkey.size() != 32) { - return; - } - char temp[3] = {0}; - for (int i = 0; i < 16; i++) { - strncpy(temp, &(bindkey.c_str()[i * 2]), 2); - this->bindkey_[i] = std::strtoul(temp, nullptr, 16); - } -} +void XiaomiXMWSDJ04MMC::set_bindkey(const char *bindkey) { parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_)); } } // namespace xiaomi_xmwsdj04mmc } // namespace esphome diff --git a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h index ed0458ce49..22cac63059 100644 --- a/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h +++ b/esphome/components/xiaomi_xmwsdj04mmc/xiaomi_xmwsdj04mmc.h @@ -13,7 +13,7 @@ namespace xiaomi_xmwsdj04mmc { class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { this->address_ = address; } - void set_bindkey(const std::string &bindkey); + void set_bindkey(const char *bindkey); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; diff --git a/esphome/components/xl9535/xl9535.cpp b/esphome/components/xl9535/xl9535.cpp index 958fc5eede..dd6c8188eb 100644 --- a/esphome/components/xl9535/xl9535.cpp +++ b/esphome/components/xl9535/xl9535.cpp @@ -110,7 +110,9 @@ void XL9535Component::pin_mode(uint8_t pin, gpio::Flags mode) { void XL9535GPIOPin::setup() { this->pin_mode(this->flags_); } -std::string XL9535GPIOPin::dump_summary() const { return str_snprintf("%u via XL9535", 15, this->pin_); } +size_t XL9535GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via XL9535", this->pin_); +} void XL9535GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool XL9535GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } diff --git a/esphome/components/xl9535/xl9535.h b/esphome/components/xl9535/xl9535.h index 3b511fd9b3..be0e2fbd82 100644 --- a/esphome/components/xl9535/xl9535.h +++ b/esphome/components/xl9535/xl9535.h @@ -39,7 +39,7 @@ class XL9535GPIOPin : public GPIOPin { gpio::Flags get_flags() const override { return this->flags_; } void setup() override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; diff --git a/esphome/components/xpt2046/touchscreen/xpt2046.cpp b/esphome/components/xpt2046/touchscreen/xpt2046.cpp index fa99e3afa7..84d3daf823 100644 --- a/esphome/components/xpt2046/touchscreen/xpt2046.cpp +++ b/esphome/components/xpt2046/touchscreen/xpt2046.cpp @@ -59,10 +59,8 @@ void XPT2046Component::update_touches() { } void XPT2046Component::dump_config() { - ESP_LOGCONFIG(TAG, "XPT2046:"); - - LOG_PIN(" IRQ Pin: ", this->irq_pin_); ESP_LOGCONFIG(TAG, + "XPT2046:\n" " X min: %d\n" " X max: %d\n" " Y min: %d\n" @@ -73,7 +71,7 @@ void XPT2046Component::dump_config() { " threshold: %d", this->x_raw_min_, this->x_raw_max_, this->y_raw_min_, this->y_raw_max_, YESNO(this->swap_x_y_), YESNO(this->invert_x_), YESNO(this->invert_y_), this->threshold_); - + LOG_PIN(" IRQ Pin: ", this->irq_pin_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 0381fbcba9..a91d976e6b 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -21,7 +21,6 @@ from .const import ( ) CODEOWNERS = ["@tomaszduda23"] -AUTO_LOAD = ["preferences"] PrjConfValueType = bool | str | int @@ -111,32 +110,15 @@ def add_extra_script(stage: str, filename: str, path: Path) -> None: def zephyr_to_code(config): - cg.add(zephyr_ns.setup_preferences()) cg.add_build_flag("-DUSE_ZEPHYR") cg.set_cpp_standard("gnu++20") # build is done by west so bypass board checking in platformio cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards")) - # c++ support zephyr_add_prj_conf("NEWLIB_LIBC", True) - zephyr_add_prj_conf("CONFIG_FPU", True) + zephyr_add_prj_conf("FPU", True) zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True) - zephyr_add_prj_conf("CPLUSPLUS", True) - zephyr_add_prj_conf("CONFIG_STD_CPP20", True) - zephyr_add_prj_conf("LIB_CPLUSPLUS", True) - # preferences - zephyr_add_prj_conf("SETTINGS", True) - zephyr_add_prj_conf("NVS", True) - zephyr_add_prj_conf("FLASH_MAP", True) - zephyr_add_prj_conf("CONFIG_FLASH", True) - # watchdog - zephyr_add_prj_conf("WATCHDOG", True) - zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False) - # disable console - zephyr_add_prj_conf("UART_CONSOLE", False) - zephyr_add_prj_conf("CONSOLE", False, False) - # use NFC pins as GPIO - zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) + zephyr_add_prj_conf("STD_CPP20", True) # os: ***** USAGE FAULT ***** # os: Illegal load of EXC_RETURN into PC @@ -149,6 +131,14 @@ def zephyr_to_code(config): ) +def zephyr_setup_preferences(): + cg.add(zephyr_ns.setup_preferences()) + zephyr_add_prj_conf("SETTINGS", True) + zephyr_add_prj_conf("NVS", True) + zephyr_add_prj_conf("FLASH_MAP", True) + zephyr_add_prj_conf("FLASH", True) + + def _format_prj_conf_val(value: PrjConfValueType) -> str: if isinstance(value, bool): return "y" if value else "n" diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index d5427a0ebf..d7027b33f5 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -10,8 +10,10 @@ namespace esphome { +#ifdef CONFIG_WATCHDOG static int wdt_channel_id = -1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0)); +#endif void yield() { ::k_yield(); } uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); } @@ -20,10 +22,16 @@ void delayMicroseconds(uint32_t us) { ::k_usleep(us); } void delay(uint32_t ms) { ::k_msleep(ms); } void arch_init() { +#ifdef CONFIG_WATCHDOG if (device_is_ready(WDT)) { static wdt_timeout_cfg wdt_config{}; wdt_config.flags = WDT_FLAG_RESET_SOC; +#ifdef USE_ZIGBEE + // zboss thread use a lot of cpu cycles during start + wdt_config.window.max = 10000; +#else wdt_config.window.max = 2000; +#endif wdt_channel_id = wdt_install_timeout(WDT, &wdt_config); if (wdt_channel_id >= 0) { uint8_t options = 0; @@ -36,12 +44,15 @@ void arch_init() { wdt_setup(WDT, options); } } +#endif } void arch_feed_wdt() { +#ifdef CONFIG_WATCHDOG if (wdt_channel_id >= 0) { wdt_feed(WDT, wdt_channel_id); } +#endif } void arch_restart() { sys_reboot(SYS_REBOOT_COLD); } @@ -72,6 +83,7 @@ bool random_bytes(uint8_t *data, size_t len) { return true; } +#ifdef USE_NRF52 void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) mac[0] = ((NRF_FICR->DEVICEADDR[1] & 0xFFFF) >> 8) | 0xC0; mac[1] = NRF_FICR->DEVICEADDR[1] & 0xFFFF; @@ -80,7 +92,7 @@ void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parame mac[4] = NRF_FICR->DEVICEADDR[0] >> 8; mac[5] = NRF_FICR->DEVICEADDR[0]; } - +#endif } // namespace esphome void setup(); diff --git a/esphome/components/zephyr/gpio.cpp b/esphome/components/zephyr/gpio.cpp index 41b983535c..1d5b0f282b 100644 --- a/esphome/components/zephyr/gpio.cpp +++ b/esphome/components/zephyr/gpio.cpp @@ -50,25 +50,7 @@ void ZephyrGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::Inte } void ZephyrGPIOPin::setup() { - const struct device *gpio = nullptr; - if (this->pin_ < 32) { -#define GPIO0 DT_NODELABEL(gpio0) -#if DT_NODE_HAS_STATUS(GPIO0, okay) - gpio = DEVICE_DT_GET(GPIO0); -#else -#error "gpio0 is disabled" -#endif - } else { -#define GPIO1 DT_NODELABEL(gpio1) -#if DT_NODE_HAS_STATUS(GPIO1, okay) - gpio = DEVICE_DT_GET(GPIO1); -#else -#error "gpio1 is disabled" -#endif - } - if (device_is_ready(gpio)) { - this->gpio_ = gpio; - } else { + if (!device_is_ready(this->gpio_)) { ESP_LOGE(TAG, "gpio %u is not ready.", this->pin_); return; } @@ -79,23 +61,22 @@ void ZephyrGPIOPin::pin_mode(gpio::Flags flags) { if (nullptr == this->gpio_) { return; } - auto ret = gpio_pin_configure(this->gpio_, this->pin_ % 32, flags_to_mode(flags, this->inverted_, this->value_)); + auto ret = gpio_pin_configure(this->gpio_, this->pin_ % this->gpio_size_, + flags_to_mode(flags, this->inverted_, this->value_)); if (ret != 0) { ESP_LOGE(TAG, "gpio %u cannot be configured %d.", this->pin_, ret); } } -std::string ZephyrGPIOPin::dump_summary() const { - char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%u, P%u.%u", this->pin_, this->pin_ / 32, this->pin_ % 32); - return buffer; +size_t ZephyrGPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "GPIO%u, %s%u", this->pin_, this->pin_name_prefix_, this->pin_ % this->gpio_size_); } bool ZephyrGPIOPin::digital_read() { if (nullptr == this->gpio_) { return false; } - return bool(gpio_pin_get(this->gpio_, this->pin_ % 32) != this->inverted_); + return bool(gpio_pin_get(this->gpio_, this->pin_ % this->gpio_size_) != this->inverted_); } void ZephyrGPIOPin::digital_write(bool value) { @@ -105,7 +86,7 @@ void ZephyrGPIOPin::digital_write(bool value) { if (nullptr == this->gpio_) { return; } - gpio_pin_set(this->gpio_, this->pin_ % 32, value != this->inverted_ ? 1 : 0); + gpio_pin_set(this->gpio_, this->pin_ % this->gpio_size_, value != this->inverted_ ? 1 : 0); } void ZephyrGPIOPin::detach_interrupt() const { // TODO diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index 6e8f81857a..c9540f4f01 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -8,6 +8,11 @@ namespace zephyr { class ZephyrGPIOPin : public InternalGPIOPin { public: + ZephyrGPIOPin(const device *gpio, int gpio_size, const char *pin_name_prefix) { + this->gpio_ = gpio; + this->gpio_size_ = gpio_size; + this->pin_name_prefix_ = pin_name_prefix; + } void set_pin(uint8_t pin) { this->pin_ = pin; } void set_inverted(bool inverted) { this->inverted_ = inverted; } void set_flags(gpio::Flags flags) { this->flags_ = flags; } @@ -16,7 +21,7 @@ class ZephyrGPIOPin : public InternalGPIOPin { void pin_mode(gpio::Flags flags) override; bool digital_read() override; void digital_write(bool value) override; - std::string dump_summary() const override; + size_t dump_summary(char *buffer, size_t len) const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; uint8_t get_pin() const override { return this->pin_; } @@ -25,10 +30,12 @@ class ZephyrGPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - uint8_t pin_; - bool inverted_{}; - gpio::Flags flags_{}; const device *gpio_{nullptr}; + const char *pin_name_prefix_{nullptr}; + gpio::Flags flags_{}; + uint8_t pin_; + uint8_t gpio_size_{}; + bool inverted_{}; bool value_{false}; }; diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp index d702366044..08b361b8fb 100644 --- a/esphome/components/zephyr/preferences.cpp +++ b/esphome/components/zephyr/preferences.cpp @@ -1,4 +1,5 @@ #ifdef USE_ZEPHYR +#ifdef CONFIG_SETTINGS #include #include "esphome/core/preferences.h" @@ -154,3 +155,4 @@ ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const } // namespace esphome #endif +#endif diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py new file mode 100644 index 0000000000..1a017f2ab2 --- /dev/null +++ b/esphome/components/zigbee/__init__.py @@ -0,0 +1,150 @@ +from typing import Any + +from esphome import automation, core +import esphome.codegen as cg +from esphome.components.nrf52.boards import BOOTLOADER_CONFIG, Section +from esphome.components.zephyr import zephyr_add_pm_static, zephyr_data +from esphome.components.zephyr.const import KEY_BOOTLOADER +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INTERNAL +from esphome.core import CORE +from esphome.types import ConfigType + +from .const_zephyr import ( + CONF_MAX_EP_NUMBER, + CONF_ON_JOIN, + CONF_POWER_SOURCE, + CONF_WIPE_ON_BOOT, + CONF_ZIGBEE_ID, + KEY_EP_NUMBER, + KEY_ZIGBEE, + POWER_SOURCE, + ZigbeeComponent, + zigbee_ns, +) +from .zigbee_zephyr import zephyr_binary_sensor, zephyr_sensor + +CODEOWNERS = ["@tomaszduda23"] + + +def zigbee_set_core_data(config: ConfigType) -> ConfigType: + if zephyr_data()[KEY_BOOTLOADER] in BOOTLOADER_CONFIG: + zephyr_add_pm_static( + [Section("empty_after_zboss_offset", 0xF4000, 0xC000, "flash_primary")] + ) + + return config + + +BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_binary_sensor) +SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(ZigbeeComponent), + cv.Optional(CONF_ON_JOIN): automation.validate_automation(single=True), + cv.Optional(CONF_WIPE_ON_BOOT, default=False): cv.All( + cv.Any( + cv.boolean, + cv.one_of(*["once"], lower=True), + ), + cv.requires_component("nrf52"), + ), + cv.Optional(CONF_POWER_SOURCE, default="DC_SOURCE"): cv.enum( + POWER_SOURCE, upper=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + zigbee_set_core_data, + cv.only_with_framework("zephyr"), +) + + +def validate_number_of_ep(config: ConfigType) -> None: + if KEY_ZIGBEE not in CORE.data: + raise cv.Invalid("At least one zigbee device need to be included") + count = len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) + if count == 1: + raise cv.Invalid( + "Single endpoint is not supported https://github.com/Koenkk/zigbee2mqtt/issues/29888" + ) + if count > CONF_MAX_EP_NUMBER: + raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER}") + + +FINAL_VALIDATE_SCHEMA = cv.All( + validate_number_of_ep, +) + + +async def to_code(config: ConfigType) -> None: + cg.add_define("USE_ZIGBEE") + if CORE.using_zephyr: + from .zigbee_zephyr import zephyr_to_code + + await zephyr_to_code(config) + + +async def setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None: + if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): + return + if CORE.using_zephyr: + from .zigbee_zephyr import zephyr_setup_binary_sensor + + await zephyr_setup_binary_sensor(entity, config) + + +async def setup_sensor(entity: cg.MockObj, config: ConfigType) -> None: + if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): + return + if CORE.using_zephyr: + from .zigbee_zephyr import zephyr_setup_sensor + + await zephyr_setup_sensor(entity, config) + + +def consume_endpoint(config: ConfigType) -> ConfigType: + if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): + return config + data: dict[str, Any] = CORE.data.setdefault(KEY_ZIGBEE, {}) + slots: list[str] = data.setdefault(KEY_EP_NUMBER, []) + slots.extend([""]) + return config + + +def validate_binary_sensor(config: ConfigType) -> ConfigType: + return consume_endpoint(config) + + +def validate_sensor(config: ConfigType) -> ConfigType: + return consume_endpoint(config) + + +ZIGBEE_ACTION_SCHEMA = automation.maybe_simple_id( + cv.Schema( + { + cv.GenerateID(): cv.use_id(ZigbeeComponent), + } + ) +) + +FactoryResetAction = zigbee_ns.class_( + "FactoryResetAction", automation.Action, cg.Parented.template(ZigbeeComponent) +) + + +@automation.register_action( + "zigbee.factory_reset", + FactoryResetAction, + ZIGBEE_ACTION_SCHEMA, +) +async def reset_zigbee_to_code( + config: ConfigType, + action_id: core.ID, + template_arg: cg.TemplateArguments, + args: list[tuple], +) -> cg.Pvariable: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/zigbee/automation.h b/esphome/components/zigbee/automation.h new file mode 100644 index 0000000000..1822e6a029 --- /dev/null +++ b/esphome/components/zigbee/automation.h @@ -0,0 +1,16 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_ZIGBEE +#ifdef USE_NRF52 +#include "zigbee_zephyr.h" +#endif +namespace esphome::zigbee { + +template class FactoryResetAction : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->factory_reset(); } +}; + +} // namespace esphome::zigbee + +#endif diff --git a/esphome/components/zigbee/const_zephyr.py b/esphome/components/zigbee/const_zephyr.py new file mode 100644 index 0000000000..8d1f229b6e --- /dev/null +++ b/esphome/components/zigbee/const_zephyr.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg + +zigbee_ns = cg.esphome_ns.namespace("zigbee") +ZigbeeComponent = zigbee_ns.class_("ZigbeeComponent", cg.Component) +BinaryAttrs = zigbee_ns.struct("BinaryAttrs") +AnalogAttrs = zigbee_ns.struct("AnalogAttrs") + +CONF_MAX_EP_NUMBER = 8 +CONF_ZIGBEE_ID = "zigbee_id" +CONF_ON_JOIN = "on_join" +CONF_WIPE_ON_BOOT = "wipe_on_boot" +CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor" +CONF_ZIGBEE_SENSOR = "zigbee_sensor" +CONF_POWER_SOURCE = "power_source" +POWER_SOURCE = { + "UNKNOWN": "ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN", + "MAINS_SINGLE_PHASE": "ZB_ZCL_BASIC_POWER_SOURCE_MAINS_SINGLE_PHASE", + "MAINS_THREE_PHASE": "ZB_ZCL_BASIC_POWER_SOURCE_MAINS_THREE_PHASE", + "BATTERY": "ZB_ZCL_BASIC_POWER_SOURCE_BATTERY", + "DC_SOURCE": "ZB_ZCL_BASIC_POWER_SOURCE_DC_SOURCE", + "EMERGENCY_MAINS_CONST": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_CONST", + "EMERGENCY_MAINS_TRANSF": "ZB_ZCL_BASIC_POWER_SOURCE_EMERGENCY_MAINS_TRANSF", +} + +# Keys for CORE.data storage +KEY_ZIGBEE = "zigbee" +KEY_EP_NUMBER = "ep_number" + +# External ZBOSS SDK types (just strings for codegen) +ZB_ZCL_BASIC_ATTRS_EXT_T = "zb_zcl_basic_attrs_ext_t" +ZB_ZCL_IDENTIFY_ATTRS_T = "zb_zcl_identify_attrs_t" + +# Cluster IDs +ZB_ZCL_CLUSTER_ID_BASIC = "ZB_ZCL_CLUSTER_ID_BASIC" +ZB_ZCL_CLUSTER_ID_IDENTIFY = "ZB_ZCL_CLUSTER_ID_IDENTIFY" +ZB_ZCL_CLUSTER_ID_BINARY_INPUT = "ZB_ZCL_CLUSTER_ID_BINARY_INPUT" +ZB_ZCL_CLUSTER_ID_ANALOG_INPUT = "ZB_ZCL_CLUSTER_ID_ANALOG_INPUT" diff --git a/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp b/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp new file mode 100644 index 0000000000..8b7aff70a8 --- /dev/null +++ b/esphome/components/zigbee/zigbee_binary_sensor_zephyr.cpp @@ -0,0 +1,37 @@ +#include "zigbee_binary_sensor_zephyr.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_BINARY_SENSOR) +#include "esphome/core/log.h" +extern "C" { +#include +#include +#include +#include +#include +} +namespace esphome::zigbee { + +static const char *const TAG = "zigbee.binary_sensor"; + +ZigbeeBinarySensor::ZigbeeBinarySensor(binary_sensor::BinarySensor *binary_sensor) : binary_sensor_(binary_sensor) {} + +void ZigbeeBinarySensor::setup() { + this->binary_sensor_->add_on_state_callback([this](bool state) { + this->cluster_attributes_->present_value = state ? ZB_TRUE : ZB_FALSE; + ESP_LOGD(TAG, "Set attribute endpoint: %d, present_value %d", this->endpoint_, + this->cluster_attributes_->present_value); + ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_BINARY_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, + ZB_ZCL_ATTR_BINARY_INPUT_PRESENT_VALUE_ID, &this->cluster_attributes_->present_value, + ZB_FALSE); + this->parent_->flush(); + }); +} + +void ZigbeeBinarySensor::dump_config() { + ESP_LOGCONFIG(TAG, + "Zigbee Binary Sensor\n" + " Endpoint: %d, present_value %u", + this->endpoint_, this->cluster_attributes_->present_value); +} + +} // namespace esphome::zigbee +#endif diff --git a/esphome/components/zigbee/zigbee_binary_sensor_zephyr.h b/esphome/components/zigbee/zigbee_binary_sensor_zephyr.h new file mode 100644 index 0000000000..aae79fa289 --- /dev/null +++ b/esphome/components/zigbee/zigbee_binary_sensor_zephyr.h @@ -0,0 +1,45 @@ +#pragma once +#include "esphome/core/defines.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_BINARY_SENSOR) +#include "esphome/components/zigbee/zigbee_zephyr.h" +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +extern "C" { +#include +#include +} + +// it should have been defined inside of sdk. It is missing though +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_INPUT_DESCRIPTION_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_BINARY_INPUT_DESCRIPTION_ID, ZB_ZCL_ATTR_TYPE_CHAR_STRING, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ + (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \ + } + +// copy of ZB_ZCL_DECLARE_BINARY_INPUT_ATTRIB_LIST + description +#define ESPHOME_ZB_ZCL_DECLARE_BINARY_INPUT_ATTRIB_LIST(attr_list, out_of_service, present_value, status_flag, \ + description) \ + ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_BINARY_INPUT) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_INPUT_OUT_OF_SERVICE_ID, (out_of_service)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_INPUT_PRESENT_VALUE_ID, (present_value)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_INPUT_STATUS_FLAG_ID, (status_flag)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_INPUT_DESCRIPTION_ID, (description)) \ + ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST + +namespace esphome::zigbee { + +class ZigbeeBinarySensor : public ZigbeeEntity, public Component { + public: + explicit ZigbeeBinarySensor(binary_sensor::BinarySensor *binary_sensor); + void set_cluster_attributes(BinaryAttrs &cluster_attributes) { this->cluster_attributes_ = &cluster_attributes; } + + void setup() override; + void dump_config() override; + + protected: + BinaryAttrs *cluster_attributes_{nullptr}; + binary_sensor::BinarySensor *binary_sensor_; +}; + +} // namespace esphome::zigbee +#endif diff --git a/esphome/components/zigbee/zigbee_sensor_zephyr.cpp b/esphome/components/zigbee/zigbee_sensor_zephyr.cpp new file mode 100644 index 0000000000..74550d6487 --- /dev/null +++ b/esphome/components/zigbee/zigbee_sensor_zephyr.cpp @@ -0,0 +1,76 @@ +#include "zigbee_sensor_zephyr.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_SENSOR) +#include "esphome/core/log.h" +extern "C" { +#include +#include +#include +#include +#include +} +namespace esphome::zigbee { + +static const char *const TAG = "zigbee.sensor"; + +ZigbeeSensor::ZigbeeSensor(sensor::Sensor *sensor) : sensor_(sensor) {} + +void ZigbeeSensor::setup() { + this->sensor_->add_on_state_callback([this](float state) { + this->cluster_attributes_->present_value = state; + ESP_LOGD(TAG, "Set attribute endpoint: %d, present_value %f", this->endpoint_, state); + ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, + ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID, + (zb_uint8_t *) &this->cluster_attributes_->present_value, ZB_FALSE); + this->parent_->flush(); + }); +} + +void ZigbeeSensor::dump_config() { + ESP_LOGCONFIG(TAG, + "Zigbee Sensor\n" + " Endpoint: %d, present_value %f", + this->endpoint_, this->cluster_attributes_->present_value); +} + +const zb_uint8_t ZB_ZCL_ANALOG_INPUT_STATUS_FLAG_MAX_VALUE = 0x0F; + +static zb_ret_t check_value_analog_server(zb_uint16_t attr_id, zb_uint8_t endpoint, + zb_uint8_t *value) { // NOLINT(readability-non-const-parameter) + zb_ret_t ret = RET_OK; + ZVUNUSED(endpoint); + + switch (attr_id) { + case ZB_ZCL_ATTR_ANALOG_INPUT_OUT_OF_SERVICE_ID: + ret = ZB_ZCL_CHECK_BOOL_VALUE(*value) ? RET_OK : RET_ERROR; + break; + case ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID: + break; + + case ZB_ZCL_ATTR_ANALOG_INPUT_STATUS_FLAG_ID: + if (*value > ZB_ZCL_ANALOG_INPUT_STATUS_FLAG_MAX_VALUE) { + ret = RET_ERROR; + } + break; + + default: + break; + } + + return ret; +} + +} // namespace esphome::zigbee + +void zb_zcl_analog_input_init_server() { + zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, + esphome::zigbee::check_value_analog_server, (zb_zcl_cluster_write_attr_hook_t) NULL, + (zb_zcl_cluster_handler_t) NULL); +} + +void zb_zcl_analog_input_init_client() { + zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, ZB_ZCL_CLUSTER_CLIENT_ROLE, + (zb_zcl_cluster_check_value_t) NULL, (zb_zcl_cluster_write_attr_hook_t) NULL, + (zb_zcl_cluster_handler_t) NULL); +} + +#endif diff --git a/esphome/components/zigbee/zigbee_sensor_zephyr.h b/esphome/components/zigbee/zigbee_sensor_zephyr.h new file mode 100644 index 0000000000..37406f21d0 --- /dev/null +++ b/esphome/components/zigbee/zigbee_sensor_zephyr.h @@ -0,0 +1,86 @@ +#pragma once + +#include "esphome/components/zigbee/zigbee_zephyr.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_SENSOR) +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +extern "C" { +#include +#include +} + +enum { + ZB_ZCL_ATTR_ANALOG_INPUT_DESCRIPTION_ID = 0x001C, + ZB_ZCL_ATTR_ANALOG_INPUT_OUT_OF_SERVICE_ID = 0x0051, + ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID = 0x0055, + ZB_ZCL_ATTR_ANALOG_INPUT_STATUS_FLAG_ID = 0x006F, + ZB_ZCL_ATTR_ANALOG_INPUT_ENGINEERING_UNITS_ID = 0x0075, +}; + +#define ZB_ZCL_ANALOG_INPUT_CLUSTER_REVISION_DEFAULT ((zb_uint16_t) 0x0001u) + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_INPUT_DESCRIPTION_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_INPUT_DESCRIPTION_ID, ZB_ZCL_ATTR_TYPE_CHAR_STRING, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ + (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \ + } + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_INPUT_OUT_OF_SERVICE_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_INPUT_OUT_OF_SERVICE_ID, ZB_ZCL_ATTR_TYPE_BOOL, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL | ZB_ZCL_ATTR_ACCESS_REPORTING, \ + (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \ + } + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_INPUT_STATUS_FLAG_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_INPUT_STATUS_FLAG_ID, ZB_ZCL_ATTR_TYPE_8BITMAP, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_REPORTING, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_INPUT_ENGINEERING_UNITS_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_INPUT_ENGINEERING_UNITS_ID, ZB_ZCL_ATTR_TYPE_16BIT_ENUM, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ + (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \ + } + +#define ESPHOME_ZB_ZCL_DECLARE_ANALOG_INPUT_ATTRIB_LIST(attr_list, out_of_service, present_value, status_flag, \ + engineering_units, description) \ + ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_ANALOG_INPUT) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_INPUT_OUT_OF_SERVICE_ID, (out_of_service)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_INPUT_PRESENT_VALUE_ID, (present_value)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_INPUT_STATUS_FLAG_ID, (status_flag)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_INPUT_ENGINEERING_UNITS_ID, (engineering_units)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_INPUT_DESCRIPTION_ID, (description)) \ + ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST + +void zb_zcl_analog_input_init_server(); +void zb_zcl_analog_input_init_client(); +#define ZB_ZCL_CLUSTER_ID_ANALOG_INPUT_SERVER_ROLE_INIT zb_zcl_analog_input_init_server +#define ZB_ZCL_CLUSTER_ID_ANALOG_INPUT_CLIENT_ROLE_INIT zb_zcl_analog_input_init_client + +namespace esphome::zigbee { + +class ZigbeeSensor : public ZigbeeEntity, public Component { + public: + explicit ZigbeeSensor(sensor::Sensor *sensor); + void set_cluster_attributes(AnalogAttrs &cluster_attributes) { this->cluster_attributes_ = &cluster_attributes; } + + void setup() override; + void dump_config() override; + + protected: + AnalogAttrs *cluster_attributes_{nullptr}; + sensor::Sensor *sensor_{nullptr}; +}; + +} // namespace esphome::zigbee +#endif diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp new file mode 100644 index 0000000000..9a421aaec1 --- /dev/null +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -0,0 +1,246 @@ +#include "zigbee_zephyr.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) +#include "esphome/core/log.h" +#include +#include + +extern "C" { +#include +#include +#include +#include +#include +} + +namespace esphome::zigbee { + +static const char *const TAG = "zigbee"; + +ZigbeeComponent *global_zigbee = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +const uint8_t IEEE_ADDR_BUF_SIZE = 17; + +void ZigbeeComponent::zboss_signal_handler_esphome(zb_bufid_t bufid) { + zb_zdo_app_signal_hdr_t *sig_hndler = nullptr; + zb_zdo_app_signal_type_t sig = zb_get_app_signal(bufid, &sig_hndler); + zb_ret_t status = ZB_GET_APP_SIGNAL_STATUS(bufid); + + switch (sig) { + case ZB_ZDO_SIGNAL_SKIP_STARTUP: + ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_SKIP_STARTUP, status: %d", status); + break; + case ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY: + ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY, status: %d", status); + break; + case ZB_ZDO_SIGNAL_LEAVE: + ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_LEAVE, status: %d", status); + break; + case ZB_BDB_SIGNAL_DEVICE_REBOOT: + ESP_LOGD(TAG, "ZB_BDB_SIGNAL_DEVICE_REBOOT, status: %d", status); + if (status == RET_OK) { + on_join_(); + } + break; + case ZB_BDB_SIGNAL_STEERING: + break; + case ZB_COMMON_SIGNAL_CAN_SLEEP: + ESP_LOGV(TAG, "ZB_COMMON_SIGNAL_CAN_SLEEP, status: %d", status); + break; + case ZB_BDB_SIGNAL_DEVICE_FIRST_START: + ESP_LOGD(TAG, "ZB_BDB_SIGNAL_DEVICE_FIRST_START, status: %d", status); + break; + case ZB_NLME_STATUS_INDICATION: + ESP_LOGD(TAG, "ZB_NLME_STATUS_INDICATION, status: %d", status); + break; + case ZB_BDB_SIGNAL_TC_REJOIN_DONE: + ESP_LOGD(TAG, "ZB_BDB_SIGNAL_TC_REJOIN_DONE, status: %d", status); + break; + default: + ESP_LOGD(TAG, "zboss_signal_handler sig: %d, status: %d", sig, status); + break; + } + + auto err = zigbee_default_signal_handler(bufid); + if (err != RET_OK) { + ESP_LOGE(TAG, "Zigbee_default_signal_handler ERROR %u [%s]", err, zb_error_to_string_get(err)); + } + + switch (sig) { + case ZB_BDB_SIGNAL_STEERING: + ESP_LOGD(TAG, "ZB_BDB_SIGNAL_STEERING, status: %d", status); + if (status == RET_OK) { + zb_ext_pan_id_t extended_pan_id; + char ieee_addr_buf[IEEE_ADDR_BUF_SIZE] = {0}; + int addr_len; + + zb_get_extended_pan_id(extended_pan_id); + addr_len = ieee_addr_to_str(ieee_addr_buf, sizeof(ieee_addr_buf), extended_pan_id); + + for (int i = 0; i < addr_len; ++i) { + if (ieee_addr_buf[i] != '0') { + on_join_(); + break; + } + } + } + break; + } + + /* All callbacks should either reuse or free passed buffers. + * If bufid == 0, the buffer is invalid (not passed). + */ + if (bufid) { + zb_buf_free(bufid); + } +} + +void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) { + zb_zcl_device_callback_param_t *p_device_cb_param = ZB_BUF_GET_PARAM(bufid, zb_zcl_device_callback_param_t); + zb_zcl_device_callback_id_t device_cb_id = p_device_cb_param->device_cb_id; + zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id; + zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id; + auto endpoint = p_device_cb_param->endpoint; + + ESP_LOGI(TAG, "Zcl_device_cb %s id %hd, cluster_id %d, attr_id %d, endpoint: %d", __func__, device_cb_id, cluster_id, + attr_id, endpoint); + + // endpoints are enumerated from 1 + if (global_zigbee->callbacks_.size() >= endpoint) { + global_zigbee->callbacks_[endpoint - 1](bufid); + return; + } + p_device_cb_param->status = RET_ERROR; +} + +void ZigbeeComponent::on_join_() { + this->defer([this]() { + ESP_LOGD(TAG, "Joined the network"); + this->join_trigger_.trigger(); + this->join_cb_.call(); + }); +} + +#ifdef USE_ZIGBEE_WIPE_ON_BOOT +void ZigbeeComponent::erase_flash_(int area) { + const struct flash_area *fap; + flash_area_open(area, &fap); + flash_area_erase(fap, 0, fap->fa_size); + flash_area_close(fap); +} +#endif + +void ZigbeeComponent::setup() { + global_zigbee = this; + auto err = settings_subsys_init(); + if (err) { + ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err); + return; + } + +#ifdef USE_ZIGBEE_WIPE_ON_BOOT + bool wipe = true; +#ifdef USE_ZIGBEE_WIPE_ON_BOOT_MAGIC + // unique hash to store preferences for this component + uint32_t hash = 88498616UL; + uint32_t wipe_value = 0; + auto wipe_pref = global_preferences->make_preference(hash, true); + if (wipe_pref.load(&wipe_value)) { + wipe = wipe_value != USE_ZIGBEE_WIPE_ON_BOOT_MAGIC; + ESP_LOGD(TAG, "Wipe value in preferences %u, in firmware %u", wipe_value, USE_ZIGBEE_WIPE_ON_BOOT_MAGIC); + } +#endif + if (wipe) { + erase_flash_(FIXED_PARTITION_ID(ZBOSS_NVRAM)); + erase_flash_(FIXED_PARTITION_ID(ZBOSS_PRODUCT_CONFIG)); + erase_flash_(FIXED_PARTITION_ID(SETTINGS_STORAGE)); +#ifdef USE_ZIGBEE_WIPE_ON_BOOT_MAGIC + wipe_value = USE_ZIGBEE_WIPE_ON_BOOT_MAGIC; + wipe_pref.save(&wipe_value); +#endif + } +#endif + + ZB_ZCL_REGISTER_DEVICE_CB(zcl_device_cb); + err = settings_load(); + if (err) { + ESP_LOGE(TAG, "Cannot load settings, err: %d", err); + return; + } + zigbee_enable(); +} + +static const char *role() { + switch (zb_get_network_role()) { + case ZB_NWK_DEVICE_TYPE_COORDINATOR: + return "coordinator"; + case ZB_NWK_DEVICE_TYPE_ROUTER: + return "router"; + case ZB_NWK_DEVICE_TYPE_ED: + return "end device"; + } + return "unknown"; +} + +static const char *get_wipe_on_boot() { +#ifdef USE_ZIGBEE_WIPE_ON_BOOT +#ifdef USE_ZIGBEE_WIPE_ON_BOOT_MAGIC + return "ONCE"; +#else + return "YES"; +#endif +#else + return "NO"; +#endif +} + +void ZigbeeComponent::dump_config() { + char ieee_addr_buf[IEEE_ADDR_BUF_SIZE] = {0}; + zb_ieee_addr_t addr; + zb_get_long_address(addr); + ieee_addr_to_str(ieee_addr_buf, sizeof(ieee_addr_buf), addr); + zb_ext_pan_id_t extended_pan_id; + char extended_pan_id_buf[IEEE_ADDR_BUF_SIZE] = {0}; + zb_get_extended_pan_id(extended_pan_id); + ieee_addr_to_str(extended_pan_id_buf, sizeof(extended_pan_id_buf), extended_pan_id); + ESP_LOGCONFIG(TAG, + "Zigbee\n" + " Wipe on boot: %s\n" + " Device is joined to the network: %s\n" + " Current channel: %d\n" + " Current page: %d\n" + " Sleep threshold: %ums\n" + " Role: %s\n" + " Long addr: 0x%s\n" + " Short addr: 0x%04X\n" + " Long pan id: 0x%s\n" + " Short pan id: 0x%04X", + get_wipe_on_boot(), YESNO(zb_zdo_joined()), zb_get_current_channel(), zb_get_current_page(), + zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), extended_pan_id_buf, + zb_get_pan_id()); +} + +static void send_attribute_report(zb_bufid_t bufid, zb_uint16_t cmd_id) { + ESP_LOGD(TAG, "Force zboss scheduler to wake and send attribute report"); + zb_buf_free(bufid); +} + +void ZigbeeComponent::flush() { this->need_flush_ = true; } + +void ZigbeeComponent::loop() { + if (this->need_flush_) { + this->need_flush_ = false; + zb_buf_get_out_delayed_ext(send_attribute_report, 0, 0); + } +} + +void ZigbeeComponent::factory_reset() { + ESP_LOGD(TAG, "Factory reset"); + ZB_SCHEDULE_APP_CALLBACK(zb_bdb_reset_via_local_action, 0); +} + +} // namespace esphome::zigbee + +extern "C" void zboss_signal_handler(zb_uint8_t param) { + esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(param); +} +#endif diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h new file mode 100644 index 0000000000..fa23907bf4 --- /dev/null +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -0,0 +1,101 @@ +#pragma once +#include "esphome/core/defines.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +extern "C" { +#include +#include +} + +// copy of ZB_DECLARE_SIMPLE_DESC. Due to https://github.com/nrfconnect/sdk-nrfxlib/pull/666 +#define ESPHOME_ZB_DECLARE_SIMPLE_DESC(ep_name, in_clusters_count, out_clusters_count) \ + typedef ZB_PACKED_PRE struct zb_af_simple_desc_##ep_name##_##in_clusters_count##_##out_clusters_count##_s { \ + zb_uint8_t endpoint; /* Endpoint */ \ + zb_uint16_t app_profile_id; /* Application profile identifier */ \ + zb_uint16_t app_device_id; /* Application device identifier */ \ + zb_bitfield_t app_device_version : 4; /* Application device version */ \ + zb_bitfield_t reserved : 4; /* Reserved */ \ + zb_uint8_t app_input_cluster_count; /* Application input cluster count */ \ + zb_uint8_t app_output_cluster_count; /* Application output cluster count */ \ + /* Application input and output cluster list */ \ + zb_uint16_t app_cluster_list[(in_clusters_count) + (out_clusters_count)]; \ + } ZB_PACKED_STRUCT zb_af_simple_desc_##ep_name##_##in_clusters_count##_##out_clusters_count##_t + +#define ESPHOME_CAT7(a, b, c, d, e, f, g) a##b##c##d##e##f##g +// needed to use ESPHOME_ZB_DECLARE_SIMPLE_DESC +#define ESPHOME_ZB_AF_SIMPLE_DESC_TYPE(ep_name, in_num, out_num) \ + ESPHOME_CAT7(zb_af_simple_desc_, ep_name, _, in_num, _, out_num, _t) + +// needed to use ESPHOME_ZB_DECLARE_SIMPLE_DESC +#define ESPHOME_ZB_ZCL_DECLARE_SIMPLE_DESC(ep_name, ep_id, in_clust_num, out_clust_num, app_device_id, ...) \ + ESPHOME_ZB_DECLARE_SIMPLE_DESC(ep_name, in_clust_num, out_clust_num); \ + ESPHOME_ZB_AF_SIMPLE_DESC_TYPE(ep_name, in_clust_num, out_clust_num) \ + simple_desc_##ep_name = {ep_id, ZB_AF_HA_PROFILE_ID, app_device_id, 0, 0, in_clust_num, out_clust_num, {__VA_ARGS__}} + +// needed to use ESPHOME_ZB_ZCL_DECLARE_SIMPLE_DESC +#define ESPHOME_ZB_HA_DECLARE_EP(ep_name, ep_id, cluster_list, in_cluster_num, out_cluster_num, report_attr_count, \ + app_device_id, ...) \ + ESPHOME_ZB_ZCL_DECLARE_SIMPLE_DESC(ep_name, ep_id, in_cluster_num, out_cluster_num, app_device_id, __VA_ARGS__); \ + ZBOSS_DEVICE_DECLARE_REPORTING_CTX(reporting_info##ep_name, report_attr_count); \ + ZB_AF_DECLARE_ENDPOINT_DESC(ep_name, ep_id, ZB_AF_HA_PROFILE_ID, 0, NULL, \ + ZB_ZCL_ARRAY_SIZE(cluster_list, zb_zcl_cluster_desc_t), cluster_list, \ + (zb_af_simple_desc_1_1_t *) &simple_desc_##ep_name, report_attr_count, \ + reporting_info##ep_name, 0, NULL) + +namespace esphome::zigbee { + +struct BinaryAttrs { + zb_bool_t out_of_service; + zb_bool_t present_value; + zb_uint8_t status_flags; + zb_uchar_t description[ZB_ZCL_MAX_STRING_SIZE]; +}; + +struct AnalogAttrs { + zb_bool_t out_of_service; + float present_value; + zb_uint8_t status_flags; + zb_uint16_t engineering_units; + zb_uchar_t description[ZB_ZCL_MAX_STRING_SIZE]; +}; + +class ZigbeeComponent : public Component { + public: + void setup() override; + void dump_config() override; + void add_callback(zb_uint8_t endpoint, std::function &&cb) { + // endpoints are enumerated from 1 + this->callbacks_[endpoint - 1] = std::move(cb); + } + void add_join_callback(std::function &&cb) { this->join_cb_.add(std::move(cb)); } + void zboss_signal_handler_esphome(zb_bufid_t bufid); + void factory_reset(); + Trigger<> *get_join_trigger() { return &this->join_trigger_; }; + void flush(); + void loop() override; + + protected: + static void zcl_device_cb(zb_bufid_t bufid); + void on_join_(); +#ifdef USE_ZIGBEE_WIPE_ON_BOOT + void erase_flash_(int area); +#endif + StaticVector, ZIGBEE_ENDPOINTS_COUNT> callbacks_; + CallbackManager join_cb_; + Trigger<> join_trigger_; + bool need_flush_{false}; +}; + +class ZigbeeEntity { + public: + void set_parent(ZigbeeComponent *parent) { this->parent_ = parent; } + void set_endpoint(zb_uint8_t endpoint) { this->endpoint_ = endpoint; } + + protected: + zb_uint8_t endpoint_{0}; + ZigbeeComponent *parent_{nullptr}; +}; + +} // namespace esphome::zigbee +#endif diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py new file mode 100644 index 0000000000..d8a2716603 --- /dev/null +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -0,0 +1,415 @@ +from datetime import datetime +import random + +from esphome import automation +import esphome.codegen as cg +from esphome.components.zephyr import zephyr_add_prj_conf +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_CENTIMETER, + UNIT_DECIBEL, + UNIT_HECTOPASCAL, + UNIT_HERTZ, + UNIT_HOUR, + UNIT_KELVIN, + UNIT_KILOMETER, + UNIT_KILOWATT, + UNIT_KILOWATT_HOURS, + UNIT_LUX, + UNIT_METER, + UNIT_MICROGRAMS_PER_CUBIC_METER, + UNIT_MILLIAMP, + UNIT_MILLIGRAMS_PER_CUBIC_METER, + UNIT_MILLIMETER, + UNIT_MILLISECOND, + UNIT_MILLIVOLT, + UNIT_MINUTE, + UNIT_OHM, + UNIT_PARTS_PER_BILLION, + UNIT_PARTS_PER_MILLION, + UNIT_PASCAL, + UNIT_PERCENT, + UNIT_SECOND, + UNIT_VOLT, + UNIT_WATT, + UNIT_WATT_HOURS, + __version__, +) +from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import ( + AssignmentExpression, + MockObj, + VariableDeclarationExpression, +) +from esphome.types import ConfigType + +from .const_zephyr import ( + CONF_ON_JOIN, + CONF_POWER_SOURCE, + CONF_WIPE_ON_BOOT, + CONF_ZIGBEE_BINARY_SENSOR, + CONF_ZIGBEE_ID, + CONF_ZIGBEE_SENSOR, + KEY_EP_NUMBER, + KEY_ZIGBEE, + POWER_SOURCE, + ZB_ZCL_BASIC_ATTRS_EXT_T, + ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, + ZB_ZCL_CLUSTER_ID_BASIC, + ZB_ZCL_CLUSTER_ID_BINARY_INPUT, + ZB_ZCL_CLUSTER_ID_IDENTIFY, + ZB_ZCL_IDENTIFY_ATTRS_T, + AnalogAttrs, + BinaryAttrs, + ZigbeeComponent, + zigbee_ns, +) + +ZigbeeBinarySensor = zigbee_ns.class_("ZigbeeBinarySensor", cg.Component) +ZigbeeSensor = zigbee_ns.class_("ZigbeeSensor", cg.Component) + +# BACnet engineering units mapping (ZCL uses BACnet unit codes) +# See: https://github.com/zigpy/zha/blob/dev/zha/application/platforms/number/bacnet.py +BACNET_UNITS = { + UNIT_CELSIUS: 62, + UNIT_KELVIN: 63, + UNIT_VOLT: 5, + UNIT_MILLIVOLT: 124, + UNIT_AMPERE: 3, + UNIT_MILLIAMP: 2, + UNIT_OHM: 4, + UNIT_WATT: 47, + UNIT_KILOWATT: 48, + UNIT_WATT_HOURS: 18, + UNIT_KILOWATT_HOURS: 19, + UNIT_PASCAL: 53, + UNIT_HECTOPASCAL: 133, + UNIT_HERTZ: 27, + UNIT_MILLIMETER: 30, + UNIT_CENTIMETER: 118, + UNIT_METER: 31, + UNIT_KILOMETER: 193, + UNIT_MILLISECOND: 159, + UNIT_SECOND: 73, + UNIT_MINUTE: 72, + UNIT_HOUR: 71, + UNIT_PARTS_PER_MILLION: 96, + UNIT_PARTS_PER_BILLION: 97, + UNIT_MICROGRAMS_PER_CUBIC_METER: 219, + UNIT_MILLIGRAMS_PER_CUBIC_METER: 218, + UNIT_LUX: 37, + UNIT_DECIBEL: 199, + UNIT_PERCENT: 98, +} +BACNET_UNIT_NO_UNITS = 95 + +zephyr_binary_sensor = cv.Schema( + { + cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(ZigbeeComponent), + cv.OnlyWith(CONF_ZIGBEE_BINARY_SENSOR, ["nrf52", "zigbee"]): cv.declare_id( + ZigbeeBinarySensor + ), + } +) + +zephyr_sensor = cv.Schema( + { + cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(ZigbeeComponent), + cv.OnlyWith(CONF_ZIGBEE_SENSOR, ["nrf52", "zigbee"]): cv.declare_id( + ZigbeeSensor + ), + } +) + + +async def zephyr_to_code(config: ConfigType) -> None: + zephyr_add_prj_conf("ZIGBEE", True) + zephyr_add_prj_conf("ZIGBEE_APP_UTILS", True) + zephyr_add_prj_conf("ZIGBEE_ROLE_END_DEVICE", True) + + zephyr_add_prj_conf("ZIGBEE_CHANNEL_SELECTION_MODE_MULTI", True) + + zephyr_add_prj_conf("CRYPTO", True) + + zephyr_add_prj_conf("NET_IPV6", False) + zephyr_add_prj_conf("NET_IP_ADDR_CHECK", False) + zephyr_add_prj_conf("NET_UDP", False) + + if config[CONF_WIPE_ON_BOOT]: + if config[CONF_WIPE_ON_BOOT] == "once": + cg.add_define( + "USE_ZIGBEE_WIPE_ON_BOOT_MAGIC", random.randint(0x000001, 0xFFFFFF) + ) + cg.add_define("USE_ZIGBEE_WIPE_ON_BOOT") + var = cg.new_Pvariable(config[CONF_ID]) + + if on_join_config := config.get(CONF_ON_JOIN): + await automation.build_automation(var.get_join_trigger(), [], on_join_config) + + await cg.register_component(var, config) + + await _attr_to_code(config) + CORE.add_job(_ctx_to_code, config) + + +async def _attr_to_code(config: ConfigType) -> None: + # Create the basic attributes structure and attribute list + basic_attrs = zigbee_new_variable("zigbee_basic_attrs", ZB_ZCL_BASIC_ATTRS_EXT_T) + zigbee_new_attr_list( + "zigbee_basic_attrib_list", + "ZB_ZCL_DECLARE_BASIC_ATTRIB_LIST_EXT", + zigbee_assign(basic_attrs.zcl_version, cg.RawExpression("ZB_ZCL_VERSION")), + zigbee_assign(basic_attrs.app_version, 0), + zigbee_assign(basic_attrs.stack_version, 0), + zigbee_assign(basic_attrs.hw_version, 0), + zigbee_set_string(basic_attrs.mf_name, "esphome"), + zigbee_set_string(basic_attrs.model_id, CORE.name), + zigbee_set_string( + basic_attrs.date_code, datetime.now().strftime("%d/%m/%y %H:%M") + ), + zigbee_assign( + basic_attrs.power_source, + cg.RawExpression(POWER_SOURCE[config[CONF_POWER_SOURCE]]), + ), + zigbee_set_string(basic_attrs.location_id, ""), + zigbee_assign( + basic_attrs.ph_env, cg.RawExpression("ZB_ZCL_BASIC_ENV_UNSPECIFIED") + ), + zigbee_set_string(basic_attrs.sw_ver, __version__), + ) + + # Create the identify attributes structure and attribute list + identify_attrs = zigbee_new_variable( + "zigbee_identify_attrs", ZB_ZCL_IDENTIFY_ATTRS_T + ) + zigbee_new_attr_list( + "zigbee_identify_attrib_list", + "ZB_ZCL_DECLARE_IDENTIFY_ATTRIB_LIST", + zigbee_assign( + identify_attrs.identify_time, + cg.RawExpression("ZB_ZCL_IDENTIFY_IDENTIFY_TIME_DEFAULT_VALUE"), + ), + ) + + +def zigbee_new_variable(name: str, type_: str) -> cg.MockObj: + """Create a global variable with the given name and type.""" + decl = VariableDeclarationExpression(type_, "", name) + CORE.add_global(decl) + return MockObj(name, ".") + + +def zigbee_assign(target: cg.MockObj, expression: cg.RawExpression | int) -> str: + """Assign an expression to a target and return a reference to it.""" + cg.add(AssignmentExpression("", "", target, expression)) + return f"&{target}" + + +def zigbee_set_string(target: cg.MockObj, value: str) -> str: + """Set a ZCL string value and return the target name (arrays decay to pointers).""" + cg.add( + cg.RawExpression( + f"ZB_ZCL_SET_STRING_VAL({target}, {cg.safe_exp(value)}, ZB_ZCL_STRING_CONST_SIZE({cg.safe_exp(value)}))" + ) + ) + return str(target) + + +def zigbee_new_attr_list(name: str, macro: str, *args: str) -> str: + """Create an attribute list using a ZBOSS macro and return the name.""" + obj = cg.RawExpression(f"{macro}({name}, {', '.join(args)})") + CORE.add_global(obj) + return name + + +class ZigbeeClusterDesc: + """Represents a Zigbee cluster descriptor for code generation.""" + + def __init__(self, cluster_id: str, attr_list_name: str | None = None) -> None: + self._cluster_id = cluster_id + self._attr_list_name = attr_list_name + + @property + def cluster_id(self) -> str: + return self._cluster_id + + @property + def has_attrs(self) -> bool: + return self._attr_list_name is not None + + def __str__(self) -> str: + role = ( + "ZB_ZCL_CLUSTER_SERVER_ROLE" + if self._attr_list_name + else "ZB_ZCL_CLUSTER_CLIENT_ROLE" + ) + if self._attr_list_name: + attr_count = f"ZB_ZCL_ARRAY_SIZE({self._attr_list_name}, zb_zcl_attr_t)" + return f"ZB_ZCL_CLUSTER_DESC({self._cluster_id}, {attr_count}, {self._attr_list_name}, {role}, ZB_ZCL_MANUF_CODE_INVALID)" + return f"ZB_ZCL_CLUSTER_DESC({self._cluster_id}, 0, NULL, {role}, ZB_ZCL_MANUF_CODE_INVALID)" + + +def zigbee_new_cluster_list( + name: str, clusters: list[ZigbeeClusterDesc] +) -> tuple[str, list[ZigbeeClusterDesc]]: + """Create a cluster list array and return its name and the clusters.""" + # Always include basic and identify clusters first + all_clusters = [ + ZigbeeClusterDesc(ZB_ZCL_CLUSTER_ID_BASIC, "zigbee_basic_attrib_list"), + ZigbeeClusterDesc(ZB_ZCL_CLUSTER_ID_IDENTIFY, "zigbee_identify_attrib_list"), + ] + all_clusters.extend(clusters) + + cluster_strs = [str(c) for c in all_clusters] + CORE.add_global( + cg.RawExpression( + f"zb_zcl_cluster_desc_t {name}[] = {{{', '.join(cluster_strs)}}}" + ) + ) + return (name, all_clusters) + + +def zigbee_register_ep( + ep_name: str, + cluster_list_name: str, + report_attr_count: int, + clusters: list[ZigbeeClusterDesc], + slot_index: int, + app_device_id: str, +) -> None: + """Register a Zigbee endpoint.""" + in_cluster_num = sum(1 for c in clusters if c.has_attrs) + out_cluster_num = len(clusters) - in_cluster_num + cluster_ids = [c.cluster_id for c in clusters] + + # Store endpoint name for device context generation + CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER][slot_index] = ep_name + + # Generate the endpoint declaration + ep_id = slot_index + 1 # Endpoints are 1-indexed + obj = cg.RawExpression( + f"ESPHOME_ZB_HA_DECLARE_EP({ep_name}, {ep_id}, {cluster_list_name}, " + f"{in_cluster_num}, {out_cluster_num}, {report_attr_count}, {app_device_id}, {', '.join(cluster_ids)})" + ) + CORE.add_global(obj) + + +@coroutine_with_priority(CoroPriority.LATE) +async def _ctx_to_code(config: ConfigType) -> None: + cg.add_define("ZIGBEE_ENDPOINTS_COUNT", len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])) + cg.add_global( + cg.RawExpression( + f"ZBOSS_DECLARE_DEVICE_CTX_EP_VA(zb_device_ctx, &{', &'.join(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])})" + ) + ) + cg.add(cg.RawExpression("ZB_AF_REGISTER_DEVICE_CTX(&zb_device_ctx)")) + + +async def zephyr_setup_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None: + CORE.add_job(_add_binary_sensor, entity, config) + + +async def zephyr_setup_sensor(entity: cg.MockObj, config: ConfigType) -> None: + CORE.add_job(_add_sensor, entity, config) + + +def _slot_index() -> int: + """Find the next available endpoint slot""" + slot = next( + (i for i, v in enumerate(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) if v == ""), None + ) + if slot is None: + raise cv.Invalid( + f"Not found empty slot, size ({len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER])})" + ) + return slot + + +async def _add_zigbee_input( + entity: cg.MockObj, + config: ConfigType, + component_key, + attrs_type, + zcl_macro: str, + cluster_id: str, + app_device_id: str, + extra_field_values: dict[str, int] | None = None, +) -> None: + slot_index = _slot_index() + + prefix = f"zigbee_ep{slot_index + 1}" + attrs_name = f"{prefix}_attrs" + attr_list_name = f"{prefix}_attrib_list" + cluster_list_name = f"{prefix}_cluster_list" + ep_name = f"{prefix}_ep" + + # Create attribute struct + attrs = zigbee_new_variable(attrs_name, attrs_type) + + # Build attribute list args + attr_args = [ + zigbee_assign(attrs.out_of_service, 0), + zigbee_assign(attrs.present_value, 0), + zigbee_assign(attrs.status_flags, 0), + ] + # Add extra field assignments (e.g., engineering_units for sensors) + if extra_field_values: + for field_name, value in extra_field_values.items(): + attr_args.append(zigbee_assign(getattr(attrs, field_name), value)) + attr_args.append(zigbee_set_string(attrs.description, config[CONF_NAME])) + + # Create attribute list + attr_list = zigbee_new_attr_list(attr_list_name, zcl_macro, *attr_args) + + # Create cluster list and register endpoint + cluster_list_name, clusters = zigbee_new_cluster_list( + cluster_list_name, + [ZigbeeClusterDesc(cluster_id, attr_list)], + ) + zigbee_register_ep( + ep_name, cluster_list_name, 2, clusters, slot_index, app_device_id + ) + + # Create ESPHome component + var = cg.new_Pvariable(config[component_key], entity) + await cg.register_component(var, {}) + + cg.add(var.set_endpoint(slot_index + 1)) + cg.add(var.set_cluster_attributes(attrs)) + + hub = await cg.get_variable(config[CONF_ZIGBEE_ID]) + cg.add(var.set_parent(hub)) + + +async def _add_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None: + await _add_zigbee_input( + entity, + config, + CONF_ZIGBEE_BINARY_SENSOR, + BinaryAttrs, + "ESPHOME_ZB_ZCL_DECLARE_BINARY_INPUT_ATTRIB_LIST", + ZB_ZCL_CLUSTER_ID_BINARY_INPUT, + "ZB_HA_SIMPLE_SENSOR_DEVICE_ID", + ) + + +async def _add_sensor(entity: cg.MockObj, config: ConfigType) -> None: + # Get BACnet engineering unit from unit_of_measurement + unit = config.get(CONF_UNIT_OF_MEASUREMENT, "") + bacnet_unit = BACNET_UNITS.get(unit, BACNET_UNIT_NO_UNITS) + + await _add_zigbee_input( + entity, + config, + CONF_ZIGBEE_SENSOR, + AnalogAttrs, + "ESPHOME_ZB_ZCL_DECLARE_ANALOG_INPUT_ATTRIB_LIST", + ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, + "ZB_HA_CUSTOM_ATTR_DEVICE_ID", + extra_field_values={"engineering_units": bacnet_unit}, + ) diff --git a/esphome/components/zwave_proxy/__init__.py b/esphome/components/zwave_proxy/__init__.py index d88f9f7041..5be05bb464 100644 --- a/esphome/components/zwave_proxy/__init__.py +++ b/esphome/components/zwave_proxy/__init__.py @@ -41,3 +41,6 @@ async def to_code(config): await cg.register_component(var, config) await uart.register_uart_device(var, config) cg.add_define("USE_ZWAVE_PROXY") + + # Request UART to wake the main loop when data arrives for low-latency processing + uart.request_wake_loop_on_rx() diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index a26a9b2335..8506b19e7f 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -1,15 +1,20 @@ #include "zwave_proxy.h" + +#ifdef USE_API + #include "esphome/components/api/api_server.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" -namespace esphome { -namespace zwave_proxy { +namespace esphome::zwave_proxy { static const char *const TAG = "zwave_proxy"; +// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512) +static constexpr size_t ZWAVE_MAX_LOG_BYTES = 168; + static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20; // GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...] static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value @@ -100,7 +105,7 @@ void ZWaveProxy::process_uart_() { this->buffer_[1] >= ZWAVE_MIN_GET_NETWORK_IDS_LENGTH && this->buffer_[0] == ZWAVE_FRAME_TYPE_START) { // Store the 4-byte Home ID, which starts at offset 4, and notify connected clients if it changed // The frame parser has already validated the checksum and ensured all bytes are present - if (this->set_home_id(&this->buffer_[4])) { + if (this->set_home_id_(&this->buffer_[4])) { this->send_homeid_changed_msg_(); } } @@ -121,10 +126,11 @@ void ZWaveProxy::process_uart_() { } void ZWaveProxy::dump_config() { + char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)]; ESP_LOGCONFIG(TAG, "Z-Wave Proxy:\n" " Home ID: %s", - format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); + format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size())); } void ZWaveProxy::api_connection_authenticated(api::APIConnection *conn) { @@ -144,6 +150,7 @@ void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::en this->api_connection_ = api_connection; ESP_LOGV(TAG, "API connection is now subscribed"); break; + case api::enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE: if (this->api_connection_ != api_connection) { ESP_LOGV(TAG, "API connection is not subscribed"); @@ -151,29 +158,47 @@ void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::en } this->api_connection_ = nullptr; break; + default: ESP_LOGW(TAG, "Unknown request type: %d", type); break; } } -bool ZWaveProxy::set_home_id(const uint8_t *new_home_id) { +bool ZWaveProxy::set_home_id_(const uint8_t *new_home_id) { if (std::memcmp(this->home_id_.data(), new_home_id, this->home_id_.size()) == 0) { ESP_LOGV(TAG, "Home ID unchanged"); return false; // No change } std::memcpy(this->home_id_.data(), new_home_id, this->home_id_.size()); - ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); + char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)]; + ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size())); this->home_id_ready_ = true; return true; // Home ID was changed } void ZWaveProxy::send_frame(const uint8_t *data, size_t length) { - if (length == 1 && data[0] == this->last_response_) { - ESP_LOGV(TAG, "Skipping sending duplicate response: 0x%02X", data[0]); + // Safety: validate pointer before any access + if (data == nullptr) { + ESP_LOGE(TAG, "Null data pointer"); return; } - ESP_LOGVV(TAG, "Sending: %s", format_hex_pretty(data, length).c_str()); + if (length == 0) { + ESP_LOGE(TAG, "Length 0"); + return; + } + + // Skip duplicate single-byte responses (ACK/NAK/CAN) + if (length == 1 && data[0] == this->last_response_) { + ESP_LOGV(TAG, "Response already sent: 0x%02X", data[0]); + return; + } + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(ZWAVE_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, "Sending: %s", format_hex_pretty_to(hex_buf, data, length)); + this->write_array(data, length); } @@ -246,7 +271,10 @@ bool ZWaveProxy::parse_byte_(uint8_t byte) { this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_NAK; } else { this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_ACK; - ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(this->buffer_.data(), this->buffer_index_).c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + char hex_buf[format_hex_pretty_size(ZWAVE_MAX_LOG_BYTES)]; +#endif + ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_index_)); frame_completed = true; } this->response_handler_(); @@ -342,5 +370,6 @@ bool ZWaveProxy::response_handler_() { ZWaveProxy *global_zwave_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace zwave_proxy -} // namespace esphome +} // namespace esphome::zwave_proxy + +#endif // USE_API diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index 20d9090d98..eb26316f49 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -1,5 +1,8 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_API + #include "esphome/components/api/api_connection.h" #include "esphome/components/api/api_pb2.h" #include "esphome/core/component.h" @@ -8,10 +11,10 @@ #include -namespace esphome { -namespace zwave_proxy { +namespace esphome::zwave_proxy { static constexpr size_t MAX_ZWAVE_FRAME_SIZE = 257; // Maximum Z-Wave frame size +static constexpr size_t ZWAVE_HOME_ID_SIZE = 4; // Z-Wave Home ID size in bytes enum ZWaveResponseTypes : uint8_t { ZWAVE_FRAME_TYPE_ACK = 0x06, @@ -57,11 +60,11 @@ class ZWaveProxy : public uart::UARTDevice, public Component { uint32_t get_home_id() { return encode_uint32(this->home_id_[0], this->home_id_[1], this->home_id_[2], this->home_id_[3]); } - bool set_home_id(const uint8_t *new_home_id); // Store a new home ID. Returns true if it changed. void send_frame(const uint8_t *data, size_t length); protected: + bool set_home_id_(const uint8_t *new_home_id); // Store a new home ID. Returns true if it changed. void send_homeid_changed_msg_(api::APIConnection *conn = nullptr); void send_simple_command_(uint8_t command_id); bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer) @@ -71,8 +74,8 @@ class ZWaveProxy : public uart::UARTDevice, public Component { // Pre-allocated message - always ready to send api::ZWaveProxyFrame outgoing_proto_msg_; - std::array buffer_; // Fixed buffer for incoming data - std::array home_id_{0, 0, 0, 0}; // Fixed buffer for home ID + std::array buffer_; // Fixed buffer for incoming data + std::array home_id_{}; // Fixed buffer for home ID // Pointers and 32-bit values (aligned together) api::APIConnection *api_connection_{nullptr}; // Current subscribed client @@ -89,5 +92,6 @@ class ZWaveProxy : public uart::UARTDevice, public Component { extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace zwave_proxy -} // namespace esphome +} // namespace esphome::zwave_proxy + +#endif // USE_API diff --git a/esphome/config.py b/esphome/config.py index e508ca585b..6f6ad4886b 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -338,21 +338,46 @@ def check_replaceme(value): ) -def _build_list_index(lst): +def _get_item_id(item: Any) -> str | Extend | Remove | None: + """Attempts to get a list item's ID""" + if not isinstance(item, dict): + return None # not a dict, can't have ID + # 1.- Check regular case: + # - id: my_id + item_id = item.get(CONF_ID) + if item_id is None and len(item) == 1: + # 2.- Check single-key dict case: + # - obj: + # id: my_id + item = next(iter(item.values())) + if isinstance(item, dict): + item_id = item.get(CONF_ID) + if isinstance(item_id, Extend): + # Remove instances of Extend so they don't overwrite the original item when merging: + del item[CONF_ID] + elif not isinstance(item_id, (str, Remove)): + return None + return item_id + + +def _build_list_index( + lst: list[Any], +) -> tuple[ + OrderedDict[str | Extend | Remove, Any], list[tuple[int, str, Any]], set[str] +]: index = OrderedDict() extensions, removals = [], set() - for item in lst: + for pos, item in enumerate(lst): if item is None: removals.add(None) continue - item_id = None - if isinstance(item, dict) and (item_id := item.get(CONF_ID)): - if isinstance(item_id, Extend): - extensions.append(item) - continue - if isinstance(item_id, Remove): - removals.add(item_id.value) - continue + item_id = _get_item_id(item) + if isinstance(item_id, Extend): + extensions.append((pos, item_id.value, item)) + continue + if isinstance(item_id, Remove): + removals.add(item_id.value) + continue if not item_id or item_id in index: # no id or duplicate -> pass through with identity-based key item_id = id(item) @@ -360,7 +385,7 @@ def _build_list_index(lst): return index, extensions, removals -def resolve_extend_remove(value, is_key=None): +def resolve_extend_remove(value: Any, is_key: bool = False) -> None: if isinstance(value, ESPLiteralValue): return # do not check inside literal blocks if isinstance(value, list): @@ -368,26 +393,16 @@ def resolve_extend_remove(value, is_key=None): if extensions or removals: # Rebuild the original list after # processing all extensions and removals - for item in extensions: - item_id = item[CONF_ID].value + for pos, item_id, item in extensions: if item_id in removals: continue old = index.get(item_id) if old is None: # Failed to find source for extension - # Find index of item to show error at correct position - i = next( - ( - i - for i, d in enumerate(value) - if d.get(CONF_ID) == item[CONF_ID] - ) - ) - with cv.prepend_path(i): + with cv.prepend_path(pos): raise cv.Invalid( f"Source for extension of ID '{item_id}' was not found." ) - item[CONF_ID] = item_id index[item_id] = merge_config(old, item) for item_id in removals: index.pop(item_id, None) @@ -995,16 +1010,22 @@ def validate_config( result.add_error(err) return result + # 1.1. Merge packages + if CONF_PACKAGES in config: + from esphome.components.packages import merge_packages + + config = merge_packages(config) + CORE.raw_config = config - # 1.1. Resolve !extend and !remove and check for REPLACEME + # 1.2. Resolve !extend and !remove and check for REPLACEME # After this step, there will not be any Extend or Remove values in the config anymore try: resolve_extend_remove(config) except vol.Invalid as err: result.add_error(err) - # 1.2. Load external_components + # 1.3. Load external_components if CONF_EXTERNAL_COMPONENTS in config: from esphome.components.external_components import do_external_components_pass diff --git a/esphome/config_validation.py b/esphome/config_validation.py index a3fd271a86..81a30cb0b7 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -71,6 +71,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + SCHEDULER_DONT_RUN, TYPE_GIT, TYPE_LOCAL, VALID_SUBSTITUTIONS_CHARACTERS, @@ -696,7 +697,16 @@ only_on_esp32 = only_on(PLATFORM_ESP32) only_on_esp8266 = only_on(PLATFORM_ESP8266) only_on_rp2040 = only_on(PLATFORM_RP2040) only_with_arduino = only_with_framework(Framework.ARDUINO) -only_with_esp_idf = only_with_framework(Framework.ESP_IDF) + + +def only_with_esp_idf(obj): + """Deprecated: use only_on_esp32 instead.""" + _LOGGER.warning( + "cv.only_with_esp_idf was deprecated in 2026.1, will change behavior in 2026.6. " + "ESP32 Arduino builds on top of ESP-IDF, so ESP-IDF features are available in both frameworks. " + "Use cv.only_on_esp32 and/or cv.only_with_arduino instead." + ) + return only_with_framework(Framework.ESP_IDF)(obj) # Adapted from: @@ -740,9 +750,10 @@ def has_at_most_one_key(*keys): if not isinstance(obj, dict): raise Invalid("expected dictionary") - number = sum(k in keys for k in obj) - if number > 1: - raise Invalid(f"Cannot specify more than one of {', '.join(keys)}.") + used = set(obj) & set(keys) + if len(used) > 1: + msg = "Cannot specify more than one of '" + "', '".join(used) + "'." + raise MultipleInvalid([Invalid(msg, path=[k]) for k in used]) return obj return validate @@ -893,7 +904,7 @@ def time_period_in_minutes_(value): def update_interval(value): if value == "never": - return 4294967295 # uint32_t max + return TimePeriodMilliseconds(milliseconds=SCHEDULER_DONT_RUN) return positive_time_period_milliseconds(value) @@ -1743,8 +1754,7 @@ class SplitDefault(Optional): def default(self): keys = [] if CORE.is_esp32: - from esphome.components.esp32 import get_esp32_variant - from esphome.components.esp32.const import VARIANT_ESP32 + from esphome.components.esp32 import VARIANT_ESP32, get_esp32_variant variant = get_esp32_variant().replace(VARIANT_ESP32, "").lower() framework = CORE.target_framework.replace("esp-", "") @@ -1971,6 +1981,41 @@ MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend( ) +# Unicode FRACTION SLASH (U+2044) - visually similar to '/' but URL-safe +FRACTION_SLASH = "\u2044" + + +def _validate_no_slash(value): + """Validate that a name does not contain '/' characters. + + The '/' character is used as a path separator in web server URLs, + so it cannot be used in entity or device names. + + During the deprecation period, '/' is automatically replaced with + the visually similar Unicode FRACTION SLASH (U+2044) character. + """ + if "/" in value: + # Remove before 2026.7.0 + new_value = value.replace("/", FRACTION_SLASH) + _LOGGER.warning( + "'%s' contains '/' which is reserved as a URL path separator. " + "Automatically replacing with '%s' (Unicode FRACTION SLASH). " + "Please update your configuration. " + "This will become an error in ESPHome 2026.7.0.", + value, + new_value, + ) + return new_value + return value + + +# Maximum length for entity, device, and area names +# This ensures web server URL IDs fit in a 280-byte buffer: +# domain(20) + "/" + device(120) + "/" + name(120) + null = 263 bytes +# Note: Must be < 255 because web_server UrlMatch uses uint8_t for length fields +NAME_MAX_LENGTH = 120 + + def _validate_entity_name(value): value = string(value) try: @@ -1981,9 +2026,28 @@ def _validate_entity_name(value): requires_friendly_name( "Name cannot be None when esphome->friendly_name is not set!" )(value) + if value is not None: + # Validate length for web server URL compatibility + if len(value) > NAME_MAX_LENGTH: + raise Invalid( + f"Name is too long ({len(value)} chars). " + f"Maximum length is {NAME_MAX_LENGTH} characters." + ) + # Validate no '/' in name for web server URL compatibility + value = _validate_no_slash(value) return value +def string_no_slash(value): + """Validate a string that cannot contain '/' characters. + + Used for device and area names where '/' is reserved as a URL path separator. + Use with cv.Length() to also enforce maximum length. + """ + value = string(value) + return _validate_no_slash(value) + + ENTITY_BASE_SCHEMA = Schema( { Optional(CONF_NAME): _validate_entity_name, @@ -2009,7 +2073,7 @@ def polling_component_schema(default_update_interval): if default_update_interval is None: return COMPONENT_SCHEMA.extend( { - Required(CONF_UPDATE_INTERVAL): default_update_interval, + Required(CONF_UPDATE_INTERVAL): update_interval, } ) assert isinstance(default_update_interval, str) diff --git a/esphome/const.py b/esphome/const.py index a25114d80e..518247aa60 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.12.0-dev" +__version__ = "2026.1.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( @@ -36,7 +36,30 @@ class Framework(StrEnum): class ThreadModel(StrEnum): - """Threading model identifiers for ESPHome scheduler.""" + """Threading model identifiers for ESPHome scheduler. + + ESPHome currently uses three threading models based on platform capabilities: + + SINGLE: + - Single-threaded platforms (ESP8266, RP2040) + - No RTOS task switching + - No concurrent access to scheduler data structures + - No atomics or locks required + - Minimal overhead + + MULTI_NO_ATOMICS: + - Multi-threaded platforms without hardware atomic RMW support (e.g. LibreTiny BK7231N) + - Uses FreeRTOS or another RTOS with multiple tasks + - CPU lacks exclusive load/store instructions (ARM968E-S has no LDREX/STREX) + - std::atomic cannot provide lock-free RMW; libatomic is avoided to save flash (4–8 KB) + - Scheduler uses explicit FreeRTOS mutexes for synchronization + + MULTI_ATOMICS: + - Multi-threaded platforms with hardware atomic RMW support (ESP32, Cortex-M, Host) + - CPU provides native atomic instructions (ESP32 S32C1I, ARM LDREX/STREX) + - std::atomic is used for lock-free synchronization + - Reduced contention and better performance + """ SINGLE = "ESPHOME_THREAD_SINGLE" MULTI_NO_ATOMICS = "ESPHOME_THREAD_MULTI_NO_ATOMICS" @@ -100,6 +123,7 @@ CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" CONF_ADVANCED = "advanced" CONF_AFTER = "after" +CONF_ALGORITHM_TUNING = "algorithm_tuning" CONF_ALL = "all" CONF_ALLOW_OTHER_USES = "allow_other_uses" CONF_ALPHA = "alpha" @@ -336,6 +360,7 @@ CONF_ENERGY = "energy" CONF_ENTITY_CATEGORY = "entity_category" CONF_ENTITY_ID = "entity_id" CONF_ENUM_DATAPOINT = "enum_datapoint" +CONF_ENVIRONMENT_VARIABLES = "environment_variables" CONF_EQUATION = "equation" CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" @@ -411,6 +436,7 @@ CONF_GAIN_FACTOR = "gain_factor" CONF_GAMMA_CORRECT = "gamma_correct" CONF_GAS_RESISTANCE = "gas_resistance" CONF_GATEWAY = "gateway" +CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes" CONF_GLASS_ATTENUATION_FACTOR = "glass_attenuation_factor" CONF_GLYPHS = "glyphs" CONF_GPIO = "gpio" @@ -473,6 +499,7 @@ CONF_INCLUDE_INTERNAL = "include_internal" CONF_INCLUDES = "includes" CONF_INCLUDES_C = "includes_c" CONF_INDEX = "index" +CONF_INDEX_OFFSET = "index_offset" CONF_INDOOR = "indoor" CONF_INFRARED = "infrared" CONF_INIT_SEQUENCE = "init_sequence" @@ -510,6 +537,8 @@ CONF_LAMBDA = "lambda" CONF_LAST_CONFIDENCE = "last_confidence" CONF_LAST_FINGER_ID = "last_finger_id" CONF_LATITUDE = "latitude" +CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours" +CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours" CONF_LED = "led" CONF_LEGEND = "legend" CONF_LENGTH = "length" @@ -535,6 +564,7 @@ CONF_LOGS = "logs" CONF_LONGITUDE = "longitude" CONF_LOOP_TIME = "loop_time" CONF_LOW = "low" +CONF_LOW_POWER_MODE = "low_power_mode" CONF_LOW_VOLTAGE_REFERENCE = "low_voltage_reference" CONF_MAC_ADDRESS = "mac_address" CONF_MAGNITUDE = "magnitude" @@ -620,7 +650,9 @@ CONF_NEVER = "never" CONF_NEW_PASSWORD = "new_password" CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide" CONF_NOISE_LEVEL = "noise_level" +CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope" CONF_NOTIFY = "notify" +CONF_NOX = "nox" CONF_NUM_ATTEMPTS = "num_attempts" CONF_NUM_CHANNELS = "num_channels" CONF_NUM_CHIPS = "num_chips" @@ -647,6 +679,7 @@ CONF_ON_CLIENT_CONNECTED = "on_client_connected" CONF_ON_CLIENT_DISCONNECTED = "on_client_disconnected" CONF_ON_CONNECT = "on_connect" CONF_ON_CONTROL = "on_control" +CONF_ON_DATA = "on_data" CONF_ON_DIRECTION_SET = "on_direction_set" CONF_ON_DISCONNECT = "on_disconnect" CONF_ON_DOUBLE_CLICK = "on_double_click" @@ -677,6 +710,7 @@ CONF_ON_RELEASE = "on_release" CONF_ON_RESPONSE = "on_response" CONF_ON_SHUTDOWN = "on_shutdown" CONF_ON_SPEED_SET = "on_speed_set" +CONF_ON_START = "on_start" CONF_ON_STATE = "on_state" CONF_ON_SUCCESS = "on_success" CONF_ON_TAG = "on_tag" @@ -914,6 +948,7 @@ CONF_STATE_TOPIC = "state_topic" CONF_STATIC_IP = "static_ip" CONF_STATUS = "status" CONF_STB_PIN = "stb_pin" +CONF_STD_INITIAL = "std_initial" CONF_STEP = "step" CONF_STEP_DELAY = "step_delay" CONF_STEP_MODE = "step_mode" @@ -981,6 +1016,7 @@ CONF_TILT_COMMAND_TOPIC = "tilt_command_topic" CONF_TILT_LAMBDA = "tilt_lambda" CONF_TILT_STATE_TOPIC = "tilt_state_topic" CONF_TIME = "time" +CONF_TIME_CONSTANT = "time_constant" CONF_TIME_ID = "time_id" CONF_TIMEOUT = "timeout" CONF_TIMES = "times" @@ -1035,6 +1071,8 @@ CONF_VERSION = "version" CONF_VIBRATIONS = "vibrations" CONF_VISIBLE = "visible" CONF_VISUAL = "visual" +CONF_VOC = "voc" +CONF_VOC_BASELINE = "voc_baseline" CONF_VOLTAGE = "voltage" CONF_VOLTAGE_ATTENUATION = "voltage_attenuation" CONF_VOLTAGE_DIVIDER = "voltage_divider" @@ -1049,6 +1087,7 @@ CONF_WARM_WHITE = "warm_white" CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature" CONF_WATCHDOG_THRESHOLD = "watchdog_threshold" CONF_WATCHDOG_TIMEOUT = "watchdog_timeout" +CONF_WATER_HEATER = "water_heater" CONF_WEB_SERVER = "web_server" CONF_WEB_SERVER_ID = "web_server_id" CONF_WEIGHT = "weight" @@ -1142,6 +1181,7 @@ ICON_TIMELAPSE = "mdi:timelapse" ICON_TIMER = "mdi:timer-outline" ICON_VIBRATE = "mdi:vibrate" ICON_WATER = "mdi:water" +ICON_WATER_HEATER = "mdi:water-boiler" ICON_WATER_PERCENT = "mdi:water-percent" ICON_WEATHER_SUNSET = "mdi:weather-sunset" ICON_WEATHER_SUNSET_DOWN = "mdi:weather-sunset-down" @@ -1322,6 +1362,9 @@ STATE_CLASS_NONE = "" # The state represents a measurement in present time STATE_CLASS_MEASUREMENT = "measurement" +# The state represents a measurement in present time for angles measured in degrees (°) +STATE_CLASS_MEASUREMENT_ANGLE = "measurement_angle" + # The state represents a total that only increases, a decrease is considered a reset. STATE_CLASS_TOTAL_INCREASING = "total_increasing" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 08753b0f2d..70593d8153 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -541,8 +541,22 @@ class EsphomeCore: self.friendly_name: str | None = None # The area / zone of the node self.area: str | None = None - # Additional data components can store temporary data in - # The first key to this dict should always be the integration name + # Additional data components can store temporary data in. + # This dict is cleared between compilation runs. + # + # Usage pattern (use @dataclass for type safety): + # DOMAIN = "my_component" + # + # @dataclass + # class MyComponentData: + # feature_enabled: bool = False + # + # def _get_data() -> MyComponentData: + # if DOMAIN not in CORE.data: + # CORE.data[DOMAIN] = MyComponentData() + # return CORE.data[DOMAIN] + # + # The first key should always be the component domain name (DOMAIN constant). self.data = {} # The relative path to the configuration YAML self.config_path: Path | None = None @@ -594,6 +608,8 @@ class EsphomeCore: self.current_component: str | None = None # Address cache for DNS and mDNS lookups from command line arguments self.address_cache: AddressCache | None = None + # Cached config hash (computed lazily) + self._config_hash: int | None = None def reset(self): from esphome.pins import PIN_SCHEMA_REGISTRY @@ -622,6 +638,7 @@ class EsphomeCore: self.unique_ids = {} self.current_component = None self.address_cache = None + self._config_hash = None PIN_SCHEMA_REGISTRY.reset() @contextmanager @@ -671,6 +688,21 @@ class EsphomeCore: return None + @property + def config_hash(self) -> int: + """Get the FNV-1a 32-bit hash of the config. + + The hash is computed lazily and cached for performance. + Uses sort_keys=True to ensure deterministic ordering. + """ + if self._config_hash is None: + from esphome import yaml_util + from esphome.helpers import fnv1a_32bit_hash + + config_str = yaml_util.dump(self.config, show_secrets=True, sort_keys=True) + self._config_hash = fnv1a_32bit_hash(config_str) + return self._config_hash + @property def config_dir(self) -> Path: if self.config_path.is_dir(): @@ -689,6 +721,25 @@ class EsphomeCore: def config_filename(self) -> str: return self.config_path.name + def has_at_least_one_component(self, *components: str) -> bool: + """ + Are any of the given components configured? + :param components: component names + :return: true if so + """ + if self.config is None: + raise ValueError("Config has not been loaded yet") + + return any(component in self.config for component in components) + + @property + def has_networking(self) -> bool: + """ + Is a network component configured? + :return: true if so + """ + return self.has_at_least_one_component("wifi", "ethernet", "openthread") + def relative_config_path(self, *path: str | Path) -> Path: path_ = Path(*path).expanduser() return self.config_dir / path_ @@ -766,6 +817,11 @@ class EsphomeCore: @property def using_esp_idf(self): + _LOGGER.warning( + "CORE.using_esp_idf was deprecated in 2026.1, will change behavior in 2026.6. " + "ESP32 Arduino builds on top of ESP-IDF, so ESP-IDF features are available in both frameworks. " + "Use CORE.is_esp32 and/or CORE.using_arduino instead." + ) return self.target_framework == "esp-idf" @property diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 75814ae253..f8fa3b333e 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -1,5 +1,15 @@ #include "esphome/core/application.h" +#include "esphome/core/build_info_data.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" +#include + +#ifdef USE_ESP8266 +#include +#endif +#ifdef USE_ESP32 +#include +#endif #include "esphome/core/version.h" #include "esphome/core/hal.h" #include @@ -12,6 +22,10 @@ #include "esphome/components/status_led/status_led.h" #endif +#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) +#include "esphome/components/socket/socket.h" +#endif + #ifdef USE_SOCKET_SELECT_SUPPORT #include @@ -187,9 +201,24 @@ void Application::loop() { if (this->dump_config_at_ < this->components_.size()) { if (this->dump_config_at_ == 0) { - ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_); + char build_time_str[Application::BUILD_TIME_STR_SIZE]; + this->get_build_time_string(build_time_str); + ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", build_time_str); #ifdef ESPHOME_PROJECT_NAME ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION); +#endif +#ifdef USE_ESP32 + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + ESP_LOGI(TAG, "ESP32 Chip: %s r%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, + chip_info.revision % 100, chip_info.cores); +#if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) + // Suggest optimization for chips that don't need the PSRAM cache workaround + if (chip_info.revision >= 300) { + ESP_LOGW(TAG, "Set minimum_chip_revision: \"%d.%d\" to reduce binary size", chip_info.revision / 100, + chip_info.revision % 100); + } +#endif #endif } @@ -627,6 +656,9 @@ void Application::yield_with_select_(uint32_t delay_ms) { // No sockets registered, use regular delay delay(delay_ms); } +#elif defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP) + // No select support but can wake on socket activity via esp_schedule() + socket::socket_delay(delay_ms); #else // No select support, use regular delay delay(delay_ms); @@ -704,4 +736,9 @@ void Application::wake_loop_threadsafe() { } #endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +void Application::get_build_time_string(std::span buffer) { + ESPHOME_strncpy_P(buffer.data(), ESPHOME_BUILD_TIME_STR, buffer.size()); + buffer[buffer.size() - 1] = '\0'; +} + } // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h index dae44d8902..13461b3ebd 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -1,16 +1,21 @@ #pragma once #include +#include #include +#include #include #include +#include "esphome/core/build_info_data.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include "esphome/core/progmem.h" #include "esphome/core/scheduler.h" #include "esphome/core/string_ref.h" +#include "esphome/core/version.h" #ifdef USE_DEVICES #include "esphome/core/device.h" @@ -83,6 +88,9 @@ #ifdef USE_ALARM_CONTROL_PANEL #include "esphome/components/alarm_control_panel/alarm_control_panel.h" #endif +#ifdef USE_WATER_HEATER +#include "esphome/components/water_heater/water_heater.h" +#endif #ifdef USE_EVENT #include "esphome/components/event/event.h" #endif @@ -100,16 +108,17 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick class Application { public: - void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment, - const char *compilation_time, bool name_add_mac_suffix) { + void pre_setup(const std::string &name, const std::string &friendly_name, bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { + // MAC address length: 12 hex chars + null terminator + constexpr size_t mac_address_len = 13; // MAC address suffix length (last 6 characters of 12-char MAC address string) constexpr size_t mac_address_suffix_len = 6; - const std::string mac_addr = get_mac_address(); - // Use pointer + offset to avoid substr() allocation - const char *mac_suffix_ptr = mac_addr.c_str() + mac_address_suffix_len; + char mac_addr[mac_address_len]; + get_mac_address_into_buffer(mac_addr); + const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; this->name_ = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len); if (!friendly_name.empty()) { this->friendly_name_ = make_name_with_suffix(friendly_name, ' ', mac_suffix_ptr, mac_address_suffix_len); @@ -118,8 +127,6 @@ class Application { this->name_ = name; this->friendly_name_ = friendly_name; } - this->comment_ = comment; - this->compilation_time_ = compilation_time; } #ifdef USE_DEVICES @@ -212,6 +219,10 @@ class Application { } #endif +#ifdef USE_WATER_HEATER + void register_water_heater(water_heater::WaterHeater *water_heater) { this->water_heaters_.push_back(water_heater); } +#endif + #ifdef USE_EVENT void register_event(event::Event *event) { this->events_.push_back(event); } #endif @@ -252,14 +263,46 @@ class Application { return ""; } - /// Get the comment of this Application set by pre_setup(). - std::string get_comment() const { return this->comment_; } + /// Copy the comment string into the provided buffer + /// Buffer must be ESPHOME_COMMENT_SIZE bytes (compile-time enforced) + void get_comment_string(std::span buffer) { + ESPHOME_strncpy_P(buffer.data(), ESPHOME_COMMENT_STR, buffer.size()); + buffer[buffer.size() - 1] = '\0'; + } + + /// Get the comment of this Application as a string + std::string get_comment() { + char buffer[ESPHOME_COMMENT_SIZE]; + this->get_comment_string(buffer); + return std::string(buffer); + } bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } - std::string get_compilation_time() const { return this->compilation_time_; } - /// Get the compilation time as StringRef (for API usage) - StringRef get_compilation_time_ref() const { return StringRef(this->compilation_time_); } + /// Size of buffer required for build time string (including null terminator) + static constexpr size_t BUILD_TIME_STR_SIZE = 26; + + /// Get the config hash as a 32-bit integer + constexpr uint32_t get_config_hash() { return ESPHOME_CONFIG_HASH; } + + /// Get the config hash extended with ESPHome version + constexpr uint32_t get_config_version_hash() { return fnv1a_hash_extend(ESPHOME_CONFIG_HASH, ESPHOME_VERSION); } + + /// Get the build time as a Unix timestamp + constexpr time_t get_build_time() { return ESPHOME_BUILD_TIME; } + + /// Copy the build time string into the provided buffer + /// Buffer must be BUILD_TIME_STR_SIZE bytes (compile-time enforced) + void get_build_time_string(std::span buffer); + + /// Get the build time as a string (deprecated, use get_build_time_string() instead) + // Remove before 2026.7.0 + ESPDEPRECATED("Use get_build_time_string() instead. Removed in 2026.7.0", "2026.1.0") + std::string get_compilation_time() { + char buf[BUILD_TIME_STR_SIZE]; + this->get_build_time_string(buf); + return std::string(buf); + } /// Get the cached time in milliseconds from when the current component started its loop execution inline uint32_t IRAM_ATTR HOT get_loop_component_start_time() const { return this->loop_component_start_time_; } @@ -409,6 +452,11 @@ class Application { GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels) #endif +#ifdef USE_WATER_HEATER + auto &get_water_heaters() const { return this->water_heaters_; } + GET_ENTITY_METHOD(water_heater::WaterHeater, water_heater, water_heaters) +#endif + #ifdef USE_EVENT auto &get_events() const { return this->events_; } GET_ENTITY_METHOD(event::Event, event, events) @@ -473,8 +521,6 @@ class Application { // Pointer-sized members first Component *current_component_{nullptr}; - const char *comment_{nullptr}; - const char *compilation_time_{nullptr}; // std::vector (3 pointers each: begin, end, capacity) // Partitioned vector design for looping components @@ -607,6 +653,9 @@ class Application { StaticVector alarm_control_panels_{}; #endif +#ifdef USE_WATER_HEATER + StaticVector water_heaters_{}; +#endif #ifdef USE_UPDATE StaticVector updates_{}; #endif diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 33e08c9c1c..61d2944acf 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -11,10 +11,26 @@ namespace esphome { +// C++20 std::index_sequence is now used for tuple unpacking +// Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 -template struct seq {}; // NOLINT -template struct gens : gens {}; // NOLINT -template struct gens<0, S...> { using type = seq; }; // NOLINT +// Remove before 2026.6.0 +// NOLINTBEGIN(readability-identifier-naming) +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +template struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {}; +template +struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens + : gens {}; +template struct gens<0, S...> { using type = seq; }; + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif +// NOLINTEND(readability-identifier-naming) #define TEMPLATABLE_VALUE_(type, name) \ protected: \ @@ -29,6 +45,12 @@ template class TemplatableValue { public: TemplatableValue() : type_(NONE) {} + // For const char* when T is std::string: store pointer directly, no heap allocation + // String remains in flash and is only converted to std::string when value() is called + TemplatableValue(const char *str) requires std::same_as : type_(STATIC_STRING) { + this->static_str_ = str; + } + template TemplatableValue(F value) requires(!std::invocable) : type_(VALUE) { new (&this->value_) T(std::move(value)); } @@ -48,24 +70,28 @@ template class TemplatableValue { // Copy constructor TemplatableValue(const TemplatableValue &other) : type_(other.type_) { - if (type_ == VALUE) { + if (this->type_ == VALUE) { new (&this->value_) T(other.value_); - } else if (type_ == LAMBDA) { + } else if (this->type_ == LAMBDA) { this->f_ = new std::function(*other.f_); - } else if (type_ == STATELESS_LAMBDA) { + } else if (this->type_ == STATELESS_LAMBDA) { this->stateless_f_ = other.stateless_f_; + } else if (this->type_ == STATIC_STRING) { + this->static_str_ = other.static_str_; } } // Move constructor TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { - if (type_ == VALUE) { + if (this->type_ == VALUE) { new (&this->value_) T(std::move(other.value_)); - } else if (type_ == LAMBDA) { + } else if (this->type_ == LAMBDA) { this->f_ = other.f_; other.f_ = nullptr; - } else if (type_ == STATELESS_LAMBDA) { + } else if (this->type_ == STATELESS_LAMBDA) { this->stateless_f_ = other.stateless_f_; + } else if (this->type_ == STATIC_STRING) { + this->static_str_ = other.static_str_; } other.type_ = NONE; } @@ -88,12 +114,12 @@ template class TemplatableValue { } ~TemplatableValue() { - if (type_ == VALUE) { + if (this->type_ == VALUE) { this->value_.~T(); - } else if (type_ == LAMBDA) { + } else if (this->type_ == LAMBDA) { delete this->f_; } - // STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty, not heap-allocated) + // STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated) } bool has_value() { return this->type_ != NONE; } @@ -106,6 +132,13 @@ template class TemplatableValue { return (*this->f_)(x...); // std::function call case VALUE: return this->value_; + case STATIC_STRING: + // if constexpr required: code must compile for all T, but STATIC_STRING + // can only be set when T is std::string (enforced by constructor constraint) + if constexpr (std::same_as) { + return std::string(this->static_str_); + } + __builtin_unreachable(); case NONE: default: return T{}; @@ -132,12 +165,14 @@ template class TemplatableValue { VALUE, LAMBDA, STATELESS_LAMBDA, + STATIC_STRING, // For const char* when T is std::string - avoids heap allocation } type_; union { T value_; std::function *f_; T (*stateless_f_)(X...); + const char *static_str_; // For STATIC_STRING type }; }; @@ -152,11 +187,11 @@ template class Condition { /// Call check with a tuple of values as parameter. bool check_tuple(const std::tuple &tuple) { - return this->check_tuple_(tuple, typename gens::type()); + return this->check_tuple_(tuple, std::make_index_sequence{}); } protected: - template bool check_tuple_(const std::tuple &tuple, seq /*unused*/) { + template bool check_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { return this->check(std::get(tuple)...); } }; @@ -231,11 +266,11 @@ template class Action { } } } - template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_next_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play_next_(std::get(tuple)...); } void play_next_tuple_(const std::tuple &tuple) { - this->play_next_tuple_(tuple, typename gens::type()); + this->play_next_tuple_(tuple, std::make_index_sequence{}); } virtual void stop() {} @@ -277,7 +312,9 @@ template class ActionList { if (this->actions_begin_ != nullptr) this->actions_begin_->play_complex(x...); } - void play_tuple(const std::tuple &tuple) { this->play_tuple_(tuple, typename gens::type()); } + void play_tuple(const std::tuple &tuple) { + this->play_tuple_(tuple, std::make_index_sequence{}); + } void stop() { if (this->actions_begin_ != nullptr) this->actions_begin_->stop_complex(); @@ -298,7 +335,7 @@ template class ActionList { } protected: - template void play_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play(std::get(tuple)...); } diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index a5e6139182..e8878ac251 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -9,8 +9,8 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include #include -#include namespace esphome { @@ -178,7 +178,6 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) void play_complex(const Ts &...x) override { - auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; // If num_running_ > 1, we have multiple instances running in parallel @@ -187,9 +186,22 @@ template class DelayAction : public Action, public Compon // WARNING: This can accumulate delays if scripts are triggered faster than they complete! // Users should set max_runs on parallel scripts to limit concurrent executions. // Issue #10264: This is a workaround for parallel script delays interfering with each other. - App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, - /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), - /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + + // Optimization: For no-argument delays (most common case), use direct lambda + // instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution) + if constexpr (sizeof...(Ts) == 0) { + App.scheduler.set_timer_common_( + this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(), [this]() { this->play_next_(); }, + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + } else { + // For delays with arguments, use std::bind to preserve argument values + // Arguments must be copied because original references may be invalid after delay + auto f = std::bind(&DelayAction::play_next_, this, x...); + App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + } } float get_setup_priority() const override { return setup_priority::HARDWARE; } @@ -433,9 +445,10 @@ template class WaitUntilAction : public Action, public Co // Store for later processing auto now = millis(); auto timeout = this->timeout_value_.optional_value(x...); - this->var_queue_.emplace_front(now, timeout, std::make_tuple(x...)); + this->var_queue_.emplace_back(now, timeout, std::make_tuple(x...)); - // Do immediate check with fresh timestamp + // Do immediate check with fresh timestamp - don't call loop() synchronously! + // Let the event loop call it to avoid reentrancy issues if (this->process_queue_(now)) { // Only enable loop if we still have pending items this->enable_loop(); @@ -487,7 +500,7 @@ template class WaitUntilAction : public Action, public Co } Condition *condition_; - std::forward_list, std::tuple>> var_queue_{}; + std::list, std::tuple>> var_queue_{}; }; template class UpdateComponentAction : public Action { diff --git a/esphome/core/build_info_data.h b/esphome/core/build_info_data.h new file mode 100644 index 0000000000..02bb465e44 --- /dev/null +++ b/esphome/core/build_info_data.h @@ -0,0 +1,12 @@ +#pragma once + +// This file is not used by the runtime, instead, a version is generated during +// compilation with the actual build info values. +// +// This file is only used by static analyzers and IDEs. + +#define ESPHOME_CONFIG_HASH 0x12345678U // NOLINT +#define ESPHOME_BUILD_TIME 1700000000 // NOLINT +#define ESPHOME_COMMENT_SIZE 1 // NOLINT +static const char ESPHOME_BUILD_TIME_STR[] = "2024-01-01 00:00:00 +0000"; +static const char ESPHOME_COMMENT_STR[] = ""; diff --git a/esphome/core/color.cpp b/esphome/core/color.cpp index 7e390b2354..14c41c2b0d 100644 --- a/esphome/core/color.cpp +++ b/esphome/core/color.cpp @@ -6,4 +6,18 @@ namespace esphome { constinit const Color Color::BLACK(0, 0, 0, 0); constinit const Color Color::WHITE(255, 255, 255, 255); +Color Color::gradient(const Color &to_color, uint8_t amnt) { + Color new_color; + float amnt_f = float(amnt) / 255.0f; + new_color.r = amnt_f * (to_color.r - this->r) + this->r; + new_color.g = amnt_f * (to_color.g - this->g) + this->g; + new_color.b = amnt_f * (to_color.b - this->b) + this->b; + new_color.w = amnt_f * (to_color.w - this->w) + this->w; + return new_color; +} + +Color Color::fade_to_white(uint8_t amnt) { return this->gradient(Color::WHITE, amnt); } + +Color Color::fade_to_black(uint8_t amnt) { return this->gradient(Color::BLACK, amnt); } + } // namespace esphome diff --git a/esphome/core/color.h b/esphome/core/color.h index 4b0ae5b57a..32d63b1856 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -174,17 +174,9 @@ struct Color { uint8_t((uint16_t(b) * 255U / max_rgb)), w); } - Color gradient(const Color &to_color, uint8_t amnt) { - Color new_color; - float amnt_f = float(amnt) / 255.0f; - new_color.r = amnt_f * (to_color.r - (*this).r) + (*this).r; - new_color.g = amnt_f * (to_color.g - (*this).g) + (*this).g; - new_color.b = amnt_f * (to_color.b - (*this).b) + (*this).b; - new_color.w = amnt_f * (to_color.w - (*this).w) + (*this).w; - return new_color; - } - Color fade_to_white(uint8_t amnt) { return (*this).gradient(Color::WHITE, amnt); } - Color fade_to_black(uint8_t amnt) { return (*this).gradient(Color::BLACK, amnt); } + Color gradient(const Color &to_color, uint8_t amnt); + Color fade_to_white(uint8_t amnt); + Color fade_to_black(uint8_t amnt); Color lighten(uint8_t delta) { return *this + delta; } Color darken(uint8_t delta) { return *this - delta; } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index de3dd99d0c..90be6cf646 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -36,6 +36,9 @@ namespace { struct ComponentErrorMessage { const Component *component; const char *message; + // Track if message is flash pointer (needs LOG_STR_ARG) or RAM pointer + // Remove before 2026.6.0 when deprecated const char* API is removed + bool is_flash_ptr; }; struct ComponentPriorityOverride { @@ -49,6 +52,25 @@ std::unique_ptr> component_error_messages; // Setup priority overrides - freed after setup completes // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::unique_ptr> setup_priority_overrides; + +// Helper to store error messages - reduces duplication between deprecated and new API +// Remove before 2026.6.0 when deprecated const char* API is removed +void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) { + // Lazy allocate the error messages vector if needed + if (!component_error_messages) { + component_error_messages = std::make_unique>(); + } + // Check if this component already has an error message + for (auto &entry : *component_error_messages) { + if (entry.component == component) { + entry.message = message; + entry.is_flash_ptr = is_flash_ptr; + return; + } + } + // Add new error message + component_error_messages->emplace_back(ComponentErrorMessage{component, message, is_flash_ptr}); +} } // namespace namespace setup_priority { @@ -116,10 +138,19 @@ void Component::set_retry(const std::string &name, uint32_t initial_wait_time, u App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); } +void Component::set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts, + std::function &&f, float backoff_increase_factor) { // NOLINT + App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor); +} + bool Component::cancel_retry(const std::string &name) { // NOLINT return App.scheduler.cancel_retry(this, name); } +bool Component::cancel_retry(const char *name) { // NOLINT + return App.scheduler.cancel_retry(this, name); +} + void Component::set_timeout(const std::string &name, uint32_t timeout, std::function &&f) { // NOLINT App.scheduler.set_timeout(this, name, timeout, std::move(f)); } @@ -143,16 +174,20 @@ void Component::call_dump_config() { if (this->is_failed()) { // Look up error message from global vector const char *error_msg = nullptr; + bool is_flash_ptr = false; if (component_error_messages) { for (const auto &entry : *component_error_messages) { if (entry.component == this) { error_msg = entry.message; + is_flash_ptr = entry.is_flash_ptr; break; } } } + // Log with appropriate format based on pointer type ESP_LOGE(TAG, " %s is marked FAILED: %s", LOG_STR_ARG(this->get_component_log_str()), - error_msg ? error_msg : LOG_STR_LITERAL("unspecified")); + error_msg ? (is_flash_ptr ? LOG_STR_ARG((const LogString *) error_msg) : error_msg) + : LOG_STR_LITERAL("unspecified")); } } @@ -170,7 +205,13 @@ void Component::call() { this->call_setup(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG uint32_t setup_time = millis() - start_time; - ESP_LOGCONFIG(TAG, "Setup %s took %ums", LOG_STR_ARG(this->get_component_log_str()), (unsigned) setup_time); + // Only log at CONFIG level if setup took longer than the blocking threshold + // to avoid spamming the log and blocking the event loop + if (setup_time >= WARN_IF_BLOCKING_OVER_MS) { + ESP_LOGCONFIG(TAG, "Setup %s took %ums", LOG_STR_ARG(this->get_component_log_str()), (unsigned) setup_time); + } else { + ESP_LOGV(TAG, "Setup %s took %ums", LOG_STR_ARG(this->get_component_log_str()), (unsigned) setup_time); + } #endif break; } @@ -307,6 +348,7 @@ void Component::status_set_warning(const LogString *message) { ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); } +void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); } void Component::status_set_error(const char *message) { if ((this->component_state_ & STATUS_LED_ERROR) != 0) return; @@ -315,19 +357,19 @@ void Component::status_set_error(const char *message) { ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? message : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { - // Lazy allocate the error messages vector if needed - if (!component_error_messages) { - component_error_messages = std::make_unique>(); - } - // Check if this component already has an error message - for (auto &entry : *component_error_messages) { - if (entry.component == this) { - entry.message = message; - return; - } - } - // Add new error message - component_error_messages->emplace_back(ComponentErrorMessage{this, message}); + store_component_error_message(this, message, false); + } +} +void Component::status_set_error(const LogString *message) { + if ((this->component_state_ & STATUS_LED_ERROR) != 0) + return; + this->component_state_ |= STATUS_LED_ERROR; + App.app_state_ |= STATUS_LED_ERROR; + ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), + message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); + if (message != nullptr) { + // Store the LogString pointer directly (safe because LogString is always in flash/static memory) + store_component_error_message(this, LOG_STR_ARG(message), true); } } void Component::status_clear_warning() { @@ -342,11 +384,11 @@ void Component::status_clear_error() { this->component_state_ &= ~STATUS_LED_ERROR; ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str())); } -void Component::status_momentary_warning(const std::string &name, uint32_t length) { +void Component::status_momentary_warning(const char *name, uint32_t length) { this->status_set_warning(); this->set_timeout(name, length, [this]() { this->status_clear_warning(); }); } -void Component::status_momentary_error(const std::string &name, uint32_t length) { +void Component::status_momentary_error(const char *name, uint32_t length) { this->status_set_error(); this->set_timeout(name, length, [this]() { this->status_clear_error(); }); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 462e0e301c..32f594d6f8 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" @@ -157,7 +158,19 @@ class Component { */ virtual void mark_failed(); + // Remove before 2026.6.0 + ESPDEPRECATED("Use mark_failed(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " + "strings. Will stop working in 2026.6.0", + "2025.12.0") void mark_failed(const char *message) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + this->status_set_error(message); +#pragma GCC diagnostic pop + this->mark_failed(); + } + + void mark_failed(const LogString *message) { this->status_set_error(message); this->mark_failed(); } @@ -216,15 +229,35 @@ class Component { void status_set_warning(const char *message = nullptr); void status_set_warning(const LogString *message); - void status_set_error(const char *message = nullptr); + void status_set_error(); // Set error flag without message + // Remove before 2026.6.0 + ESPDEPRECATED("Use status_set_error(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary " + "strings. Will stop working in 2026.6.0", + "2025.12.0") + void status_set_error(const char *message); + void status_set_error(const LogString *message); void status_clear_warning(); void status_clear_error(); - void status_momentary_warning(const std::string &name, uint32_t length = 5000); + /** Set warning status flag and automatically clear it after a timeout. + * + * @param name Identifier for the timeout (used to cancel/replace existing timeouts with the same name). + * Must be a static string literal (stored in flash/rodata), not a temporary or dynamic string. + * This is NOT a message to display - use status_set_warning() with a message if logging is needed. + * @param length Duration in milliseconds before the warning is automatically cleared. + */ + void status_momentary_warning(const char *name, uint32_t length = 5000); - void status_momentary_error(const std::string &name, uint32_t length = 5000); + /** Set error status flag and automatically clear it after a timeout. + * + * @param name Identifier for the timeout (used to cancel/replace existing timeouts with the same name). + * Must be a static string literal (stored in flash/rodata), not a temporary or dynamic string. + * This is NOT a message to display - use status_set_error() with a message if logging is needed. + * @param length Duration in milliseconds before the error is automatically cleared. + */ + void status_momentary_error(const char *name, uint32_t length = 5000); bool has_overridden_loop() const; @@ -334,6 +367,9 @@ class Component { void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT std::function &&f, float backoff_increase_factor = 1.0f); // NOLINT + void set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT + std::function &&f, float backoff_increase_factor = 1.0f); // NOLINT + void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function &&f, // NOLINT float backoff_increase_factor = 1.0f); // NOLINT @@ -343,6 +379,7 @@ class Component { * @return Whether a retry function was deleted. */ bool cancel_retry(const std::string &name); // NOLINT + bool cancel_retry(const char *name); // NOLINT /** Set a timeout function with a unique name. * diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index 668c4a1fda..4015d8ec60 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -5,7 +5,7 @@ #ifdef USE_API #include "esphome/components/api/api_server.h" #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS #include "esphome/components/api/user_services.h" #endif @@ -81,7 +81,7 @@ void ComponentIterator::advance() { break; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS case IteratorState::SERVICE: this->process_platform_item_(api::global_api_server->get_user_services(), &ComponentIterator::on_service); break; @@ -163,6 +163,12 @@ void ComponentIterator::advance() { break; #endif +#ifdef USE_WATER_HEATER + case IteratorState::WATER_HEATER: + this->process_platform_item_(App.get_water_heaters(), &ComponentIterator::on_water_heater); + break; +#endif + #ifdef USE_EVENT case IteratorState::EVENT: this->process_platform_item_(App.get_events(), &ComponentIterator::on_event); @@ -185,7 +191,7 @@ void ComponentIterator::advance() { bool ComponentIterator::on_end() { return true; } bool ComponentIterator::on_begin() { return true; } -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } #endif #ifdef USE_CAMERA diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 641d42898a..37d1960601 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -10,7 +10,7 @@ namespace esphome { -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS namespace api { class UserServiceDescriptor; } // namespace api @@ -45,7 +45,7 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS virtual bool on_service(api::UserServiceDescriptor *service); #endif #ifdef USE_CAMERA @@ -84,6 +84,9 @@ class ComponentIterator { #ifdef USE_ALARM_CONTROL_PANEL virtual bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) = 0; #endif +#ifdef USE_WATER_HEATER + virtual bool on_water_heater(water_heater::WaterHeater *water_heater) = 0; +#endif #ifdef USE_EVENT virtual bool on_event(event::Event *event) = 0; #endif @@ -122,7 +125,7 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR TEXT_SENSOR, #endif -#ifdef USE_API_SERVICES +#ifdef USE_API_USER_DEFINED_ACTIONS SERVICE, #endif #ifdef USE_CAMERA @@ -161,6 +164,9 @@ class ComponentIterator { #ifdef USE_ALARM_CONTROL_PANEL ALARM_CONTROL_PANEL, #endif +#ifdef USE_WATER_HEATER + WATER_HEATER, +#endif #ifdef USE_EVENT EVENT, #endif diff --git a/esphome/core/config.py b/esphome/core/config.py index 763f9ebd9f..f9c3011507 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, CONF_DEBUG_SCHEDULER, CONF_DEVICES, + CONF_ENVIRONMENT_VARIABLES, CONF_ESPHOME, CONF_FRIENDLY_NAME, CONF_ID, @@ -86,7 +87,7 @@ def validate_hostname(config): _LOGGER.warning( "'%s': Using the '_' (underscore) character in the hostname is discouraged " "as it can cause problems with some DHCP and local name services. " - "For more information, see https://esphome.io/guides/faq.html#why-shouldn-t-i-use-underscores-in-my-device-name", + "For more information, see https://esphome.io/guides/faq/#why-shouldnt-i-use-underscores-in-my-device-name", config[CONF_NAME], ) return config @@ -185,14 +186,14 @@ else: AREA_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Area), - cv.Required(CONF_NAME): cv.string, + cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)), } ) DEVICE_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Device), - cv.Required(CONF_NAME): cv.string, + cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)), cv.Optional(CONF_AREA_ID): cv.use_id(Area), } ) @@ -206,15 +207,22 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, - cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(cv.string, cv.Length(max=120)), + cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All( + cv.string_no_slash, cv.Length(max=120) + ), cv.Optional(CONF_AREA): validate_area_config, - cv.Optional(CONF_COMMENT): cv.string, + cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)), cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( { cv.string_strict: cv.Any([cv.string], cv.string), } ), + cv.Optional(CONF_ENVIRONMENT_VARIABLES, default={}): cv.Schema( + { + cv.string_strict: cv.string, + } + ), cv.Optional(CONF_ON_BOOT): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger), @@ -376,10 +384,15 @@ def include_file(path: Path, basename: Path, is_c_header: bool = False): ARDUINO_GLUE_CODE = """\ +#undef yield #define yield() esphome::yield() +#undef millis #define millis() esphome::millis() +#undef micros #define micros() esphome::micros() +#undef delay #define delay(x) esphome::delay(x) +#undef delayMicroseconds #define delayMicroseconds(x) esphome::delayMicroseconds(x) """ @@ -426,6 +439,12 @@ async def _add_platformio_options(pio_options): cg.add_platformio_option(key, val) +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_environment_variables(env_vars: dict[str, str]) -> None: + # Set environment variables for the build process + os.environ.update(env_vars) + + @coroutine_with_priority(CoroPriority.AUTOMATION) async def _add_automations(config): for conf in config.get(CONF_ON_BOOT, []): @@ -488,8 +507,6 @@ async def to_code(config: ConfigType) -> None: cg.App.pre_setup( config[CONF_NAME], config[CONF_FRIENDLY_NAME], - config.get(CONF_COMMENT, ""), - cg.RawExpression('__DATE__ ", " __TIME__'), config[CONF_NAME_ADD_MAC_SUFFIX], ) ) @@ -525,7 +542,7 @@ async def to_code(config: ConfigType) -> None: if config[CONF_DEBUG_SCHEDULER]: cg.add_define("ESPHOME_DEBUG_SCHEDULER") - if CORE.using_arduino and not CORE.is_bk72xx: + if CORE.using_arduino: CORE.add_job(add_arduino_global_workaround) if config[CONF_INCLUDES]: @@ -563,6 +580,9 @@ async def to_code(config: ConfigType) -> None: if config[CONF_PLATFORMIO_OPTIONS]: CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS]) + if config[CONF_ENVIRONMENT_VARIABLES]: + CORE.add_job(_add_environment_variables, config[CONF_ENVIRONMENT_VARIABLES]) + # Process areas all_areas: list[dict[str, str | core.ID]] = [] if CONF_AREA in config: diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 697017217d..632b46c893 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -58,6 +58,9 @@ #ifdef USE_ALARM_CONTROL_PANEL #include "esphome/components/alarm_control_panel/alarm_control_panel.h" #endif +#ifdef USE_WATER_HEATER +#include "esphome/components/water_heater/water_heater.h" +#endif #ifdef USE_EVENT #include "esphome/components/event/event.h" #endif @@ -123,6 +126,9 @@ class Controller { #ifdef USE_ALARM_CONTROL_PANEL virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){}; #endif +#ifdef USE_WATER_HEATER + virtual void on_water_heater_update(water_heater::WaterHeater *obj){}; +#endif #ifdef USE_EVENT virtual void on_event(event::Event *obj){}; #endif diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp index 0a84bb0d0d..13b505e8e9 100644 --- a/esphome/core/controller_registry.cpp +++ b/esphome/core/controller_registry.cpp @@ -98,6 +98,10 @@ CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player) CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel) #endif +#ifdef USE_WATER_HEATER +CONTROLLER_REGISTRY_NOTIFY(water_heater::WaterHeater, water_heater) +#endif + #ifdef USE_EVENT CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event) #endif diff --git a/esphome/core/controller_registry.h b/esphome/core/controller_registry.h index 640a276a0a..d6452d8827 100644 --- a/esphome/core/controller_registry.h +++ b/esphome/core/controller_registry.h @@ -119,6 +119,12 @@ class AlarmControlPanel; } #endif +#ifdef USE_WATER_HEATER +namespace water_heater { +class WaterHeater; +} +#endif + #ifdef USE_EVENT namespace event { class Event; @@ -228,6 +234,10 @@ class ControllerRegistry { static void notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj); #endif +#ifdef USE_WATER_HEATER + static void notify_water_heater_update(water_heater::WaterHeater *obj); +#endif + #ifdef USE_EVENT static void notify_event(event::Event *obj); #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 41f4b28cd5..f8f86e8c55 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -28,6 +28,7 @@ #define USE_BUTTON #define USE_CAMERA #define USE_CLIMATE +#define USE_CLIMATE_VISUAL_OVERRIDES #define USE_CONTROLLER_REGISTRY #define USE_COVER #define USE_DATETIME @@ -46,11 +47,13 @@ #define USE_GRAPHICAL_DISPLAY_MENU #define USE_HOMEASSISTANT_TIME #define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT +#define USE_IMAGE #define USE_IMPROV_SERIAL_NEXT_URL #define USE_JSON #define USE_LIGHT #define USE_LOCK #define USE_LOGGER +#define USE_LOGGER_LEVEL_LISTENERS #define USE_LOGGER_RUNTIME_TAG_LEVELS #define USE_LVGL #define USE_LVGL_ANIMIMG @@ -88,7 +91,8 @@ #define USE_MDNS #define USE_MDNS_STORE_SERVICES #define MDNS_SERVICE_COUNT 3 -#define MDNS_DYNAMIC_TXT_COUNT 3 +#define USE_MDNS_DYNAMIC_TXT +#define MDNS_DYNAMIC_TXT_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER #define USE_NEXTION_TFT_UPLOAD @@ -106,8 +110,11 @@ #define USE_TIME #define USE_TOUCHSCREEN #define USE_UART_DEBUGGER +#define USE_UART_WAKE_LOOP_ON_RX #define USE_UPDATE #define USE_VALVE +#define USE_WATER_HEATER +#define USE_WATER_HEATER_VISUAL_OVERRIDES #define USE_ZWAVE_PROXY // Feature flags which do not work for zephyr @@ -124,8 +131,10 @@ #define USE_API_HOMEASSISTANT_STATES #define USE_API_NOISE #define USE_API_PLAINTEXT -#define USE_API_SERVICES +#define USE_API_USER_DEFINED_ACTIONS #define USE_API_CUSTOM_SERVICES +#define USE_API_USER_DEFINED_ACTION_RESPONSES +#define USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #define API_MAX_SEND_QUEUE 8 #define USE_MD5 #define USE_SHA256 @@ -135,11 +144,8 @@ #define USE_ONLINE_IMAGE_PNG_SUPPORT #define USE_ONLINE_IMAGE_JPEG_SUPPORT #define USE_OTA -#define USE_OTA_MD5 #define USE_OTA_PASSWORD -#define USE_OTA_SHA256 -#define ALLOW_OTA_DOWNGRADE_MD5 -#define USE_OTA_STATE_CALLBACK +#define USE_OTA_STATE_LISTENER #define USE_OTA_VERSION 2 #define USE_TIME_TIMEZONE #define USE_WIFI @@ -155,15 +161,12 @@ #define USE_I2S_LEGACY #endif -// IDF-specific feature flags -#ifdef USE_ESP_IDF -#define USE_MQTT_IDF_ENQUEUE -#define ESPHOME_LOOP_TASK_STACK_SIZE 8192 -#endif - // ESP32-specific feature flags #ifdef USE_ESP32 +#define USE_MQTT_IDF_ENQUEUE #define USE_ESPHOME_TASK_LOG_BUFFER +#define USE_OTA_ROLLBACK +#define USE_ESP32_MIN_CHIP_REVISION_SET #define USE_BLUETOOTH_PROXY #define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 @@ -190,6 +193,7 @@ #define ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT 1 #define ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT 1 #define ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT 2 +#define ESPHOME_LOOP_TASK_STACK_SIZE 8192 #define USE_ESP32_CAMERA_JPEG_ENCODER #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C @@ -210,15 +214,18 @@ #define USE_WEBSERVER_SORTING #define USE_WIFI_11KV_SUPPORT #define USE_WIFI_FAST_CONNECT +#define USE_WIFI_LISTENERS +#define USE_WIFI_RUNTIME_POWER_SAVE #define USB_HOST_MAX_REQUESTS 16 #ifdef USE_ARDUINO -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 5) #define USE_ETHERNET #define USE_ETHERNET_KSZ8081 +#define USE_ETHERNET_MANUAL_IP #endif -#ifdef USE_ESP_IDF +#ifdef USE_ESP32 #define USE_MICRO_WAKE_WORD #define USE_MICRO_WAKE_WORD_VAD #if defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) @@ -228,9 +235,9 @@ #if defined(USE_ESP32_VARIANT_ESP32S2) #define USE_LOGGER_USB_CDC -#elif defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3) || \ - defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) || \ - defined(USE_ESP32_VARIANT_ESP32P4) +#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \ + defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \ + defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S3) #define USE_LOGGER_USB_CDC #define USE_LOGGER_USB_SERIAL_JTAG #endif @@ -241,7 +248,11 @@ #define USE_ADC_SENSOR_VCC #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 1, 2) #define USE_CAPTIVE_PORTAL +#define USE_ESP8266_LOGGER_SERIAL +#define USE_ESP8266_LOGGER_SERIAL1 #define USE_ESP8266_PREFERENCES_FLASH +#define USE_ESP8266_UART_SERIAL +#define USE_ESP8266_UART_SERIAL1 #define USE_HTTP_REQUEST_ESP8266_HTTPS #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C @@ -292,6 +303,10 @@ #define USE_NRF52_UICR_ERASE #define USE_SOFTDEVICE_ID 7 #define USE_SOFTDEVICE_VERSION 1 +#define USE_ZIGBEE +#define USE_ZIGBEE_WIPE_ON_BOOT +#define USE_ZIGBEE_WIPE_ON_BOOT_MAGIC 1 +#define ZIGBEE_ENDPOINTS_COUNT 8 #endif // Disabled feature flags @@ -326,3 +341,4 @@ #define ESPHOME_ENTITY_TIME_COUNT 1 #define ESPHOME_ENTITY_UPDATE_COUNT 1 #define ESPHOME_ENTITY_VALVE_COUNT 1 +#define ESPHOME_ENTITY_WATER_HEATER_COUNT 1 diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 4883c72cf1..8508b93411 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -9,7 +9,8 @@ static const char *const TAG = "entity_base"; // Entity Name const StringRef &EntityBase::get_name() const { return this->name_; } -void EntityBase::set_name(const char *name) { +void EntityBase::set_name(const char *name) { this->set_name(name, 0); } +void EntityBase::set_name(const char *name, uint32_t object_id_hash) { this->name_ = StringRef(name); if (this->name_.empty()) { #ifdef USE_DEVICES @@ -18,11 +19,29 @@ void EntityBase::set_name(const char *name) { } else #endif { - this->name_ = StringRef(App.get_friendly_name()); + // Bug-for-bug compatibility with OLD behavior: + // - With MAC suffix: OLD code used App.get_friendly_name() directly (no fallback) + // - Without MAC suffix: OLD code used pre-computed object_id with fallback to device name + const std::string &friendly = App.get_friendly_name(); + if (App.is_name_add_mac_suffix_enabled()) { + // MAC suffix enabled - use friendly_name directly (even if empty) for compatibility + this->name_ = StringRef(friendly); + } else { + // No MAC suffix - fallback to device name if friendly_name is empty + this->name_ = StringRef(!friendly.empty() ? friendly : App.get_name()); + } } this->flags_.has_own_name = false; + // Dynamic name - must calculate hash at runtime + this->calc_object_id_(); } else { this->flags_.has_own_name = true; + // Static name - use pre-computed hash if provided + if (object_id_hash != 0) { + this->object_id_hash_ = object_id_hash; + } else { + this->calc_object_id_(); + } } } @@ -45,39 +64,30 @@ void EntityBase::set_icon(const char *icon) { #endif } -// Check if the object_id is dynamic (changes with MAC suffix) -bool EntityBase::is_object_id_dynamic_() const { - return !this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled(); -} - -// Entity Object ID +// Entity Object ID - computed on-demand from name std::string EntityBase::get_object_id() const { - // Check if `App.get_friendly_name()` is constant or dynamic. - if (this->is_object_id_dynamic_()) { - // `App.get_friendly_name()` is dynamic. - return str_sanitize(str_snake_case(App.get_friendly_name())); - } - // `App.get_friendly_name()` is constant. - return this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_; -} -StringRef EntityBase::get_object_id_ref_for_api_() const { - static constexpr auto EMPTY_STRING = StringRef::from_lit(""); - // Return empty for dynamic case (MAC suffix) - if (this->is_object_id_dynamic_()) { - return EMPTY_STRING; - } - // For static case, return the string or empty if null - return this->object_id_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->object_id_c_str_); -} -void EntityBase::set_object_id(const char *object_id) { - this->object_id_c_str_ = object_id; - this->calc_object_id_(); + char buf[OBJECT_ID_MAX_LEN]; + size_t len = this->write_object_id_to(buf, sizeof(buf)); + return std::string(buf, len); } -// Calculate Object ID Hash from Entity Name +// Calculate Object ID Hash directly from name using snake_case + sanitize void EntityBase::calc_object_id_() { - this->object_id_hash_ = - fnv1_hash(this->is_object_id_dynamic_() ? this->get_object_id().c_str() : this->object_id_c_str_); + this->object_id_hash_ = fnv1_hash_object_id(this->name_.c_str(), this->name_.size()); +} + +size_t EntityBase::write_object_id_to(char *buf, size_t buf_size) const { + size_t len = std::min(this->name_.size(), buf_size - 1); + for (size_t i = 0; i < len; i++) { + buf[i] = to_sanitized_char(to_snake_case_char(this->name_[i])); + } + buf[len] = '\0'; + return len; +} + +StringRef EntityBase::get_object_id_to(std::span buf) const { + size_t len = this->write_object_id_to(buf.data(), buf.size()); + return StringRef(buf.data(), len); } uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 2b52d66f76..a45c7795bf 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -1,7 +1,8 @@ #pragma once -#include #include +#include +#include #include "string_ref.h" #include "helpers.h" #include "log.h" @@ -12,14 +13,8 @@ namespace esphome { -// Forward declaration for friend access -namespace api { -class APIConnection; -} // namespace api - -namespace web_server { -struct UrlMatch; -} // namespace web_server +// Maximum size for object_id buffer (friendly_name max ~120 + margin) +static constexpr size_t OBJECT_ID_MAX_LEN = 128; enum EntityCategory : uint8_t { ENTITY_CATEGORY_NONE = 0, @@ -33,17 +28,37 @@ class EntityBase { // Get/set the name of this Entity const StringRef &get_name() const; void set_name(const char *name); + /// Set name with pre-computed object_id hash (avoids runtime hash calculation) + /// Use hash=0 for dynamic names that need runtime calculation + void set_name(const char *name, uint32_t object_id_hash); // Get whether this Entity has its own name or it should use the device friendly_name. bool has_own_name() const { return this->flags_.has_own_name; } // Get the sanitized name of this Entity as an ID. + // Deprecated: object_id mangles names and all object_id methods are planned for removal. + // See https://github.com/esphome/backlog/issues/76 + // Now is the time to stop using object_id entirely. If you still need it temporarily, + // use get_object_id_to() which will remain available longer but will also eventually be removed. + ESPDEPRECATED("object_id mangles names and all object_id methods are planned for removal " + "(see https://github.com/esphome/backlog/issues/76). " + "Now is the time to stop using object_id. If still needed, use get_object_id_to() " + "which will remain available longer. get_object_id() will be removed in 2026.7.0", + "2025.12.0") std::string get_object_id() const; - void set_object_id(const char *object_id); // Get the unique Object ID of this Entity uint32_t get_object_id_hash(); + /// Get object_id with zero heap allocation + /// For static case: returns StringRef to internal storage (buffer unused) + /// For dynamic case: formats into buffer and returns StringRef to buffer + StringRef get_object_id_to(std::span buf) const; + + /// Write object_id directly to buffer, returns length written (excluding null) + /// Useful for building compound strings without intermediate buffer + size_t write_object_id_to(char *buf, size_t buf_size) const; + // Get/set whether this Entity should be hidden outside ESPHome bool is_internal() const { return this->flags_.internal; } void set_internal(bool internal) { this->flags_.internal = internal; } @@ -84,6 +99,8 @@ class EntityBase { return this->device_->get_device_id(); } void set_device(Device *device) { this->device_ = device; } + // Get the device this entity belongs to (nullptr if main device) + Device *get_device() const { return this->device_; } #endif // Check if this entity has state @@ -122,20 +139,9 @@ class EntityBase { } protected: - friend class api::APIConnection; - friend struct web_server::UrlMatch; - - // Get object_id as StringRef when it's static (for API usage) - // Returns empty StringRef if object_id is dynamic (needs allocation) - StringRef get_object_id_ref_for_api_() const; - void calc_object_id_(); - /// Check if the object_id is dynamic (changes with MAC suffix) - bool is_object_id_dynamic_() const; - StringRef name_; - const char *object_id_c_str_{nullptr}; #ifdef USE_ENTITY_ICON const char *icon_c_str_{nullptr}; #endif @@ -202,7 +208,7 @@ template class StatefulEntityBase : public EntityBase { virtual bool has_state() const { return this->state_.has_value(); } virtual const T &get_state() const { return this->state_.value(); } virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); } - void invalidate_state() { this->set_state_({}); } + void invalidate_state() { this->set_new_state({}); } void add_full_state_callback(std::function previous, optional current)> &&callback) { if (this->full_state_callbacks_ == nullptr) @@ -224,20 +230,20 @@ template class StatefulEntityBase : public EntityBase { /** * Set a new state for this entity. This will trigger callbacks only if the new state is different from the previous. * - * @param state The new state. + * @param new_state The new state. * @return True if the state was changed, false if it was the same as before. */ - bool set_state_(const optional &state) { - if (this->state_ != state) { + virtual bool set_new_state(const optional &new_state) { + if (this->state_ != new_state) { // call the full state callbacks with the previous and new state if (this->full_state_callbacks_ != nullptr) - this->full_state_callbacks_->call(this->state_, state); + this->full_state_callbacks_->call(this->state_, new_state); // trigger legacy callbacks only if the new state is valid and either the trigger on initial state is enabled or // the previous state was valid auto had_state = this->has_state(); - this->state_ = state; - if (this->state_callbacks_ != nullptr && state.has_value() && (this->trigger_on_initial_state_ || had_state)) - this->state_callbacks_->call(state.value()); + this->state_ = new_state; + if (this->state_callbacks_ != nullptr && new_state.has_value() && (this->trigger_on_initial_state_ || had_state)) + this->state_callbacks_->call(new_state.value()); return true; } return false; diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 9b4786f835..c1801c0bda 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -15,7 +15,7 @@ from esphome.const import ( from esphome.core import CORE, ID from esphome.cpp_generator import MockObj, add, get_variable import esphome.final_validate as fv -from esphome.helpers import sanitize, snake_case +from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case from esphome.types import ConfigType, EntityMetadata _LOGGER = logging.getLogger(__name__) @@ -75,36 +75,18 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: config: Configuration dictionary containing entity settings platform: The platform name (e.g., "sensor", "binary_sensor") """ - # Get device info - device_name: str | None = None - device_id_obj: ID | None + # Get device info if configured if device_id_obj := config.get(CONF_DEVICE_ID): device: MockObj = await get_variable(device_id_obj) add(var.set_device(device)) - # Get device name for object ID calculation - device_name = device_id_obj.id - add(var.set_name(config[CONF_NAME])) - - # Calculate base object_id using the same logic as C++ - # This must match the C++ behavior in esphome/core/entity_base.cpp - base_object_id = get_base_entity_object_id( - config[CONF_NAME], CORE.friendly_name, device_name - ) - - if not config[CONF_NAME]: - _LOGGER.debug( - "Entity has empty name, using '%s' as object_id base", base_object_id - ) - - # Set the object ID - add(var.set_object_id(base_object_id)) - _LOGGER.debug( - "Setting object_id '%s' for entity '%s' on platform '%s'", - base_object_id, - config[CONF_NAME], - platform, - ) + # Set the entity name with pre-computed object_id hash + # For named entities: pre-compute hash from entity name + # For empty-name entities: pass 0, C++ calculates hash at runtime from + # device name, friendly_name, or app name (bug-for-bug compatibility) + entity_name = config[CONF_NAME] + object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0 + add(var.set_name(entity_name, object_id_hash)) # Only set disabled_by_default if True (default is False) if config[CONF_DISABLED_BY_DEFAULT]: add(var.set_disabled_by_default(True)) diff --git a/esphome/core/gpio.cpp b/esphome/core/gpio.cpp new file mode 100644 index 0000000000..21e88b5b6d --- /dev/null +++ b/esphome/core/gpio.cpp @@ -0,0 +1,24 @@ +#include "esphome/core/gpio.h" +#include "esphome/core/log.h" + +namespace esphome { + +#ifdef USE_ESP8266 +void log_pin(const char *tag, const __FlashStringHelper *prefix, GPIOPin *pin) { + if (pin == nullptr) + return; + static constexpr size_t LOG_PIN_PREFIX_MAX_LEN = 32; + char prefix_buf[LOG_PIN_PREFIX_MAX_LEN]; + strncpy_P(prefix_buf, reinterpret_cast(prefix), sizeof(prefix_buf) - 1); + prefix_buf[sizeof(prefix_buf) - 1] = '\0'; + log_pin_with_prefix(tag, prefix_buf, pin); +} +#else +void log_pin(const char *tag, const char *prefix, GPIOPin *pin) { + if (pin == nullptr) + return; + log_pin_with_prefix(tag, prefix, pin); +} +#endif + +} // namespace esphome diff --git a/esphome/core/gpio.h b/esphome/core/gpio.h index dd6f14fef9..f2f85e18bc 100644 --- a/esphome/core/gpio.h +++ b/esphome/core/gpio.h @@ -1,13 +1,22 @@ #pragma once +#include #include +#include #include +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + namespace esphome { -#define LOG_PIN(prefix, pin) \ - if ((pin) != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix "%s", (pin)->dump_summary().c_str()); \ - } +/// Maximum buffer size for dump_summary output +inline constexpr size_t GPIO_SUMMARY_MAX_LEN = 48; + +#ifdef USE_ESP8266 +#define LOG_PIN(prefix, pin) log_pin(TAG, F(prefix), pin) +#else +#define LOG_PIN(prefix, pin) log_pin(TAG, prefix, pin) +#endif // put GPIO flags in a namespace to not pollute esphome namespace namespace gpio { @@ -64,7 +73,17 @@ class GPIOPin { virtual void digital_write(bool value) = 0; - virtual std::string dump_summary() const = 0; + /// Write a summary of this pin to the provided buffer. + /// @param buffer The buffer to write to + /// @param len The size of the buffer (must be > 0) + /// @return The number of characters that would be written (excluding null terminator), + /// which may exceed len-1 if truncation occurred (snprintf semantics) + virtual size_t dump_summary(char *buffer, size_t len) const; + + /// Get a summary of this pin as a string. + /// @deprecated Use dump_summary(char*, size_t) instead. Will be removed in 2026.7.0. + ESPDEPRECATED("Override dump_summary(char*, size_t) instead. Will be removed in 2026.7.0.", "2026.1.0") + virtual std::string dump_summary() const; virtual bool is_internal() { return false; } }; @@ -103,4 +122,41 @@ class InternalGPIOPin : public GPIOPin { virtual void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const = 0; }; +// Inline default implementations for GPIOPin virtual methods. +// These provide bridge functionality for backwards compatibility with external components. + +// Default implementation bridges to old std::string method for backwards compatibility. +inline size_t GPIOPin::dump_summary(char *buffer, size_t len) const { + if (len == 0) + return 0; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + std::string s = this->dump_summary(); +#pragma GCC diagnostic pop + size_t copy_len = std::min(s.size(), len - 1); + memcpy(buffer, s.c_str(), copy_len); + buffer[copy_len] = '\0'; + return s.size(); // Return would-be length (snprintf semantics) +} + +// Default implementation returns empty string. +// External components should override this if they haven't migrated to buffer-based version. +// Remove before 2026.7.0 +inline std::string GPIOPin::dump_summary() const { return {}; } + +// Inline helper for log_pin - allows compiler to inline into log_pin in gpio.cpp +inline void log_pin_with_prefix(const char *tag, const char *prefix, GPIOPin *pin) { + char buffer[GPIO_SUMMARY_MAX_LEN]; + size_t len = pin->dump_summary(buffer, sizeof(buffer)); + len = std::min(len, sizeof(buffer) - 1); + esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, "%s%.*s", prefix, (int) len, buffer); +} + +// log_pin function declarations - implementation in gpio.cpp +#ifdef USE_ESP8266 +void log_pin(const char *tag, const __FlashStringHelper *prefix, GPIOPin *pin); +#else +void log_pin(const char *tag, const char *prefix, GPIOPin *pin); +#endif + } // namespace esphome diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 0ccf21ad83..1a4230e421 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -3,20 +3,12 @@ #include #include "gpio.h" -#if defined(USE_ESP32_FRAMEWORK_ESP_IDF) +#if defined(USE_ESP32) #include #ifndef PROGMEM #define PROGMEM #endif -#elif defined(USE_ESP32_FRAMEWORK_ARDUINO) - -#include - -#ifndef PROGMEM -#define PROGMEM -#endif - #elif defined(USE_ESP8266) #include diff --git a/esphome/core/hash_base.h b/esphome/core/hash_base.h index c45c4df70b..0c1c2dce33 100644 --- a/esphome/core/hash_base.h +++ b/esphome/core/hash_base.h @@ -25,14 +25,8 @@ class HashBase { /// Retrieve the hash as bytes void get_bytes(uint8_t *output) { memcpy(output, this->digest_, this->get_size()); } - /// Retrieve the hash as hex characters - void get_hex(char *output) { - for (size_t i = 0; i < this->get_size(); i++) { - uint8_t byte = this->digest_[i]; - output[i * 2] = format_hex_char(byte >> 4); - output[i * 2 + 1] = format_hex_char(byte & 0x0F); - } - } + /// Retrieve the hash as hex characters. Output buffer must hold get_size() * 2 + 1 bytes. + void get_hex(char *output) { format_hex_to(output, this->get_size() * 2 + 1, this->digest_, this->get_size()); } /// Compare the hash against a provided byte-encoded hash bool equals_bytes(const uint8_t *expected) { return memcmp(this->digest_, expected, this->get_size()) == 0; } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 568acb9f1b..8671dc7f82 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -143,11 +143,12 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t poly, return refout ? (crc ^ 0xffff) : crc; } +// FNV-1 hash - deprecated, use fnv1a_hash() for new code uint32_t fnv1_hash(const char *str) { - uint32_t hash = 2166136261UL; + uint32_t hash = FNV1_OFFSET_BASIS; if (str) { while (*str) { - hash *= 16777619UL; + hash *= FNV1_PRIME; hash ^= *str++; } } @@ -189,21 +190,18 @@ template std::string str_ctype_transform(const std::string &str) std::string str_lower_case(const std::string &str) { return str_ctype_transform(str); } std::string str_upper_case(const std::string &str) { return str_ctype_transform(str); } std::string str_snake_case(const std::string &str) { - std::string result; - result.resize(str.length()); - std::transform(str.begin(), str.end(), result.begin(), ::tolower); - std::replace(result.begin(), result.end(), ' ', '_'); + std::string result = str; + for (char &c : result) { + c = to_snake_case_char(c); + } return result; } std::string str_sanitize(const std::string &str) { - std::string out = str; - std::replace_if( - out.begin(), out.end(), - [](const char &c) { - return c != '-' && c != '_' && (c < '0' || c > '9') && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z'); - }, - '_'); - return out; + std::string result = str; + for (char &c : result) { + c = to_sanitized_char(c); + } + return result; } std::string str_snprintf(const char *fmt, size_t len, ...) { std::string str; @@ -238,43 +236,46 @@ std::string str_sprintf(const char *fmt, ...) { // Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term) static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128; -std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) { - char buffer[MAX_NAME_WITH_SUFFIX_SIZE]; - size_t name_len = name.size(); +size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *name, size_t name_len, char sep, + const char *suffix_ptr, size_t suffix_len) { size_t total_len = name_len + 1 + suffix_len; // Silently truncate if needed: prioritize keeping the full suffix - if (total_len >= MAX_NAME_WITH_SUFFIX_SIZE) { - // NOTE: This calculation could underflow if suffix_len >= MAX_NAME_WITH_SUFFIX_SIZE - 2, + if (total_len >= buffer_size) { + // NOTE: This calculation could underflow if suffix_len >= buffer_size - 2, // but this is safe because this helper is only called with small suffixes: // MAC suffixes (6-12 bytes), ".local" (5 bytes), etc. - name_len = MAX_NAME_WITH_SUFFIX_SIZE - suffix_len - 2; // -2 for separator and null terminator + name_len = buffer_size - suffix_len - 2; // -2 for separator and null terminator total_len = name_len + 1 + suffix_len; } - memcpy(buffer, name.c_str(), name_len); + memcpy(buffer, name, name_len); buffer[name_len] = sep; memcpy(buffer + name_len + 1, suffix_ptr, suffix_len); buffer[total_len] = '\0'; - return std::string(buffer, total_len); + return total_len; +} + +std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, + size_t suffix_len) { + char buffer[MAX_NAME_WITH_SUFFIX_SIZE]; + size_t len = make_name_with_suffix_to(buffer, sizeof(buffer), name, name_len, sep, suffix_ptr, suffix_len); + return std::string(buffer, len); +} + +std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) { + return make_name_with_suffix(name.c_str(), name.size(), sep, suffix_ptr, suffix_len); } // Parsing & formatting size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { - uint8_t val; size_t chars = std::min(length, 2 * count); for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) { - if (*str >= '0' && *str <= '9') { - val = *str - '0'; - } else if (*str >= 'A' && *str <= 'F') { - val = 10 + (*str - 'A'); - } else if (*str >= 'a' && *str <= 'f') { - val = 10 + (*str - 'a'); - } else { + uint8_t val = parse_hex_char(*str); + if (val > 15) return 0; - } - data[i >> 1] = !(i & 1) ? val << 4 : data[i >> 1] | val; + data[i >> 1] = (i & 1) ? data[i >> 1] | val : val << 4; } return chars; } @@ -285,30 +286,91 @@ std::string format_mac_address_pretty(const uint8_t *mac) { return std::string(buf); } +// Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase +static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator, + char base) { + if (length == 0) { + buffer[0] = '\0'; + return buffer; + } + // With separator: total length is 3*length (2*length hex chars, (length-1) separators, 1 null terminator) + // Without separator: total length is 2*length + 1 (2*length hex chars, 1 null terminator) + uint8_t stride = separator ? 3 : 2; + size_t max_bytes = separator ? (buffer_size / stride) : ((buffer_size - 1) / stride); + if (max_bytes == 0) { + buffer[0] = '\0'; + return buffer; + } + if (length > max_bytes) { + length = max_bytes; + } + for (size_t i = 0; i < length; i++) { + size_t pos = i * stride; + buffer[pos] = format_hex_char(data[i] >> 4, base); + buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base); + if (separator && i < length - 1) { + buffer[pos + 2] = separator; + } + } + buffer[length * stride - (separator ? 1 : 0)] = '\0'; + return buffer; +} + +char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { + return format_hex_internal(buffer, buffer_size, data, length, 0, 'a'); +} + std::string format_hex(const uint8_t *data, size_t length) { std::string ret; ret.resize(length * 2); - for (size_t i = 0; i < length; i++) { - ret[2 * i] = format_hex_char(data[i] >> 4); - ret[2 * i + 1] = format_hex_char(data[i] & 0x0F); - } + format_hex_to(&ret[0], length * 2 + 1, data, length); return ret; } std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } +char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) { + return format_hex_internal(buffer, buffer_size, data, length, separator, 'A'); +} + +char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint16_t *data, size_t length, char separator) { + if (length == 0 || buffer_size == 0) { + if (buffer_size > 0) + buffer[0] = '\0'; + return buffer; + } + // With separator: each uint16_t needs 5 chars (4 hex + 1 sep), except last has no separator + // Without separator: each uint16_t needs 4 chars, plus null terminator + uint8_t stride = separator ? 5 : 4; + size_t max_values = separator ? (buffer_size / stride) : ((buffer_size - 1) / stride); + if (max_values == 0) { + buffer[0] = '\0'; + return buffer; + } + if (length > max_values) { + length = max_values; + } + for (size_t i = 0; i < length; i++) { + size_t pos = i * stride; + buffer[pos] = format_hex_pretty_char((data[i] & 0xF000) >> 12); + buffer[pos + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8); + buffer[pos + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4); + buffer[pos + 3] = format_hex_pretty_char(data[i] & 0x000F); + if (separator && i < length - 1) { + buffer[pos + 4] = separator; + } + } + buffer[length * stride - (separator ? 1 : 0)] = '\0'; + return buffer; +} + // Shared implementation for uint8_t and string hex formatting static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { if (data == nullptr || length == 0) return ""; std::string ret; - uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise - ret.resize(multiple * length - (separator ? 1 : 0)); - for (size_t i = 0; i < length; i++) { - ret[multiple * i] = format_hex_pretty_char(data[i] >> 4); - ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F); - if (separator && i != length - 1) - ret[multiple * i + 2] = separator; - } + size_t hex_len = separator ? (length * 3 - 1) : (length * 2); + ret.resize(hex_len); + format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator); if (show_length && length > 4) return ret + " (" + std::to_string(length) + ")"; return ret; @@ -325,16 +387,9 @@ std::string format_hex_pretty(const uint16_t *data, size_t length, char separato if (data == nullptr || length == 0) return ""; std::string ret; - uint8_t multiple = separator ? 5 : 4; // 5 if separator is not \0, 4 otherwise - ret.resize(multiple * length - (separator ? 1 : 0)); - for (size_t i = 0; i < length; i++) { - ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12); - ret[multiple * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8); - ret[multiple * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4); - ret[multiple * i + 3] = format_hex_pretty_char(data[i] & 0x000F); - if (separator && i != length - 1) - ret[multiple * i + 4] = separator; - } + size_t hex_len = separator ? (length * 5 - 1) : (length * 4); + ret.resize(hex_len); + format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator); if (show_length && length > 4) return ret + " (" + std::to_string(length) + ")"; return ret; @@ -382,23 +437,33 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de } std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { - normalize_accuracy_decimals(value, accuracy_decimals); - char tmp[32]; // should be enough, but we should maybe improve this at some point. - snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); - return std::string(tmp); + char buf[VALUE_ACCURACY_MAX_LEN]; + value_accuracy_to_buf(buf, value, accuracy_decimals); + return std::string(buf); } -std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement) { +size_t value_accuracy_to_buf(std::span buf, float value, int8_t accuracy_decimals) { normalize_accuracy_decimals(value, accuracy_decimals); - // Buffer sized for float (up to ~15 chars) + space + typical UOM (usually <20 chars like "μS/cm") - // snprintf truncates safely if exceeded, though ESPHome UOMs are typically short - char tmp[64]; + // snprintf returns chars that would be written (excluding null), or negative on error + int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, value); + if (len < 0) + return 0; // encoding error + // On truncation, snprintf returns would-be length; actual written is buf.size() - 1 + return static_cast(len) >= buf.size() ? buf.size() - 1 : static_cast(len); +} + +size_t value_accuracy_with_uom_to_buf(std::span buf, float value, + int8_t accuracy_decimals, StringRef unit_of_measurement) { if (unit_of_measurement.empty()) { - snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); - } else { - snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + return value_accuracy_to_buf(buf, value, accuracy_decimals); } - return std::string(tmp); + normalize_accuracy_decimals(value, accuracy_decimals); + // snprintf returns chars that would be written (excluding null), or negative on error + int len = snprintf(buf.data(), buf.size(), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + if (len < 0) + return 0; // encoding error + // On truncation, snprintf returns would-be length; actual written is buf.size() - 1 + return static_cast(len) >= buf.size() ? buf.size() - 1 : static_cast(len); } int8_t step_to_accuracy_decimals(float step) { @@ -476,28 +541,23 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) { } size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) { - std::vector decoded = base64_decode(encoded_string); - if (decoded.size() > buf_len) { - ESP_LOGW(TAG, "Base64 decode: buffer too small, truncating"); - decoded.resize(buf_len); - } - memcpy(buf, decoded.data(), decoded.size()); - return decoded.size(); + return base64_decode(reinterpret_cast(encoded_string.data()), encoded_string.size(), buf, buf_len); } -std::vector base64_decode(const std::string &encoded_string) { - int in_len = encoded_string.size(); +size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len) { + size_t in_len = encoded_len; int i = 0; int j = 0; - int in = 0; + size_t in = 0; + size_t out = 0; uint8_t char_array_4[4], char_array_3[3]; - std::vector ret; + bool truncated = false; // SAFETY: The loop condition checks is_base64() before processing each character. // This ensures base64_find_char() is only called on valid base64 characters, // preventing the edge case where invalid chars would return 0 (same as 'A'). - while (in_len-- && (encoded_string[in] != '=') && is_base64(encoded_string[in])) { - char_array_4[i++] = encoded_string[in]; + while (in_len-- && (encoded_data[in] != '=') && is_base64(encoded_data[in])) { + char_array_4[i++] = encoded_data[in]; in++; if (i == 4) { for (i = 0; i < 4; i++) @@ -507,8 +567,13 @@ std::vector base64_decode(const std::string &encoded_string) { char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; - for (i = 0; (i < 3); i++) - ret.push_back(char_array_3[i]); + for (i = 0; i < 3; i++) { + if (out < buf_len) { + buf[out++] = char_array_3[i]; + } else { + truncated = true; + } + } i = 0; } } @@ -524,10 +589,28 @@ std::vector base64_decode(const std::string &encoded_string) { char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; - for (j = 0; (j < i - 1); j++) - ret.push_back(char_array_3[j]); + for (j = 0; j < i - 1; j++) { + if (out < buf_len) { + buf[out++] = char_array_3[j]; + } else { + truncated = true; + } + } } + if (truncated) { + ESP_LOGW(TAG, "Base64 decode: buffer too small, truncating"); + } + + return out; +} + +std::vector base64_decode(const std::string &encoded_string) { + // Calculate maximum decoded size: every 4 base64 chars = 3 bytes + size_t max_len = ((encoded_string.size() + 3) / 4) * 3; + std::vector ret(max_len); + size_t actual_len = base64_decode(encoded_string, ret.data(), max_len); + ret.resize(actual_len); return ret; } @@ -638,17 +721,23 @@ std::string get_mac_address() { } std::string get_mac_address_pretty() { - uint8_t mac[6]; - get_mac_address_raw(mac); - return format_mac_address_pretty(mac); + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + return std::string(get_mac_address_pretty_into_buffer(buf)); } -void get_mac_address_into_buffer(std::span buf) { +void get_mac_address_into_buffer(std::span buf) { uint8_t mac[6]; get_mac_address_raw(mac); format_mac_addr_lower_no_sep(mac, buf.data()); } +const char *get_mac_address_pretty_into_buffer(std::span buf) { + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_upper(mac, buf.data()); + return buf.data(); +} + #ifndef USE_ESP32 bool has_custom_mac_address() { return false; } #endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 16eab8b8f6..acba420d3e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -162,6 +162,10 @@ template class StaticVector { size_t size() const { return count_; } bool empty() const { return count_ == 0; } + // Direct access to underlying data + T *data() { return data_.data(); } + const T *data() const { return data_.data(); } + T &operator[](size_t i) { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; } @@ -242,6 +246,9 @@ template class FixedVector { other.reset_(); } + // Allow conversion to std::vector + operator std::vector() const { return {data_, data_ + size_}; } + FixedVector &operator=(FixedVector &&other) noexcept { if (this != &other) { // Delete our current data @@ -375,9 +382,42 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc = 0, uint16_t p bool refout = false); /// Calculate a FNV-1 hash of \p str. +/// Note: FNV-1a (fnv1a_hash) is preferred for new code due to better avalanche characteristics. uint32_t fnv1_hash(const char *str); inline uint32_t fnv1_hash(const std::string &str) { return fnv1_hash(str.c_str()); } +/// FNV-1 32-bit offset basis +constexpr uint32_t FNV1_OFFSET_BASIS = 2166136261UL; +/// FNV-1 32-bit prime +constexpr uint32_t FNV1_PRIME = 16777619UL; + +/// Extend a FNV-1a hash with additional string data. +constexpr uint32_t fnv1a_hash_extend(uint32_t hash, const char *str) { + if (str) { + while (*str) { + hash ^= *str++; + hash *= FNV1_PRIME; + } + } + return hash; +} +inline uint32_t fnv1a_hash_extend(uint32_t hash, const std::string &str) { + return fnv1a_hash_extend(hash, str.c_str()); +} +/// Extend a FNV-1a hash with an integer (hashes each byte). +template constexpr uint32_t fnv1a_hash_extend(uint32_t hash, T value) { + using UnsignedT = std::make_unsigned_t; + UnsignedT uvalue = static_cast(value); + for (size_t i = 0; i < sizeof(T); i++) { + hash ^= (uvalue >> (i * 8)) & 0xFF; + hash *= FNV1_PRIME; + } + return hash; +} +/// Calculate a FNV-1a hash of \p str. +constexpr uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); } +inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str()); } + /// Return a random 32-bit unsigned integer. uint32_t random_uint32(); /// Return a random float between 0 and 1. @@ -490,12 +530,33 @@ std::string str_until(const std::string &str, char ch); std::string str_lower_case(const std::string &str); /// Convert the string to upper case. std::string str_upper_case(const std::string &str); + +/// Convert a single char to snake_case: lowercase and space to underscore. +constexpr char to_snake_case_char(char c) { return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; } /// Convert the string to snake case (lowercase with underscores). std::string str_snake_case(const std::string &str); +/// Sanitize a single char: keep alphanumerics, dashes, underscores; replace others with underscore. +constexpr char to_sanitized_char(char c) { + return (c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) ? c : '_'; +} /// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores. std::string str_sanitize(const std::string &str); +/// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations. +/// This computes object_id hashes directly from names without creating an intermediate buffer. +/// IMPORTANT: Must match Python fnv1_hash_object_id() in esphome/helpers.py. +/// If you modify this function, update the Python version and tests in both places. +inline uint32_t fnv1_hash_object_id(const char *str, size_t len) { + uint32_t hash = FNV1_OFFSET_BASIS; + for (size_t i = 0; i < len; i++) { + hash *= FNV1_PRIME; + // Apply snake_case (space->underscore, uppercase->lowercase) then sanitize + hash ^= static_cast(to_sanitized_char(to_snake_case_char(str[i]))); + } + return hash; +} + /// snprintf-like function returning std::string of maximum length \p len (excluding null terminator). std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...); @@ -512,6 +573,29 @@ std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, . /// @return The concatenated string: name + sep + suffix std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len); +/// Optimized string concatenation: name + separator + suffix (const char* overload) +/// Uses a fixed stack buffer to avoid heap allocations. +/// @param name The base name string +/// @param name_len Length of the name +/// @param sep Single character separator +/// @param suffix_ptr Pointer to the suffix characters +/// @param suffix_len Length of the suffix +/// @return The concatenated string: name + sep + suffix +std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, + size_t suffix_len); + +/// Zero-allocation version: format name + separator + suffix directly into buffer. +/// @param buffer Output buffer (must have space for result + null terminator) +/// @param buffer_size Size of the output buffer +/// @param name The base name string +/// @param name_len Length of the name +/// @param sep Single character separator +/// @param suffix_ptr Pointer to the suffix characters +/// @param suffix_len Length of the suffix +/// @return Length written (excluding null terminator) +size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *name, size_t name_len, char sep, + const char *suffix_ptr, size_t suffix_len); + ///@} /// @name Parsing & formatting @@ -610,33 +694,134 @@ template::value, int> = 0> optional< return parse_hex(str.c_str(), str.length()); } +/// Parse a hex character to its nibble value (0-15), returns 255 on invalid input +constexpr uint8_t parse_hex_char(char c) { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + return 255; +} + +/// Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase) +inline char format_hex_char(uint8_t v, char base) { return v >= 10 ? base + (v - 10) : '0' + v; } + /// Convert a nibble (0-15) to lowercase hex char -inline char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; } +inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); } /// Convert a nibble (0-15) to uppercase hex char (used for pretty printing) -/// This always uses uppercase (A-F) for pretty/human-readable output -inline char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } +inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); } -/// Format MAC address as XX:XX:XX:XX:XX:XX (uppercase) -inline void format_mac_addr_upper(const uint8_t *mac, char *output) { - for (size_t i = 0; i < 6; i++) { - uint8_t byte = mac[i]; - output[i * 3] = format_hex_pretty_char(byte >> 4); - output[i * 3 + 1] = format_hex_pretty_char(byte & 0x0F); - if (i < 5) - output[i * 3 + 2] = ':'; +/// Write int8 value to buffer without modulo operations. +/// Buffer must have at least 4 bytes free. Returns pointer past last char written. +inline char *int8_to_str(char *buf, int8_t val) { + int32_t v = val; + if (v < 0) { + *buf++ = '-'; + v = -v; } - output[17] = '\0'; + if (v >= 100) { + *buf++ = '1'; // int8 max is 128, so hundreds digit is always 1 + v -= 100; + // Must write tens digit (even if 0) after hundreds + int32_t tens = v / 10; + *buf++ = '0' + tens; + v -= tens * 10; + } else if (v >= 10) { + int32_t tens = v / 10; + *buf++ = '0' + tens; + v -= tens * 10; + } + *buf++ = '0' + v; + return buf; +} + +/// Format byte array as lowercase hex to buffer (base implementation). +char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length); + +/// Format byte array as lowercase hex to buffer. Automatically deduces buffer size. +/// Truncates output if data exceeds buffer capacity. Returns pointer to buffer. +template inline char *format_hex_to(char (&buffer)[N], const uint8_t *data, size_t length) { + static_assert(N >= 3, "Buffer must hold at least one hex byte (3 chars)"); + return format_hex_to(buffer, N, data, length); +} + +/// Format an unsigned integer in lowercased hex to buffer, starting with the most significant byte. +template::value, int> = 0> +inline char *format_hex_to(char (&buffer)[N], T val) { + static_assert(N >= sizeof(T) * 2 + 1, "Buffer too small for type"); + val = convert_big_endian(val); + return format_hex_to(buffer, reinterpret_cast(&val), sizeof(T)); +} + +/// Calculate buffer size needed for format_hex_to: "XXXXXXXX...\0" = bytes * 2 + 1 +constexpr size_t format_hex_size(size_t byte_count) { return byte_count * 2 + 1; } + +/// Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0" +constexpr size_t format_hex_pretty_size(size_t byte_count) { return byte_count * 3; } + +/** Format byte array as uppercase hex to buffer (base implementation). + * + * @param buffer Output buffer to write to. + * @param buffer_size Size of the output buffer. + * @param data Pointer to the byte array to format. + * @param length Number of bytes in the array. + * @param separator Character to use between hex bytes, or '\0' for no separator. + * @return Pointer to buffer. + * + * Buffer size needed: length * 3 with separator (for "XX:XX:XX\0"), length * 2 + 1 without. + */ +char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator = ':'); + +/// Format byte array as uppercase hex with separator to buffer. Automatically deduces buffer size. +template +inline char *format_hex_pretty_to(char (&buffer)[N], const uint8_t *data, size_t length, char separator = ':') { + static_assert(N >= 3, "Buffer must hold at least one hex byte"); + return format_hex_pretty_to(buffer, N, data, length, separator); +} + +/// Calculate buffer size needed for format_hex_pretty_to with uint16_t data: "XXXX:XXXX:...:XXXX\0" +constexpr size_t format_hex_pretty_uint16_size(size_t count) { return count * 5; } + +/** + * Format uint16_t array as uppercase hex with separator to pre-allocated buffer. + * Each uint16_t is formatted as 4 hex chars in big-endian order. + * + * @param buffer Output buffer to write to. + * @param buffer_size Size of the output buffer. + * @param data Pointer to uint16_t array. + * @param length Number of uint16_t values. + * @param separator Character to use between values, or '\0' for no separator. + * @return Pointer to buffer. + * + * Buffer size needed: length * 5 with separator (for "XXXX:XXXX\0"), length * 4 + 1 without. + */ +char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint16_t *data, size_t length, char separator = ':'); + +/// Format uint16_t array as uppercase hex with separator to buffer. Automatically deduces buffer size. +template +inline char *format_hex_pretty_to(char (&buffer)[N], const uint16_t *data, size_t length, char separator = ':') { + static_assert(N >= 5, "Buffer must hold at least one hex uint16_t"); + return format_hex_pretty_to(buffer, N, data, length, separator); +} + +/// MAC address size in bytes +static constexpr size_t MAC_ADDRESS_SIZE = 6; +/// Buffer size for MAC address with separators: "XX:XX:XX:XX:XX:XX\0" +static constexpr size_t MAC_ADDRESS_PRETTY_BUFFER_SIZE = format_hex_pretty_size(MAC_ADDRESS_SIZE); +/// Buffer size for MAC address without separators: "XXXXXXXXXXXX\0" +static constexpr size_t MAC_ADDRESS_BUFFER_SIZE = MAC_ADDRESS_SIZE * 2 + 1; + +/// Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators) +inline void format_mac_addr_upper(const uint8_t *mac, char *output) { + format_hex_pretty_to(output, MAC_ADDRESS_PRETTY_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE, ':'); } /// Format MAC address as xxxxxxxxxxxxxx (lowercase, no separators) inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) { - for (size_t i = 0; i < 6; i++) { - uint8_t byte = mac[i]; - output[i * 2] = format_hex_char(byte >> 4); - output[i * 2 + 1] = format_hex_char(byte & 0x0F); - } - output[12] = '\0'; + format_hex_to(output, MAC_ADDRESS_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE); } /// Format the six-byte array \p mac into a MAC address. @@ -819,8 +1004,15 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); -/// Create a string from a value, an accuracy in decimals, and a unit of measurement. -std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement); + +/// Maximum buffer size for value_accuracy formatting (float ~15 chars + space + UOM ~40 chars + null) +static constexpr size_t VALUE_ACCURACY_MAX_LEN = 64; + +/// Format value with accuracy to buffer, returns chars written (excluding null) +size_t value_accuracy_to_buf(std::span buf, float value, int8_t accuracy_decimals); +/// Format value with accuracy and UOM to buffer, returns chars written (excluding null) +size_t value_accuracy_with_uom_to_buf(std::span buf, float value, + int8_t accuracy_decimals, StringRef unit_of_measurement); /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step); @@ -830,6 +1022,7 @@ std::string base64_encode(const std::vector &buf); std::vector base64_decode(const std::string &encoded_string); size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len); +size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len); ///@} @@ -886,6 +1079,50 @@ template class CallbackManager { std::vector> callbacks_; }; +template class LazyCallbackManager; + +/** Lazy-allocating callback manager that only allocates memory when callbacks are registered. + * + * This is a drop-in replacement for CallbackManager that saves memory when no callbacks + * are registered (common case after the Controller Registry eliminated per-entity callbacks + * from API and web_server components). + * + * Memory overhead comparison (32-bit systems): + * - CallbackManager: 12 bytes (empty std::vector) + * - LazyCallbackManager: 4 bytes (nullptr unique_ptr) + * + * @tparam Ts The arguments for the callbacks, wrapped in void(). + */ +template class LazyCallbackManager { + public: + /// Add a callback to the list. Allocates the underlying CallbackManager on first use. + void add(std::function &&callback) { + if (!this->callbacks_) { + this->callbacks_ = make_unique>(); + } + this->callbacks_->add(std::move(callback)); + } + + /// Call all callbacks in this manager. No-op if no callbacks registered. + void call(Ts... args) { + if (this->callbacks_) { + this->callbacks_->call(args...); + } + } + + /// Return the number of registered callbacks. + size_t size() const { return this->callbacks_ ? this->callbacks_->size() : 0; } + + /// Check if any callbacks are registered. + bool empty() const { return !this->callbacks_ || this->callbacks_->size() == 0; } + + /// Call all callbacks in this manager. + void operator()(Ts... args) { this->call(args...); } + + protected: + std::unique_ptr> callbacks_; +}; + /// Helper class to deduplicate items in a series of values. template class Deduplicator { public: @@ -1049,8 +1286,14 @@ std::string get_mac_address(); std::string get_mac_address_pretty(); /// Get the device MAC address into the given buffer, in lowercase hex notation. -/// Assumes buffer length is 13 (12 digits for hexadecimal representation followed by null terminator). -void get_mac_address_into_buffer(std::span buf); +/// Assumes buffer length is MAC_ADDRESS_BUFFER_SIZE (12 digits for hexadecimal representation followed by null +/// terminator). +void get_mac_address_into_buffer(std::span buf); + +/// Get the device MAC address into the given buffer, in colon-separated uppercase hex notation. +/// Buffer must be exactly MAC_ADDRESS_PRETTY_BUFFER_SIZE bytes (17 for "XX:XX:XX:XX:XX:XX" + null terminator). +/// Returns pointer to the buffer for convenience. +const char *get_mac_address_pretty_into_buffer(std::span buf); #ifdef USE_ESP32 /// Set the MAC address to use from the provided byte array (6 bytes). diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index 68e2825d09..e96b739b58 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -1,12 +1,12 @@ #pragma once -#if defined(USE_ESP32) - #include #include +#ifdef USE_ESP32 #include #include +#endif /* * Lock-free queue for single-producer single-consumer scenarios. @@ -95,7 +95,7 @@ template class LockFreeQueue { } protected: - T *buffer_[SIZE]; + T *buffer_[SIZE]{}; // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) std::atomic dropped_count_; // 65535 max - more than enough for drop tracking // Atomic: written by consumer (pop), read by producer (push) to check if full @@ -106,6 +106,7 @@ template class LockFreeQueue { std::atomic tail_; }; +#ifdef USE_ESP32 // Extended queue with task notification support template class NotifyingLockFreeQueue : public LockFreeQueue { public: @@ -140,7 +141,6 @@ template class NotifyingLockFreeQueue : public LockFreeQu private: TaskHandle_t task_to_notify_; }; +#endif } // namespace esphome - -#endif // defined(USE_ESP32) diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 909319dd28..8338efbb33 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -46,7 +46,7 @@ void HOT esp_log_vprintf_(int level, const char *tag, int line, const __FlashStr } #endif -#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) +#ifdef USE_ESP32 int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT #ifdef USE_LOGGER auto *log = logger::global_logger; diff --git a/esphome/core/log.h b/esphome/core/log.h index cade6a74c1..a2c4b35c6e 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -14,13 +14,10 @@ #endif // Include ESP-IDF/Arduino based logging methods here so they don't undefine ours later -#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) +#if defined(USE_ESP32) #include #include #endif -#ifdef USE_ESP32_FRAMEWORK_ARDUINO -#include -#endif #ifdef USE_LIBRETINY #include #endif @@ -66,7 +63,7 @@ void esp_log_vprintf_(int level, const char *tag, int line, const char *format, #ifdef USE_STORE_LOG_STR_IN_FLASH void esp_log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); #endif -#if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_ESP_IDF) +#if defined(USE_ESP32) int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #endif diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h new file mode 100644 index 0000000000..d1594f47e7 --- /dev/null +++ b/esphome/core/progmem.h @@ -0,0 +1,18 @@ +#pragma once + +// Platform-agnostic macros for PROGMEM string handling +// On ESP8266/Arduino: Use Arduino's F() macro for PROGMEM strings +// On other platforms: Use plain strings (no PROGMEM) + +#ifdef USE_ESP8266 +// ESP8266 uses Arduino macros +#define ESPHOME_F(string_literal) F(string_literal) +#define ESPHOME_PGM_P PGM_P +#define ESPHOME_strncpy_P strncpy_P +#define ESPHOME_strncat_P strncat_P +#else +#define ESPHOME_F(string_literal) (string_literal) +#define ESPHOME_PGM_P const char * +#define ESPHOME_strncpy_P strncpy +#define ESPHOME_strncat_P strncat +#endif diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d2e0f0dab4..8b713523b6 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -154,8 +154,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // For retries, check if there's a cancelled timeout first if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT && - (has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) || - has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { + (has_cancelled_timeout_in_container_locked_(this->items_, component, name_cstr, /* match_retry= */ true) || + has_cancelled_timeout_in_container_locked_(this->to_add_, component, name_cstr, /* match_retry= */ true))) { // Skip scheduling - the retry was cancelled #ifdef ESPHOME_DEBUG_SCHEDULER ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr); @@ -204,13 +204,21 @@ bool HOT Scheduler::cancel_interval(Component *component, const char *name) { } struct RetryArgs { + // Ordered to minimize padding on 32-bit systems std::function func; - uint8_t retry_countdown; - uint32_t current_interval; Component *component; - std::string name; // Keep as std::string since retry uses it dynamically - float backoff_increase_factor; Scheduler *scheduler; + const char *name; // Points to static string or owned copy + uint32_t current_interval; + float backoff_increase_factor; + uint8_t retry_countdown; + bool name_is_dynamic; // True if name needs delete[] + + ~RetryArgs() { + if (this->name_is_dynamic && this->name) { + delete[] this->name; + } + } }; void retry_handler(const std::shared_ptr &args) { @@ -218,8 +226,10 @@ void retry_handler(const std::shared_ptr &args) { if (retry_result == RetryResult::DONE || args->retry_countdown <= 0) return; // second execution of `func` happens after `initial_wait_time` + // Pass is_static_string=true because args->name is owned by the shared_ptr + // which is captured in the lambda and outlives the SchedulerItem args->scheduler->set_timer_common_( - args->component, Scheduler::SchedulerItem::TIMEOUT, false, &args->name, args->current_interval, + args->component, Scheduler::SchedulerItem::TIMEOUT, true, args->name, args->current_interval, [args]() { retry_handler(args); }, /* is_retry= */ true); // backoff_increase_factor applied to third & later executions args->current_interval *= args->backoff_increase_factor; @@ -246,16 +256,35 @@ void HOT Scheduler::set_retry_common_(Component *component, bool is_static_strin auto args = std::make_shared(); args->func = std::move(func); - args->retry_countdown = max_attempts; - args->current_interval = initial_wait_time; args->component = component; - args->name = name_cstr ? name_cstr : ""; // Convert to std::string for RetryArgs - args->backoff_increase_factor = backoff_increase_factor; args->scheduler = this; + args->current_interval = initial_wait_time; + args->backoff_increase_factor = backoff_increase_factor; + args->retry_countdown = max_attempts; + + // Store name - either as static pointer or owned copy + if (name_cstr == nullptr || name_cstr[0] == '\0') { + // Empty or null name - use empty string literal + args->name = ""; + args->name_is_dynamic = false; + } else if (is_static_string) { + // Static string - just store the pointer + args->name = name_cstr; + args->name_is_dynamic = false; + } else { + // Dynamic string - make a copy + size_t len = strlen(name_cstr); + char *copy = new char[len + 1]; + memcpy(copy, name_cstr, len + 1); + args->name = copy; + args->name_is_dynamic = true; + } // First execution of `func` immediately - use set_timer_common_ with is_retry=true + // Pass is_static_string=true because args->name is owned by the shared_ptr + // which is captured in the lambda and outlives the SchedulerItem this->set_timer_common_( - component, SchedulerItem::TIMEOUT, false, &args->name, 0, [args]() { retry_handler(args); }, + component, SchedulerItem::TIMEOUT, true, args->name, 0, [args]() { retry_handler(args); }, /* is_retry= */ true); } @@ -315,7 +344,7 @@ void Scheduler::full_cleanup_removed_items_() { valid_items.push_back(std::move(item)); } else { // Recycle removed items - this->recycle_item_(std::move(item)); + this->recycle_item_main_loop_(std::move(item)); } } @@ -359,8 +388,7 @@ void HOT Scheduler::call(uint32_t now) { std::unique_ptr item; { LockGuard guard{this->lock_}; - item = std::move(this->items_[0]); - this->pop_raw_(); + item = this->pop_raw_locked_(); } const char *name = item->get_name(); @@ -401,7 +429,7 @@ void HOT Scheduler::call(uint32_t now) { // Don't run on failed components if (item->component != nullptr && item->component->is_failed()) { LockGuard guard{this->lock_}; - this->pop_raw_(); + this->recycle_item_main_loop_(this->pop_raw_locked_()); continue; } @@ -414,7 +442,7 @@ void HOT Scheduler::call(uint32_t now) { { LockGuard guard{this->lock_}; if (is_item_removed_(item.get())) { - this->pop_raw_(); + this->recycle_item_main_loop_(this->pop_raw_locked_()); this->to_remove_--; continue; } @@ -423,7 +451,7 @@ void HOT Scheduler::call(uint32_t now) { // Single-threaded or multi-threaded with atomics: can check without lock if (is_item_removed_(item.get())) { LockGuard guard{this->lock_}; - this->pop_raw_(); + this->recycle_item_main_loop_(this->pop_raw_locked_()); this->to_remove_--; continue; } @@ -443,14 +471,14 @@ void HOT Scheduler::call(uint32_t now) { LockGuard guard{this->lock_}; - auto executed_item = std::move(this->items_[0]); // Only pop after function call, this ensures we were reachable // during the function call and know if we were cancelled. - this->pop_raw_(); + auto executed_item = this->pop_raw_locked_(); if (executed_item->remove) { - // We were removed/cancelled in the function call, stop + // We were removed/cancelled in the function call, recycle and continue this->to_remove_--; + this->recycle_item_main_loop_(std::move(executed_item)); continue; } @@ -461,7 +489,7 @@ void HOT Scheduler::call(uint32_t now) { this->to_add_.push_back(std::move(executed_item)); } else { // Timeout completed - recycle it - this->recycle_item_(std::move(executed_item)); + this->recycle_item_main_loop_(std::move(executed_item)); } has_added_items |= !this->to_add_.empty(); @@ -476,7 +504,7 @@ void HOT Scheduler::process_to_add() { for (auto &it : this->to_add_) { if (is_item_removed_(it.get())) { // Recycle cancelled items - this->recycle_item_(std::move(it)); + this->recycle_item_main_loop_(std::move(it)); continue; } @@ -497,7 +525,7 @@ size_t HOT Scheduler::cleanup_() { return this->items_.size(); // We must hold the lock for the entire cleanup operation because: - // 1. We're modifying items_ (via pop_raw_) which requires exclusive access + // 1. We're modifying items_ (via pop_raw_locked_) which requires exclusive access // 2. We're decrementing to_remove_ which is also modified by other threads // (though all modifications are already under lock) // 3. Other threads read items_ when searching for items to cancel in cancel_item_locked_() @@ -510,17 +538,18 @@ size_t HOT Scheduler::cleanup_() { if (!item->remove) break; this->to_remove_--; - this->pop_raw_(); + this->recycle_item_main_loop_(this->pop_raw_locked_()); } return this->items_.size(); } -void HOT Scheduler::pop_raw_() { +std::unique_ptr HOT Scheduler::pop_raw_locked_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); - // Instead of destroying, recycle the item - this->recycle_item_(std::move(this->items_.back())); + // Move the item out before popping - this is the item that was at the front of the heap + auto item = std::move(this->items_.back()); this->items_.pop_back(); + return item; } // Helper to execute a scheduler item @@ -556,28 +585,25 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c #ifndef ESPHOME_THREAD_SINGLE // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { - total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry); + total_cancelled += + this->mark_matching_items_removed_locked_(this->defer_queue_, component, name_cstr, type, match_retry); } #endif /* not ESPHOME_THREAD_SINGLE */ // Cancel items in the main heap - // Special case: if the last item in the heap matches, we can remove it immediately - // (removing the last element doesn't break heap structure) + // We only mark items for removal here - never recycle directly. + // The main loop may be executing an item's callback right now, and recycling + // would destroy the callback while it's running (use-after-free). + // Only the main loop in call() should recycle items after execution completes. if (!this->items_.empty()) { - auto &last_item = this->items_.back(); - if (this->matches_item_(last_item, component, name_cstr, type, match_retry)) { - this->recycle_item_(std::move(this->items_.back())); - this->items_.pop_back(); - total_cancelled++; - } - // For other items in heap, we can only mark for removal (can't remove from middle of heap) - size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry); + size_t heap_cancelled = + this->mark_matching_items_removed_locked_(this->items_, component, name_cstr, type, match_retry); total_cancelled += heap_cancelled; - this->to_remove_ += heap_cancelled; // Track removals for heap items + this->to_remove_ += heap_cancelled; } // Cancel items in to_add_ - total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry); + total_cancelled += this->mark_matching_items_removed_locked_(this->to_add_, component, name_cstr, type, match_retry); return total_cancelled > 0; } @@ -747,7 +773,11 @@ bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, : (a->next_execution_high_ > b->next_execution_high_); } -void Scheduler::recycle_item_(std::unique_ptr item) { +// Recycle a SchedulerItem back to the pool for reuse. +// IMPORTANT: Caller must hold the scheduler lock before calling this function. +// This protects scheduler_item_pool_ from concurrent access by other threads +// that may be acquiring items from the pool in set_timer_common_(). +void Scheduler::recycle_item_main_loop_(std::unique_ptr item) { if (!item) return; diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index fd16840240..5bf3d19adb 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -219,7 +219,9 @@ class Scheduler { // Returns the number of items remaining after cleanup // IMPORTANT: This method should only be called from the main thread (loop task). size_t cleanup_(); - void pop_raw_(); + // Remove and return the front item from the heap + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + std::unique_ptr pop_raw_locked_(); private: // Helper to cancel items by name - must be called with lock held @@ -243,8 +245,18 @@ class Scheduler { } // Helper function to check if item matches criteria for cancellation - inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { + // IMPORTANT: Must be called with scheduler lock held + inline bool HOT matches_item_locked_(const std::unique_ptr &item, Component *component, + const char *name_cstr, SchedulerItem::Type type, bool match_retry, + bool skip_removed = true) const { + // THREAD SAFETY: Check for nullptr first to prevent LoadProhibited crashes. On multi-threaded + // platforms, items can be moved out of defer_queue_ during processing, leaving nullptr entries. + // PR #11305 added nullptr checks in callers (mark_matching_items_removed_locked_() and + // has_cancelled_timeout_in_container_locked_()), but this check provides defense-in-depth: helper + // functions should be safe regardless of caller behavior. + // Fixes: https://github.com/esphome/esphome/issues/11940 + if (!item) + return false; if (item->component != component || item->type != type || (skip_removed && item->remove) || (match_retry && !item->is_retry)) { return false; @@ -260,8 +272,11 @@ class Scheduler { return is_item_removed_(item) || (item->component != nullptr && item->component->is_failed()); } - // Helper to recycle a SchedulerItem - void recycle_item_(std::unique_ptr item); + // Helper to recycle a SchedulerItem back to the pool. + // IMPORTANT: Only call from main loop context! Recycling clears the callback, + // so calling from another thread while the callback is executing causes use-after-free. + // IMPORTANT: Caller must hold the scheduler lock before calling this function. + void recycle_item_main_loop_(std::unique_ptr item); // Helper to perform full cleanup when too many items are cancelled void full_cleanup_removed_items_(); @@ -304,8 +319,8 @@ class Scheduler { // SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_. // This is intentional and safe because: // 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function - // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_ - // and has_cancelled_timeout_in_container_ in scheduler.h) + // 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_locked_ + // and has_cancelled_timeout_in_container_locked_ in scheduler.h) // 3. The lock protects concurrent access, but the nullptr remains until cleanup item = std::move(this->defer_queue_[this->defer_queue_front_]); this->defer_queue_front_++; @@ -317,7 +332,10 @@ class Scheduler { now = this->execute_item_(item.get(), now); } // Recycle the defer item after execution - this->recycle_item_(std::move(item)); + { + LockGuard lock(this->lock_); + this->recycle_item_main_loop_(std::move(item)); + } } // If we've consumed all items up to the snapshot point, clean up the dead space @@ -393,10 +411,10 @@ class Scheduler { // Helper to mark matching items in a container as removed // Returns the number of items marked for removal - // IMPORTANT: Caller must hold the scheduler lock before calling this function. + // IMPORTANT: Must be called with scheduler lock held template - size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr, - SchedulerItem::Type type, bool match_retry) { + size_t mark_matching_items_removed_locked_(Container &container, Component *component, const char *name_cstr, + SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) @@ -405,7 +423,7 @@ class Scheduler { // the vector can still contain nullptr items from the processing loop. This check prevents crashes. if (!item) continue; - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { + if (this->matches_item_locked_(item, component, name_cstr, type, match_retry)) { // Mark item for removal (platform-specific) this->set_item_removed_(item.get(), true); count++; @@ -415,9 +433,10 @@ class Scheduler { } // Template helper to check if any item in a container matches our criteria + // IMPORTANT: Must be called with scheduler lock held template - bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, - bool match_retry) const { + bool has_cancelled_timeout_in_container_locked_(const Container &container, Component *component, + const char *name_cstr, bool match_retry) const { for (const auto &item : container) { // Skip nullptr items (can happen in defer_queue_ when items are being processed) // The defer_queue_ uses index-based processing: items are std::moved out but left in the @@ -426,8 +445,8 @@ class Scheduler { if (!item) continue; if (is_item_removed_(item.get()) && - this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, - /* skip_removed= */ false)) { + this->matches_item_locked_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, + /* skip_removed= */ false)) { return true; } } diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index efaa17181d..505fdd906a 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -128,6 +128,17 @@ inline std::string operator+(const StringRef &lhs, const char *rhs) { return str; } +inline std::string operator+(const StringRef &lhs, const std::string &rhs) { + auto str = lhs.str(); + str.append(rhs); + return str; +} + +inline std::string operator+(const std::string &lhs, const StringRef &rhs) { + std::string str(lhs); + str.append(rhs.c_str(), rhs.size()); + return str; +} #ifdef USE_JSON // NOLINTNEXTLINE(readability-identifier-naming) inline void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); } diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index d30dac4394..4047033f84 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -1,6 +1,7 @@ #include "time.h" // NOLINT #include "helpers.h" +#include #include namespace esphome { @@ -17,6 +18,18 @@ size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { return ::strftime(buffer, buffer_len, format, &c_tm); } +size_t ESPTime::strftime_to(std::span buffer, const char *format) { + struct tm c_tm = this->to_c_tm(); + size_t len = ::strftime(buffer.data(), buffer.size(), format, &c_tm); + if (len > 0) { + return len; + } + // Write "ERROR" to buffer on failure for consistent behavior + constexpr char error_str[] = "ERROR"; + std::copy_n(error_str, sizeof(error_str), buffer.data()); + return sizeof(error_str) - 1; // Length excluding null terminator +} + ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) { ESPTime res{}; res.second = uint8_t(c_tm->tm_sec); @@ -47,13 +60,9 @@ struct tm ESPTime::to_c_tm() { } std::string ESPTime::strftime(const char *format) { - struct tm c_tm = this->to_c_tm(); - char buf[128]; - size_t len = ::strftime(buf, sizeof(buf), format, &c_tm); - if (len > 0) { - return std::string(buf, len); - } - return "ERROR"; + char buf[STRFTIME_BUFFER_SIZE]; + size_t len = this->strftime_to(buf, format); + return std::string(buf, len); } std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); } diff --git a/esphome/core/time.h b/esphome/core/time.h index 68826dabdc..f6f1d57dbb 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace esphome { @@ -13,6 +14,9 @@ uint8_t days_in_month(uint8_t month, uint16_t year); /// A more user-friendly version of struct tm from time.h struct ESPTime { + /// Buffer size required for strftime output + static constexpr size_t STRFTIME_BUFFER_SIZE = 128; + /** seconds after the minute [0-60] * @note second is generally 0-59; the extra range is to accommodate leap seconds. */ @@ -43,14 +47,22 @@ struct ESPTime { */ size_t strftime(char *buffer, size_t buffer_len, const char *format); + /** Format time into a fixed-size buffer, returns length written. + * + * This is the preferred method for avoiding heap allocations. The buffer size is enforced at compile-time. + * On format error, writes "ERROR" to the buffer and returns 5. + * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime + */ + size_t strftime_to(std::span buffer, const char *format); + /** Convert this ESPTime struct to a string as specified by the format argument. * @see https://en.cppreference.com/w/c/chrono/strftime * * @warning This method returns a dynamically allocated string which can cause heap fragmentation with some - * microcontrollers. + * microcontrollers. Prefer strftime_to() for heap-free formatting. * * @warning This method can return "ERROR" when the underlying strftime() call fails or when the - * output exceeds 128 bytes. + * output exceeds STRFTIME_BUFFER_SIZE bytes. */ std::string strftime(const std::string &format); diff --git a/esphome/coroutine.py b/esphome/coroutine.py index 0331c602c5..f5d512e510 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -114,6 +114,14 @@ class CoroPriority(enum.IntEnum): # Examples: web_server_ota (52) WEB_SERVER_OTA = 52 + # Preferences - must run before APPLICATION (safe_mode) because safe_mode + # uses an early return when entering safe mode, skipping all lower priority + # component registration. Without IntervalSyncer registered, preferences + # cannot be synced during shutdown in safe mode, causing issues like the + # boot counter never being cleared and devices getting stuck in safe mode. + # Examples: preferences (51) + PREFERENCES = 51 + # Application-level services # Examples: safe_mode (50) APPLICATION = 50 diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 6f1af01a5b..cff0748c95 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -51,15 +51,19 @@ class AssignmentExpression(Expression): class VariableDeclarationExpression(Expression): - __slots__ = ("type", "modifier", "name") + __slots__ = ("type", "modifier", "name", "static") - def __init__(self, type_, modifier, name): + def __init__( + self, type_: "MockObj", modifier: str, name: ID, *, static: bool = False + ) -> None: self.type = type_ self.modifier = modifier self.name = name + self.static = static - def __str__(self): - return f"{self.type} {self.modifier}{self.name}" + def __str__(self) -> str: + prefix = "static " if self.static else "" + return f"{prefix}{self.type} {self.modifier}{self.name}" class ExpressionList(Expression): @@ -507,13 +511,17 @@ def with_local_variable(id_: ID, rhs: SafeExpType, callback: Callable, *args) -> CORE.add(RawStatement("}")) # output closing curly brace -def new_variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": +def new_variable( + id_: ID, rhs: SafeExpType, type_: "MockObj" = None, *, static: bool = True +) -> "MockObj": """Declare and define a new variable, not pointer type, in the code generation. :param id_: The ID used to declare the variable. :param rhs: The expression to place on the right hand side of the assignment. :param type_: Manually define a type for the variable, only use this when it's not possible to do so during config validation phase (for example because of template arguments). + :param static: If True (default), declare with static storage class for optimization. + Set to False when the variable must have external linkage (e.g., to match library declarations). :return: The new variable as a MockObj. """ @@ -522,7 +530,7 @@ def new_variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj obj = MockObj(id_, ".") if type_ is not None: id_.type = type_ - decl = VariableDeclarationExpression(id_.type, "", id_) + decl = VariableDeclarationExpression(id_.type, "", id_, static=static) CORE.add_global(decl) assignment = AssignmentExpression(None, "", id_, rhs) CORE.add(assignment) @@ -544,7 +552,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": obj = MockObj(id_, "->") if type_ is not None: id_.type = type_ - decl = VariableDeclarationExpression(id_.type, "*", id_) + decl = VariableDeclarationExpression(id_.type, "*", id_, static=True) CORE.add_global(decl) assignment = AssignmentExpression(None, None, id_, rhs) CORE.add(assignment) @@ -635,7 +643,7 @@ async def get_variable(id_: ID) -> "MockObj": Wait for the given ID to be defined in the code generation and return it as a MockObj. - This is a coroutine, you need to await it with a 'await' expression! + This is a coroutine, you need to await it with an 'await' expression! :param id_: The ID to retrieve :return: The variable as a MockObj. @@ -648,7 +656,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: Wait for the given ID to be defined in the code generation and return it as a MockObj. - This is a coroutine, you need to await it with a 'await' expression! + This is a coroutine, you need to await it with an 'await' expression! :param id_: The ID to retrieve :return: The variable as a MockObj. @@ -659,7 +667,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: async def process_lambda( value: Lambda, parameters: TemplateArgsType, - capture: str = "=", + capture: str = "", return_type: SafeExpType = None, ) -> LambdaExpression | None: """Process the given lambda value into a LambdaExpression. @@ -702,12 +710,6 @@ async def process_lambda( parts[i * 3 + 1] = var parts[i * 3 + 2] = "" - # All id() references are global variables in generated C++ code. - # Global variables should not be captured - they're accessible everywhere. - # Use empty capture instead of capture-by-value. - if capture == "=": - capture = "" - if isinstance(value, ESPHomeDataBase) and value.esp_range is not None: location = value.esp_range.start_mark location.line += value.content_offset diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 804a2b99af..f94d8eea22 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -164,8 +164,24 @@ def websocket_method(name): return wrap +class CheckOriginMixin: + """Mixin to handle WebSocket origin checks for reverse proxy setups.""" + + def check_origin(self, origin: str) -> bool: + if "ESPHOME_TRUSTED_DOMAINS" not in os.environ: + return super().check_origin(origin) + trusted_domains = [ + s.strip() for s in os.environ["ESPHOME_TRUSTED_DOMAINS"].split(",") + ] + url = urlparse(origin) + if url.hostname in trusted_domains: + return True + _LOGGER.info("check_origin %s, domain is not trusted", origin) + return False + + @websocket_class -class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): +class EsphomeCommandWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandler): """Base class for ESPHome websocket commands.""" def __init__( @@ -183,18 +199,6 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): # use Popen() with a reading thread instead self._use_popen = os.name == "nt" - def check_origin(self, origin): - if "ESPHOME_TRUSTED_DOMAINS" not in os.environ: - return super().check_origin(origin) - trusted_domains = [ - s.strip() for s in os.environ["ESPHOME_TRUSTED_DOMAINS"].split(",") - ] - url = urlparse(origin) - if url.hostname in trusted_domains: - return True - _LOGGER.info("check_origin %s, domain is not trusted", origin) - return False - def open(self, *args: str, **kwargs: str) -> None: """Handle new WebSocket connection.""" # Ensure messages from the subprocess are sent immediately @@ -601,7 +605,7 @@ DASHBOARD_SUBSCRIBER = DashboardSubscriber() @websocket_class -class DashboardEventsWebSocket(tornado.websocket.WebSocketHandler): +class DashboardEventsWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandler): """WebSocket handler for real-time dashboard events.""" _event_listeners: list[Callable[[], None]] | None = None diff --git a/esphome/espota2.py b/esphome/espota2.py index 2b1b9a8328..6349ad0fa8 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -322,8 +322,8 @@ def perform_ota( hash_func, nonce_size, hash_name = _AUTH_METHODS[auth] perform_auth(sock, password, hash_func, nonce_size, hash_name) - # Set higher timeout during upload - sock.settimeout(30.0) + # Timeout must match device-side OTA_SOCKET_TIMEOUT_DATA to prevent premature failures + sock.settimeout(90.0) upload_size = len(upload_contents) upload_size_encoded = [ @@ -402,7 +402,7 @@ def run_ota_impl_( ) _LOGGER.error( "(If this error persists, please set a static IP address: " - "https://esphome.io/components/wifi.html#manual-ips)" + "https://esphome.io/components/wifi/#manual-ips)" ) raise OTAError(err) from err diff --git a/esphome/helpers.py b/esphome/helpers.py index ea6abff50a..ae142b7f8b 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -35,6 +35,10 @@ IS_MACOS = platform.system() == "Darwin" IS_WINDOWS = platform.system() == "Windows" IS_LINUX = platform.system() == "Linux" +# FNV-1 hash constants (must match C++ in esphome/core/helpers.h) +FNV1_OFFSET_BASIS = 2166136261 +FNV1_PRIME = 16777619 + def ensure_unique_string(preferred_string, current_strings): test_string = preferred_string @@ -49,8 +53,17 @@ def ensure_unique_string(preferred_string, current_strings): return test_string +def fnv1_hash(string: str) -> int: + """FNV-1 32-bit hash function (multiply then XOR).""" + hash_value = FNV1_OFFSET_BASIS + for char in string: + hash_value = (hash_value * FNV1_PRIME) & 0xFFFFFFFF + hash_value ^= ord(char) + return hash_value + + def fnv1a_32bit_hash(string: str) -> int: - """FNV-1a 32-bit hash function. + """FNV-1a 32-bit hash function (XOR then multiply). Note: This uses 32-bit hash instead of 64-bit for several reasons: 1. ESPHome targets 32-bit microcontrollers with limited RAM (often <320KB) @@ -63,13 +76,22 @@ def fnv1a_32bit_hash(string: str) -> int: a handful of area_ids and device_ids (typically <10 areas and <100 devices), making collisions virtually impossible. """ - hash_value = 2166136261 + hash_value = FNV1_OFFSET_BASIS for char in string: hash_value ^= ord(char) - hash_value = (hash_value * 16777619) & 0xFFFFFFFF + hash_value = (hash_value * FNV1_PRIME) & 0xFFFFFFFF return hash_value +def fnv1_hash_object_id(name: str) -> int: + """Compute FNV-1 hash of name with snake_case + sanitize transformations. + + IMPORTANT: Must produce same result as C++ fnv1_hash_object_id() in helpers.h. + Used for pre-computing entity object_id hashes at code generation time. + """ + return fnv1_hash(sanitize(snake_case(name))) + + def strip_accents(value: str) -> str: """Remove accents from a string.""" import unicodedata @@ -424,9 +446,13 @@ def write_file_if_changed(path: Path, text: str) -> bool: return True -def copy_file_if_changed(src: Path, dst: Path) -> None: +def copy_file_if_changed(src: Path, dst: Path) -> bool: + """Copy file from src to dst if contents differ. + + Returns True if file was copied, False if files already matched. + """ if file_compare(src, dst): - return + return False dst.parent.mkdir(parents=True, exist_ok=True) try: shutil.copyfile(src, dst) @@ -441,11 +467,12 @@ def copy_file_if_changed(src: Path, dst: Path) -> None: with suppress(OSError): os.unlink(dst) shutil.copyfile(src, dst) - return + return True from esphome.core import EsphomeError raise EsphomeError(f"Error copying file {src} to {dst}: {err}") from err + return True def list_starts_with(list_, sub): diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index fcb3a4f438..2dc5b94847 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,9 +4,9 @@ dependencies: espressif/esp32-camera: version: 2.1.1 espressif/mdns: - version: 1.8.2 + version: 1.9.1 espressif/esp_wifi_remote: - version: 1.1.5 + version: 1.2.2 rules: - if: "target in [esp32h2, esp32p4]" espressif/eppp_link: @@ -14,7 +14,7 @@ dependencies: rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.6.1 + version: 2.7.0 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: @@ -27,3 +27,9 @@ dependencies: version: "1.7.6~1" rules: - if: "target in [esp32s2, esp32s3, esp32p4]" + esphome/esp-hub75: + version: 0.2.2 + rules: + - if: "target in [esp32, esp32s2, esp32s3, esp32p4]" + esp32async/asynctcp: + version: 3.4.91 diff --git a/esphome/loader.py b/esphome/loader.py index 387443c032..968c8cf3e0 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -187,7 +187,14 @@ def install_meta_finder( def install_custom_components_meta_finder(): + # Remove before 2026.6.0 custom_components_dir = (Path(CORE.config_dir) / "custom_components").resolve() + if custom_components_dir.is_dir() and any(custom_components_dir.iterdir()): + _LOGGER.warning( + "The 'custom_components' folder is deprecated and will be removed in 2026.6.0. " + "Please use 'external_components' instead. " + "See https://esphome.io/components/external_components.html for more information." + ) install_meta_finder(custom_components_dir) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 0d50edbc2c..042df12d67 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -192,7 +192,7 @@ def get_esphome_device_ip( data = json.loads(payload) if "name" not in data or data["name"] != dev_name: - _LOGGER.Warn("Wrong device answer") + _LOGGER.warning("Wrong device answer") return dev_ip = [] diff --git a/esphome/pins.py b/esphome/pins.py index 601c05880a..bdaa0e28ab 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -274,7 +274,7 @@ def check_strapping_pin(conf, strapping_pin_list: set[int], logger: Logger): logger.warning( f"GPIO{num} is a strapping PIN and should only be used for I/O with care.\n" "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n" - "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", + "See https://esphome.io/guides/faq/#why-am-i-getting-a-warning-about-strapping-pins", ) # mitigate undisciplined use of strapping: if num not in strapping_pin_list and conf.get(CONF_IGNORE_STRAPPING_WARNING): diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index d59523a74a..e66f9a2c97 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -107,9 +107,24 @@ FILTER_PLATFORMIO_LINES = [ r"Warning: DEPRECATED: 'esptool.py' is deprecated. Please use 'esptool' instead. The '.py' suffix will be removed in a future major release.", r"Warning: esp-idf-size exited with code 2", r"esp_idf_size: error: unrecognized arguments: --ng", + r"Package configuration completed successfully", ] +class PlatformioLogFilter(logging.Filter): + """Filter to suppress noisy platformio log messages.""" + + _PATTERN = re.compile( + r"|".join(r"(?:" + pattern + r")" for pattern in FILTER_PLATFORMIO_LINES) + ) + + def filter(self, record: logging.LogRecord) -> bool: + # Only filter messages from platformio-related loggers + if "platformio" not in record.name.lower(): + return True + return self._PATTERN.match(record.getMessage()) is None + + def run_platformio_cli(*args, **kwargs) -> str | int: os.environ["PLATFORMIO_FORCE_COLOR"] = "true" os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute()) @@ -130,7 +145,18 @@ def run_platformio_cli(*args, **kwargs) -> str | int: patch_structhash() patch_file_downloader() - return run_external_command(platformio.__main__.main, *cmd, **kwargs) + + # Add log filter to suppress noisy platformio messages + log_filter = PlatformioLogFilter() if not CORE.verbose else None + if log_filter: + for handler in logging.getLogger().handlers: + handler.addFilter(log_filter) + try: + return run_external_command(platformio.__main__.main, *cmd, **kwargs) + finally: + if log_filter: + for handler in logging.getLogger().handlers: + handler.removeFilter(log_filter) def run_platformio_cli_run(config, verbose, *args, **kwargs) -> str | int: @@ -394,3 +420,8 @@ class IDEData: if path.endswith(".exe") else f"{path[:-3]}readelf" ) + + @property + def defines(self) -> list[str]: + """Return the list of preprocessor defines from idedata.""" + return self.raw.get("defines", []) diff --git a/esphome/util.py b/esphome/util.py index d41800dc20..7b896de27e 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -375,6 +375,6 @@ def get_esp32_arduino_flash_error_help() -> str | None: + "For detailed migration instructions, see:\n" + color( AnsiFore.BLUE, - "https://esphome.io/guides/esp32_arduino_to_idf.html\n\n", + "https://esphome.io/guides/esp32_arduino_to_idf/\n\n", ) ) diff --git a/esphome/wizard.py b/esphome/wizard.py index 97343eea99..d77450b04d 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -411,9 +411,7 @@ def wizard(path: Path) -> int: "https://docs.platformio.org/en/latest/platforms/espressif8266.html#boards" ) elif platform == "RP2040": - board_link = ( - "https://www.raspberrypi.com/documentation/microcontrollers/rp2040.html" - ) + board_link = "https://www.raspberrypi.com/documentation/microcontrollers/silicon.html#rp2040" elif platform in ["BK72XX", "LN882X", "RTL87XX"]: board_link = "https://docs.libretiny.eu/docs/status/supported/" else: @@ -555,7 +553,7 @@ def wizard(path: Path) -> int: safe_print("Next steps:") safe_print(" > Follow the rest of the getting started guide:") safe_print( - " > https://esphome.io/guides/getting_started_command_line.html#adding-some-features" + " > https://esphome.io/guides/getting_started_command_line/#adding-some-features" ) safe_print(" > to learn how to customize ESPHome and install it to your device.") return 0 diff --git a/esphome/writer.py b/esphome/writer.py index 8eee445cf1..cb9c921693 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,8 +1,14 @@ +from collections.abc import Callable import importlib +import json import logging import os from pathlib import Path import re +import shutil +import stat +import time +from types import TracebackType from esphome import loader from esphome.config import iter_component_configs, iter_components @@ -15,10 +21,12 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import ( copy_file_if_changed, + cpp_string_escape, get_str_env, is_ha_addon, read_file, walk_files, + write_file, write_file_if_changed, ) from esphome.storage_json import StorageJSON, storage_path @@ -95,14 +103,11 @@ def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool: def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool: - if ( + # ESP32 uses CMake for both Arduino and ESP-IDF frameworks + return ( old.loaded_integrations != new.loaded_integrations or old.loaded_platforms != new.loaded_platforms - ) and new.core_platform == PLATFORM_ESP32: - from esphome.components.esp32 import FRAMEWORK_ESP_IDF - - return new.framework == FRAMEWORK_ESP_IDF - return False + ) and new.core_platform == PLATFORM_ESP32 def update_storage_json() -> None: @@ -121,7 +126,7 @@ def update_storage_json() -> None: ) else: _LOGGER.info("Core config or version changed, cleaning build files...") - clean_build() + clean_build(clear_pio_cache=False) elif storage_should_update_cmake_cache(old, new): _LOGGER.info("Integrations changed, cleaning cmake cache...") clean_cmake_cache() @@ -169,6 +174,7 @@ VERSION_H_FORMAT = """\ """ DEFINES_H_TARGET = "esphome/core/defines.h" VERSION_H_TARGET = "esphome/core/version.h" +BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h" ESPHOME_README_TXT = """ THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY @@ -202,10 +208,16 @@ def copy_src_tree(): include_s = "\n".join(include_l) source_files_copy = source_files_map.copy() - ignore_targets = [Path(x) for x in (DEFINES_H_TARGET, VERSION_H_TARGET)] + ignore_targets = [ + Path(x) for x in (DEFINES_H_TARGET, VERSION_H_TARGET, BUILD_INFO_DATA_H_TARGET) + ] for t in ignore_targets: - source_files_copy.pop(t) + source_files_copy.pop(t, None) + # Files to exclude from sources_changed tracking (generated files) + generated_files = {Path("esphome/core/build_info_data.h")} + + sources_changed = False for fname in walk_files(CORE.relative_src_path("esphome")): p = Path(fname) if p.suffix not in SOURCE_FILE_EXTENSIONS: @@ -219,28 +231,82 @@ def copy_src_tree(): if target not in source_files_copy: # Source file removed, delete target p.unlink() + if target not in generated_files: + sources_changed = True else: src_file = source_files_copy.pop(target) with src_file.path() as src_path: - copy_file_if_changed(src_path, p) + if copy_file_if_changed(src_path, p) and target not in generated_files: + sources_changed = True # Now copy new files for target, src_file in source_files_copy.items(): dst_path = CORE.relative_src_path(*target.parts) with src_file.path() as src_path: - copy_file_if_changed(src_path, dst_path) + if ( + copy_file_if_changed(src_path, dst_path) + and target not in generated_files + ): + sources_changed = True # Finally copy defines - write_file_if_changed( + if write_file_if_changed( CORE.relative_src_path("esphome", "core", "defines.h"), generate_defines_h() - ) + ): + sources_changed = True write_file_if_changed(CORE.relative_build_path("README.txt"), ESPHOME_README_TXT) - write_file_if_changed( + if write_file_if_changed( CORE.relative_src_path("esphome.h"), ESPHOME_H_FORMAT.format(include_s) - ) - write_file_if_changed( + ): + sources_changed = True + if write_file_if_changed( CORE.relative_src_path("esphome", "core", "version.h"), generate_version_h() + ): + sources_changed = True + + # Generate new build_info files if needed + build_info_data_h_path = CORE.relative_src_path( + "esphome", "core", "build_info_data.h" ) + build_info_json_path = CORE.relative_build_path("build_info.json") + config_hash, build_time, build_time_str, comment = get_build_info() + + # Defensively force a rebuild if the build_info files don't exist, or if + # there was a config change which didn't actually cause a source change + if not build_info_data_h_path.exists(): + sources_changed = True + else: + try: + existing = json.loads(build_info_json_path.read_text(encoding="utf-8")) + if ( + existing.get("config_hash") != config_hash + or existing.get("esphome_version") != __version__ + ): + sources_changed = True + except (json.JSONDecodeError, KeyError, OSError): + sources_changed = True + + # Write build_info header and JSON metadata + if sources_changed: + write_file( + build_info_data_h_path, + generate_build_info_data_h( + config_hash, build_time, build_time_str, comment + ), + ) + write_file( + build_info_json_path, + json.dumps( + { + "config_hash": config_hash, + "build_time": build_time, + "build_time_str": build_time_str, + "esphome_version": __version__, + }, + indent=2, + ) + + "\n", + ) platform = "esphome.components." + CORE.target_platform try: @@ -266,6 +332,43 @@ def generate_version_h(): ) +def get_build_info() -> tuple[int, int, str, str]: + """Calculate build_info values from current config. + + Returns: + Tuple of (config_hash, build_time, build_time_str, comment) + """ + config_hash = CORE.config_hash + build_time = int(time.time()) + build_time_str = time.strftime("%Y-%m-%d %H:%M:%S %z", time.localtime(build_time)) + comment = CORE.comment or "" + return config_hash, build_time, build_time_str, comment + + +def generate_build_info_data_h( + config_hash: int, build_time: int, build_time_str: str, comment: str +) -> str: + """Generate build_info_data.h header with config hash, build time, and comment.""" + # cpp_string_escape returns '"escaped"', slice off the quotes since template has them + escaped_comment = cpp_string_escape(comment)[1:-1] + # +1 for null terminator + comment_size = len(comment) + 1 + return f"""#pragma once +// Auto-generated build_info data +#define ESPHOME_CONFIG_HASH 0x{config_hash:08x}U // NOLINT +#define ESPHOME_BUILD_TIME {build_time} // NOLINT +#define ESPHOME_COMMENT_SIZE {comment_size} // NOLINT +#ifdef USE_ESP8266 +#include +static const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}"; +static const char ESPHOME_COMMENT_STR[] PROGMEM = "{escaped_comment}"; +#else +static const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}"; +static const char ESPHOME_COMMENT_STR[] = "{escaped_comment}"; +#endif +""" + + def write_cpp(code_s): path = CORE.relative_src_path("main.cpp") if path.is_file(): @@ -301,9 +404,24 @@ def clean_cmake_cache(): pioenvs_cmake_path.unlink() -def clean_build(): - import shutil +def _rmtree_error_handler( + func: Callable[[str], object], + path: str, + exc_info: tuple[type[BaseException], BaseException, TracebackType | None], +) -> None: + """Error handler for shutil.rmtree to handle read-only files on Windows. + On Windows, git pack files and other files may be marked read-only, + causing shutil.rmtree to fail with "Access is denied". This handler + removes the read-only flag and retries the deletion. + """ + if os.access(path, os.W_OK): + raise exc_info[1].with_traceback(exc_info[2]) + os.chmod(path, stat.S_IWUSR | stat.S_IRUSR) + func(path) + + +def clean_build(clear_pio_cache: bool = True): # Allow skipping cache cleaning for integration tests if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): _LOGGER.warning("Skipping build cleaning (ESPHOME_SKIP_CLEAN_BUILD set)") @@ -312,16 +430,19 @@ def clean_build(): pioenvs = CORE.relative_pioenvs_path() if pioenvs.is_dir(): _LOGGER.info("Deleting %s", pioenvs) - shutil.rmtree(pioenvs) + shutil.rmtree(pioenvs, onerror=_rmtree_error_handler) piolibdeps = CORE.relative_piolibdeps_path() if piolibdeps.is_dir(): _LOGGER.info("Deleting %s", piolibdeps) - shutil.rmtree(piolibdeps) + shutil.rmtree(piolibdeps, onerror=_rmtree_error_handler) dependencies_lock = CORE.relative_build_path("dependencies.lock") if dependencies_lock.is_file(): _LOGGER.info("Deleting %s", dependencies_lock) dependencies_lock.unlink() + if not clear_pio_cache: + return + # Clean PlatformIO cache to resolve CMake compiler detection issues # This helps when toolchain paths change or get corrupted try: @@ -334,13 +455,17 @@ def clean_build(): cache_dir = Path(config.get("platformio", "cache_dir")) if cache_dir.is_dir(): _LOGGER.info("Deleting PlatformIO cache %s", cache_dir) - shutil.rmtree(cache_dir) + shutil.rmtree(cache_dir, onerror=_rmtree_error_handler) def clean_all(configuration: list[str]): - import shutil - - data_dirs = [Path(dir) / ".esphome" for dir in configuration] + data_dirs = [] + for config in configuration: + item = Path(config) + if item.is_file() and item.suffix in (".yaml", ".yml"): + data_dirs.append(item.parent / ".esphome") + else: + data_dirs.append(item / ".esphome") if is_ha_addon(): data_dirs.append(Path("/data")) if "ESPHOME_DATA_DIR" in os.environ: @@ -355,7 +480,7 @@ def clean_all(configuration: list[str]): if item.is_file() and not item.name.endswith(".json"): item.unlink() elif item.is_dir() and item.name != "storage": - shutil.rmtree(item) + shutil.rmtree(item, onerror=_rmtree_error_handler) # Clean PlatformIO project files try: @@ -369,7 +494,7 @@ def clean_all(configuration: list[str]): path = Path(config.get("platformio", pio_dir)) if path.is_dir(): _LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path) - shutil.rmtree(path) + shutil.rmtree(path, onerror=_rmtree_error_handler) GITIGNORE_CONTENT = """# Gitignore settings for ESPHome diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 359b72b48f..bba4bbf487 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections.abc import Callable +from contextlib import suppress import functools import inspect from io import BytesIO, TextIOBase, TextIOWrapper @@ -501,13 +502,17 @@ def _load_yaml_internal_with_type( loader.dispose() -def dump(dict_, show_secrets=False): +def dump(dict_, show_secrets=False, sort_keys=False): """Dump YAML to a string and remove null.""" if show_secrets: _SECRET_VALUES.clear() _SECRET_CACHE.clear() return yaml.dump( - dict_, default_flow_style=False, allow_unicode=True, Dumper=ESPHomeDumper + dict_, + default_flow_style=False, + allow_unicode=True, + Dumper=ESPHomeDumper, + sort_keys=sort_keys, ) @@ -543,6 +548,9 @@ class ESPHomeDumper(yaml.SafeDumper): best_style = True if hasattr(mapping, "items"): mapping = list(mapping.items()) + if self.sort_keys: + with suppress(TypeError): + mapping = sorted(mapping) for item_key, item_value in mapping: node_key = self.represent_data(item_key) node_value = self.represent_data(item_value) diff --git a/platformio.ini b/platformio.ini index 94f58f84ab..d96e9ad2cc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -32,20 +32,26 @@ build_flags = ; This are common settings for all environments. [common] -lib_deps = - esphome/noise-c@0.1.10 ; api - improv/Improv@1.2.4 ; improv_serial / esp32_improv +; Base dependencies for all environments +lib_deps_base = bblanchon/ArduinoJson@7.4.2 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 pavlodn/HaierProtocol@0.9.31 ; haier - kikuchan98/pngle@1.1.0 ; online_image + esphome/dsmr_parser@1.0.0 ; dsmr + polargoose/Crypto-no-arduino@0.4.0 ; dsmr https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps +; This is using the repository until a new release is published to PlatformIO + https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library + lvgl/lvgl@8.4.0 ; lvgl + +lib_deps = + ${common.lib_deps_base} + esphome/noise-c@0.1.10 ; api + improv/Improv@1.2.4 ; improv_serial / esp32_improv + kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library https://github.com/bitbank2/JPEGDEC.git#ca1e0f2 ; online_image - ; This is using the repository until a new release is published to PlatformIO - https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library - lvgl/lvgl@8.4.0 ; lvgl ; This dependency is used only in unit tests. ; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py ; See scripts/cpp_unit_test.py and tests/components/README.md @@ -78,8 +84,6 @@ lib_deps = heman/AsyncMqttClient-esphome@1.0.0 ; mqtt fastled/FastLED@3.9.16 ; fastled_base freekode/TM1651@1.0.1 ; tm1651 - glmnet/Dsmr@0.7 ; dsmr - rweather/Crypto@0.4.0 ; dsmr dudanov/MideaUART@1.1.9 ; midea tonia/HeatpumpIR@1.0.37 ; heatpumpir build_flags = @@ -129,9 +133,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-1/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip platform_packages = - pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.2/esp32-3.3.2.zip + pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.5/esp32-3.3.5.zip framework = arduino, espidf ; Arduino as an ESP-IDF component lib_deps = @@ -142,7 +146,6 @@ lib_deps = WiFi ; wifi,web_server_base,ethernet (Arduino built-in) Update ; ota,web_server_base (Arduino built-in) ${common:arduino.lib_deps} - ESP32Async/AsyncTCP@3.4.5 ; async_tcp NetworkClientSecure ; http_request,nextion (Arduino built-in) HTTPClient ; http_request,nextion (Arduino built-in) ESPmDNS ; mdns (Arduino built-in) @@ -152,6 +155,7 @@ lib_deps = esphome/ESP32-audioI2S@2.3.0 ; i2s_audio droscy/esp_wireguard@0.4.2 ; wireguard esphome/esp-audio-libs@2.0.1 ; audio + kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word build_flags = ${common:arduino.build_flags} @@ -165,9 +169,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-1/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip platform_packages = - pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.1/esp-idf-v5.5.1.zip + pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz framework = espidf lib_deps = @@ -234,13 +238,7 @@ build_flags = -DUSE_ZEPHYR -DUSE_NRF52 lib_deps = - bblanchon/ArduinoJson@7.4.2 ; json - wjtje/qr-code-generator-library@1.7.0 ; qr_code - pavlodn/HaierProtocol@0.9.31 ; haier - functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 - https://github.com/esphome/TinyGPSPlus.git#v1.1.0 ; gps - https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library - lvgl/lvgl@8.4.0 ; lvgl + ${common.lib_deps_base} ; All the actual environments are defined below. @@ -378,6 +376,18 @@ build_flags = build_unflags = ${common.build_unflags} +;;;;;;;; ESP32-P4 ;;;;;;;; + +[env:esp32p4-idf] +extends = common:esp32-idf +board = esp32-p4-evboard + +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32p4-idf +build_flags = + ${common:esp32-idf.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32P4 + ;;;;;;;; ESP32-S2 ;;;;;;;; [env:esp32s2-arduino] @@ -466,18 +476,6 @@ build_flags = build_unflags = ${common.build_unflags} -;;;;;;;; ESP32-P4 ;;;;;;;; - -[env:esp32p4-idf] -extends = common:esp32-idf -board = esp32-p4-evboard - -board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32p4-idf -build_flags = - ${common:esp32-idf.build_flags} - ${flags:runtime.build_flags} - -DUSE_ESP32_VARIANT_ESP32P4 - ;;;;;;;; RP2040 ;;;;;;;; [env:rp2040-pico-arduino] diff --git a/requirements.txt b/requirements.txt index 40802422f2..56df559cd7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ cryptography==45.0.1 -voluptuous==0.15.2 +voluptuous==0.16.0 PyYAML==6.0.3 paho-mqtt==1.6.1 colorama==0.4.6 icmplib==3.0.4 -tornado==6.5.2 +tornado==6.5.4 tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 @@ -12,17 +12,17 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==42.7.0 +aioesphomeapi==43.10.1 zeroconf==0.148.0 puremagic==1.30 -ruamel.yaml==0.18.16 # dashboard_import -ruamel.yaml.clib==0.2.14 # dashboard_import +ruamel.yaml==0.19.1 # dashboard_import +ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 pillow==11.3.0 -cairosvg==2.8.2 +resvg-py==0.2.5 freetype-py==2.5.1 jinja2==3.1.6 -bleak==1.1.1 +bleak==2.1.1 # esp-idf >= 5.0 requires this pyparsing >= 3.0 diff --git a/requirements_test.txt b/requirements_test.txt index e238faa77e..f00bcd0a0d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,11 +1,11 @@ -pylint==4.0.3 +pylint==4.0.4 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.5 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating +ruff==0.14.10 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==9.0.1 +pytest==9.0.2 pytest-cov==7.0.0 pytest-mock==3.15.1 pytest-asyncio==1.3.0 diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index 27a36f889f..427602dff2 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -87,6 +87,7 @@ ISOLATED_COMPONENTS = { "neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)", "packages": "cannot merge packages", "tinyusb": "Conflicts with usb_host component - cannot be used together", + "usb_cdc_acm": "Depends on tinyusb which conflicts with usb_host", } diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 3b756095a1..274a672c7c 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -354,41 +354,30 @@ def create_field_type_info( return FixedArrayRepeatedType(field, size_define) return RepeatedTypeInfo(field) - # Check for mutually exclusive options on bytes fields - if field.type == 12: - has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False) - fixed_size = get_field_opt(field, pb.fixed_array_size, None) - - if has_pointer_to_buffer and fixed_size is not None: - raise ValueError( - f"Field '{field.name}' has both pointer_to_buffer and fixed_array_size. " - "These options are mutually exclusive. Use pointer_to_buffer for zero-copy " - "or fixed_array_size for traditional array storage." - ) - - if has_pointer_to_buffer: - # Zero-copy pointer approach - no size needed, will use size_t for length - return PointerToBytesBufferType(field, None) - - if fixed_size is not None: - # Traditional fixed array approach with copy - return FixedArrayBytesType(field, fixed_size) - - # Check for pointer_to_buffer option on string fields - if field.type == 9: - has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False) - - if has_pointer_to_buffer: - # Zero-copy pointer approach for strings - return PointerToBytesBufferType(field, None) - # Special handling for bytes fields if field.type == 12: + fixed_size = get_field_opt(field, pb.fixed_array_size, None) + + if fixed_size is not None: + # Traditional fixed array approach with copy (takes priority) + return FixedArrayBytesType(field, fixed_size) + + # For messages that decode (SOURCE_CLIENT or SOURCE_BOTH), use pointer + # for zero-copy access to the receive buffer + if needs_decode: + return PointerToBytesBufferType(field, None) + + # For SOURCE_SERVER (encode only), explicit annotation is still needed + if get_field_opt(field, pb.pointer_to_buffer, False): + return PointerToBytesBufferType(field, None) + return BytesType(field, needs_decode, needs_encode) - # Special handling for string fields + # Special handling for string fields - use StringRef for zero-copy unless no_zero_copy is set if field.type == 9: - return StringType(field, needs_decode, needs_encode) + if get_field_opt(field, pb.no_zero_copy, False): + return StringType(field, needs_decode, needs_encode) + return PointerToStringBufferType(field, None) validate_field_type(field.type, field.name) return TYPE_INFO[field.type](field) @@ -462,7 +451,7 @@ class Int64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -482,7 +471,7 @@ class UInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu64, {name});\n' o += "out.append(buffer);" return o @@ -522,7 +511,7 @@ class Fixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%llu", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRIu64, {name});\n' o += "out.append(buffer);" return o @@ -840,8 +829,8 @@ class BytesType(TypeInfo): return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes -class PointerToBytesBufferType(TypeInfo): - """Type for bytes fields that use pointer_to_buffer option for zero-copy.""" +class PointerToBufferTypeBase(TypeInfo): + """Base class for pointer_to_buffer types (bytes and strings) for zero-copy decoding.""" @classmethod def can_use_dump_field(cls) -> bool: @@ -851,29 +840,34 @@ class PointerToBytesBufferType(TypeInfo): self, field: descriptor.FieldDescriptorProto, size: int | None = None ) -> None: super().__init__(field) - # Size is not used for pointer_to_buffer - we always use size_t for length self.array_size = 0 @property - def cpp_type(self) -> str: - return "const uint8_t*" + def decode_length(self) -> str | None: + # This is handled in decode_length_content + return None @property - def default_value(self) -> str: - return "nullptr" + def wire_type(self) -> WireType: + """Get the wire type for this field.""" + return WireType.LENGTH_DELIMITED # Uses wire type 2 - @property - def reference_type(self) -> str: - return "const uint8_t*" + def get_estimated_size(self) -> int: + # field ID + length varint + typical data (assume small for pointer fields) + return self.calculate_field_id_size() + 2 + 16 - @property - def const_reference_type(self) -> str: - return "const uint8_t*" + +class PointerToBytesBufferType(PointerToBufferTypeBase): + """Type for bytes fields that use pointer_to_buffer option for zero-copy.""" + + cpp_type = "const uint8_t*" + default_value = "nullptr" + reference_type = "const uint8_t*" + const_reference_type = "const uint8_t*" @property def public_content(self) -> list[str]: # Use uint16_t for length - max packet size is well below 65535 - # Add pointer and length fields return [ f"const uint8_t* {self.field_name}{{nullptr}};", f"uint16_t {self.field_name}_len{{0}};", @@ -885,24 +879,12 @@ class PointerToBytesBufferType(TypeInfo): @property def decode_length_content(self) -> str | None: - # Decode directly stores the pointer to avoid allocation return f"""case {self.number}: {{ - // Use raw data directly to avoid allocation this->{self.field_name} = value.data(); this->{self.field_name}_len = value.size(); break; }}""" - @property - def decode_length(self) -> str | None: - # This is handled in decode_length_content - return None - - @property - def wire_type(self) -> WireType: - """Get the wire type for this bytes field.""" - return WireType.LENGTH_DELIMITED # Uses wire type 2 - def dump(self, name: str) -> str: return ( f"format_hex_pretty(this->{self.field_name}, this->{self.field_name}_len)" @@ -910,7 +892,6 @@ class PointerToBytesBufferType(TypeInfo): @property def dump_content(self) -> str: - # Custom dump that doesn't use dump_field template return ( f'out.append(" {self.name}: ");\n' + f"out.append({self.dump(self.field_name)});\n" @@ -918,11 +899,52 @@ class PointerToBytesBufferType(TypeInfo): ) def get_size_calculation(self, name: str, force: bool = False) -> str: - return f"size.add_length({self.number}, this->{self.field_name}_len);" + return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}_len);" + + +class PointerToStringBufferType(PointerToBufferTypeBase): + """Type for string fields that use pointer_to_buffer option for zero-copy. + + Uses StringRef instead of separate pointer and length fields. + """ + + cpp_type = "StringRef" + default_value = "" + reference_type = "StringRef &" + const_reference_type = "const StringRef &" + + @classmethod + def can_use_dump_field(cls) -> bool: + return True + + @property + def public_content(self) -> list[str]: + return [f"StringRef {self.field_name}{{}};"] + + @property + def encode_content(self) -> str: + return f"buffer.encode_string({self.number}, this->{self.field_name});" + + @property + def decode_length_content(self) -> str | None: + return f"""case {self.number}: {{ + this->{self.field_name} = StringRef(reinterpret_cast(value.data()), value.size()); + break; + }}""" + + def dump(self, name: str) -> str: + # Not used since we use dump_field, but required by abstract base class + return f'out.append("\'").append({name}.c_str(), {name}.size()).append("\'");' + + @property + def dump_content(self) -> str: + return f'dump_field(out, "{self.name}", this->{self.field_name});' + + def get_size_calculation(self, name: str, force: bool = False) -> str: + return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}.size());" def get_estimated_size(self) -> int: - # field ID + length varint + typical data (assume small for pointer fields) - return self.calculate_field_id_size() + 2 + 16 + return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string class FixedArrayBytesType(TypeInfo): @@ -1106,7 +1128,7 @@ class SFixed64Type(TypeInfo): wire_type = WireType.FIXED64 # Uses wire type 1 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -1150,7 +1172,7 @@ class SInt64Type(TypeInfo): wire_type = WireType.VARINT # Uses wire type 0 def dump(self, name: str) -> str: - o = f'snprintf(buffer, sizeof(buffer), "%lld", {name});\n' + o = f'snprintf(buffer, sizeof(buffer), "%" PRId64, {name});\n' o += "out.append(buffer);" return o @@ -1215,6 +1237,9 @@ class FixedArrayRepeatedType(TypeInfo): """Helper to generate encode statement for a single element.""" if isinstance(self._ti, EnumType): return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" + # MessageType.encode_message doesn't have a force parameter + if isinstance(self._ti, MessageType): + return f"buffer.{self._ti.encode_func}({self.number}, {element});" return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" @property @@ -1536,6 +1561,15 @@ class RepeatedTypeInfo(TypeInfo): # std::vector is specialized for bool, reference does not work return isinstance(self._ti, BoolType) + def _encode_element_call(self, element: str) -> str: + """Helper to generate encode call for a single element.""" + if isinstance(self._ti, EnumType): + return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" + # MessageType.encode_message doesn't have a force parameter + if isinstance(self._ti, MessageType): + return f"buffer.{self._ti.encode_func}({self.number}, {element});" + return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" + @property def encode_content(self) -> str: if self._use_pointer: @@ -1546,17 +1580,11 @@ class RepeatedTypeInfo(TypeInfo): o += f" buffer.{self._ti.encode_func}({self.number}, it, strlen(it), true);\n" else: o = f"for (const auto &it : *this->{self.field_name}) {{\n" - if isinstance(self._ti, EnumType): - o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" - else: - o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" + o += f" {self._encode_element_call('it')}\n" o += "}" return o o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n" - if isinstance(self._ti, EnumType): - o += f" buffer.{self._ti.encode_func}({self.number}, static_cast(it), true);\n" - else: - o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n" + o += f" {self._encode_element_call('it')}\n" o += "}" return o @@ -2123,12 +2151,10 @@ def build_message_type( # dump_to implementation will go in dump_cpp dump_impl = f"void {desc.name}::dump_to(std::string &out) const {{" if dump: - if len(dump) == 1 and len(dump[0]) + len(dump_impl) + 3 < 120: - dump_impl += f" {dump[0]} " - else: - dump_impl += "\n" - dump_impl += f' MessageDumpHelper helper(out, "{desc.name}");\n' - dump_impl += indent("\n".join(dump)) + "\n" + # Always use MessageDumpHelper for consistent output formatting + dump_impl += "\n" + dump_impl += f' MessageDumpHelper helper(out, "{desc.name}");\n' + dump_impl += indent("\n".join(dump)) + "\n" else: o2 = f'out.append("{desc.name} {{}}");' if len(dump_impl) + len(o2) + 3 < 120: @@ -2546,7 +2572,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) { char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%llu", value); + snprintf(buffer, 64, "%" PRIu64, value); append_with_newline(out, buffer); } @@ -2769,8 +2795,8 @@ static const char *const TAG = "api.service"; cases = list(RECEIVE_CASES.items()) cases.sort() hpp += " protected:\n" - hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" - out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" + out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" out += " switch (msg_type) {\n" for i, (case, ifdef, message_name) in cases: if ifdef is not None: @@ -2878,9 +2904,9 @@ static const char *const TAG = "api.service"; result += "#endif\n" return result - hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n" + hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n" - cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n" + cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n" cpp += " // Check authentication/connection requirements for messages\n" cpp += " switch (msg_type) {\n" diff --git a/script/ci-custom.py b/script/ci-custom.py index 106aa438fe..77d2ab287d 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -552,12 +552,14 @@ def convert_path_to_relative(abspath, current): exclude=[ "esphome/components/libretiny/generate_components.py", "esphome/components/web_server/__init__.py", + # const.py has absolute import in docstring example for external components + "esphome/components/esp8266/const.py", ], ) -def lint_relative_py_import(fname, line, col, content): +def lint_relative_py_import(fname: Path, line, col, content): import_line = content.splitlines()[line] abspath = import_line[col:].split(" ")[0] - current = fname.removesuffix(".py").replace(os.path.sep, ".") + current = str(fname).removesuffix(".py").replace(os.path.sep, ".") replacement = convert_path_to_relative(abspath, current) newline = import_line.replace(abspath, replacement) return ( @@ -578,6 +580,7 @@ def lint_relative_py_import(fname, line, col, content): ], exclude=[ "esphome/components/socket/headers.h", + "esphome/components/async_tcp/async_tcp.h", "esphome/components/esp32/core.cpp", "esphome/components/esp8266/core.cpp", "esphome/components/rp2040/core.cpp", @@ -616,6 +619,19 @@ def lint_esphome_h(fname, line, col, content): ) +@lint_content_find_check( + "CORE.using_esp_idf", + include=py_include, + exclude=["esphome/core/__init__.py", "script/ci-custom.py"], +) +def lint_using_esp_idf_deprecated(fname, line, col, content): + return ( + f"{highlight('CORE.using_esp_idf')} is deprecated and will change behavior in 2026.6. " + "ESP32 Arduino builds on top of ESP-IDF, so ESP-IDF features are available in both frameworks. " + f"Please use {highlight('CORE.is_esp32')} and/or {highlight('CORE.using_arduino')} instead." + ) + + @lint_content_check(include=["*.h"]) def lint_pragma_once(fname, content): if "#pragma once" not in content: diff --git a/script/ci_memory_impact_comment.py b/script/ci_memory_impact_comment.py index 1331a44d03..a296130645 100755 --- a/script/ci_memory_impact_comment.py +++ b/script/ci_memory_impact_comment.py @@ -215,6 +215,20 @@ def prepare_symbol_changes_data( } +def format_components_str(components: list[str]) -> str: + """Format a list of components for display. + + Args: + components: List of component names + + Returns: + Formatted string with backtick-quoted component names + """ + if len(components) == 1: + return f"`{components[0]}`" + return ", ".join(f"`{c}`" for c in sorted(components)) + + def prepare_component_breakdown_data( target_analysis: dict | None, pr_analysis: dict | None ) -> list[tuple[str, int, int, int]] | None: @@ -316,11 +330,10 @@ def create_comment_body( } # Format components list + context["components_str"] = format_components_str(components) if len(components) == 1: - context["components_str"] = f"`{components[0]}`" context["config_note"] = "a representative test configuration" else: - context["components_str"] = ", ".join(f"`{c}`" for c in sorted(components)) context["config_note"] = ( f"a merged configuration with {len(components)} components" ) @@ -502,6 +515,43 @@ def post_or_update_comment(pr_number: str, comment_body: str) -> None: print("Comment posted/updated successfully", file=sys.stderr) +def create_target_unavailable_comment( + pr_data: dict, +) -> str: + """Create a comment body when target branch data is unavailable. + + This happens when the target branch (dev/beta/release) fails to build. + This can occur because: + 1. The target branch has a build issue independent of this PR + 2. This PR fixes a build issue on the target branch + In either case, we only care that the PR branch builds successfully. + + Args: + pr_data: Dictionary with PR branch analysis results + + Returns: + Formatted comment body + """ + components = pr_data.get("components", []) + platform = pr_data.get("platform", "unknown") + pr_ram = pr_data.get("ram_bytes", 0) + pr_flash = pr_data.get("flash_bytes", 0) + + env = Environment( + loader=FileSystemLoader(TEMPLATE_DIR), + trim_blocks=True, + lstrip_blocks=True, + ) + template = env.get_template("ci_memory_impact_target_unavailable.j2") + return template.render( + comment_marker=COMMENT_MARKER, + components_str=format_components_str(components), + platform=platform, + pr_ram=format_bytes(pr_ram), + pr_flash=format_bytes(pr_flash), + ) + + def main() -> int: """Main entry point.""" parser = argparse.ArgumentParser( @@ -523,15 +573,25 @@ def main() -> int: # Load analysis JSON files (all data comes from JSON for security) target_data: dict | None = load_analysis_json(args.target_json) - if not target_data: - print("Error: Failed to load target analysis JSON", file=sys.stderr) - sys.exit(1) - pr_data: dict | None = load_analysis_json(args.pr_json) + + # PR data is required - if the PR branch can't build, that's a real error if not pr_data: print("Error: Failed to load PR analysis JSON", file=sys.stderr) sys.exit(1) + # Target data is optional - target branch (dev) may fail to build because: + # 1. The target branch has a build issue independent of this PR + # 2. This PR fixes a build issue on the target branch + if not target_data: + print( + "Warning: Target branch analysis unavailable, posting limited comment", + file=sys.stderr, + ) + comment_body = create_target_unavailable_comment(pr_data) + post_or_update_comment(args.pr_number, comment_body) + return 0 + # Extract detailed analysis if available target_analysis: dict | None = None pr_analysis: dict | None = None diff --git a/script/ci_memory_impact_extract.py b/script/ci_memory_impact_extract.py index 77d59417e3..dd91fa861c 100755 --- a/script/ci_memory_impact_extract.py +++ b/script/ci_memory_impact_extract.py @@ -92,18 +92,23 @@ def run_detailed_analysis(build_dir: str) -> dict | None: print(f"Build directory not found: {build_dir}", file=sys.stderr) return None - # Find firmware.elf + # Find firmware.elf (or raw_firmware.elf for LibreTiny) elf_path = None for elf_candidate in [ build_path / "firmware.elf", build_path / ".pioenvs" / build_path.name / "firmware.elf", + # LibreTiny uses raw_firmware.elf + build_path / "raw_firmware.elf", + build_path / ".pioenvs" / build_path.name / "raw_firmware.elf", ]: if elf_candidate.exists(): elf_path = str(elf_candidate) break if not elf_path: - print(f"firmware.elf not found in {build_dir}", file=sys.stderr) + print( + f"firmware.elf/raw_firmware.elf not found in {build_dir}", file=sys.stderr + ) return None # Find idedata.json - check multiple locations diff --git a/script/clang-tidy b/script/clang-tidy index 142b616119..17bcafacc7 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -253,19 +253,31 @@ def main(): print(f"Split {args.split_at}/{args.split_num}: checking {len(files)} files") # Print file count before adding header file - print(f"\nTotal files to check: {len(files)}") + print(f"\nTotal cpp files to check: {len(files)}") + + # Add header file for checking (before early exit check) + if args.all_headers and args.split_at in (None, 1): + # When --changed is used, only include changed headers instead of all headers + if args.changed: + all_headers = [ + os.path.relpath(p, cwd) for p in git_ls_files(["esphome/**/*.h"]) + ] + changed_headers = filter_changed(all_headers) + if changed_headers: + build_all_include(changed_headers) + files.insert(0, temp_header_file) + else: + print("No changed headers to check") + else: + build_all_include() + files.insert(0, temp_header_file) + print(f"Added all-include header file, new total: {len(files)}") # Early exit if no files to check if not files: print("No files to check - exiting early") return 0 - # Only build header file if we have actual files to check - if args.all_headers and args.split_at in (None, 1): - build_all_include() - files.insert(0, temp_header_file) - print(f"Added all-include header file, new total: {len(files)}") - # Print final file list before loading idedata print_file_list(files, "Final files to process:") diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 5cc3f2570a..44e8e4b5ab 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -89,6 +89,7 @@ class Platform(StrEnum): ESP32_C6_IDF = "esp32-c6-idf" ESP32_S2_IDF = "esp32-s2-idf" ESP32_S3_IDF = "esp32-s3-idf" + BK72XX_ARD = "bk72xx-ard" # LibreTiny BK7231N # Memory impact analysis constants @@ -120,6 +121,7 @@ PLATFORM_SPECIFIC_COMPONENTS = frozenset( # fastest build times, most sensitive to code size changes # 3. ESP32 IDF - Primary ESP32 platform, most representative of modern ESPHome # 4-6. Other ESP32 variants - Less commonly used but still supported +# 7. BK72XX - LibreTiny platform (good for detecting LibreTiny-specific changes) MEMORY_IMPACT_PLATFORM_PREFERENCE = [ Platform.ESP32_C6_IDF, # ESP32-C6 IDF (newest, supports Thread/Zigbee) Platform.ESP8266_ARD, # ESP8266 Arduino (most memory constrained, fastest builds) @@ -127,6 +129,7 @@ MEMORY_IMPACT_PLATFORM_PREFERENCE = [ Platform.ESP32_C3_IDF, # ESP32-C3 IDF Platform.ESP32_S2_IDF, # ESP32-S2 IDF Platform.ESP32_S3_IDF, # ESP32-S3 IDF + Platform.BK72XX_ARD, # LibreTiny BK7231N ] @@ -404,7 +407,7 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None: - wifi_component_esp_idf.cpp, *_idf.h -> ESP32 IDF variants - wifi_component_esp8266.cpp, *_esp8266.h -> ESP8266_ARD - *_esp32*.cpp -> ESP32 IDF (generic) - - *_libretiny.cpp, *_retiny.* -> LibreTiny (not in preference list) + - *_libretiny.cpp, *_bk72*.* -> BK72XX (LibreTiny) - *_pico.cpp, *_rp2040.* -> RP2040 (not in preference list) Args: @@ -438,10 +441,11 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None: if "esp32" in filename_lower: return Platform.ESP32_IDF - # LibreTiny and RP2040 are not in MEMORY_IMPACT_PLATFORM_PREFERENCE - # so we don't return them as hints - # if "retiny" in filename_lower or "libretiny" in filename_lower: - # return None # No specific LibreTiny platform preference + # LibreTiny (via 'libretiny' pattern or BK72xx-specific files) + if "libretiny" in filename_lower or "bk72" in filename_lower: + return Platform.BK72XX_ARD + + # RP2040 is not in MEMORY_IMPACT_PLATFORM_PREFERENCE # if "pico" in filename_lower or "rp2040" in filename_lower: # return None # No RP2040 platform preference diff --git a/script/helpers.py b/script/helpers.py index 1039ef39ac..202ac9b5fc 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -156,22 +156,25 @@ def print_error_for_file(file: str | Path, body: str | None) -> None: print() -def build_all_include() -> None: - # Build a cpp file that includes all header files in this repo. - # Otherwise header-only integrations would not be tested by clang-tidy +def build_all_include(header_files: list[str] | None = None) -> None: + # Build a cpp file that includes header files for clang-tidy to check. + # If header_files is provided, only include those headers. + # Otherwise, include all header files in the esphome directory. - # Use git ls-files to find all .h files in the esphome directory - # This is much faster than walking the filesystem - cmd = ["git", "ls-files", "esphome/**/*.h"] - proc = subprocess.run(cmd, capture_output=True, text=True, check=True) + if header_files is None: + # Use git ls-files to find all .h files in the esphome directory + # This is much faster than walking the filesystem + cmd = ["git", "ls-files", "esphome/**/*.h"] + proc = subprocess.run(cmd, capture_output=True, text=True, check=True) - # Process git output - git already returns paths relative to repo root - headers = [ - f'#include "{include_p}"' - for line in proc.stdout.strip().split("\n") - if (include_p := line.replace(os.path.sep, "/")) - ] + # Process git output - git already returns paths relative to repo root + header_files = [ + line.replace(os.path.sep, "/") + for line in proc.stdout.strip().split("\n") + if line + ] + headers = [f'#include "{h}"' for h in header_files] headers.sort() headers.append("") content = "\n".join(headers) @@ -630,7 +633,12 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: Returns: Set of all components including dependencies and auto-loaded components """ - from esphome.const import KEY_CORE + from esphome.const import ( + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PLATFORM_HOST, + ) from esphome.core import CORE from esphome.loader import get_component @@ -642,7 +650,10 @@ def get_all_dependencies(component_names: set[str]) -> set[str]: # Set up fake config path for component loading root = Path(__file__).parent.parent CORE.config_path = root - CORE.data[KEY_CORE] = {} + CORE.data[KEY_CORE] = { + KEY_TARGET_PLATFORM: PLATFORM_HOST, + KEY_TARGET_FRAMEWORK: "host-native", + } # Keep finding dependencies until no new ones are found while True: diff --git a/script/helpers_zephyr.py b/script/helpers_zephyr.py index f72b335e64..1242a60cf4 100644 --- a/script/helpers_zephyr.py +++ b/script/helpers_zephyr.py @@ -17,6 +17,7 @@ def load_idedata(environment, temp_folder, platformio_ini): """ #include int main() { return 0;} +extern "C" void zboss_signal_handler() {}; """, encoding="utf-8", ) @@ -27,6 +28,12 @@ int main() { return 0;} CONFIG_NEWLIB_LIBC=y CONFIG_BT=y CONFIG_ADC=y +#zigbee begin +CONFIG_ZIGBEE=y +CONFIG_CRYPTO=y +CONFIG_NVS=y +CONFIG_SETTINGS=y +#zigbee end """, encoding="utf-8", ) @@ -44,10 +51,11 @@ CONFIG_ADC=y def extract_defines(command): define_pattern = re.compile(r"-D\s*([^\s]+)") + ignore_prefixes = ("_ASMLANGUAGE", "NRF_802154_ECB_PRIORITY=") return [ - match + match.replace("\\", "") for match in define_pattern.findall(command) - if match not in ("_ASMLANGUAGE") + if not any(match.startswith(prefix) for prefix in ignore_prefixes) ] def find_cxx_path(commands): diff --git a/script/templates/ci_memory_impact_target_unavailable.j2 b/script/templates/ci_memory_impact_target_unavailable.j2 new file mode 100644 index 0000000000..542bd49d85 --- /dev/null +++ b/script/templates/ci_memory_impact_target_unavailable.j2 @@ -0,0 +1,19 @@ +{{ comment_marker }} +## Memory Impact Analysis + +**Components:** {{ components_str }} +**Platform:** `{{ platform }}` + +| Metric | This PR | +|--------|---------| +| **RAM** | {{ pr_ram }} | +| **Flash** | {{ pr_flash }} | + +> ⚠️ **Target branch comparison unavailable** - The target branch failed to build. +> This can happen when the target branch has a build issue, or when this PR fixes a build issue on the target branch. +> The PR branch compiled successfully with the memory usage shown above. + +--- +> **Note:** This analysis measures **static RAM and Flash usage** only (compile-time allocation). + +*This analysis runs automatically when components change.* diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 322efb701a..72ca3f6e9c 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -13,7 +13,6 @@ CONFIG_ESP_TASK_WDT=y CONFIG_ESP_TASK_WDT_PANIC=y CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n -CONFIG_AUTOSTART_ARDUINO=y # esp32_ble CONFIG_BT_ENABLED=y diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index 32d74027ba..ce4e64681f 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -29,7 +29,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main): ) # Then - assert 'bs_1->set_name("test bs1");' in main_cpp + assert 'bs_1->set_name("test bs1",' in main_cpp assert "bs_1->set_pin(" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 512ef42b44..797b6fb1a4 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -26,7 +26,7 @@ def test_button_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert 'wol_1->set_name("wol_test_1");' in main_cpp + assert 'wol_1->set_name("wol_test_1",' in main_cpp assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 91e96f24d6..68bd3a5965 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -17,8 +17,7 @@ def test_esp32_config( ) -> None: set_core_config(PlatformFramework.ESP32_IDF) - from esphome.components.esp32 import CONFIG_SCHEMA - from esphome.components.esp32.const import VARIANT_ESP32, VARIANT_FRIENDLY + from esphome.components.esp32 import CONFIG_SCHEMA, VARIANT_ESP32, VARIANT_FRIENDLY # Example ESP32 configuration config = { diff --git a/tests/component_tests/mipi_spi/conftest.py b/tests/component_tests/mipi_spi/conftest.py index c3070c7965..eddf0987d0 100644 --- a/tests/component_tests/mipi_spi/conftest.py +++ b/tests/component_tests/mipi_spi/conftest.py @@ -20,9 +20,9 @@ def choose_variant_with_pins() -> Generator[Callable[[list], None]]: """ def chooser(pins: list) -> None: - for v in VARIANTS: + for variant in VARIANTS: try: - CORE.data[KEY_ESP32][KEY_VARIANT] = v + CORE.data[KEY_ESP32][KEY_VARIANT] = variant for pin in pins: if pin is not None: pin = gpio_pin_schema( diff --git a/tests/component_tests/mipi_spi/test_init.py b/tests/component_tests/mipi_spi/test_init.py index e68f6fbfba..bae39d3879 100644 --- a/tests/component_tests/mipi_spi/test_init.py +++ b/tests/component_tests/mipi_spi/test_init.py @@ -1,4 +1,4 @@ -"""Tests for mpip_spi configuration validation.""" +"""Tests for mipi_spi configuration validation.""" from collections.abc import Callable from pathlib import Path @@ -220,37 +220,20 @@ def test_esp32s3_specific_errors( set_core_config( PlatformFramework.ESP32_IDF, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) with pytest.raises(cv.Invalid, match=error_match): run_schema_validation(config) -def test_framework_specific_errors( - set_core_config: SetCoreConfigCallable, -) -> None: - """Test framework-specific configuration errors""" - - set_core_config( - PlatformFramework.ESP32_ARDUINO, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, - ) - - with pytest.raises( - cv.Invalid, - match=r"This feature is only available with framework\(s\) esp-idf", - ): - run_schema_validation({"model": "wt32-sc01-plus"}) - - def test_custom_model_with_all_options( set_core_config: SetCoreConfigCallable, ) -> None: """Test custom model configuration with all available options.""" set_core_config( PlatformFramework.ESP32_IDF, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) run_schema_validation( @@ -293,7 +276,7 @@ def test_all_predefined_models( """Test all predefined display models validate successfully with appropriate defaults.""" set_core_config( PlatformFramework.ESP32_IDF, - platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32S3}, + platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3}, ) # Enable PSRAM which is required for some models @@ -304,14 +287,14 @@ def test_all_predefined_models( config = {"model": name} # Get the pins required by this model and find a compatible variant - pins = [ - pin - for pin in [ - model.get_default(pin, None) - for pin in ("dc_pin", "reset_pin", "cs_pin") - ] - if pin is not None - ] + pins = [] + for pin_name in ("dc_pin", "reset_pin", "cs_pin", "enable_pin"): + pin_value = model.get_default(pin_name, None) + if pin_value is not None: + if isinstance(pin_value, list): + pins.extend(pin_value) + else: + pins.append(pin_value) choose_variant_with_pins(pins) # Add required fields that don't have defaults diff --git a/tests/component_tests/packages/test_packages.py b/tests/component_tests/packages/test_packages.py index 1c4c91aa52..22fb2c4e32 100644 --- a/tests/component_tests/packages/test_packages.py +++ b/tests/component_tests/packages/test_packages.py @@ -5,7 +5,8 @@ from unittest.mock import MagicMock, patch import pytest -from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass +from esphome.components.packages import CONFIG_SCHEMA, do_packages_pass, merge_packages +import esphome.config as config_module from esphome.config import resolve_extend_remove from esphome.config_helpers import Extend, Remove import esphome.config_validation as cv @@ -27,11 +28,13 @@ from esphome.const import ( CONF_REFRESH, CONF_SENSOR, CONF_SSID, + CONF_SUBSTITUTIONS, CONF_UPDATE_INTERVAL, CONF_URL, CONF_VARS, CONF_WIFI, ) +from esphome.core import CORE from esphome.util import OrderedDict # Test strings @@ -68,11 +71,12 @@ def fixture_basic_esphome(): def packages_pass(config): """Wrapper around packages_pass that also resolves Extend and Remove.""" config = do_packages_pass(config) + config = merge_packages(config) resolve_extend_remove(config) return config -def test_package_unused(basic_esphome, basic_wifi): +def test_package_unused(basic_esphome, basic_wifi) -> None: """ Ensures do_package_pass does not change a config if packages aren't used. """ @@ -82,7 +86,7 @@ def test_package_unused(basic_esphome, basic_wifi): assert actual == config -def test_package_invalid_dict(basic_esphome, basic_wifi): +def test_package_invalid_dict(basic_esphome, basic_wifi) -> None: """ If a url: key is present, it's expected to be well-formed remote package spec. Ensure an error is raised if not. Any other simple dict passed as a package will be merged as usual but may fail later validation. @@ -95,7 +99,7 @@ def test_package_invalid_dict(basic_esphome, basic_wifi): @pytest.mark.parametrize( - "package", + "packages", [ {"package1": "github://esphome/non-existant-repo/file1.yml@main"}, {"package2": "github://esphome/non-existant-repo/file1.yml"}, @@ -107,12 +111,12 @@ def test_package_invalid_dict(basic_esphome, basic_wifi): ], ], ) -def test_package_shorthand(package): - CONFIG_SCHEMA(package) +def test_package_shorthand(packages) -> None: + CONFIG_SCHEMA(packages) @pytest.mark.parametrize( - "package", + "packages", [ # not github {"package1": "someplace://esphome/non-existant-repo/file1.yml@main"}, @@ -133,12 +137,12 @@ def test_package_shorthand(package): [3], ], ) -def test_package_invalid(package): +def test_package_invalid(packages) -> None: with pytest.raises(cv.Invalid): - CONFIG_SCHEMA(package) + CONFIG_SCHEMA(packages) -def test_package_include(basic_wifi, basic_esphome): +def test_package_include(basic_wifi, basic_esphome) -> None: """ Tests the simple case where an independent config present in a package is added to the top-level config as is. @@ -155,7 +159,31 @@ def test_package_include(basic_wifi, basic_esphome): assert actual == expected -def test_package_append(basic_wifi, basic_esphome): +def test_single_package( + basic_esphome, + basic_wifi, + caplog: pytest.LogCaptureFixture, +) -> None: + """ + Tests the simple case where a single package is added to the top-level config as is. + In this test, the CONF_WIFI config is expected to be simply added to the top-level config. + This tests the case where the user just put packages: !include package.yaml, not + part of a list or mapping of packages. + This behavior is deprecated, the test also checks if a warning is issued. + """ + config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: {CONF_WIFI: basic_wifi}} + + expected = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi} + + with caplog.at_level("WARNING"): + actual = packages_pass(config) + + assert actual == expected + + assert "This method for including packages will go away in 2026.7.0" in caplog.text + + +def test_package_append(basic_wifi, basic_esphome) -> None: """ Tests the case where a key is present in both a package and top-level config. @@ -180,7 +208,7 @@ def test_package_append(basic_wifi, basic_esphome): assert actual == expected -def test_package_override(basic_wifi, basic_esphome): +def test_package_override(basic_wifi, basic_esphome) -> None: """ Ensures that the top-level configuration takes precedence over duplicate keys defined in a package. @@ -204,7 +232,7 @@ def test_package_override(basic_wifi, basic_esphome): assert actual == expected -def test_multiple_package_order(): +def test_multiple_package_order() -> None: """ Ensures that mutiple packages are merged in order. """ @@ -233,7 +261,7 @@ def test_multiple_package_order(): assert actual == expected -def test_package_list_merge(): +def test_package_list_merge() -> None: """ Ensures lists defined in both a package and the top-level config are merged correctly """ @@ -289,7 +317,7 @@ def test_package_list_merge(): assert actual == expected -def test_package_list_merge_by_id(): +def test_package_list_merge_by_id() -> None: """ Ensures that components with matching IDs are merged correctly. @@ -367,7 +395,7 @@ def test_package_list_merge_by_id(): assert actual == expected -def test_package_merge_by_id_with_list(): +def test_package_merge_by_id_with_list() -> None: """ Ensures that components with matching IDs are merged correctly when their configuration contains lists. @@ -406,7 +434,7 @@ def test_package_merge_by_id_with_list(): assert actual == expected -def test_package_merge_by_missing_id(): +def test_package_merge_by_missing_id() -> None: """ Ensures that a validation error is thrown when trying to extend a missing ID. """ @@ -442,7 +470,7 @@ def test_package_merge_by_missing_id(): assert error_raised -def test_package_list_remove_by_id(): +def test_package_list_remove_by_id() -> None: """ Ensures that components with matching IDs are removed correctly. @@ -493,7 +521,7 @@ def test_package_list_remove_by_id(): assert actual == expected -def test_multiple_package_list_remove_by_id(): +def test_multiple_package_list_remove_by_id() -> None: """ Ensures that components with matching IDs are removed correctly. @@ -539,7 +567,7 @@ def test_multiple_package_list_remove_by_id(): assert actual == expected -def test_package_dict_remove_by_id(basic_wifi, basic_esphome): +def test_package_dict_remove_by_id(basic_wifi, basic_esphome) -> None: """ Ensures that components with missing IDs are removed from dict. Ensures that the top-level configuration takes precedence over duplicate keys defined in a package. @@ -560,7 +588,7 @@ def test_package_dict_remove_by_id(basic_wifi, basic_esphome): assert actual == expected -def test_package_remove_by_missing_id(): +def test_package_remove_by_missing_id() -> None: """ Ensures that components with missing IDs are not merged. """ @@ -608,7 +636,7 @@ def test_package_remove_by_missing_id(): @patch("esphome.git.clone_or_update") def test_remote_packages_with_files_list( mock_clone_or_update, mock_is_file, mock_load_yaml -): +) -> None: """ Ensures that packages are loaded as mixed list of dictionary and strings """ @@ -680,7 +708,7 @@ def test_remote_packages_with_files_list( @patch("esphome.git.clone_or_update") def test_remote_packages_with_files_and_vars( mock_clone_or_update, mock_is_file, mock_load_yaml -): +) -> None: """ Ensures that packages are loaded as mixed list of dictionary and strings with vars """ @@ -769,3 +797,231 @@ def test_remote_packages_with_files_and_vars( actual = packages_pass(config) assert actual == expected + + +def test_packages_merge_substitutions() -> None: + """ + Tests that substitutions from packages in a complex package hierarchy + are extracted and merged into the top-level config. + """ + config = { + CONF_SUBSTITUTIONS: { + "a": 1, + "b": 2, + "c": 3, + }, + CONF_PACKAGES: { + "package1": { + "logger": { + "level": "DEBUG", + }, + CONF_PACKAGES: [ + { + CONF_SUBSTITUTIONS: { + "a": 10, + "e": 5, + }, + "sensor": [ + {"platform": "template", "id": "sensor1"}, + ], + }, + ], + "sensor": [ + {"platform": "template", "id": "sensor2"}, + ], + }, + "package2": { + "logger": { + "level": "VERBOSE", + }, + }, + "package3": { + CONF_PACKAGES: [ + { + CONF_PACKAGES: [ + { + CONF_SUBSTITUTIONS: { + "b": 20, + "d": 4, + }, + "sensor": [ + {"platform": "template", "id": "sensor3"}, + ], + }, + ], + CONF_SUBSTITUTIONS: { + "b": 20, + "d": 6, + }, + "sensor": [ + {"platform": "template", "id": "sensor4"}, + ], + }, + ], + }, + }, + } + + expected = { + CONF_SUBSTITUTIONS: {"a": 1, "e": 5, "b": 2, "d": 6, "c": 3}, + CONF_PACKAGES: { + "package1": { + "logger": { + "level": "DEBUG", + }, + CONF_PACKAGES: [ + { + "sensor": [ + {"platform": "template", "id": "sensor1"}, + ], + }, + ], + "sensor": [ + {"platform": "template", "id": "sensor2"}, + ], + }, + "package2": { + "logger": { + "level": "VERBOSE", + }, + }, + "package3": { + CONF_PACKAGES: [ + { + CONF_PACKAGES: [ + { + "sensor": [ + {"platform": "template", "id": "sensor3"}, + ], + }, + ], + "sensor": [ + {"platform": "template", "id": "sensor4"}, + ], + }, + ], + }, + }, + } + + actual = do_packages_pass(config) + assert actual == expected + + +def test_package_merge() -> None: + """ + Tests that all packages are merged into the top-level config. + """ + config = { + CONF_SUBSTITUTIONS: {"a": 1, "e": 5, "b": 2, "d": 6, "c": 3}, + CONF_PACKAGES: { + "package1": { + "logger": { + "level": "DEBUG", + }, + CONF_PACKAGES: [ + { + "sensor": [ + {"platform": "template", "id": "sensor1"}, + ], + }, + ], + "sensor": [ + {"platform": "template", "id": "sensor2"}, + ], + }, + "package2": { + "logger": { + "level": "VERBOSE", + }, + }, + "package3": { + CONF_PACKAGES: [ + { + CONF_PACKAGES: [ + { + "sensor": [ + {"platform": "template", "id": "sensor3"}, + ], + }, + ], + "sensor": [ + {"platform": "template", "id": "sensor4"}, + ], + }, + ], + }, + }, + } + expected = { + "sensor": [ + {"platform": "template", "id": "sensor1"}, + {"platform": "template", "id": "sensor2"}, + {"platform": "template", "id": "sensor3"}, + {"platform": "template", "id": "sensor4"}, + ], + "logger": {"level": "VERBOSE"}, + CONF_SUBSTITUTIONS: {"a": 1, "e": 5, "b": 2, "d": 6, "c": 3}, + } + actual = merge_packages(config) + + assert actual == expected + + +@pytest.mark.parametrize( + "invalid_package", + [ + 6, + "some string", + ["some string"], + None, + True, + {"some_component": 8}, + {3: 2}, + {"some_component": r"${unevaluated expression}"}, + ], +) +def test_package_merge_invalid(invalid_package) -> None: + """ + Tests that trying to merge an invalid package raises an error. + """ + config = { + CONF_PACKAGES: { + "some_package": invalid_package, + }, + } + + with pytest.raises(cv.Invalid): + merge_packages(config) + + +def test_raw_config_contains_merged_esphome_from_package(tmp_path) -> None: + """Test that CORE.raw_config contains esphome section from merged package. + + This is a regression test for the bug where CORE.raw_config was set before + packages were merged, causing KeyError when components accessed + CORE.raw_config[CONF_ESPHOME] and the esphome section came from a package. + """ + # Create a config where esphome section comes from a package + test_config = OrderedDict() + test_config[CONF_PACKAGES] = { + "base": { + CONF_ESPHOME: {CONF_NAME: TEST_DEVICE_NAME}, + } + } + test_config["esp32"] = {"board": "esp32dev"} + + # Set up CORE for the test + test_yaml = tmp_path / "test.yaml" + test_yaml.write_text("# test config") + CORE.reset() + CORE.config_path = test_yaml + + # Call validate_config - this should merge packages and set CORE.raw_config + config_module.validate_config(test_config, {}) + + # Verify that CORE.raw_config contains the esphome section from the package + assert CONF_ESPHOME in CORE.raw_config, ( + "CORE.raw_config should contain esphome section after package merge" + ) + assert CORE.raw_config[CONF_ESPHOME][CONF_NAME] == TEST_DEVICE_NAME diff --git a/tests/component_tests/psram/test_psram.py b/tests/component_tests/psram/test_psram.py index f8ad013689..0924e66adc 100644 --- a/tests/component_tests/psram/test_psram.py +++ b/tests/component_tests/psram/test_psram.py @@ -4,7 +4,7 @@ from typing import Any import pytest -from esphome.components.esp32.const import ( +from esphome.components.esp32 import ( KEY_VARIANT, VARIANT_ESP32, VARIANT_ESP32C2, @@ -23,22 +23,23 @@ from tests.component_tests.types import SetCoreConfigCallable UNSUPPORTED_PSRAM_VARIANTS = [ VARIANT_ESP32C2, VARIANT_ESP32C3, - VARIANT_ESP32C5, VARIANT_ESP32C6, VARIANT_ESP32H2, ] SUPPORTED_PSRAM_VARIANTS = [ VARIANT_ESP32, + VARIANT_ESP32C5, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, - VARIANT_ESP32P4, ] SUPPORTED_PSRAM_MODES = { VARIANT_ESP32: ["quad"], + VARIANT_ESP32C5: ["quad"], + VARIANT_ESP32P4: ["hex"], VARIANT_ESP32S2: ["quad"], VARIANT_ESP32S3: ["quad", "octal"], - VARIANT_ESP32P4: ["hex"], } diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 99ddd78ee7..6b047bc62f 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -25,7 +25,7 @@ def test_text_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert 'it_1->set_name("test 1 text");' in main_cpp + assert 'it_1->set_name("test 1 text",' in main_cpp def test_text_config_value_internal_set(generate_main): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 1c4ef6633d..1593d0b6d8 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -25,9 +25,9 @@ def test_text_sensor_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert 'ts_1->set_name("Template Text Sensor 1");' in main_cpp - assert 'ts_2->set_name("Template Text Sensor 2");' in main_cpp - assert 'ts_3->set_name("Template Text Sensor 3");' in main_cpp + assert 'ts_1->set_name("Template Text Sensor 1",' in main_cpp + assert 'ts_2->set_name("Template Text Sensor 2",' in main_cpp + assert 'ts_3->set_name("Template Text Sensor 3",' in main_cpp def test_text_sensor_config_value_internal_set(generate_main): diff --git a/tests/components/animation/test.rp2040-ard.yaml b/tests/components/animation/test.rp2040-ard.yaml index 32fb4efb04..2c99e937f3 100644 --- a/tests/components/animation/test.rp2040-ard.yaml +++ b/tests/components/animation/test.rp2040-ard.yaml @@ -11,3 +11,4 @@ display: dc_pin: 21 reset_pin: 22 invert_colors: false + data_rate: 10MHz diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml index fc53b8ac7e..c766b61b13 100644 --- a/tests/components/api/common-base.yaml +++ b/tests/components/api/common-base.yaml @@ -1,6 +1,10 @@ esphome: on_boot: then: + - wait_until: + condition: + api.connected: + state_subscription_only: true - homeassistant.event: event: esphome.button_pressed data: @@ -177,6 +181,99 @@ api: else: - logger.log: "Skipped loops" - logger.log: "After combined test" + # ========================================================================== + # supports_response: status (auto-detected - api.respond without data) + # Has call_id only - reports success/error without data payload + # ========================================================================== + - action: test_respond_status + then: + - api.respond: + success: true + - logger.log: + format: "Status response sent (call_id=%d)" + args: [call_id] + + - action: test_respond_status_error + variables: + error_msg: string + then: + - api.respond: + success: false + error_message: !lambda 'return error_msg;' + + # ========================================================================== + # supports_response: optional (auto-detected - api.respond with data) + # Has call_id and return_response - client decides if it wants response + # ========================================================================== + - action: test_respond_optional + variables: + sensor_name: string + value: float + then: + - logger.log: + format: "Optional response (call_id=%d, return_response=%d)" + args: [call_id, return_response] + - api.respond: + data: !lambda |- + root["sensor"] = sensor_name; + root["value"] = value; + root["unit"] = "°C"; + + - action: test_respond_optional_conditional + variables: + do_succeed: bool + then: + - if: + condition: + lambda: 'return do_succeed;' + then: + - api.respond: + success: true + data: !lambda |- + root["status"] = "ok"; + else: + - api.respond: + success: false + error_message: "Operation failed" + + # ========================================================================== + # supports_response: only (explicit - always expects data response) + # Has call_id only - response is always expected with data + # ========================================================================== + - action: test_respond_only + supports_response: only + variables: + input: string + then: + - logger.log: + format: "Only response (call_id=%d)" + args: [call_id] + - api.respond: + data: !lambda |- + root["input"] = input; + root["processed"] = true; + + - action: test_respond_only_nested + supports_response: only + then: + - api.respond: + data: !lambda |- + root["config"]["wifi"] = "connected"; + root["config"]["api"] = true; + root["items"][0] = "item1"; + root["items"][1] = "item2"; + + # ========================================================================== + # supports_response: none (no api.respond action) + # No call_id or return_response - just user variables + # ========================================================================== + - action: test_no_response + variables: + message: string + then: + - logger.log: + format: "No response action: %s" + args: [message.c_str()] event: - platform: template diff --git a/tests/components/aqi/common.yaml b/tests/components/aqi/common.yaml new file mode 100644 index 0000000000..4c8cbbfa3f --- /dev/null +++ b/tests/components/aqi/common.yaml @@ -0,0 +1,22 @@ +sensor: + - platform: template + id: pm25_sensor + name: "PM2.5" + lambda: "return 25.0;" + + - platform: template + id: pm10_sensor + name: "PM10" + lambda: "return 50.0;" + + - platform: aqi + name: "Air Quality Index (AQI)" + pm_2_5: pm25_sensor + pm_10_0: pm10_sensor + calculation_type: AQI + + - platform: aqi + name: "Air Quality Index (CAQI)" + pm_2_5: pm25_sensor + pm_10_0: pm10_sensor + calculation_type: CAQI diff --git a/tests/components/aqi/test.esp32-idf.yaml b/tests/components/aqi/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/aqi/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/aqi/test.esp8266-ard.yaml b/tests/components/aqi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/aqi/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/aqi/test.rp2040-ard.yaml b/tests/components/aqi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/aqi/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bm8563/common.yaml b/tests/components/bm8563/common.yaml new file mode 100644 index 0000000000..ec3fdd1518 --- /dev/null +++ b/tests/components/bm8563/common.yaml @@ -0,0 +1,10 @@ +esphome: + on_boot: + - bm8563.read_time + - bm8563.write_time + - bm8563.start_timer: + duration: 300s + +time: + - platform: bm8563 + i2c_id: i2c_bus diff --git a/tests/components/bm8563/test.esp32-ard.yaml b/tests/components/bm8563/test.esp32-ard.yaml new file mode 100644 index 0000000000..7c503b0ccb --- /dev/null +++ b/tests/components/bm8563/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.esp32-idf.yaml b/tests/components/bm8563/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/bm8563/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.esp8266-ard.yaml b/tests/components/bm8563/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/bm8563/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bm8563/test.rp2040-ard.yaml b/tests/components/bm8563/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/bm8563/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/common.yaml b/tests/components/bme68x_bsec2_i2c/common.yaml index bee964f433..a462bdaf7f 100644 --- a/tests/components/bme68x_bsec2_i2c/common.yaml +++ b/tests/components/bme68x_bsec2_i2c/common.yaml @@ -9,6 +9,7 @@ bme68x_bsec2_i2c: sensor: - platform: bme68x_bsec2 + id: bme_sensor temperature: name: BME68X Temperature pressure: diff --git a/tests/components/bthome_mithermometer/common.yaml b/tests/components/bthome_mithermometer/common.yaml new file mode 100644 index 0000000000..ba94e46878 --- /dev/null +++ b/tests/components/bthome_mithermometer/common.yaml @@ -0,0 +1,15 @@ +esp32_ble_tracker: + +sensor: + - platform: bthome_mithermometer + mac_address: A4:C1:38:4E:16:78 + temperature: + name: "BTHome Temperature" + humidity: + name: "BTHome Humidity" + battery_level: + name: "BTHome Battery" + battery_voltage: + name: "BTHome Battery Voltage" + signal_strength: + name: "BTHome Signal" diff --git a/tests/components/bthome_mithermometer/test.esp32-idf.yaml b/tests/components/bthome_mithermometer/test.esp32-idf.yaml new file mode 100644 index 0000000000..7a6541ae76 --- /dev/null +++ b/tests/components/bthome_mithermometer/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/cc1101/common.yaml b/tests/components/cc1101/common.yaml new file mode 100644 index 0000000000..42ec50911f --- /dev/null +++ b/tests/components/cc1101/common.yaml @@ -0,0 +1,37 @@ +cc1101: + id: transceiver + cs_pin: ${cs_pin} + gdo0_pin: ${gdo0_pin} + frequency: 433.92MHz + if_frequency: 153kHz + filter_bandwidth: 203kHz + channel: 0 + channel_spacing: 200kHz + symbol_rate: 4800 + modulation_type: GFSK + packet_mode: true + packet_length: 8 + crc_enable: true + whitening: false + sync_mode: "16/16" + sync0: 0x91 + sync1: 0xD3 + num_preamble: 2 + on_packet: + then: + - lambda: |- + ESP_LOGD("cc1101", "packet %s freq_offset %.0f Hz rssi %.1f dBm lqi %u", format_hex(x).c_str(), freq_offset, rssi, lqi); + +button: + - platform: template + name: "CC1101 Button" + on_press: + then: + - cc1101.begin_tx: transceiver + - cc1101.begin_rx: transceiver + - cc1101.set_idle: transceiver + - cc1101.reset: transceiver + - cc1101.send_packet: + data: [0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef] + - cc1101.send_packet: !lambda |- + return {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; diff --git a/tests/components/cc1101/test.esp32-idf.yaml b/tests/components/cc1101/test.esp32-idf.yaml new file mode 100644 index 0000000000..966f11bb64 --- /dev/null +++ b/tests/components/cc1101/test.esp32-idf.yaml @@ -0,0 +1,8 @@ +substitutions: + cs_pin: GPIO5 + gdo0_pin: GPIO4 + +packages: + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/cc1101/test.esp8266.yaml b/tests/components/cc1101/test.esp8266.yaml new file mode 100644 index 0000000000..6f0f078507 --- /dev/null +++ b/tests/components/cc1101/test.esp8266.yaml @@ -0,0 +1,8 @@ +substitutions: + cs_pin: GPIO5 + gdo0_pin: GPIO4 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/chsc6x/test.rp2040-ard.yaml b/tests/components/chsc6x/test.rp2040-ard.yaml index 2e3613a4a3..eb21b8ec4b 100644 --- a/tests/components/chsc6x/test.rp2040-ard.yaml +++ b/tests/components/chsc6x/test.rp2040-ard.yaml @@ -9,6 +9,7 @@ display: invert_colors: True cs_pin: 20 dc_pin: 21 + data_rate: 20MHz pages: - id: page1 lambda: |- diff --git a/tests/components/climate_ir_lg/common.yaml b/tests/components/climate_ir_lg/common.yaml index da0d656b21..37011b16ee 100644 --- a/tests/components/climate_ir_lg/common.yaml +++ b/tests/components/climate_ir_lg/common.yaml @@ -1,4 +1,16 @@ +sensor: + - platform: template + id: temp_sensor + lambda: return 22.0; + update_interval: 60s + - platform: template + id: humidity_sensor + lambda: return 50.0; + update_interval: 60s + climate: - platform: climate_ir_lg name: LG Climate transmitter_id: xmitr + sensor: temp_sensor + humidity_sensor: humidity_sensor diff --git a/tests/components/deep_sleep/test.bk72xx-ard.yaml b/tests/components/deep_sleep/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..2385fbb4db --- /dev/null +++ b/tests/components/deep_sleep/test.bk72xx-ard.yaml @@ -0,0 +1,14 @@ +deep_sleep: + run_duration: 30s + sleep_duration: 12h + wakeup_pin: + - pin: + number: P6 + - pin: P7 + wakeup_pin_mode: KEEP_AWAKE + - pin: + number: P10 + inverted: true + wakeup_pin_mode: INVERT_WAKEUP + +<<: !include common.yaml diff --git a/tests/components/display/common.yaml b/tests/components/display/common.yaml index 27abb23e03..a722a5f7c2 100644 --- a/tests/components/display/common.yaml +++ b/tests/components/display/common.yaml @@ -6,6 +6,7 @@ display: dc_pin: 13 reset_pin: 21 invert_colors: false + data_rate: 20MHz lambda: |- // Draw an analog clock in the center of the screen int centerX = it.get_width() / 2; diff --git a/tests/components/dsmr/test.esp32-idf.yaml b/tests/components/dsmr/test.esp32-idf.yaml new file mode 100644 index 0000000000..522f60db49 --- /dev/null +++ b/tests/components/dsmr/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + request_pin: GPIO15 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index cff1f51897..d330b4127d 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -19,3 +19,8 @@ display: - platform: epaper_spi model: seeed-reterminal-e1002 + - platform: epaper_spi + model: seeed-ee04-mono-4.26 + # Override pins to avoid conflict with other display configs + busy_pin: 43 + dc_pin: 42 diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index 6338fe98dd..0e220623a1 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -3,6 +3,7 @@ esp32: framework: type: esp-idf advanced: + enable_ota_rollback: true enable_lwip_mdns_queries: true enable_lwip_bridge_interface: true disable_libc_locks_in_iram: false # Test explicit opt-out of RAM optimization diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml index a4c930f236..00a4ceec27 100644 --- a/tests/components/esp32/test.esp32-p4-idf.yaml +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -4,6 +4,10 @@ esp32: cpu_frequency: 400MHz framework: type: esp-idf + components: + - espressif/mdns^1.8.2 + - name: espressif/esp_hosted + ref: 2.7.0 advanced: enable_idf_experimental_features: yes diff --git a/tests/components/esphome/common.yaml b/tests/components/esphome/common.yaml index b2d7bccaa5..db75b08b38 100644 --- a/tests/components/esphome/common.yaml +++ b/tests/components/esphome/common.yaml @@ -2,6 +2,9 @@ esphome: debug_scheduler: true platformio_options: board_build.flash_mode: dio + environment_variables: + TEST_ENV_VAR: "test_value" + BUILD_NUMBER: "12345" area: id: testing_area name: Testing Area diff --git a/tests/components/espnow/common.yaml b/tests/components/espnow/common.yaml index 895ffb9d15..b724af54e0 100644 --- a/tests/components/espnow/common.yaml +++ b/tests/components/espnow/common.yaml @@ -62,7 +62,7 @@ packet_transport: sensors: - temp_sensor providers: - - name: test_provider + - name: test-provider encryption: key: "0123456789abcdef0123456789abcdef" @@ -71,6 +71,6 @@ sensor: id: temp_sensor - platform: packet_transport - provider: test_provider + provider: test-provider remote_id: temp_sensor id: remote_temp diff --git a/tests/components/ethernet/test-lan8720-with-expander.esp32-idf.yaml b/tests/components/ethernet/test-lan8720-with-expander.esp32-idf.yaml new file mode 100644 index 0000000000..09da8d90d9 --- /dev/null +++ b/tests/components/ethernet/test-lan8720-with-expander.esp32-idf.yaml @@ -0,0 +1,15 @@ +<<: !include common-lan8720.yaml + +sn74hc165: + - id: sn74hc165_hub + clock_pin: GPIO13 + data_pin: GPIO14 + load_pin: GPIO15 + sr_count: 3 + +binary_sensor: + - platform: gpio + pin: + sn74hc165: sn74hc165_hub + number: 19 + id: relay_2 diff --git a/tests/components/gree/common.yaml b/tests/components/gree/common.yaml index e706076034..1ddce781bb 100644 --- a/tests/components/gree/common.yaml +++ b/tests/components/gree/common.yaml @@ -1,5 +1,18 @@ climate: - platform: gree name: GREE - model: generic + id: my_gree_ac + model: YAN transmitter_id: xmitr + +switch: + - platform: gree + gree_id: my_gree_ac + light: + name: "AC Lights" + turbo: + name: "AC Turbo" + health: + name: "AC Health" + xfan: + name: "AC X-Fan" diff --git a/tests/components/hc8/common.yaml b/tests/components/hc8/common.yaml new file mode 100644 index 0000000000..ac3b454315 --- /dev/null +++ b/tests/components/hc8/common.yaml @@ -0,0 +1,13 @@ +esphome: + on_boot: + then: + - hc8.calibrate: + id: hc8_sensor + baseline: 420 + +sensor: + - platform: hc8 + id: hc8_sensor + co2: + name: HC8 CO2 Value + update_interval: 15s diff --git a/tests/components/hc8/test.esp32-idf.yaml b/tests/components/hc8/test.esp32-idf.yaml new file mode 100644 index 0000000000..2d29656c94 --- /dev/null +++ b/tests/components/hc8/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hc8/test.esp8266-ard.yaml b/tests/components/hc8/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5a05efa259 --- /dev/null +++ b/tests/components/hc8/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hc8/test.rp2040-ard.yaml b/tests/components/hc8/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f1df2daf83 --- /dev/null +++ b/tests/components/hc8/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hlw8032/common.yaml b/tests/components/hlw8032/common.yaml new file mode 100644 index 0000000000..1b4e537576 --- /dev/null +++ b/tests/components/hlw8032/common.yaml @@ -0,0 +1,17 @@ +sensor: + - platform: hlw8032 + voltage: + name: HLW8032 Voltage + id: hlw8032_voltage + current: + name: HLW8032 Current + id: hlw8032_current + power: + name: HLW8032 Power + id: hlw8032_power + apparent_power: + name: HLW8032 Apparent Power + id: hlw8032_apparent_power + power_factor: + name: HLW8032 Power Factor + id: hlw8032_power_factor diff --git a/tests/components/hlw8032/test.esp32-idf.yaml b/tests/components/hlw8032/test.esp32-idf.yaml new file mode 100644 index 0000000000..911b867708 --- /dev/null +++ b/tests/components/hlw8032/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart_4800_even: !include ../../test_build_components/common/uart_4800_even/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hlw8032/test.esp8266-ard.yaml b/tests/components/hlw8032/test.esp8266-ard.yaml new file mode 100644 index 0000000000..9c1c11c6a1 --- /dev/null +++ b/tests/components/hlw8032/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart_4800_even: !include ../../test_build_components/common/uart_4800_even/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hlw8032/test.rp2040-ard.yaml b/tests/components/hlw8032/test.rp2040-ard.yaml new file mode 100644 index 0000000000..40b6e81bb2 --- /dev/null +++ b/tests/components/hlw8032/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart_4800_even: !include ../../test_build_components/common/uart_4800_even/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hmac_md5/common.yaml b/tests/components/hmac_md5/common.yaml new file mode 100644 index 0000000000..ac6d7ecbaa --- /dev/null +++ b/tests/components/hmac_md5/common.yaml @@ -0,0 +1,34 @@ +esphome: + on_boot: + - lambda: |- + // Test HMAC-MD5 functionality + #ifdef USE_MD5 + using esphome::hmac_md5::HmacMD5; + HmacMD5 hmac; + + // Test with key "key" and message "The quick brown fox jumps over the lazy dog" + const char* key = "key"; + const char* message = "The quick brown fox jumps over the lazy dog"; + + hmac.init(key, strlen(key)); + hmac.add(message, strlen(message)); + hmac.calculate(); + + char hex_output[33]; + hmac.get_hex(hex_output); + hex_output[32] = '\0'; + + ESP_LOGD("HMAC_MD5", "HMAC-MD5('%s', '%s') = %s", key, message, hex_output); + + // Expected: 80070713463e7749b90c2dc24911e275 + const char* expected = "80070713463e7749b90c2dc24911e275"; + if (strcmp(hex_output, expected) == 0) { + ESP_LOGI("HMAC_MD5", "Test PASSED"); + } else { + ESP_LOGE("HMAC_MD5", "Test FAILED. Expected %s", expected); + } + #else + ESP_LOGW("HMAC_MD5", "HMAC-MD5 not available on this platform"); + #endif + +hmac_md5: diff --git a/tests/components/hmac_md5/test.bk72xx-ard.yaml b/tests/components/hmac_md5/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/hmac_md5/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hmac_md5/test.esp32-idf.yaml b/tests/components/hmac_md5/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/hmac_md5/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hmac_md5/test.esp8266-ard.yaml b/tests/components/hmac_md5/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/hmac_md5/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hmac_md5/test.host.yaml b/tests/components/hmac_md5/test.host.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/hmac_md5/test.host.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hmac_md5/test.rp2040-ard.yaml b/tests/components/hmac_md5/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/hmac_md5/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hmac_sha256/common.yaml b/tests/components/hmac_sha256/common.yaml new file mode 100644 index 0000000000..9bbed295fd --- /dev/null +++ b/tests/components/hmac_sha256/common.yaml @@ -0,0 +1,34 @@ +esphome: + on_boot: + - lambda: |- + // Test HMAC-SHA256 functionality + #ifdef USE_SHA256 + using esphome::hmac_sha256::HmacSHA256; + HmacSHA256 hmac; + + // Test with key "key" and message "The quick brown fox jumps over the lazy dog" + const char* key = "key"; + const char* message = "The quick brown fox jumps over the lazy dog"; + + hmac.init(key, strlen(key)); + hmac.add(message, strlen(message)); + hmac.calculate(); + + char hex_output[65]; + hmac.get_hex(hex_output); + hex_output[64] = '\0'; + + ESP_LOGD("HMAC_SHA256", "HMAC-SHA256('%s', '%s') = %s", key, message, hex_output); + + // Expected: f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8 + const char* expected = "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8"; + if (strcmp(hex_output, expected) == 0) { + ESP_LOGI("HMAC_SHA256", "Test PASSED"); + } else { + ESP_LOGE("HMAC_SHA256", "Test FAILED. Expected %s", expected); + } + #else + ESP_LOGW("HMAC_SHA256", "HMAC-SHA256 not available on this platform"); + #endif + +hmac_sha256: diff --git a/tests/components/hmac_sha256/test.bk72xx-ard.yaml b/tests/components/hmac_sha256/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/hmac_sha256/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hmac_sha256/test.esp32-ard.yaml b/tests/components/hmac_sha256/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/hmac_sha256/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hmac_sha256/test.esp32-idf.yaml b/tests/components/hmac_sha256/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/hmac_sha256/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hmac_sha256/test.esp8266-ard.yaml b/tests/components/hmac_sha256/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/hmac_sha256/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hmac_sha256/test.host.yaml b/tests/components/hmac_sha256/test.host.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/hmac_sha256/test.host.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hmac_sha256/test.rp2040-ard.yaml b/tests/components/hmac_sha256/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/hmac_sha256/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hmac_sha256/test.rtl87xx-ard.yaml b/tests/components/hmac_sha256/test.rtl87xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/hmac_sha256/test.rtl87xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/hub75/common.yaml b/tests/components/hub75/common.yaml new file mode 100644 index 0000000000..87e9e1c128 --- /dev/null +++ b/tests/components/hub75/common.yaml @@ -0,0 +1,12 @@ +esphome: + on_boot: + # Test simple value + - hub75.set_brightness: 200 + + # Test templatable value + - hub75.set_brightness: !lambda 'return 100;' + + # Test with explicit ID + - hub75.set_brightness: + id: my_hub75 + brightness: 50 diff --git a/tests/components/hub75/test.esp32-idf.yaml b/tests/components/hub75/test.esp32-idf.yaml new file mode 100644 index 0000000000..dad2a02c24 --- /dev/null +++ b/tests/components/hub75/test.esp32-idf.yaml @@ -0,0 +1,36 @@ +display: + - platform: hub75 + id: my_hub75 + panel_width: 64 + panel_height: 32 + double_buffer: true + brightness: 128 + r1_pin: GPIO25 + g1_pin: GPIO26 + b1_pin: GPIO27 + r2_pin: GPIO14 + g2_pin: GPIO12 + b2_pin: GPIO13 + a_pin: GPIO23 + b_pin: GPIO19 + c_pin: GPIO5 + d_pin: GPIO17 + e_pin: GPIO21 + lat_pin: GPIO4 + oe_pin: GPIO15 + clk_pin: GPIO16 + pages: + - id: page1_hub75 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2_hub75 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + on_page_change: + from: page1_hub75 + to: page2_hub75 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); + +<<: !include common.yaml diff --git a/tests/components/hub75/test.esp32-s3-idf-board.yaml b/tests/components/hub75/test.esp32-s3-idf-board.yaml new file mode 100644 index 0000000000..3723a80006 --- /dev/null +++ b/tests/components/hub75/test.esp32-s3-idf-board.yaml @@ -0,0 +1,23 @@ +display: + - platform: hub75 + id: hub75_display_board + board: adafruit-matrix-portal-s3 + panel_width: 64 + panel_height: 32 + double_buffer: true + brightness: 128 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); + +<<: !include common.yaml diff --git a/tests/components/hub75/test.esp32-s3-idf-rotate.yaml b/tests/components/hub75/test.esp32-s3-idf-rotate.yaml new file mode 100644 index 0000000000..9855fcb4e6 --- /dev/null +++ b/tests/components/hub75/test.esp32-s3-idf-rotate.yaml @@ -0,0 +1,39 @@ +display: + - platform: hub75 + id: my_hub75 + board: apollo-automation-rev6 + panel_width: 64 + panel_height: 64 + layout_rows: 1 + layout_cols: 2 + rotation: 90 + bit_depth: 4 + double_buffer: true + auto_clear_enabled: true + update_interval: 16ms + latch_blanking: 1 + clock_speed: 20MHz + lambda: |- + // Test clipping: 8 columns x 4 rows of 16x16 colored squares + Color colors[32] = { + Color(255, 0, 0), Color(0, 255, 0), Color(0, 0, 255), Color(255, 255, 0), + Color(255, 0, 255), Color(0, 255, 255), Color(255, 128, 0), Color(128, 0, 255), + Color(0, 128, 255), Color(255, 0, 128), Color(128, 255, 0), Color(0, 255, 128), + Color(255, 128, 128), Color(128, 255, 128), Color(128, 128, 255), Color(255, 255, 128), + Color(255, 128, 255), Color(128, 255, 255), Color(192, 64, 0), Color(64, 192, 0), + Color(0, 64, 192), Color(192, 0, 64), Color(64, 0, 192), Color(0, 192, 64), + Color(128, 64, 64), Color(64, 128, 64), Color(64, 64, 128), Color(128, 128, 64), + Color(128, 64, 128), Color(64, 128, 128), Color(255, 255, 255), Color(128, 128, 128) + }; + int idx = 0; + for (int row = 0; row < 4; row++) { + for (int col = 0; col < 8; col++) { + // Clipping mode: clip to square bounds, then fill "entire screen" + it.start_clipping(col * 16, row * 16, (col + 1) * 16, (row + 1) * 16); + it.fill(colors[idx]); + it.end_clipping(); + idx++; + } + } + +<<: !include common.yaml diff --git a/tests/components/hub75/test.esp32-s3-idf.yaml b/tests/components/hub75/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..f8ee26e73d --- /dev/null +++ b/tests/components/hub75/test.esp32-s3-idf.yaml @@ -0,0 +1,36 @@ +display: + - platform: hub75 + id: my_hub75 + panel_width: 64 + panel_height: 32 + double_buffer: true + brightness: 128 + r1_pin: GPIO42 + g1_pin: GPIO41 + b1_pin: GPIO40 + r2_pin: GPIO38 + g2_pin: GPIO39 + b2_pin: GPIO37 + a_pin: GPIO45 + b_pin: GPIO36 + c_pin: GPIO48 + d_pin: GPIO35 + e_pin: GPIO21 + lat_pin: GPIO47 + oe_pin: GPIO14 + clk_pin: GPIO2 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + on_page_change: + from: page1 + to: page2 + then: + lambda: |- + ESP_LOGD("display", "1 -> 2"); + +<<: !include common.yaml diff --git a/tests/components/ili9xxx/common.yaml b/tests/components/ili9xxx/common.yaml index 47384b1398..4665e55e4b 100644 --- a/tests/components/ili9xxx/common.yaml +++ b/tests/components/ili9xxx/common.yaml @@ -11,6 +11,7 @@ display: cs_pin: ${cs_pin1} dc_pin: ${dc_pin1} reset_pin: ${reset_pin1} + data_rate: 20MHz lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx @@ -27,5 +28,6 @@ display: reset_pin: ${reset_pin2} auto_clear_enabled: false rotation: 90 + data_rate: 20MHz lambda: |- it.fill(Color::WHITE); diff --git a/tests/components/image/test.rp2040-ard.yaml b/tests/components/image/test.rp2040-ard.yaml index 40f17d57fe..03a9c42a38 100644 --- a/tests/components/image/test.rp2040-ard.yaml +++ b/tests/components/image/test.rp2040-ard.yaml @@ -9,6 +9,7 @@ display: cs_pin: 20 dc_pin: 21 reset_pin: 22 + data_rate: 20MHz invert_colors: true <<: !include common.yaml diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index c70dd7568d..652ae7e7a1 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -115,8 +115,8 @@ wifi: password: PASSWORD123 time: - platform: sntp - id: time_id + - platform: sntp + id: sntp_time text: - id: lvgl_text diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index e42a813b40..65d629bcdf 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -16,6 +16,18 @@ binary_sensor: platform: template - id: left_sensor platform: template + - platform: lvgl + id: button_checker + name: LVGL button + widget: button_button + on_state: + then: + - lvgl.checkbox.update: + id: checkbox_id + state: + checked: !lambda |- + auto y = x; // block inlining of one line return + return y; lvgl: log_level: debug @@ -414,6 +426,14 @@ lvgl: logger.log: Long pressed repeated - buttons: - id: button_e + - button: + id: button_with_text + text: Button + on_click: + lvgl.button.update: + id: button_with_text + text: Clicked + - button: layout: 2x1 id: button_button @@ -478,19 +498,19 @@ lvgl: id: hello_label text: time_format: "%c" - time: time_id + time: sntp_time - lvgl.label.update: id: hello_label text: time_format: "%c" - time: !lambda return id(time_id).now(); + time: !lambda return id(sntp_time).now(); - lvgl.label.update: id: hello_label text: time_format: "%c" time: !lambda |- ESP_LOGD("label", "multi-line lambda"); - return id(time_id).now(); + return id(sntp_time).now(); on_value: logger.log: format: "state now %d" @@ -537,6 +557,9 @@ lvgl: - tileview: id: tileview_id scrollbar_mode: active + scroll_dir: all + scroll_elastic: true + scroll_momentum: true on_value: then: - if: @@ -546,7 +569,10 @@ lvgl: - logger.log: "tile 1 is now showing" tiles: - id: tile_1 + scroll_snap_y: center + scroll_snap_x: start layout: vertical + pad_all: 6px row: 0 column: 0 dir: ALL @@ -703,7 +729,9 @@ lvgl: on_value: - lvgl.spinbox.update: id: spinbox_id - value: !lambda return x; + value: !lambda |- + static float yyy = 83.0; + return yyy + .8; - button: styles: spin_button id: spin_up @@ -779,6 +807,18 @@ lvgl: arc_color: 0xFFFF00 focused: arc_color: 0x808080 + on_click: + then: + - lvgl.arc.update: + id: lv_arc_1 + value: !lambda return (int)((float)rand() / RAND_MAX * 100); + min_value: !lambda return (int)((float)rand() / RAND_MAX * 100); + max_value: !lambda return (int)((float)rand() / RAND_MAX * 100); + start_angle: !lambda return (int)((float)rand() / RAND_MAX * 100); + end_angle: !lambda return (int)((float)rand() / RAND_MAX * 100); + rotation: !lambda return (int)((float)rand() / RAND_MAX * 100); + change_rate: !lambda return (uint)((float)rand() / RAND_MAX * 100); + mode: NORMAL - bar: id: bar_id align: top_mid @@ -879,6 +919,7 @@ lvgl: grid_columns: [40, fr(1), fr(1)] pad_row: 6px pad_column: 0 + multiple_widgets_per_cell: true widgets: - image: grid_cell_row_pos: 0 @@ -903,6 +944,10 @@ lvgl: grid_cell_row_pos: 1 grid_cell_column_pos: 0 text: "Grid cell 1/0" + - label: + grid_cell_row_pos: 1 + grid_cell_column_pos: 0 + text: "Duplicate for 1/0" - label: styles: bdr_style grid_cell_row_pos: 1 @@ -1025,6 +1070,7 @@ lvgl: opa: 0% - id: page3 layout: Horizontal + pad_all: 6px widgets: - keyboard: id: lv_keyboard diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index 2450d28eb8..e6025e17fc 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -60,6 +60,7 @@ display: update_interval: never lvgl: + update_when_display_idle: true displays: - tft_display - second_display diff --git a/tests/components/mcp3204/common.yaml b/tests/components/mcp3204/common.yaml index eca6ec44f4..9750f0af8e 100644 --- a/tests/components/mcp3204/common.yaml +++ b/tests/components/mcp3204/common.yaml @@ -4,7 +4,21 @@ mcp3204: sensor: - platform: mcp3204 - id: mcp3204_sensor + id: mcp3204_default_single_0 mcp3204_id: mcp3204_hub number: 0 update_interval: 5s + + - platform: mcp3204 + id: mcp3204_single_0 + mcp3204_id: mcp3204_hub + number: 0 + diff_mode: false + update_interval: 5s + + - platform: mcp3204 + id: mcp3204_diff_0_1 + mcp3204_id: mcp3204_hub + number: 0 + diff_mode: true + update_interval: 5s diff --git a/tests/components/mhz19/common.yaml b/tests/components/mhz19/common.yaml index 94989fecbe..b12ca50197 100644 --- a/tests/components/mhz19/common.yaml +++ b/tests/components/mhz19/common.yaml @@ -6,3 +6,4 @@ sensor: name: MH-Z19 Temperature automatic_baseline_calibration: false update_interval: 15s + detection_range: 5000ppm diff --git a/tests/components/micronova/common.yaml b/tests/components/micronova/common.yaml index 3cf8e36fb6..5870d3726f 100644 --- a/tests/components/micronova/common.yaml +++ b/tests/components/micronova/common.yaml @@ -5,7 +5,7 @@ button: - platform: micronova custom_button: name: Custom Micronova Button - memory_location: 0xA0 + memory_location: 0x20 memory_address: 0x7D memory_data: 0x0F @@ -16,6 +16,7 @@ number: step: 1 power_level: name: Micronova Power level + update_interval: 10s sensor: - platform: micronova @@ -40,4 +41,10 @@ sensor: switch: - platform: micronova stove: - name: Stove on/off + name: Stove + +text_sensor: + - platform: micronova + stove_state: + name: Stove status + update_interval: 5s diff --git a/tests/components/micronova/test.esp32-idf.yaml b/tests/components/micronova/test.esp32-idf.yaml index 5cc3a234ca..6e5602818f 100644 --- a/tests/components/micronova/test.esp32-idf.yaml +++ b/tests/components/micronova/test.esp32-idf.yaml @@ -2,6 +2,6 @@ substitutions: enable_rx_pin: GPIO13 packages: - uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/micronova/test.esp8266-ard.yaml b/tests/components/micronova/test.esp8266-ard.yaml index ffe1e0a063..80792813ad 100644 --- a/tests/components/micronova/test.esp8266-ard.yaml +++ b/tests/components/micronova/test.esp8266-ard.yaml @@ -2,6 +2,6 @@ substitutions: enable_rx_pin: GPIO15 packages: - uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/micronova/test.rp2040-ard.yaml b/tests/components/micronova/test.rp2040-ard.yaml index 6dc030e6b6..f069760378 100644 --- a/tests/components/micronova/test.rp2040-ard.yaml +++ b/tests/components/micronova/test.rp2040-ard.yaml @@ -2,6 +2,6 @@ substitutions: enable_rx_pin: GPIO3 packages: - uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/mmc5603/common.yaml b/tests/components/mmc5603/common.yaml index 6f6e35e9af..ddb5c3b25e 100644 --- a/tests/components/mmc5603/common.yaml +++ b/tests/components/mmc5603/common.yaml @@ -2,6 +2,7 @@ sensor: - platform: mmc5603 i2c_id: i2c_bus address: 0x30 + auto_set_reset: true field_strength_x: name: HMC5883L Field Strength X field_strength_y: diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 3f1b83bb01..284ac30337 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -4,6 +4,7 @@ wifi: time: - platform: sntp + id: sntp_time mqtt: broker: "192.168.178.84" diff --git a/tests/components/mqtt/test.rtl87xx-ard.yaml b/tests/components/mqtt/test.rtl87xx-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/mqtt/test.rtl87xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/online_image/common-rp2040.yaml b/tests/components/online_image/common-rp2040.yaml index 25891b94bc..bbb514bded 100644 --- a/tests/components/online_image/common-rp2040.yaml +++ b/tests/components/online_image/common-rp2040.yaml @@ -8,6 +8,7 @@ display: spi_id: spi_bus id: main_lcd model: ili9342 + data_rate: 20MHz cs_pin: 20 dc_pin: 17 reset_pin: 21 diff --git a/tests/components/opentherm/common.yaml b/tests/components/opentherm/common.yaml index 1e58a04bf0..fb5fb39eb8 100644 --- a/tests/components/opentherm/common.yaml +++ b/tests/components/opentherm/common.yaml @@ -92,7 +92,7 @@ sensor: ch_pump_starts: name: "Boiler Number of starts CH pump" dhw_pump_valve_starts: - name: "Boiler Number of starts DHW pump/valve" + name: "Boiler Number of starts DHW pump valve" dhw_burner_starts: name: "Boiler Number of starts burner during DHW mode" burner_operation_hours: @@ -139,7 +139,7 @@ binary_sensor: dhw_present: name: "Boiler DHW present" control_type_on_off: - name: "Boiler Control type is on/off" + name: "Boiler Control type is on-off" cooling_supported: name: "Boiler Cooling supported" dhw_storage_tank: @@ -153,9 +153,9 @@ binary_sensor: max_ch_setpoint_transfer_enabled: name: "Boiler CH maximum setpoint transfer enabled" dhw_setpoint_rw: - name: "Boiler DHW setpoint read/write" + name: "Boiler DHW setpoint read-write" max_ch_setpoint_rw: - name: "Boiler CH maximum setpoint read/write" + name: "Boiler CH maximum setpoint read-write" switch: - platform: opentherm diff --git a/tests/components/ota/test.host.yaml b/tests/components/ota/test.host.yaml new file mode 100644 index 0000000000..ae7c4d0add --- /dev/null +++ b/tests/components/ota/test.host.yaml @@ -0,0 +1,4 @@ +<<: !include common.yaml + +#host platform does not support wifi / network is automatically included +wifi: !remove diff --git a/tests/components/pca9685/common.yaml b/tests/components/pca9685/common.yaml index 2e238b481c..9e2de6257a 100644 --- a/tests/components/pca9685/common.yaml +++ b/tests/components/pca9685/common.yaml @@ -2,6 +2,7 @@ pca9685: i2c_id: i2c_bus frequency: 500 address: 0x0 + phase_balancer: linear output: - platform: pca9685 diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index cf46e882a7..7ff416dccb 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -39,6 +39,15 @@ sensor: return 0.0; update_interval: 60s +text: + - platform: template + name: "Template text" + optimistic: true + min_length: 0 + max_length: 100 + mode: text + initial_value: "Hello World" + text_sensor: - platform: version name: "ESPHome Version" @@ -52,6 +61,25 @@ text_sensor: return {"Goodbye (cruel) World"}; update_interval: 60s +event: + - platform: template + name: "Template Event" + id: template_event1 + event_types: + - "custom_event_1" + - "custom_event_2" + +button: + - platform: template + name: "Template Event Button" + on_press: + - logger.log: "Template Event Button pressed" + - lambda: |- + ESP_LOGD("template_event_button", "Template Event Button pressed"); + - event.trigger: + id: template_event1 + event_type: custom_event_1 + binary_sensor: - platform: template id: template_binary_sensor1 @@ -84,6 +112,25 @@ cover: } return COVER_CLOSED; +light: + - platform: binary + name: "Binary Light" + output: test_output + - platform: monochromatic + name: "Brightness Light" + output: test_output + - platform: rgb + name: "RGB Light" + red: test_output + green: test_output + blue: test_output + - platform: rgbw + name: "RGBW Light" + red: test_output + green: test_output + blue: test_output + white: test_output + lock: - platform: template id: template_lock1 @@ -94,6 +141,14 @@ lock: return LOCK_STATE_UNLOCKED; optimistic: true +output: + - platform: template + id: test_output + type: float + write_action: + - lambda: |- + // no-op for CI/build tests + (void)state; select: - platform: template id: template_select1 diff --git a/tests/components/psram/test.esp32-c5-idf.yaml b/tests/components/psram/test.esp32-c5-idf.yaml new file mode 100644 index 0000000000..fbd0132e2d --- /dev/null +++ b/tests/components/psram/test.esp32-c5-idf.yaml @@ -0,0 +1,8 @@ +esp32: + cpu_frequency: 240MHz + framework: + type: esp-idf + +psram: + speed: 120MHz + ignore_not_found: false diff --git a/tests/components/qr_code/common.yaml b/tests/components/qr_code/common.yaml index 5fec26c1cc..2ffba67763 100644 --- a/tests/components/qr_code/common.yaml +++ b/tests/components/qr_code/common.yaml @@ -5,6 +5,7 @@ display: cs_pin: ${cs_pin} dc_pin: ${dc_pin} reset_pin: ${reset_pin} + data_rate: 500kHz invert_colors: false lambda: |- // Draw a QR code in the center of the screen @@ -16,4 +17,4 @@ display: qr_code: - id: qr_code_homepage_qr - value: https://esphome.io/index.html + value: https://esphome.io/ diff --git a/tests/components/rd03d/common.yaml b/tests/components/rd03d/common.yaml new file mode 100644 index 0000000000..b13d899ab3 --- /dev/null +++ b/tests/components/rd03d/common.yaml @@ -0,0 +1,59 @@ +rd03d: + id: rd03d_radar + +sensor: + - platform: rd03d + rd03d_id: rd03d_radar + target_count: + name: Target Count + target_1: + x: + name: Target-1 X + y: + name: Target-1 Y + speed: + name: Target-1 Speed + angle: + name: Target-1 Angle + distance: + name: Target-1 Distance + resolution: + name: Target-1 Resolution + target_2: + x: + name: Target-2 X + y: + name: Target-2 Y + speed: + name: Target-2 Speed + angle: + name: Target-2 Angle + distance: + name: Target-2 Distance + resolution: + name: Target-2 Resolution + target_3: + x: + name: Target-3 X + y: + name: Target-3 Y + speed: + name: Target-3 Speed + angle: + name: Target-3 Angle + distance: + name: Target-3 Distance + resolution: + name: Target-3 Resolution + +binary_sensor: + - platform: rd03d + rd03d_id: rd03d_radar + target: + name: Presence + target_1: + name: Target-1 Presence + target_2: + name: Target-2 Presence + target_3: + name: Target-3 Presence diff --git a/tests/components/rd03d/test.esp32-idf.yaml b/tests/components/rd03d/test.esp32-idf.yaml new file mode 100644 index 0000000000..2d29656c94 --- /dev/null +++ b/tests/components/rd03d/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/rd03d/test.esp8266-ard.yaml b/tests/components/rd03d/test.esp8266-ard.yaml new file mode 100644 index 0000000000..5a05efa259 --- /dev/null +++ b/tests/components/rd03d/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/rd03d/test.rp2040-ard.yaml b/tests/components/rd03d/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f1df2daf83 --- /dev/null +++ b/tests/components/rd03d/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/remote_receiver/test.rp2040-ard.yaml b/tests/components/remote_receiver/test.rp2040-ard.yaml new file mode 100644 index 0000000000..c9784ae003 --- /dev/null +++ b/tests/components/remote_receiver/test.rp2040-ard.yaml @@ -0,0 +1,12 @@ +remote_receiver: + id: rcvr + pin: GPIO5 + dump: all + <<: !include common-actions.yaml + +binary_sensor: + - platform: remote_receiver + name: Panasonic Remote Input + panasonic: + address: 0x4004 + command: 0x100BCBD diff --git a/tests/components/remote_transmitter/test.rp2040-ard.yaml b/tests/components/remote_transmitter/test.rp2040-ard.yaml new file mode 100644 index 0000000000..19759360f4 --- /dev/null +++ b/tests/components/remote_transmitter/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +remote_transmitter: + id: xmitr + pin: GPIO5 + carrier_duty_percent: 50% + +packages: + buttons: !include common-buttons.yaml diff --git a/tests/components/sensor/common.yaml b/tests/components/sensor/common.yaml index 2180f66da8..1961c98685 100644 --- a/tests/components/sensor/common.yaml +++ b/tests/components/sensor/common.yaml @@ -236,3 +236,10 @@ sensor: - multiply: 2.0 - offset: 10.0 - lambda: return x * 3.0; + + # Testing measurement_angle state class + - platform: template + name: "Angle Sensor" + lambda: return 42.0; + update_interval: 1s + state_class: "measurement_angle" diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py index 45e5ea2211..b4bc95176d 100644 --- a/tests/components/socket/test_wake_loop_threadsafe.py +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -4,6 +4,7 @@ from esphome.core import CORE def test_require_wake_loop_threadsafe__first_call() -> None: """Test that first call sets up define and consumes socket.""" + CORE.config = {"wifi": True} socket.require_wake_loop_threadsafe() # Verify CORE.data was updated @@ -17,6 +18,7 @@ def test_require_wake_loop_threadsafe__idempotent() -> None: """Test that subsequent calls are idempotent.""" # Set up initial state as if already called CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True + CORE.config = {"ethernet": True} # Call again - should not raise or fail socket.require_wake_loop_threadsafe() @@ -31,6 +33,7 @@ def test_require_wake_loop_threadsafe__idempotent() -> None: def test_require_wake_loop_threadsafe__multiple_calls() -> None: """Test that multiple calls only set up once.""" # Call three times + CORE.config = {"openthread": True} socket.require_wake_loop_threadsafe() socket.require_wake_loop_threadsafe() socket.require_wake_loop_threadsafe() @@ -40,3 +43,35 @@ def test_require_wake_loop_threadsafe__multiple_calls() -> None: # Verify the define was added (only once, but we can just check it exists) assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) + + +def test_require_wake_loop_threadsafe__no_networking() -> None: + """Test that wake loop is NOT configured when no networking is configured.""" + # Set up config without any networking components + CORE.config = {"esphome": {"name": "test"}, "logger": {}} + + # Call require_wake_loop_threadsafe + socket.require_wake_loop_threadsafe() + + # Verify CORE.data flag was NOT set (since has_networking returns False) + assert socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED not in CORE.data + + # Verify the define was NOT added + assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines) + + +def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -> None: + """Test that no socket is consumed when no networking is configured.""" + # Set up config without any networking components + CORE.config = {"logger": {}} + + # Track initial socket consumer state + initial_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {}) + + # Call require_wake_loop_threadsafe + socket.require_wake_loop_threadsafe() + + # Verify no socket was consumed + consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {}) + assert "socket.wake_loop_threadsafe" not in consumers + assert consumers == initial_consumers diff --git a/tests/components/sps30/common.yaml b/tests/components/sps30/common.yaml index d40cd16b6d..a83477b764 100644 --- a/tests/components/sps30/common.yaml +++ b/tests/components/sps30/common.yaml @@ -30,3 +30,4 @@ sensor: id: workshop_PMC_10_0 address: 0x69 update_interval: 10s + idle_interval: 5min diff --git a/tests/components/stts22h/common.yaml b/tests/components/stts22h/common.yaml new file mode 100644 index 0000000000..2e332f9276 --- /dev/null +++ b/tests/components/stts22h/common.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: stts22h + i2c_id: i2c_bus + name: Temperature + update_interval: 15s diff --git a/tests/components/stts22h/test.esp32-idf.yaml b/tests/components/stts22h/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/stts22h/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/stts22h/test.esp8266-ard.yaml b/tests/components/stts22h/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/stts22h/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/stts22h/test.nrf52-adafruit.yaml b/tests/components/stts22h/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..2a0de6241c --- /dev/null +++ b/tests/components/stts22h/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/stts22h/test.rp2040-ard.yaml b/tests/components/stts22h/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/stts22h/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index f101eea942..e050c0b307 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -9,6 +9,18 @@ esphome: id: template_sens state: !lambda "return 42.0;" + - water_heater.template.publish: + id: template_water_heater + target_temperature: 50.0 + mode: ECO + + # Templated + - water_heater.template.publish: + id: template_water_heater + current_temperature: !lambda "return 45.0;" + target_temperature: !lambda "return 55.0;" + mode: !lambda "return water_heater::WATER_HEATER_MODE_GAS;" + # Test C++ API: set_template() with stateless lambda (no captures) # NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break. - lambda: |- @@ -299,6 +311,24 @@ alarm_control_panel: codes: - "1234" +water_heater: + - platform: template + id: template_water_heater + name: "Template Water Heater" + optimistic: true + current_temperature: !lambda "return 42.0f;" + mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;" + supported_modes: + - "OFF" + - ECO + - GAS + - ELECTRIC + - HEAT_PUMP + - HIGH_DEMAND + - PERFORMANCE + set_action: + - logger.log: "set_action" + datetime: - platform: template name: Date diff --git a/tests/components/text/common.yaml b/tests/components/text/common.yaml new file mode 100644 index 0000000000..26618be03a --- /dev/null +++ b/tests/components/text/common.yaml @@ -0,0 +1,25 @@ +text: + - platform: template + name: "Test Text" + id: test_text + optimistic: true + min_length: 0 + max_length: 100 + mode: text + + - platform: template + name: "Test Text with Pattern" + id: test_text_pattern + optimistic: true + min_length: 1 + max_length: 50 + pattern: "[A-Za-z0-9 ]+" + mode: text + + - platform: template + name: "Test Password" + id: test_password + optimistic: true + min_length: 8 + max_length: 32 + mode: password diff --git a/tests/components/text/test.esp32-idf.yaml b/tests/components/text/test.esp32-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/text/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/text/test.esp8266-ard.yaml b/tests/components/text/test.esp8266-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/text/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/thermopro_ble/common.yaml b/tests/components/thermopro_ble/common.yaml new file mode 100644 index 0000000000..297725e1c3 --- /dev/null +++ b/tests/components/thermopro_ble/common.yaml @@ -0,0 +1,13 @@ +esp32_ble_tracker: + +sensor: + - platform: thermopro_ble + mac_address: FE:74:B8:6A:97:B7 + temperature: + name: "ThermoPro Temperature" + humidity: + name: "ThermoPro Humidity" + battery_level: + name: "ThermoPro Battery Level" + signal_strength: + name: "ThermoPro Signal Strength" diff --git a/tests/components/thermopro_ble/test.esp32-idf.yaml b/tests/components/thermopro_ble/test.esp32-idf.yaml new file mode 100644 index 0000000000..7a6541ae76 --- /dev/null +++ b/tests/components/thermopro_ble/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml index 6ffd0d7282..2a97f9a5de 100644 --- a/tests/components/uart/test.esp32-idf.yaml +++ b/tests/components/uart/test.esp32-idf.yaml @@ -75,3 +75,11 @@ button: - uart.write: !lambda |- std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n"; return std::vector(cmd.begin(), cmd.end()); + +event: + - platform: uart + uart_id: uart_uart + name: "UART Event" + event_types: + - "string_event_A": "*A#" + - "bytes_event_B": [0x2A, 0x42, 0x23] diff --git a/tests/components/uart/test.esp8266-ard.yaml b/tests/components/uart/test.esp8266-ard.yaml index 566038ee3e..c2670b289a 100644 --- a/tests/components/uart/test.esp8266-ard.yaml +++ b/tests/components/uart/test.esp8266-ard.yaml @@ -31,3 +31,11 @@ button: name: "UART Button" uart_id: uart_uart data: [0xFF, 0xEE] + +event: + - platform: uart + uart_id: uart_uart + name: "UART Event" + event_types: + - "string_event_A": "*A#" + - "bytes_event_B": [0x2A, 0x42, 0x23] diff --git a/tests/components/update/common.yaml b/tests/components/update/common.yaml index 521a0a6a5c..40042945c8 100644 --- a/tests/components/update/common.yaml +++ b/tests/components/update/common.yaml @@ -9,6 +9,8 @@ esphome: update.is_available: then: - logger.log: "Update available" + else: + - update.check: - update.perform: force_update: true diff --git a/tests/components/uptime/common.yaml b/tests/components/uptime/common.yaml index 86b764e7ff..279258c670 100644 --- a/tests/components/uptime/common.yaml +++ b/tests/components/uptime/common.yaml @@ -3,6 +3,7 @@ wifi: time: - platform: sntp + id: sntp_time sensor: - platform: uptime diff --git a/tests/components/usb_cdc_acm/test.esp32-p4-idf.yaml b/tests/components/usb_cdc_acm/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..4786c96bcc --- /dev/null +++ b/tests/components/usb_cdc_acm/test.esp32-p4-idf.yaml @@ -0,0 +1,5 @@ +<<: !include tinyusb_common.yaml + +usb_cdc_acm: + interfaces: + id: usb_cdc_acm1 diff --git a/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml b/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml new file mode 100644 index 0000000000..f159b38ff6 --- /dev/null +++ b/tests/components/usb_cdc_acm/test.esp32-s2-idf.yaml @@ -0,0 +1,5 @@ +<<: !include tinyusb_common.yaml + +usb_cdc_acm: + interfaces: + - id: usb_cdc_acm1 diff --git a/tests/components/usb_cdc_acm/test.esp32-s3-idf.yaml b/tests/components/usb_cdc_acm/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..6913fe21d5 --- /dev/null +++ b/tests/components/usb_cdc_acm/test.esp32-s3-idf.yaml @@ -0,0 +1,6 @@ +<<: !include tinyusb_common.yaml + +usb_cdc_acm: + interfaces: + - id: usb_cdc_acm1 + - id: usb_cdc_acm2 diff --git a/tests/components/usb_cdc_acm/tinyusb_common.yaml b/tests/components/usb_cdc_acm/tinyusb_common.yaml new file mode 100644 index 0000000000..cb3f48836a --- /dev/null +++ b/tests/components/usb_cdc_acm/tinyusb_common.yaml @@ -0,0 +1,8 @@ +tinyusb: + id: tinyusb_test + usb_lang_id: 0x0123 + usb_manufacturer_str: ESPHomeTestManufacturer + usb_product_id: 0x1234 + usb_product_str: ESPHomeTestProduct + usb_serial_str: ESPHomeTestSerialNumber + usb_vendor_id: 0x2345 diff --git a/tests/components/water_heater/common.yaml b/tests/components/water_heater/common.yaml new file mode 100644 index 0000000000..8ec2b1b297 --- /dev/null +++ b/tests/components/water_heater/common.yaml @@ -0,0 +1,16 @@ +water_heater: + - platform: template + id: my_boiler + name: "Test Boiler" + min_temperature: 10 + max_temperature: 85 + target_temperature_step: 0.5 + current_temperature_step: 0.1 + optimistic: true + current_temperature: 45.0 + mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;" + visual: + min_temperature: 10 + max_temperature: 85 + target_temperature_step: 0.5 + current_temperature_step: 0.1 diff --git a/tests/components/web_server/common.yaml b/tests/components/web_server/common.yaml index eb768eeb91..82307c189c 100644 --- a/tests/components/web_server/common.yaml +++ b/tests/components/web_server/common.yaml @@ -36,3 +36,4 @@ datetime: optimistic: yes event: update: +water_heater: diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml index 5d9973cbc8..7ce74ab00d 100644 --- a/tests/components/wifi/common.yaml +++ b/tests/components/wifi/common.yaml @@ -10,6 +10,10 @@ esphome: - logger.log: "Connected to WiFi!" on_error: - logger.log: "Failed to connect to WiFi!" + - if: + condition: wifi.ap_active + then: + - logger.log: "WiFi AP is active!" wifi: networks: diff --git a/tests/components/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml index 6b3ef20963..b2b2233ef3 100644 --- a/tests/components/wifi/test.esp32-idf.yaml +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -1,8 +1,20 @@ psram: +# Tests the high performance request and release; requires the USE_WIFI_RUNTIME_POWER_SAVE define +esphome: + platformio_options: + build_flags: + - "-DUSE_WIFI_RUNTIME_POWER_SAVE" + on_boot: + - then: + - lambda: |- + esphome::wifi::global_wifi_component->request_high_performance(); + esphome::wifi::global_wifi_component->release_high_performance(); + wifi: use_psram: true min_auth_mode: WPA + post_connect_roaming: false manual_ip: static_ip: 192.168.1.100 gateway: 192.168.1.1 diff --git a/tests/components/wifi/test.esp8266-ard.yaml b/tests/components/wifi/test.esp8266-ard.yaml index 9cb0e3cf48..709a639ad6 100644 --- a/tests/components/wifi/test.esp8266-ard.yaml +++ b/tests/components/wifi/test.esp8266-ard.yaml @@ -1,5 +1,6 @@ wifi: min_auth_mode: WPA2 + post_connect_roaming: true packages: - !include common.yaml diff --git a/tests/components/wifi_info/common-mac.yaml b/tests/components/wifi_info/common-mac.yaml new file mode 100644 index 0000000000..3571cd08c4 --- /dev/null +++ b/tests/components/wifi_info/common-mac.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: MySSID + password: password1 + +text_sensor: + - platform: wifi_info + mac_address: + name: MAC Address diff --git a/tests/components/wifi_info/common.yaml b/tests/components/wifi_info/common.yaml index cf5ea563ba..91dea6c66e 100644 --- a/tests/components/wifi_info/common.yaml +++ b/tests/components/wifi_info/common.yaml @@ -13,6 +13,8 @@ text_sensor: bssid: name: BSSID mac_address: - name: Mac Address + name: MAC Address dns_address: - name: DNS ADdress + name: DNS Address + power_save_mode: + name: "WiFi Power Save Mode" diff --git a/tests/components/wifi_info/test-mac.esp32-idf.yaml b/tests/components/wifi_info/test-mac.esp32-idf.yaml new file mode 100644 index 0000000000..9d561ca2ce --- /dev/null +++ b/tests/components/wifi_info/test-mac.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common-mac.yaml diff --git a/tests/components/wifi_info/test-mac.esp8266-ard.yaml b/tests/components/wifi_info/test-mac.esp8266-ard.yaml new file mode 100644 index 0000000000..05f6344fb6 --- /dev/null +++ b/tests/components/wifi_info/test-mac.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common-mac.yaml diff --git a/tests/components/wifi_info/test-mac.rp2040-ard.yaml b/tests/components/wifi_info/test-mac.rp2040-ard.yaml new file mode 100644 index 0000000000..d2d54def9d --- /dev/null +++ b/tests/components/wifi_info/test-mac.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common-mac.yaml diff --git a/tests/components/wireguard/common.yaml b/tests/components/wireguard/common.yaml index cd7ab1075e..342ffa32f6 100644 --- a/tests/components/wireguard/common.yaml +++ b/tests/components/wireguard/common.yaml @@ -4,8 +4,10 @@ wifi: time: - platform: sntp + id: sntp_time wireguard: + time_id: sntp_time address: 172.16.34.100 netmask: 255.255.255.0 # NEVER use the following keys for your VPN -- they are now public! diff --git a/tests/components/xpt2046/common.yaml b/tests/components/xpt2046/common.yaml index 3a8e3286a6..eddbd24d6a 100644 --- a/tests/components/xpt2046/common.yaml +++ b/tests/components/xpt2046/common.yaml @@ -6,6 +6,7 @@ display: cs_pin: ${disp_cs_pin} dc_pin: ${dc_pin} reset_pin: ${reset_pin} + data_rate: 20MHz invert_colors: false lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/zhlt01/common.yaml b/tests/components/zhlt01/common.yaml index d0fd531c87..483f9f3c4e 100644 --- a/tests/components/zhlt01/common.yaml +++ b/tests/components/zhlt01/common.yaml @@ -1,4 +1,4 @@ climate: - platform: zhlt01 - name: ZH/LT-01 Climate + name: ZH-LT-01 Climate transmitter_id: xmitr diff --git a/tests/components/zigbee/common.yaml b/tests/components/zigbee/common.yaml new file mode 100644 index 0000000000..c91569bdbe --- /dev/null +++ b/tests/components/zigbee/common.yaml @@ -0,0 +1,38 @@ +--- +binary_sensor: + - platform: template + name: "Garage Door Open 1" + - platform: template + name: "Garage Door Open 2" + - platform: template + name: "Garage Door Open 3" + - platform: template + name: "Garage Door Open 4" + - platform: template + name: "Garage Door Open 5" + - platform: template + name: "Garage Door Open 6" + - platform: template + name: "Garage Door Open 7" + internal: True + +sensor: + - platform: template + name: "Analog 1" + lambda: return 10.0; + - platform: template + name: "Analog 2" + lambda: return 11.0; + +zigbee: + wipe_on_boot: true + on_join: + then: + - logger.log: "Joined network" + +output: + - platform: template + id: output_factory + type: binary + write_action: + - zigbee.factory_reset diff --git a/tests/components/zigbee/test.nrf52-adafruit.yaml b/tests/components/zigbee/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/zigbee/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/zigbee/test.nrf52-mcumgr.yaml b/tests/components/zigbee/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/zigbee/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/zigbee/test.nrf52-xiao-ble.yaml b/tests/components/zigbee/test.nrf52-xiao-ble.yaml new file mode 100644 index 0000000000..d2ce552de3 --- /dev/null +++ b/tests/components/zigbee/test.nrf52-xiao-ble.yaml @@ -0,0 +1,5 @@ +<<: !include common.yaml + +zigbee: + wipe_on_boot: once + power_source: battery diff --git a/tests/dashboard/test_web_server.py b/tests/dashboard/test_web_server.py index 385841b1c8..10ca6061e6 100644 --- a/tests/dashboard/test_web_server.py +++ b/tests/dashboard/test_web_server.py @@ -1567,3 +1567,90 @@ async def test_dashboard_yaml_loading_with_packages_and_secrets( # If we get here, secret resolution worked! assert "esphome" in config assert config["esphome"]["name"] == "test-download-secrets" + + +@pytest.mark.asyncio +async def test_websocket_check_origin_default_same_origin( + dashboard: DashboardTestHelper, +) -> None: + """Test WebSocket uses default same-origin check when ESPHOME_TRUSTED_DOMAINS not set.""" + # Ensure ESPHOME_TRUSTED_DOMAINS is not set + env = os.environ.copy() + env.pop("ESPHOME_TRUSTED_DOMAINS", None) + with patch.dict(os.environ, env, clear=True): + from tornado.httpclient import HTTPRequest + + url = f"ws://127.0.0.1:{dashboard.port}/events" + # Same origin should work (default Tornado behavior) + request = HTTPRequest( + url, headers={"Origin": f"http://127.0.0.1:{dashboard.port}"} + ) + ws = await websocket_connect(request) + try: + msg = await ws.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "initial_state" + finally: + ws.close() + + +@pytest.mark.asyncio +async def test_websocket_check_origin_trusted_domain( + dashboard: DashboardTestHelper, +) -> None: + """Test WebSocket accepts connections from trusted domains.""" + with patch.dict(os.environ, {"ESPHOME_TRUSTED_DOMAINS": "trusted.example.com"}): + from tornado.httpclient import HTTPRequest + + url = f"ws://127.0.0.1:{dashboard.port}/events" + request = HTTPRequest(url, headers={"Origin": "https://trusted.example.com"}) + ws = await websocket_connect(request) + try: + # Should receive initial state + msg = await ws.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "initial_state" + finally: + ws.close() + + +@pytest.mark.asyncio +async def test_websocket_check_origin_untrusted_domain( + dashboard: DashboardTestHelper, +) -> None: + """Test WebSocket rejects connections from untrusted domains.""" + with patch.dict(os.environ, {"ESPHOME_TRUSTED_DOMAINS": "trusted.example.com"}): + from tornado.httpclient import HTTPRequest + + url = f"ws://127.0.0.1:{dashboard.port}/events" + request = HTTPRequest(url, headers={"Origin": "https://untrusted.example.com"}) + with pytest.raises(HTTPClientError) as exc_info: + await websocket_connect(request) + # Should get HTTP 403 Forbidden due to origin check failure + assert exc_info.value.code == 403 + + +@pytest.mark.asyncio +async def test_websocket_check_origin_multiple_trusted_domains( + dashboard: DashboardTestHelper, +) -> None: + """Test WebSocket accepts connections from multiple trusted domains.""" + with patch.dict( + os.environ, + {"ESPHOME_TRUSTED_DOMAINS": "first.example.com, second.example.com"}, + ): + from tornado.httpclient import HTTPRequest + + url = f"ws://127.0.0.1:{dashboard.port}/events" + # Test second domain in list (with space after comma) + request = HTTPRequest(url, headers={"Origin": "https://second.example.com"}) + ws = await websocket_connect(request) + try: + msg = await ws.read_message() + assert msg is not None + data = json.loads(msg) + assert data["event"] == "initial_state" + finally: + ws.close() diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index afd393c095..e6fe733807 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -12,7 +12,7 @@ using namespace esphome; void setup() { - App.pre_setup("livingroom", "LivingRoom", "comment", __DATE__ ", " __TIME__, false); + App.pre_setup("livingroom", "LivingRoom", false); auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0); diff --git a/tests/integration/README.md b/tests/integration/README.md index 2a6b6fe564..4de08777b0 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -7,7 +7,7 @@ This directory contains end-to-end integration tests for ESPHome, focusing on te - `conftest.py` - Common fixtures and utilities - `const.py` - Constants used throughout the integration tests - `types.py` - Type definitions for fixtures and functions -- `state_utils.py` - State handling utilities (e.g., `InitialStateHelper`, `build_key_to_entity_mapping`) +- `state_utils.py` - State handling utilities (e.g., `InitialStateHelper`, `find_entity`, `require_entity`) - `fixtures/` - YAML configuration files for tests - `test_*.py` - Individual test files @@ -53,6 +53,28 @@ The `InitialStateHelper` class solves a common problem in integration tests: whe **Future work:** Consider converting existing integration tests to use `InitialStateHelper` for more reliable state tracking and to eliminate race conditions related to initial state broadcasts. +#### Entity Lookup Helpers (`state_utils.py`) + +Two helper functions simplify finding entities in test code: + +**`find_entity(entities, object_id_substring, entity_type=None)`** +- Finds an entity by searching for a substring in its `object_id` (case-insensitive) +- Optionally filters by entity type (e.g., `BinarySensorInfo`) +- Returns `None` if not found + +**`require_entity(entities, object_id_substring, entity_type=None, description=None)`** +- Same as `find_entity` but raises `AssertionError` if not found +- Use `description` parameter for clearer error messages + +```python +from aioesphomeapi import BinarySensorInfo +from .state_utils import require_entity + +# Find entities with clear error messages +binary_sensor = require_entity(entities, "test_sensor", BinarySensorInfo) +button = require_entity(entities, "set_true", description="Set True button") +``` + ### Writing Tests The simplest way to write a test is to use the `run_compiled` and `api_client_connected` fixtures: @@ -230,7 +252,7 @@ my_service = next((s for s in services if s.name == "my_service"), None) assert my_service is not None # Execute with parameters -client.execute_service(my_service, {"param1": "value1", "param2": 42}) +await client.execute_service(my_service, {"param1": "value1", "param2": 42}) ``` ##### Multiple Entity Tracking diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 965363972f..50e8d4122b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -51,6 +51,9 @@ if platform.system() == "Windows": import pty # not available on Windows +# Register assert rewrite for entity_utils so assertions have proper error messages +pytest.register_assert_rewrite("tests.integration.entity_utils") + def _get_platformio_env(cache_dir: Path) -> dict[str, str]: """Get environment variables for PlatformIO with shared cache.""" diff --git a/tests/integration/entity_utils.py b/tests/integration/entity_utils.py new file mode 100644 index 0000000000..7596983ee2 --- /dev/null +++ b/tests/integration/entity_utils.py @@ -0,0 +1,145 @@ +"""Utilities for computing entity object_id in integration tests. + +This module contains the algorithm that aioesphomeapi will use to compute +object_id client-side from API data. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from esphome.helpers import fnv1_hash_object_id, sanitize, snake_case + +if TYPE_CHECKING: + from aioesphomeapi import DeviceInfo, EntityInfo + + +def compute_object_id(name: str) -> str: + """Compute object_id from name using snake_case + sanitize.""" + return sanitize(snake_case(name)) + + +def infer_name_add_mac_suffix(device_info: DeviceInfo) -> bool: + """Infer name_add_mac_suffix from device name ending with MAC suffix.""" + mac_suffix = device_info.mac_address.replace(":", "")[-6:].lower() + return device_info.name.endswith(f"-{mac_suffix}") + + +def _get_name_for_object_id( + entity: EntityInfo, + device_info: DeviceInfo, + device_id_to_name: dict[int, str], +) -> str: + """Get the name used for object_id computation. + + This is the algorithm that aioesphomeapi will use to determine which + name to use for computing object_id client-side from API data. + + Args: + entity: The entity to get name for + device_info: Device info from the API + device_id_to_name: Mapping of device_id to device name for sub-devices + + Returns: + The name to use for object_id computation + """ + if entity.name: + # Named entity: use entity name + return entity.name + if entity.device_id != 0: + # Empty name on sub-device: use sub-device name + return device_id_to_name[entity.device_id] + if infer_name_add_mac_suffix(device_info) or device_info.friendly_name: + # Empty name on main device with MAC suffix or friendly_name: use friendly_name + # (even if empty - this is bug-for-bug compatibility for MAC suffix case) + return device_info.friendly_name + # Empty name on main device, no friendly_name: use device name + return device_info.name + + +def compute_entity_object_id( + entity: EntityInfo, + device_info: DeviceInfo, + device_id_to_name: dict[int, str], +) -> str: + """Compute expected object_id for an entity. + + Args: + entity: The entity to compute object_id for + device_info: Device info from the API + device_id_to_name: Mapping of device_id to device name for sub-devices + + Returns: + The computed object_id string + """ + name_for_id = _get_name_for_object_id(entity, device_info, device_id_to_name) + return compute_object_id(name_for_id) + + +def compute_entity_hash( + entity: EntityInfo, + device_info: DeviceInfo, + device_id_to_name: dict[int, str], +) -> int: + """Compute expected object_id hash for an entity. + + Args: + entity: The entity to compute hash for + device_info: Device info from the API + device_id_to_name: Mapping of device_id to device name for sub-devices + + Returns: + The computed FNV-1 hash + """ + name_for_id = _get_name_for_object_id(entity, device_info, device_id_to_name) + return fnv1_hash_object_id(name_for_id) + + +def verify_entity_object_id( + entity: EntityInfo, + device_info: DeviceInfo, + device_id_to_name: dict[int, str], +) -> None: + """Verify an entity's object_id and hash match the expected values. + + Args: + entity: The entity to verify + device_info: Device info from the API + device_id_to_name: Mapping of device_id to device name for sub-devices + + Raises: + AssertionError: If object_id or hash doesn't match expected value + """ + expected_object_id = compute_entity_object_id( + entity, device_info, device_id_to_name + ) + assert entity.object_id == expected_object_id, ( + f"object_id mismatch for entity '{entity.name}': " + f"expected '{expected_object_id}', got '{entity.object_id}'" + ) + + expected_hash = compute_entity_hash(entity, device_info, device_id_to_name) + assert entity.key == expected_hash, ( + f"hash mismatch for entity '{entity.name}': " + f"expected {expected_hash:#x}, got {entity.key:#x}" + ) + + +def verify_all_entities( + entities: list[EntityInfo], + device_info: DeviceInfo, +) -> None: + """Verify all entities have correct object_id and hash values. + + Args: + entities: List of entities to verify + device_info: Device info from the API + + Raises: + AssertionError: If any entity's object_id or hash doesn't match + """ + # Build device_id -> name lookup from sub-devices + device_id_to_name = {d.device_id: d.name for d in device_info.devices} + + for entity in entities: + verify_entity_object_id(entity, device_info, device_id_to_name) diff --git a/tests/integration/fixtures/alarm_control_panel_state_transitions.yaml b/tests/integration/fixtures/alarm_control_panel_state_transitions.yaml new file mode 100644 index 0000000000..1edb401a0d --- /dev/null +++ b/tests/integration/fixtures/alarm_control_panel_state_transitions.yaml @@ -0,0 +1,106 @@ +esphome: + name: alarm-state-transitions + friendly_name: "Alarm Control Panel State Transitions Test" + +logger: + +host: + +globals: + - id: door_sensor_state + type: bool + initial_value: "false" + - id: chime_sensor_state + type: bool + initial_value: "false" + +switch: + # Switch to control the door sensor state + - platform: template + id: door_sensor_switch + name: "Door Sensor Switch" + optimistic: true + turn_on_action: + - globals.set: + id: door_sensor_state + value: "true" + turn_off_action: + - globals.set: + id: door_sensor_state + value: "false" + # Switch to control the chime sensor state + - platform: template + id: chime_sensor_switch + name: "Chime Sensor Switch" + optimistic: true + turn_on_action: + - globals.set: + id: chime_sensor_state + value: "true" + turn_off_action: + - globals.set: + id: chime_sensor_state + value: "false" + +binary_sensor: + - platform: template + id: door_sensor + name: "Door Sensor" + lambda: |- + return id(door_sensor_state); + - platform: template + id: chime_sensor + name: "Chime Sensor" + lambda: |- + return id(chime_sensor_state); + +alarm_control_panel: + - platform: template + id: test_alarm + name: "Test Alarm" + codes: + - "1234" + requires_code_to_arm: true + # Short timeouts for faster testing + arming_away_time: 50ms + arming_home_time: 50ms + arming_night_time: 50ms + pending_time: 50ms + trigger_time: 100ms + restore_mode: ALWAYS_DISARMED + binary_sensors: + - input: door_sensor + bypass_armed_home: false + bypass_armed_night: false + chime: false + trigger_mode: DELAYED + - input: chime_sensor + bypass_armed_home: true + bypass_armed_night: true + chime: true + trigger_mode: DELAYED + on_state: + - logger.log: "State changed" + on_disarmed: + - logger.log: "Alarm disarmed" + on_arming: + - logger.log: "Alarm arming" + on_armed_away: + - logger.log: "Alarm armed away" + on_armed_home: + - logger.log: "Alarm armed home" + on_armed_night: + - logger.log: "Alarm armed night" + on_pending: + - logger.log: "Alarm pending" + on_triggered: + - logger.log: "Alarm triggered" + on_cleared: + - logger.log: "Alarm cleared" + on_chime: + - logger.log: "Chime activated" + on_ready: + - logger.log: "Sensors ready state changed" + +api: + batch_delay: 0ms diff --git a/tests/integration/fixtures/api_action_responses.yaml b/tests/integration/fixtures/api_action_responses.yaml new file mode 100644 index 0000000000..755623b7bb --- /dev/null +++ b/tests/integration/fixtures/api_action_responses.yaml @@ -0,0 +1,93 @@ +esphome: + name: api-action-responses-test + +host: + +logger: + level: DEBUG + +api: + actions: + # ========================================================================== + # supports_response: none (default - no api.respond action) + # No call_id or return_response - just user variables + # ========================================================================== + - action: action_no_response + variables: + message: string + then: + - logger.log: + format: "ACTION_NO_RESPONSE called with: %s" + args: [message.c_str()] + + # ========================================================================== + # supports_response: status (auto-detected - api.respond without data) + # Has call_id only - reports success/error without data payload + # ========================================================================== + - action: action_status_response + variables: + should_succeed: bool + then: + - if: + condition: + lambda: 'return should_succeed;' + then: + - api.respond: + success: true + - logger.log: + format: "ACTION_STATUS_RESPONSE success (call_id=%d)" + args: [call_id] + else: + - api.respond: + success: false + error_message: "Intentional failure for testing" + - logger.log: + format: "ACTION_STATUS_RESPONSE error (call_id=%d)" + args: [call_id] + + # ========================================================================== + # supports_response: optional (auto-detected - api.respond with data) + # Has call_id and return_response - client decides if it wants response + # ========================================================================== + - action: action_optional_response + variables: + value: int + then: + - logger.log: + format: "ACTION_OPTIONAL_RESPONSE (call_id=%d, return_response=%d, value=%d)" + args: [call_id, return_response, value] + - api.respond: + data: !lambda |- + root["input"] = value; + root["doubled"] = value * 2; + + # ========================================================================== + # supports_response: only (explicit - always expects data response) + # Has call_id only - response is always expected with data + # ========================================================================== + - action: action_only_response + supports_response: only + variables: + name: string + then: + - logger.log: + format: "ACTION_ONLY_RESPONSE (call_id=%d, name=%s)" + args: [call_id, name.c_str()] + - api.respond: + data: !lambda |- + root["greeting"] = "Hello, " + name + "!"; + root["length"] = name.length(); + + # Test action with nested JSON response + - action: action_nested_json + supports_response: only + then: + - logger.log: + format: "ACTION_NESTED_JSON (call_id=%d)" + args: [call_id] + - api.respond: + data: !lambda |- + root["config"]["wifi"]["connected"] = true; + root["config"]["api"]["port"] = 6053; + root["items"][0] = "first"; + root["items"][1] = "second"; diff --git a/tests/integration/fixtures/api_action_timeout.yaml b/tests/integration/fixtures/api_action_timeout.yaml new file mode 100644 index 0000000000..405d9d0e2b --- /dev/null +++ b/tests/integration/fixtures/api_action_timeout.yaml @@ -0,0 +1,45 @@ +esphome: + name: api-action-timeout-test + # Use a short timeout for testing (500ms instead of 30s) + platformio_options: + build_flags: + - "-DUSE_API_ACTION_CALL_TIMEOUT_MS=500" + +host: + +logger: + level: DEBUG + +api: + actions: + # Action that responds immediately - should work fine + - action: action_immediate + supports_response: only + then: + - logger.log: "ACTION_IMMEDIATE responding" + - api.respond: + data: !lambda |- + root["status"] = "immediate"; + + # Action that delays 200ms before responding - should work (within 500ms timeout) + - action: action_short_delay + supports_response: only + then: + - logger.log: "ACTION_SHORT_DELAY starting" + - delay: 200ms + - logger.log: "ACTION_SHORT_DELAY responding" + - api.respond: + data: !lambda |- + root["status"] = "short_delay"; + + # Action that delays 1s before responding - should fail (exceeds 500ms timeout) + # The api.respond will log a warning because the action call was already cleaned up + - action: action_long_delay + supports_response: only + then: + - logger.log: "ACTION_LONG_DELAY starting" + - delay: 1s + - logger.log: "ACTION_LONG_DELAY responding (after timeout)" + - api.respond: + data: !lambda |- + root["status"] = "long_delay"; diff --git a/tests/integration/fixtures/api_custom_services.yaml b/tests/integration/fixtures/api_custom_services.yaml index a597c74126..827bee93a6 100644 --- a/tests/integration/fixtures/api_custom_services.yaml +++ b/tests/integration/fixtures/api_custom_services.yaml @@ -5,6 +5,7 @@ host: # This is required for CustomAPIDevice to work api: custom_services: true + homeassistant_states: true # Also test that YAML services still work actions: - action: test_yaml_service diff --git a/tests/integration/fixtures/api_homeassistant.yaml b/tests/integration/fixtures/api_homeassistant.yaml index ce8628977a..8fe23b9a19 100644 --- a/tests/integration/fixtures/api_homeassistant.yaml +++ b/tests/integration/fixtures/api_homeassistant.yaml @@ -17,6 +17,7 @@ api: - button.press: test_all_empty_service - button.press: test_rapid_service_calls - button.press: test_read_ha_states + - button.press: test_action_response_error - number.set: id: ha_number value: 42.5 @@ -309,3 +310,24 @@ button: } else { ESP_LOGI("test", "HA Empty State has no value (expected)"); } + + # Test 9: Action response error handling (tests StringRef error message) + - platform: template + name: "Test Action Response Error" + id: test_action_response_error + on_press: + - logger.log: "Testing action response error handling" + - homeassistant.action: + action: nonexistent.action_for_error_test + data: + test_field: "test_value" + on_error: + - lambda: |- + // This tests that StringRef error message works correctly + // The error variable is std::string (converted from StringRef) + ESP_LOGI("test", "Action error received: %s", error.c_str()); + - logger.log: + format: "Action failed with error message length: %d" + args: ['error.size()'] + on_success: + - logger.log: "Action succeeded unexpectedly" diff --git a/tests/integration/fixtures/api_message_size_batching.yaml b/tests/integration/fixtures/api_message_size_batching.yaml index c730dc1aa3..0fed311e63 100644 --- a/tests/integration/fixtures/api_message_size_batching.yaml +++ b/tests/integration/fixtures/api_message_size_batching.yaml @@ -143,6 +143,7 @@ text: mode: text min_length: 0 max_length: 255 + pattern: "[A-Za-z0-9 ]+" initial_value: "Initial value" update_interval: 5.0s diff --git a/tests/integration/fixtures/binary_sensor_invalidate_state.yaml b/tests/integration/fixtures/binary_sensor_invalidate_state.yaml new file mode 100644 index 0000000000..4016cfe281 --- /dev/null +++ b/tests/integration/fixtures/binary_sensor_invalidate_state.yaml @@ -0,0 +1,39 @@ +esphome: + name: test-binary-sensor-invalidate + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Template binary sensor that we can control +binary_sensor: + - platform: template + name: "Test Binary Sensor" + id: test_binary_sensor + +# Buttons to control the binary sensor state +button: + - platform: template + name: "Set True" + id: set_true_button + on_press: + - binary_sensor.template.publish: + id: test_binary_sensor + state: true + + - platform: template + name: "Set False" + id: set_false_button + on_press: + - binary_sensor.template.publish: + id: test_binary_sensor + state: false + + - platform: template + name: "Invalidate State" + id: invalidate_button + on_press: + - binary_sensor.invalidate_state: + id: test_binary_sensor diff --git a/tests/integration/fixtures/build_info.yaml b/tests/integration/fixtures/build_info.yaml new file mode 100644 index 0000000000..5d6101543a --- /dev/null +++ b/tests/integration/fixtures/build_info.yaml @@ -0,0 +1,31 @@ +esphome: + name: build-info-test +host: +api: +logger: + +text_sensor: + - platform: template + name: "Config Hash" + id: config_hash_sensor + update_interval: 100ms + lambda: |- + char buf[16]; + snprintf(buf, sizeof(buf), "0x%08x", App.get_config_hash()); + return std::string(buf); + - platform: template + name: "Build Time" + id: build_time_sensor + update_interval: 100ms + lambda: |- + char buf[32]; + snprintf(buf, sizeof(buf), "%ld", (long)App.get_build_time()); + return std::string(buf); + - platform: template + name: "Build Time String" + id: build_time_str_sensor + update_interval: 100ms + lambda: |- + char buf[Application::BUILD_TIME_STR_SIZE]; + App.get_build_time_string(buf); + return std::string(buf); diff --git a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp index c8581b3d2f..c86ab99242 100644 --- a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp +++ b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.cpp @@ -17,6 +17,10 @@ void CustomAPIDeviceComponent::setup() { // Test array types register_service(&CustomAPIDeviceComponent::on_service_with_arrays, "custom_service_with_arrays", {"bool_array", "int_array", "float_array", "string_array"}); + + // Test Home Assistant state subscription using std::string API (custom_api_device.h) + // This tests the backward compatibility of the std::string overloads + subscribe_homeassistant_state(&CustomAPIDeviceComponent::on_ha_state_changed, std::string("sensor.custom_test")); } void CustomAPIDeviceComponent::on_test_service() { ESP_LOGI(TAG, "Custom test service called!"); } @@ -48,6 +52,12 @@ void CustomAPIDeviceComponent::on_service_with_arrays(std::vector bool_arr } } +// NOLINTNEXTLINE(performance-unnecessary-value-param) +void CustomAPIDeviceComponent::on_ha_state_changed(std::string entity_id, std::string state) { + ESP_LOGI(TAG, "Home Assistant state changed for %s: %s", entity_id.c_str(), state.c_str()); + ESP_LOGI(TAG, "This subscription uses std::string API for backward compatibility"); +} + } // namespace custom_api_device_component } // namespace esphome #endif // USE_API diff --git a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h index 92960746d9..4d519d3ed1 100644 --- a/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h +++ b/tests/integration/fixtures/external_components/custom_api_device_component/custom_api_device_component.h @@ -22,6 +22,10 @@ class CustomAPIDeviceComponent : public Component, public CustomAPIDevice { void on_service_with_arrays(std::vector bool_array, std::vector int_array, std::vector float_array, std::vector string_array); + + // Test Home Assistant state subscription with std::string API + // NOLINTNEXTLINE(performance-unnecessary-value-param) + void on_ha_state_changed(std::string entity_id, std::string state); }; } // namespace custom_api_device_component diff --git a/tests/integration/fixtures/fnv1_hash_object_id.yaml b/tests/integration/fixtures/fnv1_hash_object_id.yaml new file mode 100644 index 0000000000..2097b2fbf9 --- /dev/null +++ b/tests/integration/fixtures/fnv1_hash_object_id.yaml @@ -0,0 +1,76 @@ +esphome: + name: fnv1-hash-object-id-test + platformio_options: + build_flags: + - "-DDEBUG" + on_boot: + - lambda: |- + using esphome::fnv1_hash_object_id; + + // Test basic lowercase (hash matches Python fnv1_hash_object_id("foo")) + uint32_t hash_foo = fnv1_hash_object_id("foo", 3); + if (hash_foo == 0x408f5e13) { + ESP_LOGI("FNV1_OID", "foo PASSED"); + } else { + ESP_LOGE("FNV1_OID", "foo FAILED: 0x%08x != 0x408f5e13", hash_foo); + } + + // Test uppercase conversion (should match lowercase) + uint32_t hash_Foo = fnv1_hash_object_id("Foo", 3); + if (hash_Foo == 0x408f5e13) { + ESP_LOGI("FNV1_OID", "upper PASSED"); + } else { + ESP_LOGE("FNV1_OID", "upper FAILED: 0x%08x != 0x408f5e13", hash_Foo); + } + + // Test space to underscore conversion ("foo bar" -> "foo_bar") + uint32_t hash_space = fnv1_hash_object_id("foo bar", 7); + if (hash_space == 0x3ae35aa1) { + ESP_LOGI("FNV1_OID", "space PASSED"); + } else { + ESP_LOGE("FNV1_OID", "space FAILED: 0x%08x != 0x3ae35aa1", hash_space); + } + + // Test underscore preserved ("foo_bar") + uint32_t hash_underscore = fnv1_hash_object_id("foo_bar", 7); + if (hash_underscore == 0x3ae35aa1) { + ESP_LOGI("FNV1_OID", "underscore PASSED"); + } else { + ESP_LOGE("FNV1_OID", "underscore FAILED: 0x%08x != 0x3ae35aa1", hash_underscore); + } + + // Test hyphen preserved ("foo-bar") + uint32_t hash_hyphen = fnv1_hash_object_id("foo-bar", 7); + if (hash_hyphen == 0x438b12e3) { + ESP_LOGI("FNV1_OID", "hyphen PASSED"); + } else { + ESP_LOGE("FNV1_OID", "hyphen FAILED: 0x%08x != 0x438b12e3", hash_hyphen); + } + + // Test special chars become underscore ("foo!bar" -> "foo_bar") + uint32_t hash_special = fnv1_hash_object_id("foo!bar", 7); + if (hash_special == 0x3ae35aa1) { + ESP_LOGI("FNV1_OID", "special PASSED"); + } else { + ESP_LOGE("FNV1_OID", "special FAILED: 0x%08x != 0x3ae35aa1", hash_special); + } + + // Test complex name ("My Sensor Name" -> "my_sensor_name") + uint32_t hash_complex = fnv1_hash_object_id("My Sensor Name", 14); + if (hash_complex == 0x2760962a) { + ESP_LOGI("FNV1_OID", "complex PASSED"); + } else { + ESP_LOGE("FNV1_OID", "complex FAILED: 0x%08x != 0x2760962a", hash_complex); + } + + // Test empty string returns FNV1_OFFSET_BASIS + uint32_t hash_empty = fnv1_hash_object_id("", 0); + if (hash_empty == 0x811c9dc5) { + ESP_LOGI("FNV1_OID", "empty PASSED"); + } else { + ESP_LOGE("FNV1_OID", "empty FAILED: 0x%08x != 0x811c9dc5", hash_empty); + } + +host: +api: +logger: diff --git a/tests/integration/fixtures/fnv1a_hash.yaml b/tests/integration/fixtures/fnv1a_hash.yaml new file mode 100644 index 0000000000..d9c80601b8 --- /dev/null +++ b/tests/integration/fixtures/fnv1a_hash.yaml @@ -0,0 +1,60 @@ +esphome: + name: fnv1a-hash-test + platformio_options: + build_flags: + - "-DDEBUG" + on_boot: + - lambda: |- + using esphome::fnv1a_hash; + using esphome::fnv1a_hash_extend; + + // Test empty string (should return offset basis) + uint32_t hash_empty = fnv1a_hash(""); + if (hash_empty == 0x811c9dc5) { + ESP_LOGI("FNV1A", "empty PASSED"); + } else { + ESP_LOGE("FNV1A", "empty FAILED: 0x%08x != 0x811c9dc5", hash_empty); + } + + // Test known FNV-1a hashes + uint32_t hash_hello = fnv1a_hash("hello"); + if (hash_hello == 0x4f9f2cab) { + ESP_LOGI("FNV1A", "known_hello PASSED"); + } else { + ESP_LOGE("FNV1A", "known_hello FAILED: 0x%08x != 0x4f9f2cab", hash_hello); + } + + uint32_t hash_helloworld = fnv1a_hash("helloworld"); + if (hash_helloworld == 0x3b9f5c61) { + ESP_LOGI("FNV1A", "known_helloworld PASSED"); + } else { + ESP_LOGE("FNV1A", "known_helloworld FAILED: 0x%08x != 0x3b9f5c61", hash_helloworld); + } + + // Test fnv1a_hash_extend consistency + uint32_t hash1 = fnv1a_hash("hello"); + hash1 = fnv1a_hash_extend(hash1, "world"); + uint32_t hash2 = fnv1a_hash("helloworld"); + + if (hash1 == hash2) { + ESP_LOGI("FNV1A", "extend PASSED"); + } else { + ESP_LOGE("FNV1A", "extend FAILED: 0x%08x != 0x%08x", hash1, hash2); + } + + // Test with std::string + std::string str1 = "foo"; + std::string str2 = "bar"; + uint32_t hash3 = fnv1a_hash(str1); + hash3 = fnv1a_hash_extend(hash3, str2); + uint32_t hash4 = fnv1a_hash("foobar"); + + if (hash3 == hash4) { + ESP_LOGI("FNV1A", "string PASSED"); + } else { + ESP_LOGE("FNV1A", "string FAILED: 0x%08x != 0x%08x", hash3, hash4); + } + +host: +api: +logger: diff --git a/tests/integration/fixtures/host_logger_thread_safety.yaml b/tests/integration/fixtures/host_logger_thread_safety.yaml new file mode 100644 index 0000000000..e44a217b2b --- /dev/null +++ b/tests/integration/fixtures/host_logger_thread_safety.yaml @@ -0,0 +1,91 @@ +esphome: + name: host-logger-thread-test +host: +api: +logger: + +button: + - platform: template + name: "Start Thread Race Test" + id: start_test_button + on_press: + - lambda: |- + // Number of threads and messages per thread + static const int NUM_THREADS = 3; + static const int MESSAGES_PER_THREAD = 100; + + // Counters + static std::atomic total_messages_logged{0}; + + // Thread function - must be a regular function pointer for pthread + struct ThreadTest { + static void *thread_func(void *arg) { + int thread_id = *static_cast(arg); + + // Set thread name (different signatures on macOS vs Linux) + char thread_name[16]; + snprintf(thread_name, sizeof(thread_name), "LogThread%d", thread_id); + #ifdef __APPLE__ + pthread_setname_np(thread_name); + #else + pthread_setname_np(pthread_self(), thread_name); + #endif + + // Log messages with different log levels + for (int i = 0; i < MESSAGES_PER_THREAD; i++) { + switch (i % 4) { + case 0: + ESP_LOGI("thread_test", "THREAD%d_MSG%03d_INFO_MESSAGE_WITH_DATA_%08X", + thread_id, i, i * 12345); + break; + case 1: + ESP_LOGD("thread_test", "THREAD%d_MSG%03d_DEBUG_MESSAGE_WITH_DATA_%08X", + thread_id, i, i * 12345); + break; + case 2: + ESP_LOGW("thread_test", "THREAD%d_MSG%03d_WARN_MESSAGE_WITH_DATA_%08X", + thread_id, i, i * 12345); + break; + case 3: + ESP_LOGE("thread_test", "THREAD%d_MSG%03d_ERROR_MESSAGE_WITH_DATA_%08X", + thread_id, i, i * 12345); + break; + } + total_messages_logged.fetch_add(1, std::memory_order_relaxed); + + // Small busy loop to vary timing between threads + int delay_count = (thread_id + 1) * 10; + while (delay_count-- > 0) { + asm volatile("" ::: "memory"); // Prevent optimization + } + } + return nullptr; + } + }; + + ESP_LOGI("thread_test", "RACE_TEST_START: Starting %d threads with %d messages each", + NUM_THREADS, MESSAGES_PER_THREAD); + + // Reset counter for this test run + total_messages_logged.store(0, std::memory_order_relaxed); + + pthread_t threads[NUM_THREADS]; + int thread_ids[NUM_THREADS]; + + // Create all threads + for (int i = 0; i < NUM_THREADS; i++) { + thread_ids[i] = i; + int ret = pthread_create(&threads[i], nullptr, ThreadTest::thread_func, &thread_ids[i]); + if (ret != 0) { + ESP_LOGE("thread_test", "RACE_TEST_ERROR: Failed to create thread %d", i); + return; + } + } + + // Wait for all threads to complete + for (int i = 0; i < NUM_THREADS; i++) { + pthread_join(threads[i], nullptr); + } + + ESP_LOGI("thread_test", "RACE_TEST_COMPLETE: All threads finished, total messages: %d", + total_messages_logged.load(std::memory_order_relaxed)); diff --git a/tests/integration/fixtures/host_mode_api_password.yaml b/tests/integration/fixtures/host_mode_api_password.yaml deleted file mode 100644 index 038b6871e0..0000000000 --- a/tests/integration/fixtures/host_mode_api_password.yaml +++ /dev/null @@ -1,14 +0,0 @@ -esphome: - name: host-mode-api-password -host: -api: - password: "test_password_123" -logger: - level: DEBUG -# Test sensor to verify connection works -sensor: - - platform: template - name: Test Sensor - id: test_sensor - lambda: return 42.0; - update_interval: 0.1s diff --git a/tests/integration/fixtures/light_automations.yaml b/tests/integration/fixtures/light_automations.yaml new file mode 100644 index 0000000000..b5b88d95e7 --- /dev/null +++ b/tests/integration/fixtures/light_automations.yaml @@ -0,0 +1,26 @@ +esphome: + name: light-automations-test + +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +output: + - platform: template + id: test_output + type: binary + write_action: + - lambda: "" + +light: + - platform: binary + id: test_light + name: "Test Light" + output: test_output + on_turn_on: + - logger.log: "TRIGGER: on_turn_on fired" + on_turn_off: + - logger.log: "TRIGGER: on_turn_off fired" + on_state: + - logger.log: "TRIGGER: on_state fired" diff --git a/tests/integration/fixtures/lock_automations.yaml b/tests/integration/fixtures/lock_automations.yaml new file mode 100644 index 0000000000..fe11e656fa --- /dev/null +++ b/tests/integration/fixtures/lock_automations.yaml @@ -0,0 +1,17 @@ +esphome: + name: lock-automations-test + +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +lock: + - platform: template + id: test_lock + name: "Test Lock" + optimistic: true + on_lock: + - logger.log: "TRIGGER: on_lock fired" + on_unlock: + - logger.log: "TRIGGER: on_unlock fired" diff --git a/tests/integration/fixtures/object_id_api_verification.yaml b/tests/integration/fixtures/object_id_api_verification.yaml new file mode 100644 index 0000000000..386270fc2c --- /dev/null +++ b/tests/integration/fixtures/object_id_api_verification.yaml @@ -0,0 +1,125 @@ +esphome: + name: object-id-test + friendly_name: Test Device + # Enable MAC suffix - host MAC is 98:35:69:ab:f6:79, suffix is "abf679" + # friendly_name becomes "Test Device abf679" + name_add_mac_suffix: true + # Sub-devices for testing empty-name entities on devices + devices: + - id: sub_device_1 + name: Sub Device One + - id: sub_device_2 + name: Sub Device Two + +host: + +api: + +logger: + +sensor: + # Test 1: Basic name -> object_id = "temperature_sensor" + - platform: template + name: "Temperature Sensor" + id: sensor_basic + lambda: return 42.0; + update_interval: 60s + + # Test 2: Uppercase name -> object_id = "uppercase_name" + - platform: template + name: "UPPERCASE NAME" + id: sensor_uppercase + lambda: return 43.0; + update_interval: 60s + + # Test 3: Special characters -> object_id = "special__chars_" + - platform: template + name: "Special!@Chars#" + id: sensor_special + lambda: return 44.0; + update_interval: 60s + + # Test 4: Hyphen preserved -> object_id = "temp-sensor" + - platform: template + name: "Temp-Sensor" + id: sensor_hyphen + lambda: return 45.0; + update_interval: 60s + + # Test 5: Underscore preserved -> object_id = "temp_sensor" + - platform: template + name: "Temp_Sensor" + id: sensor_underscore + lambda: return 46.0; + update_interval: 60s + + # Test 6: Mixed case with spaces -> object_id = "living_room_temperature" + - platform: template + name: "Living Room Temperature" + id: sensor_mixed + lambda: return 47.0; + update_interval: 60s + + # Test 7: Empty name - uses friendly_name with MAC suffix + # friendly_name = "Test Device abf679" -> object_id = "test_device_abf679" + - platform: template + name: "" + id: sensor_empty_name + lambda: return 48.0; + update_interval: 60s + +binary_sensor: + # Test 8: Different platform same conversion rules + - platform: template + name: "Door Open" + id: binary_door + lambda: return true; + + # Test 9: Numbers in name -> object_id = "sensor_123" + - platform: template + name: "Sensor 123" + id: binary_numbers + lambda: return false; + +switch: + # Test 10: Long name with multiple spaces + - platform: template + name: "My Very Long Switch Name Here" + id: switch_long + lambda: return false; + turn_on_action: + - logger.log: "on" + turn_off_action: + - logger.log: "off" + +text_sensor: + # Test 11: Name starting with number (should work fine) + - platform: template + name: "123 Start" + id: text_num_start + lambda: return {"test"}; + update_interval: 60s + +button: + # Test 12: Named entity on sub-device -> object_id from entity name + - platform: template + name: "Device Button" + id: button_on_device + device_id: sub_device_1 + on_press: [] + + # Test 13: Empty name on sub-device -> object_id from device name + # Device name "Sub Device One" -> object_id = "sub_device_one" + - platform: template + name: "" + id: button_empty_on_device1 + device_id: sub_device_1 + on_press: [] + + # Test 14: Empty name on different sub-device + # Device name "Sub Device Two" -> object_id = "sub_device_two" + - platform: template + name: "" + id: button_empty_on_device2 + device_id: sub_device_2 + on_press: [] diff --git a/tests/integration/fixtures/object_id_friendly_name_no_mac_suffix.yaml b/tests/integration/fixtures/object_id_friendly_name_no_mac_suffix.yaml new file mode 100644 index 0000000000..7a86e37d08 --- /dev/null +++ b/tests/integration/fixtures/object_id_friendly_name_no_mac_suffix.yaml @@ -0,0 +1,27 @@ +esphome: + name: test-device + # friendly_name set but NO MAC suffix + # Empty-name entity should use friendly_name for object_id + friendly_name: My Friendly Device + +host: + +api: + +logger: + +sensor: + # Empty name entity - should use friendly_name for object_id + # friendly_name = "My Friendly Device" -> object_id = "my_friendly_device" + - platform: template + name: "" + id: sensor_empty_name + lambda: return 42.0; + update_interval: 60s + + # Named entity for comparison + - platform: template + name: "Temperature" + id: sensor_named + lambda: return 43.0; + update_interval: 60s diff --git a/tests/integration/fixtures/object_id_no_friendly_name_no_mac_suffix.yaml b/tests/integration/fixtures/object_id_no_friendly_name_no_mac_suffix.yaml new file mode 100644 index 0000000000..4a947e0f6a --- /dev/null +++ b/tests/integration/fixtures/object_id_no_friendly_name_no_mac_suffix.yaml @@ -0,0 +1,25 @@ +esphome: + name: test-device + # No friendly_name set, no MAC suffix + # OLD behavior: object_id = device name because Python pre-computed with fallback + +host: + +api: + +logger: + +sensor: + # Empty name entity - OLD behavior used device name as fallback + - platform: template + name: "" + id: sensor_empty_name + lambda: return 42.0; + update_interval: 60s + + # Named entity for comparison + - platform: template + name: "Temperature" + id: sensor_named + lambda: return 43.0; + update_interval: 60s diff --git a/tests/integration/fixtures/object_id_no_friendly_name_with_mac_suffix.yaml b/tests/integration/fixtures/object_id_no_friendly_name_with_mac_suffix.yaml new file mode 100644 index 0000000000..ab12e670a0 --- /dev/null +++ b/tests/integration/fixtures/object_id_no_friendly_name_with_mac_suffix.yaml @@ -0,0 +1,26 @@ +esphome: + name: test-device + # No friendly_name set, MAC suffix enabled + # OLD behavior: object_id = "" (empty) because is_object_id_dynamic_() used App.get_friendly_name() directly + name_add_mac_suffix: true + +host: + +api: + +logger: + +sensor: + # Empty name entity - OLD behavior produced empty object_id when MAC suffix enabled + - platform: template + name: "" + id: sensor_empty_name + lambda: return 42.0; + update_interval: 60s + + # Named entity for comparison + - platform: template + name: "Temperature" + id: sensor_named + lambda: return 43.0; + update_interval: 60s diff --git a/tests/integration/fixtures/script_delay_with_params.yaml b/tests/integration/fixtures/script_delay_with_params.yaml new file mode 100644 index 0000000000..2a0f16d9fe --- /dev/null +++ b/tests/integration/fixtures/script_delay_with_params.yaml @@ -0,0 +1,131 @@ +esphome: + name: test-script-delay-params + +host: + +api: + actions: + # Test case from issue #12044: parent script with repeat calling child with delay + - action: test_repeat_with_delay + then: + - logger.log: "=== TEST: Repeat loop calling script with delay and parameters ===" + - script.execute: father_script + + # Test case from issue #12043: script.wait with delayed child script + - action: test_script_wait + then: + - logger.log: "=== TEST: script.wait with delayed child script ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "After wait: script completed successfully" + + # Test: Delay with different parameter types + - action: test_delay_param_types + then: + - logger.log: "=== TEST: Delay with various parameter types ===" + - script.execute: + id: delay_with_int + val: 42 + - delay: 50ms + - script.execute: + id: delay_with_string + msg: "test message" + - delay: 50ms + - script.execute: + id: delay_with_float + num: 3.14 + +logger: + level: DEBUG + +script: + # Reproduces issue #12044: child script with conditional delay + - id: son_script + mode: single + parameters: + iteration: int + then: + - logger.log: + format: "Son script started with iteration %d" + args: ['iteration'] + - if: + condition: + lambda: 'return iteration >= 5;' + then: + - logger.log: + format: "Son script delaying for iteration %d" + args: ['iteration'] + - delay: 100ms + - logger.log: + format: "Son script finished with iteration %d" + args: ['iteration'] + + # Reproduces issue #12044: parent script with repeat loop + - id: father_script + mode: single + then: + - repeat: + count: 10 + then: + - logger.log: + format: "Father iteration %d: calling son" + args: ['iteration'] + - script.execute: + id: son_script + iteration: !lambda 'return iteration;' + - script.wait: son_script + - logger.log: + format: "Father iteration %d: son finished, wait returned" + args: ['iteration'] + + # Reproduces issue #12043: script.wait hangs + - id: show_start_page + mode: single + then: + - logger.log: "Start page: beginning" + - delay: 100ms + - logger.log: "Start page: after delay" + - delay: 100ms + - logger.log: "Start page: completed" + + # Test delay with int parameter + - id: delay_with_int + mode: single + parameters: + val: int + then: + - logger.log: + format: "Int test: before delay, val=%d" + args: ['val'] + - delay: 50ms + - logger.log: + format: "Int test: after delay, val=%d" + args: ['val'] + + # Test delay with string parameter + - id: delay_with_string + mode: single + parameters: + msg: string + then: + - logger.log: + format: "String test: before delay, msg=%s" + args: ['msg.c_str()'] + - delay: 50ms + - logger.log: + format: "String test: after delay, msg=%s" + args: ['msg.c_str()'] + + # Test delay with float parameter + - id: delay_with_float + mode: single + parameters: + num: float + then: + - logger.log: + format: "Float test: before delay, num=%.2f" + args: ['num'] + - delay: 50ms + - logger.log: + format: "Float test: after delay, num=%.2f" + args: ['num'] diff --git a/tests/integration/fixtures/script_wait_on_boot.yaml b/tests/integration/fixtures/script_wait_on_boot.yaml new file mode 100644 index 0000000000..8736b02294 --- /dev/null +++ b/tests/integration/fixtures/script_wait_on_boot.yaml @@ -0,0 +1,54 @@ +esphome: + name: test-script-wait-on-boot + on_boot: + # Use default priority (600.0) which is same as ScriptWaitAction's setup priority + # This tests the race condition where on_boot runs before ScriptWaitAction::setup() + then: + - logger.log: "=== on_boot: Starting boot sequence ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "=== on_boot: First script completed, starting second ===" + - script.execute: flip_thru_pages + - script.wait: flip_thru_pages + - logger.log: "=== on_boot: All boot scripts completed successfully ===" + +host: + +api: + actions: + # Manual trigger for additional testing + - action: test_script_wait + then: + - logger.log: "=== Manual test: Starting ===" + - script.execute: show_start_page + - script.wait: show_start_page + - logger.log: "=== Manual test: First script completed ===" + - script.execute: flip_thru_pages + - script.wait: flip_thru_pages + - logger.log: "=== Manual test: All completed ===" + +logger: + level: DEBUG + +script: + # First script - simulates display initialization + - id: show_start_page + mode: single + then: + - logger.log: "show_start_page: Starting" + - delay: 100ms + - logger.log: "show_start_page: After delay 1" + - delay: 100ms + - logger.log: "show_start_page: Completed" + + # Second script - simulates page flip sequence + - id: flip_thru_pages + mode: single + then: + - logger.log: "flip_thru_pages: Starting" + - delay: 50ms + - logger.log: "flip_thru_pages: Page 1" + - delay: 50ms + - logger.log: "flip_thru_pages: Page 2" + - delay: 50ms + - logger.log: "flip_thru_pages: Completed" diff --git a/tests/integration/fixtures/strftime_to.yaml b/tests/integration/fixtures/strftime_to.yaml new file mode 100644 index 0000000000..bd157e110c --- /dev/null +++ b/tests/integration/fixtures/strftime_to.yaml @@ -0,0 +1,53 @@ +esphome: + name: strftime-to-test +host: +api: +logger: + +time: + - platform: homeassistant + id: ha_time + +text_sensor: + # Test strftime_to with a valid format + - platform: template + name: "Time Format Test" + id: time_format_test + update_interval: 100ms + lambda: |- + auto now = ESPTime::from_epoch_local(1704067200); // 2024-01-01 00:00:00 UTC + char buf[ESPTime::STRFTIME_BUFFER_SIZE]; + size_t len = now.strftime_to(buf, "%Y-%m-%d %H:%M:%S"); + return std::string(buf, len); + + # Test strftime_to with a short format + - platform: template + name: "Time Short Format" + id: time_short_format + update_interval: 100ms + lambda: |- + auto now = ESPTime::from_epoch_local(1704067200); + char buf[ESPTime::STRFTIME_BUFFER_SIZE]; + size_t len = now.strftime_to(buf, "%H:%M"); + return std::string(buf, len); + + # Test strftime (std::string version) still works + - platform: template + name: "Time String Format" + id: time_string_format + update_interval: 100ms + lambda: |- + auto now = ESPTime::from_epoch_local(1704067200); + return now.strftime("%Y-%m-%d"); + + # Test strftime_to with empty/invalid format returns ERROR + - platform: template + name: "Time Error Format" + id: time_error_format + update_interval: 100ms + lambda: |- + auto now = ESPTime::from_epoch_local(1704067200); + char buf[ESPTime::STRFTIME_BUFFER_SIZE]; + // Empty format string causes strftime to return 0 + size_t len = now.strftime_to(buf, ""); + return std::string(buf, len); diff --git a/tests/integration/fixtures/syslog.yaml b/tests/integration/fixtures/syslog.yaml new file mode 100644 index 0000000000..df376087e3 --- /dev/null +++ b/tests/integration/fixtures/syslog.yaml @@ -0,0 +1,43 @@ +esphome: + name: syslog-test + +host: + +api: + services: + - service: log_long_message + then: + - lambda: |- + // Log a message that exceeds 508 bytes to test truncation + ESP_LOGI("trunctest", "START|%s|END", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" + "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE" + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + - service: log_short_message + then: + - lambda: |- + // Log a short message that should arrive complete (not truncated) + ESP_LOGI("shorttest", "BEGIN|SHORT_MESSAGE_CONTENT|FINISH"); + +logger: + level: DEBUG + +time: + - platform: host + id: host_time + +udp: + - id: syslog_udp + addresses: + - "127.0.0.1" + +syslog: + udp_id: syslog_udp + time_id: host_time + port: SYSLOG_PORT_PLACEHOLDER + level: DEBUG + strip: true + facility: 16 diff --git a/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml b/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml new file mode 100644 index 0000000000..836d3f11d5 --- /dev/null +++ b/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml @@ -0,0 +1,136 @@ +esphome: + name: template-alarm-many-sensors + friendly_name: "Template Alarm Control Panel with Many Sensors" + +logger: + +host: + +api: + +binary_sensor: + - platform: template + id: sensor1 + name: "Door 1" + - platform: template + id: sensor2 + name: "Door 2" + - platform: template + id: sensor3 + name: "Window 1" + - platform: template + id: sensor4 + name: "Window 2" + - platform: template + id: sensor5 + name: "Motion 1" + - platform: template + id: sensor6 + name: "Motion 2" + - platform: template + id: sensor7 + name: "Glass Break 1" + - platform: template + id: sensor8 + name: "Glass Break 2" + - platform: template + id: sensor9 + name: "Smoke Detector" + - platform: template + id: sensor10 + name: "CO Detector" + +alarm_control_panel: + - platform: template + id: test_alarm + name: "Test Alarm" + codes: + - "1234" + requires_code_to_arm: true + arming_away_time: 5s + arming_home_time: 3s + arming_night_time: 3s + pending_time: 10s + trigger_time: 300s + restore_mode: ALWAYS_DISARMED + binary_sensors: + - input: sensor1 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: true + chime: true + trigger_mode: DELAYED + - input: sensor2 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: true + chime: true + trigger_mode: DELAYED + - input: sensor3 + bypass_armed_home: true + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: DELAYED + - input: sensor4 + bypass_armed_home: true + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: DELAYED + - input: sensor5 + bypass_armed_home: false + bypass_armed_night: true + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor6 + bypass_armed_home: false + bypass_armed_night: true + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor7 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor8 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor9 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT_ALWAYS + - input: sensor10 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT_ALWAYS + on_disarmed: + - logger.log: "Alarm disarmed" + on_arming: + - logger.log: "Alarm arming" + on_armed_away: + - logger.log: "Alarm armed away" + on_armed_home: + - logger.log: "Alarm armed home" + on_armed_night: + - logger.log: "Alarm armed night" + on_pending: + - logger.log: "Alarm pending" + on_triggered: + - logger.log: "Alarm triggered" + on_cleared: + - logger.log: "Alarm cleared" + on_chime: + - logger.log: "Chime activated" + on_ready: + - logger.log: "Sensors ready state changed" diff --git a/tests/integration/fixtures/text_command.yaml b/tests/integration/fixtures/text_command.yaml new file mode 100644 index 0000000000..cc91e2f792 --- /dev/null +++ b/tests/integration/fixtures/text_command.yaml @@ -0,0 +1,37 @@ +esphome: + name: host-text-command-test + +host: + +api: + batch_delay: 0ms + +logger: + +text: + - platform: template + name: "Test Text" + id: test_text + optimistic: true + min_length: 0 + max_length: 255 + mode: text + initial_value: "initial" + + - platform: template + name: "Test Password" + id: test_password + optimistic: true + min_length: 4 + max_length: 32 + mode: password + initial_value: "secret" + + - platform: template + name: "Test Text Long" + id: test_text_long + optimistic: true + min_length: 0 + max_length: 255 + mode: text + initial_value: "" diff --git a/tests/integration/fixtures/text_sensor_raw_state.yaml b/tests/integration/fixtures/text_sensor_raw_state.yaml new file mode 100644 index 0000000000..54ab2e8dcc --- /dev/null +++ b/tests/integration/fixtures/text_sensor_raw_state.yaml @@ -0,0 +1,181 @@ +esphome: + name: test-text-sensor-raw-state + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Text sensor WITHOUT filters - get_raw_state() should return same as state +text_sensor: + - platform: template + name: "No Filter Sensor" + id: no_filter_sensor + + # Text sensor WITH filter - get_raw_state() should return original value + - platform: template + name: "With Filter Sensor" + id: with_filter_sensor + filters: + - to_upper + + # StringRef-based filters (append, prepend, substitute, map) + - platform: template + name: "Append Sensor" + id: append_sensor + filters: + - append: " suffix" + + - platform: template + name: "Prepend Sensor" + id: prepend_sensor + filters: + - prepend: "prefix " + + - platform: template + name: "Substitute Sensor" + id: substitute_sensor + filters: + - substitute: + - foo -> bar + - hello -> world + + - platform: template + name: "Map Sensor" + id: map_sensor + filters: + - map: + - ON -> Active + - OFF -> Inactive + + - platform: template + name: "Chained Sensor" + id: chained_sensor + filters: + - prepend: "[" + - append: "]" + +# Button to publish values and log raw_state vs state +button: + - platform: template + name: "Test No Filter Button" + id: test_no_filter_button + on_press: + - text_sensor.template.publish: + id: no_filter_sensor + state: "hello world" + - delay: 50ms + # Log both state and get_raw_state() to verify they match + - logger.log: + format: "NO_FILTER: state='%s' raw_state='%s'" + args: + - id(no_filter_sensor).state.c_str() + - id(no_filter_sensor).get_raw_state().c_str() + + - platform: template + name: "Test With Filter Button" + id: test_with_filter_button + on_press: + - text_sensor.template.publish: + id: with_filter_sensor + state: "hello world" + - delay: 50ms + # Log both state and get_raw_state() to verify filter works + # state should be "HELLO WORLD" (filtered), raw_state should be "hello world" (original) + - logger.log: + format: "WITH_FILTER: state='%s' raw_state='%s'" + args: + - id(with_filter_sensor).state.c_str() + - id(with_filter_sensor).get_raw_state().c_str() + + - platform: template + name: "Test Append Button" + id: test_append_button + on_press: + - text_sensor.template.publish: + id: append_sensor + state: "test" + - delay: 50ms + - logger.log: + format: "APPEND: state='%s'" + args: + - id(append_sensor).state.c_str() + + - platform: template + name: "Test Prepend Button" + id: test_prepend_button + on_press: + - text_sensor.template.publish: + id: prepend_sensor + state: "test" + - delay: 50ms + - logger.log: + format: "PREPEND: state='%s'" + args: + - id(prepend_sensor).state.c_str() + + - platform: template + name: "Test Substitute Button" + id: test_substitute_button + on_press: + - text_sensor.template.publish: + id: substitute_sensor + state: "foo says hello" + - delay: 50ms + - logger.log: + format: "SUBSTITUTE: state='%s'" + args: + - id(substitute_sensor).state.c_str() + + - platform: template + name: "Test Map ON Button" + id: test_map_on_button + on_press: + - text_sensor.template.publish: + id: map_sensor + state: "ON" + - delay: 50ms + - logger.log: + format: "MAP_ON: state='%s'" + args: + - id(map_sensor).state.c_str() + + - platform: template + name: "Test Map OFF Button" + id: test_map_off_button + on_press: + - text_sensor.template.publish: + id: map_sensor + state: "OFF" + - delay: 50ms + - logger.log: + format: "MAP_OFF: state='%s'" + args: + - id(map_sensor).state.c_str() + + - platform: template + name: "Test Map Unknown Button" + id: test_map_unknown_button + on_press: + - text_sensor.template.publish: + id: map_sensor + state: "UNKNOWN" + - delay: 50ms + - logger.log: + format: "MAP_UNKNOWN: state='%s'" + args: + - id(map_sensor).state.c_str() + + - platform: template + name: "Test Chained Button" + id: test_chained_button + on_press: + - text_sensor.template.publish: + id: chained_sensor + state: "value" + - delay: 50ms + - logger.log: + format: "CHAINED: state='%s'" + args: + - id(chained_sensor).state.c_str() diff --git a/tests/integration/fixtures/wait_until_fifo_ordering.yaml b/tests/integration/fixtures/wait_until_fifo_ordering.yaml new file mode 100644 index 0000000000..5dd60c8755 --- /dev/null +++ b/tests/integration/fixtures/wait_until_fifo_ordering.yaml @@ -0,0 +1,82 @@ +esphome: + name: test-wait-until-ordering + +host: + +api: + actions: + - action: test_wait_until_fifo + then: + - logger.log: "=== TEST: wait_until should execute in FIFO order ===" + - globals.set: + id: gate_open + value: 'false' + - delay: 100ms + # Start multiple parallel executions of coordinator script + # Each will call the shared waiter script, queueing in same wait_until + - script.execute: coordinator_0 + - script.execute: coordinator_1 + - script.execute: coordinator_2 + - script.execute: coordinator_3 + - script.execute: coordinator_4 + # Give scripts time to reach wait_until and queue + - delay: 200ms + - logger.log: "Opening gate - all wait_until should complete now" + - globals.set: + id: gate_open + value: 'true' + - delay: 500ms + - logger.log: "Test complete" + +globals: + - id: gate_open + type: bool + initial_value: 'false' + +script: + # Shared waiter with single wait_until action (all coordinators call this) + - id: waiter + mode: parallel + parameters: + iter: int + then: + - lambda: 'ESP_LOGD("main", "Queueing iteration %d", iter);' + - wait_until: + condition: + lambda: 'return id(gate_open);' + timeout: 5s + - lambda: 'ESP_LOGD("main", "Completed iteration %d", iter);' + + # Coordinator scripts - each calls shared waiter with different iteration number + - id: coordinator_0 + then: + - script.execute: + id: waiter + iter: 0 + + - id: coordinator_1 + then: + - script.execute: + id: waiter + iter: 1 + + - id: coordinator_2 + then: + - script.execute: + id: waiter + iter: 2 + + - id: coordinator_3 + then: + - script.execute: + id: waiter + iter: 3 + + - id: coordinator_4 + then: + - script.execute: + id: waiter + iter: 4 + +logger: + level: DEBUG diff --git a/tests/integration/fixtures/water_heater_template.yaml b/tests/integration/fixtures/water_heater_template.yaml new file mode 100644 index 0000000000..b54ebed789 --- /dev/null +++ b/tests/integration/fixtures/water_heater_template.yaml @@ -0,0 +1,23 @@ +esphome: + name: water-heater-template-test +host: +api: +logger: + +water_heater: + - platform: template + id: test_boiler + name: Test Boiler + optimistic: true + current_temperature: !lambda "return 45.0f;" + # Note: No mode lambda - we want optimistic mode changes to stick + # A mode lambda would override mode changes in loop() + supported_modes: + - "off" + - eco + - gas + - performance + visual: + min_temperature: 30.0 + max_temperature: 85.0 + target_temperature_step: 0.5 diff --git a/tests/integration/state_utils.py b/tests/integration/state_utils.py index 6434a41ddf..b649056f2b 100644 --- a/tests/integration/state_utils.py +++ b/tests/integration/state_utils.py @@ -4,11 +4,74 @@ from __future__ import annotations import asyncio import logging +from typing import TypeVar from aioesphomeapi import ButtonInfo, EntityInfo, EntityState _LOGGER = logging.getLogger(__name__) +T = TypeVar("T", bound=EntityInfo) + + +def find_entity( + entities: list[EntityInfo], + object_id_substring: str, + entity_type: type[T] | None = None, +) -> T | EntityInfo | None: + """Find an entity by object_id substring and optionally by type. + + Args: + entities: List of entity info objects from the API + object_id_substring: Substring to search for in object_id (case-insensitive) + entity_type: Optional entity type to filter by (e.g., BinarySensorInfo) + + Returns: + The first matching entity, or None if not found + + Example: + binary_sensor = find_entity(entities, "test_binary_sensor", BinarySensorInfo) + button = find_entity(entities, "set_true") # Any entity type + """ + substring_lower = object_id_substring.lower() + for entity in entities: + if substring_lower in entity.object_id.lower() and ( + entity_type is None or isinstance(entity, entity_type) + ): + return entity + return None + + +def require_entity( + entities: list[EntityInfo], + object_id_substring: str, + entity_type: type[T] | None = None, + description: str | None = None, +) -> T | EntityInfo: + """Find an entity or raise AssertionError if not found. + + Args: + entities: List of entity info objects from the API + object_id_substring: Substring to search for in object_id (case-insensitive) + entity_type: Optional entity type to filter by (e.g., BinarySensorInfo) + description: Human-readable description for error message + + Returns: + The first matching entity + + Raises: + AssertionError: If no matching entity is found + + Example: + binary_sensor = require_entity(entities, "test_sensor", BinarySensorInfo) + button = require_entity(entities, "set_true", description="Set True button") + """ + entity = find_entity(entities, object_id_substring, entity_type) + if entity is None: + desc = description or f"entity with '{object_id_substring}' in object_id" + type_info = f" of type {entity_type.__name__}" if entity_type else "" + raise AssertionError(f"{desc}{type_info} not found in entities") + return entity + def build_key_to_entity_mapping( entities: list[EntityInfo], entity_names: list[str] diff --git a/tests/integration/test_alarm_control_panel_state_transitions.py b/tests/integration/test_alarm_control_panel_state_transitions.py new file mode 100644 index 0000000000..09348f5bea --- /dev/null +++ b/tests/integration/test_alarm_control_panel_state_transitions.py @@ -0,0 +1,335 @@ +"""Integration test for alarm control panel state transitions.""" + +from __future__ import annotations + +import asyncio +import re + +import aioesphomeapi +from aioesphomeapi import ( + AlarmControlPanelCommand, + AlarmControlPanelEntityState, + AlarmControlPanelInfo, + AlarmControlPanelState, + SwitchInfo, +) +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_alarm_control_panel_state_transitions( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test alarm control panel state transitions. + + This comprehensive test verifies all state transitions and listener callbacks: + + 1. Basic arm/disarm sequences: + - DISARMED -> ARMING -> ARMED_AWAY -> DISARMED + - DISARMED -> ARMING -> ARMED_HOME -> DISARMED + - DISARMED -> ARMING -> ARMED_NIGHT -> DISARMED + + 2. Wrong code rejection + + 3. Sensor triggering while armed: + - ARMED_AWAY -> PENDING -> TRIGGERED (delayed sensor) + - TRIGGERED -> ARMED_AWAY (auto-reset after trigger_time, fires on_cleared) + + 4. Chime functionality: + - Sensor open while DISARMED triggers on_chime + + 5. Ready state: + - Sensor state changes trigger on_ready + """ + loop = asyncio.get_running_loop() + + # Track log messages for callback verification + log_lines: list[str] = [] + chime_future: asyncio.Future[bool] = loop.create_future() + ready_futures: list[asyncio.Future[bool]] = [] + cleared_future: asyncio.Future[bool] = loop.create_future() + + # Patterns to match log output from callbacks + chime_pattern = re.compile(r"Chime activated") + ready_pattern = re.compile(r"Sensors ready state changed") + cleared_pattern = re.compile(r"Alarm cleared") + + def on_log_line(line: str) -> None: + log_lines.append(line) + if not chime_future.done() and chime_pattern.search(line): + chime_future.set_result(True) + if ready_pattern.search(line): + # Create new future for each ready event + for fut in ready_futures: + if not fut.done(): + fut.set_result(True) + break + if not cleared_future.done() and cleared_pattern.search(line): + cleared_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + # Find entities + alarm_info: AlarmControlPanelInfo | None = None + door_switch_info: SwitchInfo | None = None + chime_switch_info: SwitchInfo | None = None + + for entity in entities: + if isinstance(entity, AlarmControlPanelInfo): + alarm_info = entity + elif isinstance(entity, SwitchInfo): + if entity.name == "Door Sensor Switch": + door_switch_info = entity + elif entity.name == "Chime Sensor Switch": + chime_switch_info = entity + + assert alarm_info is not None, "Alarm control panel not found" + assert door_switch_info is not None, "Door sensor switch not found" + assert chime_switch_info is not None, "Chime sensor switch not found" + + # Track state changes + states_received: list[AlarmControlPanelState] = [] + state_event = asyncio.Event() + + def on_state(state: aioesphomeapi.EntityState) -> None: + if ( + isinstance(state, AlarmControlPanelEntityState) + and state.key == alarm_info.key + ): + states_received.append(state.state) + state_event.set() + + # Use InitialStateHelper to handle initial state broadcast + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for initial states from all entities + await initial_state_helper.wait_for_initial_states() + + # Verify alarm panel started in DISARMED state + initial_alarm_state = initial_state_helper.initial_states.get(alarm_info.key) + assert initial_alarm_state is not None, "No initial alarm state received" + assert isinstance(initial_alarm_state, AlarmControlPanelEntityState) + assert initial_alarm_state.state == AlarmControlPanelState.DISARMED + + # Helper to wait for specific state + async def wait_for_state( + expected: AlarmControlPanelState, timeout: float = 5.0 + ) -> None: + deadline = loop.time() + timeout + while True: + remaining = deadline - loop.time() + if remaining <= 0: + raise TimeoutError( + f"Timeout waiting for state {expected}, " + f"last state: {states_received[-1] if states_received else 'none'}" + ) + await asyncio.wait_for(state_event.wait(), timeout=remaining) + state_event.clear() + if states_received[-1] == expected: + return + + # ===== Test wrong code rejection ===== + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.ARM_AWAY, + code="0000", # Wrong code + ) + + # Should NOT transition - wait a bit and verify no state changes + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(state_event.wait(), timeout=0.5) + # No state changes should have occurred (list is empty) + assert len(states_received) == 0, f"Unexpected state changes: {states_received}" + + # ===== Test ARM_AWAY sequence ===== + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.ARM_AWAY, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.ARMING) + await wait_for_state(AlarmControlPanelState.ARMED_AWAY) + + # Disarm + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.DISARM, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.DISARMED) + + # ===== Test ARM_HOME sequence ===== + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.ARM_HOME, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.ARMING) + await wait_for_state(AlarmControlPanelState.ARMED_HOME) + + # Disarm + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.DISARM, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.DISARMED) + + # ===== Test ARM_NIGHT sequence ===== + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.ARM_NIGHT, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.ARMING) + await wait_for_state(AlarmControlPanelState.ARMED_NIGHT) + + # Disarm + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.DISARM, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.DISARMED) + + # Verify basic state sequence (initial DISARMED is handled by InitialStateHelper) + expected_states = [ + AlarmControlPanelState.ARMING, # Arm away + AlarmControlPanelState.ARMED_AWAY, + AlarmControlPanelState.DISARMED, + AlarmControlPanelState.ARMING, # Arm home + AlarmControlPanelState.ARMED_HOME, + AlarmControlPanelState.DISARMED, + AlarmControlPanelState.ARMING, # Arm night + AlarmControlPanelState.ARMED_NIGHT, + AlarmControlPanelState.DISARMED, + ] + assert states_received == expected_states, ( + f"State sequence mismatch.\nExpected: {expected_states}\n" + f"Got: {states_received}" + ) + + # ===== Test PENDING -> TRIGGERED -> CLEARED sequence ===== + # This tests on_pending, on_triggered, and on_cleared callbacks + + # Arm away first + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.ARM_AWAY, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.ARMING) + await wait_for_state(AlarmControlPanelState.ARMED_AWAY) + + # Trip the door sensor (delayed mode triggers PENDING first) + client.switch_command(door_switch_info.key, True) + + # Should go to PENDING (delayed sensor) + await wait_for_state(AlarmControlPanelState.PENDING) + + # Should go to TRIGGERED after pending_time (50ms) + await wait_for_state(AlarmControlPanelState.TRIGGERED) + + # Close the sensor + client.switch_command(door_switch_info.key, False) + + # Wait for trigger_time to expire and auto-reset (100ms) + # The alarm should go back to ARMED_AWAY after trigger_time + # This transition FROM TRIGGERED fires on_cleared + await wait_for_state(AlarmControlPanelState.ARMED_AWAY, timeout=2.0) + + # Verify on_cleared was logged + try: + await asyncio.wait_for(cleared_future, timeout=1.0) + except TimeoutError: + pytest.fail(f"on_cleared callback not fired. Log lines: {log_lines[-20:]}") + + # Disarm + client.alarm_control_panel_command( + alarm_info.key, + AlarmControlPanelCommand.DISARM, + code="1234", + ) + await wait_for_state(AlarmControlPanelState.DISARMED) + + # Verify trigger sequence was added + assert AlarmControlPanelState.PENDING in states_received + assert AlarmControlPanelState.TRIGGERED in states_received + + # ===== Test chime (sensor open while disarmed) ===== + # The chime_sensor has chime: true, so opening it while disarmed + # should trigger on_chime callback + + # We're currently DISARMED - open the chime sensor + client.switch_command(chime_switch_info.key, True) + + # Wait for chime callback to be logged + try: + await asyncio.wait_for(chime_future, timeout=2.0) + except TimeoutError: + pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}") + + # Close the chime sensor and wait for alarm to become ready again + # We need to wait for this transition before testing door sensor, + # otherwise there's a race where the door sensor state change could + # arrive before the chime sensor state change, leaving the alarm in + # a continuous "not ready" state with no on_ready callback fired. + ready_after_chime_close: asyncio.Future[bool] = loop.create_future() + ready_futures.append(ready_after_chime_close) + + client.switch_command(chime_switch_info.key, False) + + # Wait for alarm to become ready again (chime sensor closed) + try: + await asyncio.wait_for(ready_after_chime_close, timeout=2.0) + except TimeoutError: + pytest.fail( + f"on_ready callback not fired when chime sensor closed. " + f"Log lines: {log_lines[-20:]}" + ) + + # ===== Test ready state changes ===== + # Now the alarm is confirmed ready. Opening/closing door sensor + # should trigger on_ready callbacks. + + # Set up futures for door sensor state changes + ready_future_1: asyncio.Future[bool] = loop.create_future() + ready_future_2: asyncio.Future[bool] = loop.create_future() + ready_futures.extend([ready_future_1, ready_future_2]) + + # Open door sensor (makes alarm not ready) + client.switch_command(door_switch_info.key, True) + + # Wait for first on_ready callback (not ready) + try: + await asyncio.wait_for(ready_future_1, timeout=2.0) + except TimeoutError: + pytest.fail( + f"on_ready callback not fired when sensor opened. " + f"Log lines: {log_lines[-20:]}" + ) + + # Close door sensor (makes alarm ready again) + client.switch_command(door_switch_info.key, False) + + # Wait for second on_ready callback (ready) + try: + await asyncio.wait_for(ready_future_2, timeout=2.0) + except TimeoutError: + pytest.fail( + f"on_ready callback not fired when sensor closed. " + f"Log lines: {log_lines[-20:]}" + ) + + # Final state should still be DISARMED + assert states_received[-1] == AlarmControlPanelState.DISARMED diff --git a/tests/integration/test_api_action_responses.py b/tests/integration/test_api_action_responses.py new file mode 100644 index 0000000000..d441a231aa --- /dev/null +++ b/tests/integration/test_api_action_responses.py @@ -0,0 +1,258 @@ +"""Integration test for API action responses feature. + +Tests the supports_response modes: none, status, optional, only. +""" + +from __future__ import annotations + +import asyncio +import json +import re + +from aioesphomeapi import SupportsResponseType, UserService, UserServiceArgType +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_action_responses( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test API action response modes work correctly.""" + loop = asyncio.get_running_loop() + + # Track log messages for each action type + no_response_future = loop.create_future() + status_success_future = loop.create_future() + status_error_future = loop.create_future() + optional_response_future = loop.create_future() + only_response_future = loop.create_future() + nested_json_future = loop.create_future() + + # Patterns to match in logs + no_response_pattern = re.compile(r"ACTION_NO_RESPONSE called with: test_message") + status_success_pattern = re.compile( + r"ACTION_STATUS_RESPONSE success \(call_id=\d+\)" + ) + status_error_pattern = re.compile(r"ACTION_STATUS_RESPONSE error \(call_id=\d+\)") + optional_response_pattern = re.compile( + r"ACTION_OPTIONAL_RESPONSE \(call_id=\d+, return_response=\d+, value=42\)" + ) + only_response_pattern = re.compile( + r"ACTION_ONLY_RESPONSE \(call_id=\d+, name=World\)" + ) + nested_json_pattern = re.compile(r"ACTION_NESTED_JSON \(call_id=\d+\)") + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if not no_response_future.done() and no_response_pattern.search(line): + no_response_future.set_result(True) + elif not status_success_future.done() and status_success_pattern.search(line): + status_success_future.set_result(True) + elif not status_error_future.done() and status_error_pattern.search(line): + status_error_future.set_result(True) + elif not optional_response_future.done() and optional_response_pattern.search( + line + ): + optional_response_future.set_result(True) + elif not only_response_future.done() and only_response_pattern.search(line): + only_response_future.set_result(True) + elif not nested_json_future.done() and nested_json_pattern.search(line): + nested_json_future.set_result(True) + + # Run with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "api-action-responses-test" + + # List services + _, services = await client.list_entities_services() + + # Should have 5 services + assert len(services) == 5, f"Expected 5 services, found {len(services)}" + + # Find our services + action_no_response: UserService | None = None + action_status_response: UserService | None = None + action_optional_response: UserService | None = None + action_only_response: UserService | None = None + action_nested_json: UserService | None = None + + for service in services: + if service.name == "action_no_response": + action_no_response = service + elif service.name == "action_status_response": + action_status_response = service + elif service.name == "action_optional_response": + action_optional_response = service + elif service.name == "action_only_response": + action_only_response = service + elif service.name == "action_nested_json": + action_nested_json = service + + assert action_no_response is not None, "action_no_response not found" + assert action_status_response is not None, "action_status_response not found" + assert action_optional_response is not None, ( + "action_optional_response not found" + ) + assert action_only_response is not None, "action_only_response not found" + assert action_nested_json is not None, "action_nested_json not found" + + # Verify supports_response modes + assert action_no_response.supports_response is None or ( + action_no_response.supports_response == SupportsResponseType.NONE + ), ( + f"action_no_response should have supports_response=NONE, got {action_no_response.supports_response}" + ) + + assert ( + action_status_response.supports_response == SupportsResponseType.STATUS + ), ( + f"action_status_response should have supports_response=STATUS, " + f"got {action_status_response.supports_response}" + ) + + assert ( + action_optional_response.supports_response == SupportsResponseType.OPTIONAL + ), ( + f"action_optional_response should have supports_response=OPTIONAL, " + f"got {action_optional_response.supports_response}" + ) + + assert action_only_response.supports_response == SupportsResponseType.ONLY, ( + f"action_only_response should have supports_response=ONLY, " + f"got {action_only_response.supports_response}" + ) + + assert action_nested_json.supports_response == SupportsResponseType.ONLY, ( + f"action_nested_json should have supports_response=ONLY, " + f"got {action_nested_json.supports_response}" + ) + + # Verify argument types + # action_no_response: string message + assert len(action_no_response.args) == 1 + assert action_no_response.args[0].name == "message" + assert action_no_response.args[0].type == UserServiceArgType.STRING + + # action_status_response: bool should_succeed + assert len(action_status_response.args) == 1 + assert action_status_response.args[0].name == "should_succeed" + assert action_status_response.args[0].type == UserServiceArgType.BOOL + + # action_optional_response: int value + assert len(action_optional_response.args) == 1 + assert action_optional_response.args[0].name == "value" + assert action_optional_response.args[0].type == UserServiceArgType.INT + + # action_only_response: string name + assert len(action_only_response.args) == 1 + assert action_only_response.args[0].name == "name" + assert action_only_response.args[0].type == UserServiceArgType.STRING + + # action_nested_json: no args + assert len(action_nested_json.args) == 0 + + # Test action_no_response (supports_response: none) + # No response expected for this action + response = await client.execute_service( + action_no_response, {"message": "test_message"} + ) + assert response is None, "action_no_response should not return a response" + await asyncio.wait_for(no_response_future, timeout=5.0) + + # Test action_status_response with success (supports_response: status) + response = await client.execute_service( + action_status_response, + {"should_succeed": True}, + return_response=True, + ) + await asyncio.wait_for(status_success_future, timeout=5.0) + assert response is not None, "Expected response for status action" + assert response.success is True, ( + f"Expected success=True, got {response.success}" + ) + assert response.error_message == "", ( + f"Expected empty error_message, got '{response.error_message}'" + ) + + # Test action_status_response with error + response = await client.execute_service( + action_status_response, + {"should_succeed": False}, + return_response=True, + ) + await asyncio.wait_for(status_error_future, timeout=5.0) + assert response is not None, "Expected response for status action" + assert response.success is False, ( + f"Expected success=False, got {response.success}" + ) + assert "Intentional failure" in response.error_message, ( + f"Expected error message containing 'Intentional failure', " + f"got '{response.error_message}'" + ) + + # Test action_optional_response (supports_response: optional) + response = await client.execute_service( + action_optional_response, + {"value": 42}, + return_response=True, + ) + await asyncio.wait_for(optional_response_future, timeout=5.0) + assert response is not None, "Expected response for optional action" + assert response.success is True, ( + f"Expected success=True, got {response.success}" + ) + # Parse response data as JSON + response_json = json.loads(response.response_data.decode("utf-8")) + assert response_json["input"] == 42, ( + f"Expected input=42, got {response_json.get('input')}" + ) + assert response_json["doubled"] == 84, ( + f"Expected doubled=84, got {response_json.get('doubled')}" + ) + + # Test action_only_response (supports_response: only) + response = await client.execute_service( + action_only_response, + {"name": "World"}, + return_response=True, + ) + await asyncio.wait_for(only_response_future, timeout=5.0) + assert response is not None, "Expected response for only action" + assert response.success is True, ( + f"Expected success=True, got {response.success}" + ) + response_json = json.loads(response.response_data.decode("utf-8")) + assert response_json["greeting"] == "Hello, World!", ( + f"Expected greeting='Hello, World!', got {response_json.get('greeting')}" + ) + assert response_json["length"] == 5, ( + f"Expected length=5, got {response_json.get('length')}" + ) + + # Test action_nested_json + response = await client.execute_service( + action_nested_json, + {}, + return_response=True, + ) + await asyncio.wait_for(nested_json_future, timeout=5.0) + assert response is not None, "Expected response for nested json action" + assert response.success is True, ( + f"Expected success=True, got {response.success}" + ) + response_json = json.loads(response.response_data.decode("utf-8")) + # Verify nested structure + assert response_json["config"]["wifi"]["connected"] is True + assert response_json["config"]["api"]["port"] == 6053 + assert response_json["items"][0] == "first" + assert response_json["items"][1] == "second" diff --git a/tests/integration/test_api_action_timeout.py b/tests/integration/test_api_action_timeout.py new file mode 100644 index 0000000000..cec0967131 --- /dev/null +++ b/tests/integration/test_api_action_timeout.py @@ -0,0 +1,172 @@ +"""Integration test for API action call timeout functionality. + +Tests that action calls are automatically cleaned up after timeout, +and that late responses are handled gracefully. +""" + +from __future__ import annotations + +import asyncio +import contextlib +import re + +from aioesphomeapi import UserService +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_action_timeout( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test API action call timeout behavior. + + This test uses a 500ms timeout (set via USE_API_ACTION_CALL_TIMEOUT_MS define) + to verify: + 1. Actions that respond within the timeout work correctly + 2. Actions that exceed the timeout have their calls cleaned up + 3. Late responses log a warning but don't crash + """ + loop = asyncio.get_running_loop() + + # Track log messages + immediate_future = loop.create_future() + short_delay_responding_future = loop.create_future() + long_delay_starting_future = loop.create_future() + long_delay_responding_future = loop.create_future() + timeout_warning_future = loop.create_future() + + # Patterns to match in logs + immediate_pattern = re.compile(r"ACTION_IMMEDIATE responding") + short_delay_responding_pattern = re.compile(r"ACTION_SHORT_DELAY responding") + long_delay_starting_pattern = re.compile(r"ACTION_LONG_DELAY starting") + long_delay_responding_pattern = re.compile( + r"ACTION_LONG_DELAY responding \(after timeout\)" + ) + # This warning is logged when api.respond is called after the action call timed out + timeout_warning_pattern = re.compile( + r"Cannot send response: no active call found for action_call_id" + ) + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if not immediate_future.done() and immediate_pattern.search(line): + immediate_future.set_result(True) + elif ( + not short_delay_responding_future.done() + and short_delay_responding_pattern.search(line) + ): + short_delay_responding_future.set_result(True) + elif ( + not long_delay_starting_future.done() + and long_delay_starting_pattern.search(line) + ): + long_delay_starting_future.set_result(True) + elif ( + not long_delay_responding_future.done() + and long_delay_responding_pattern.search(line) + ): + long_delay_responding_future.set_result(True) + elif not timeout_warning_future.done() and timeout_warning_pattern.search(line): + timeout_warning_future.set_result(True) + + # Run with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "api-action-timeout-test" + + # List services + _, services = await client.list_entities_services() + + # Should have 3 services + assert len(services) == 3, f"Expected 3 services, found {len(services)}" + + # Find our services + action_immediate: UserService | None = None + action_short_delay: UserService | None = None + action_long_delay: UserService | None = None + + for service in services: + if service.name == "action_immediate": + action_immediate = service + elif service.name == "action_short_delay": + action_short_delay = service + elif service.name == "action_long_delay": + action_long_delay = service + + assert action_immediate is not None, "action_immediate not found" + assert action_short_delay is not None, "action_short_delay not found" + assert action_long_delay is not None, "action_long_delay not found" + + # Test 1: Immediate response should work + response = await client.execute_service( + action_immediate, + {}, + return_response=True, + ) + await asyncio.wait_for(immediate_future, timeout=1.0) + assert response is not None, "Expected response for immediate action" + assert response.success is True + + # Test 2: Short delay (200ms) should work within the 500ms timeout + response = await client.execute_service( + action_short_delay, + {}, + return_response=True, + ) + await asyncio.wait_for(short_delay_responding_future, timeout=1.0) + assert response is not None, "Expected response for short delay action" + assert response.success is True + + # Test 3: Long delay (1s) should exceed the 500ms timeout + # The server-side timeout will clean up the action call after 500ms + # The client will timeout waiting for the response + # When the action finally tries to respond after 1s, it will log a warning + + # Start the long delay action (don't await it fully - it will timeout) + long_delay_task = asyncio.create_task( + client.execute_service( + action_long_delay, + {}, + return_response=True, + timeout=2.0, # Give client enough time to see the late response attempt + ) + ) + + # Wait for the action to start + await asyncio.wait_for(long_delay_starting_future, timeout=1.0) + + # Wait for the action to try to respond (after 1s delay) + await asyncio.wait_for(long_delay_responding_future, timeout=2.0) + + # Wait for the warning log about no active call + await asyncio.wait_for(timeout_warning_future, timeout=1.0) + + # The client task should complete (either with None response or timeout) + # Client timing out is acceptable - the server-side timeout already cleaned up the call + with contextlib.suppress(TimeoutError): + await asyncio.wait_for(long_delay_task, timeout=1.0) + + # Verify the system is still functional after the timeout + # Call the immediate action again to prove cleanup worked + immediate_future_2 = loop.create_future() + + def check_output_2(line: str) -> None: + if not immediate_future_2.done() and immediate_pattern.search(line): + immediate_future_2.set_result(True) + + response = await client.execute_service( + action_immediate, + {}, + return_response=True, + ) + assert response is not None, "System should still work after timeout" + assert response.success is True diff --git a/tests/integration/test_api_conditional_memory.py b/tests/integration/test_api_conditional_memory.py index cfa32c431d..349b572859 100644 --- a/tests/integration/test_api_conditional_memory.py +++ b/tests/integration/test_api_conditional_memory.py @@ -88,13 +88,13 @@ async def test_api_conditional_memory( assert arg_types["arg_float"] == UserServiceArgType.FLOAT # Call simple service - client.execute_service(simple_service, {}) + await client.execute_service(simple_service, {}) # Wait for service log await asyncio.wait_for(service_simple_future, timeout=5.0) # Call service with arguments - client.execute_service( + await client.execute_service( service_with_args, { "arg_string": "test_string", diff --git a/tests/integration/test_api_custom_services.py b/tests/integration/test_api_custom_services.py index 967c504112..acf69bf092 100644 --- a/tests/integration/test_api_custom_services.py +++ b/tests/integration/test_api_custom_services.py @@ -38,6 +38,7 @@ async def test_api_custom_services( custom_service_future = loop.create_future() custom_args_future = loop.create_future() custom_arrays_future = loop.create_future() + ha_state_future = loop.create_future() # Patterns to match in logs yaml_service_pattern = re.compile(r"YAML service called") @@ -50,6 +51,9 @@ async def test_api_custom_services( custom_arrays_pattern = re.compile( r"Array service called with 2 bools, 3 ints, 2 floats, 2 strings" ) + ha_state_pattern = re.compile( + r"This subscription uses std::string API for backward compatibility" + ) def check_output(line: str) -> None: """Check log output for expected messages.""" @@ -65,6 +69,8 @@ async def test_api_custom_services( custom_args_future.set_result(True) elif not custom_arrays_future.done() and custom_arrays_pattern.search(line): custom_arrays_future.set_result(True) + elif not ha_state_future.done() and ha_state_pattern.search(line): + ha_state_future.set_result(True) # Run with log monitoring async with ( @@ -114,7 +120,7 @@ async def test_api_custom_services( assert custom_arrays_service is not None, "custom_service_with_arrays not found" # Test YAML service - client.execute_service(yaml_service, {}) + await client.execute_service(yaml_service, {}) await asyncio.wait_for(yaml_service_future, timeout=5.0) # Verify YAML service with args arguments @@ -124,7 +130,7 @@ async def test_api_custom_services( assert yaml_args_types["my_string"] == UserServiceArgType.STRING # Test YAML service with arguments - client.execute_service( + await client.execute_service( yaml_args_service, { "my_int": 123, @@ -144,7 +150,7 @@ async def test_api_custom_services( assert yaml_many_args_types["arg4"] == UserServiceArgType.STRING # Test YAML service with many arguments - client.execute_service( + await client.execute_service( yaml_many_args_service, { "arg1": 42, @@ -156,7 +162,7 @@ async def test_api_custom_services( await asyncio.wait_for(yaml_many_args_future, timeout=5.0) # Test simple CustomAPIDevice service - client.execute_service(custom_service, {}) + await client.execute_service(custom_service, {}) await asyncio.wait_for(custom_service_future, timeout=5.0) # Verify custom_args_service arguments @@ -168,7 +174,7 @@ async def test_api_custom_services( assert arg_types["arg_float"] == UserServiceArgType.FLOAT # Test CustomAPIDevice service with arguments - client.execute_service( + await client.execute_service( custom_args_service, { "arg_string": "test_string", @@ -188,7 +194,7 @@ async def test_api_custom_services( assert array_arg_types["string_array"] == UserServiceArgType.STRING_ARRAY # Test CustomAPIDevice service with arrays - client.execute_service( + await client.execute_service( custom_arrays_service, { "bool_array": [True, False], @@ -198,3 +204,8 @@ async def test_api_custom_services( }, ) await asyncio.wait_for(custom_arrays_future, timeout=5.0) + + # Test Home Assistant state subscription (std::string API backward compatibility) + # This verifies that custom_api_device.h can still use std::string overloads + client.send_home_assistant_state("sensor.custom_test", "", "42.5") + await asyncio.wait_for(ha_state_future, timeout=5.0) diff --git a/tests/integration/test_api_homeassistant.py b/tests/integration/test_api_homeassistant.py index f69838396d..3fe0dfe045 100644 --- a/tests/integration/test_api_homeassistant.py +++ b/tests/integration/test_api_homeassistant.py @@ -81,8 +81,15 @@ async def test_api_homeassistant( "input_number.set_value": loop.create_future(), # ha_number_service_call "switch.turn_on": loop.create_future(), # ha_switch_on_service_call "switch.turn_off": loop.create_future(), # ha_switch_off_service_call + "nonexistent.action_for_error_test": loop.create_future(), # error_test_call } + # Future for error message test + action_error_received_future = loop.create_future() + + # Store client reference for use in callback + client_ref: list = [] # Use list to allow modification in nested function + def on_service_call(service_call: HomeassistantServiceCall) -> None: """Capture HomeAssistant service calls.""" ha_service_calls.append(service_call) @@ -93,6 +100,17 @@ async def test_api_homeassistant( if not future.done(): future.set_result(service_call) + # Immediately respond to the error test call so the test can proceed + # This needs to happen synchronously so ESPHome receives the response + # before logging "=== All tests completed ===" + if service_call.service == "nonexistent.action_for_error_test" and client_ref: + test_error_message = "Test error: action not found" + client_ref[0].send_homeassistant_action_response( + call_id=service_call.call_id, + success=False, + error_message=test_error_message, + ) + def check_output(line: str) -> None: """Check log output for expected messages.""" log_lines.append(line) @@ -131,7 +149,12 @@ async def test_api_homeassistant( if match: ha_number_future.set_result(match.group(1)) - elif not tests_complete_future.done() and tests_complete_pattern.search(line): + # Check for action error message (tests StringRef -> std::string conversion) + # Use separate if (not elif) since this can come after tests_complete + if not action_error_received_future.done() and "Action error received:" in line: + action_error_received_future.set_result(line) + + if not tests_complete_future.done() and tests_complete_pattern.search(line): tests_complete_future.set_result(True) # Run with log monitoring @@ -144,6 +167,9 @@ async def test_api_homeassistant( assert device_info is not None assert device_info.name == "test-ha-api" + # Store client reference for use in service call callback + client_ref.append(client) + # Subscribe to HomeAssistant service calls client.subscribe_service_calls(on_service_call) @@ -153,6 +179,12 @@ async def test_api_homeassistant( client.send_home_assistant_state("binary_sensor.external_motion", "", "ON") client.send_home_assistant_state("weather.home", "condition", "sunny") + # Test edge cases for zero-copy implementation safety + # Empty entity_id should be silently ignored (no crash) + client.send_home_assistant_state("", "", "should_be_ignored") + # Empty state with valid entity should work (use different entity to not interfere with test) + client.send_home_assistant_state("sensor.edge_case_empty_state", "", "") + # List entities and services _, services = await client.list_entities_services() @@ -163,7 +195,7 @@ async def test_api_homeassistant( assert trigger_service is not None, "trigger_all_tests service not found" # Execute all tests - client.execute_service(trigger_service, {}) + await client.execute_service(trigger_service, {}) # Wait for all tests to complete with appropriate timeouts try: @@ -292,6 +324,17 @@ async def test_api_homeassistant( assert switch_off_call.service == "switch.turn_off" assert switch_off_call.data["entity_id"] == "switch.test_switch" + # 9. Action response error test (tests StringRef error message) + # The error response is sent automatically in on_service_call callback + # Wait for the error to be logged (proves StringRef -> std::string works) + error_log_line = await asyncio.wait_for( + action_error_received_future, timeout=2.0 + ) + test_error_message = "Test error: action not found" + assert test_error_message in error_log_line, ( + f"Expected error message '{test_error_message}' not found in: {error_log_line}" + ) + except TimeoutError as e: # Show recent log lines for debugging recent_logs = "\n".join(log_lines[-20:]) diff --git a/tests/integration/test_api_message_size_batching.py b/tests/integration/test_api_message_size_batching.py index f7859eb902..5b123318c4 100644 --- a/tests/integration/test_api_message_size_batching.py +++ b/tests/integration/test_api_message_size_batching.py @@ -141,6 +141,9 @@ async def test_api_message_size_batching( assert text_input.max_length == 255, ( f"Expected max_length 255, got {text_input.max_length}" ) + assert text_input.pattern == "[A-Za-z0-9 ]+", ( + f"Expected pattern '[A-Za-z0-9 ]+', got '{text_input.pattern}'" + ) # Verify total entity count - messages of various sizes were batched successfully # We have: 3 selects + 3 text sensors + 1 text input + 1 number = 8 total diff --git a/tests/integration/test_api_string_lambda.py b/tests/integration/test_api_string_lambda.py index f4ef77bad8..ece8b192a2 100644 --- a/tests/integration/test_api_string_lambda.py +++ b/tests/integration/test_api_string_lambda.py @@ -75,10 +75,12 @@ async def test_api_string_lambda( assert char_ptr_service is not None, "test_char_ptr_lambda service not found" # Execute all four services to test different lambda return types - client.execute_service(string_service, {"input_string": "STRING_FROM_LAMBDA"}) - client.execute_service(int_service, {"input_number": 42}) - client.execute_service(float_service, {"input_float": 3.14}) - client.execute_service( + await client.execute_service( + string_service, {"input_string": "STRING_FROM_LAMBDA"} + ) + await client.execute_service(int_service, {"input_number": 42}) + await client.execute_service(float_service, {"input_float": 3.14}) + await client.execute_service( char_ptr_service, {"input_number": 123, "input_string": "test_string"} ) diff --git a/tests/integration/test_automation_wait_actions.py b/tests/integration/test_automation_wait_actions.py index adcb8ba487..f4db247231 100644 --- a/tests/integration/test_automation_wait_actions.py +++ b/tests/integration/test_automation_wait_actions.py @@ -71,7 +71,7 @@ async def test_automation_wait_actions( # Test 1: wait_until in automation - trigger 5 times rapidly test_service = next((s for s in services if s.name == "test_wait_until"), None) assert test_service is not None, "test_wait_until service not found" - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) await asyncio.wait_for(test1_complete, timeout=3.0) # Verify Test 1: All 5 triggers should complete @@ -82,7 +82,7 @@ async def test_automation_wait_actions( # Test 2: script.wait in automation - trigger 5 times rapidly test_service = next((s for s in services if s.name == "test_script_wait"), None) assert test_service is not None, "test_script_wait service not found" - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) await asyncio.wait_for(test2_complete, timeout=3.0) # Verify Test 2: All 5 triggers should complete @@ -95,7 +95,7 @@ async def test_automation_wait_actions( (s for s in services if s.name == "test_wait_timeout"), None ) assert test_service is not None, "test_wait_timeout service not found" - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) await asyncio.wait_for(test3_complete, timeout=3.0) # Verify Test 3: All 5 triggers should timeout and complete diff --git a/tests/integration/test_automations.py b/tests/integration/test_automations.py index 83268c1eea..ffd7f5c587 100644 --- a/tests/integration/test_automations.py +++ b/tests/integration/test_automations.py @@ -67,7 +67,7 @@ async def test_delay_action_cancellation( assert test_service is not None, "start_delay_then_restart service not found" # Execute the test sequence - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) # Wait for the second script to start await asyncio.wait_for(second_script_started, timeout=5.0) @@ -138,7 +138,7 @@ async def test_parallel_script_delays( assert test_service is not None, "test_parallel_delays service not found" # Execute the test - this will start 3 parallel scripts with 1 second delays - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) # Wait for all scripts to complete (should take ~1 second, not 3) await asyncio.wait_for(all_scripts_completed, timeout=2.0) diff --git a/tests/integration/test_binary_sensor_invalidate_state.py b/tests/integration/test_binary_sensor_invalidate_state.py new file mode 100644 index 0000000000..ee9e57319c --- /dev/null +++ b/tests/integration/test_binary_sensor_invalidate_state.py @@ -0,0 +1,138 @@ +"""Integration test for binary_sensor.invalidate_state() functionality. + +This tests the fix in PR #12296 where invalidate_state() was not properly +reporting the 'unknown' state to the API. The binary sensor should report +missing_state=True when invalidated. + +Regression test for: https://github.com/esphome/esphome/issues/12252 +""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import BinarySensorInfo, BinarySensorState, EntityState +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_binary_sensor_invalidate_state( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that binary_sensor.invalidate_state() reports unknown to the API. + + This verifies that: + 1. Binary sensor starts with missing_state=True (no initial state) + 2. Publishing true sets missing_state=False and state=True + 3. Publishing false sets missing_state=False and state=False + 4. Invalidating state sets missing_state=True (unknown state) + """ + loop = asyncio.get_running_loop() + + # Track state changes + states_received: list[BinarySensorState] = [] + state_future: asyncio.Future[BinarySensorState] = loop.create_future() + + def on_state(state: EntityState) -> None: + """Track binary sensor state changes.""" + if isinstance(state, BinarySensorState): + states_received.append(state) + if not state_future.done(): + state_future.set_result(state) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "test-binary-sensor-invalidate" + + # Get entities + entities, _ = await client.list_entities_services() + + # Find our binary sensor and buttons using helper + binary_sensor = require_entity(entities, "test_binary_sensor", BinarySensorInfo) + set_true_button = require_entity( + entities, "set_true", description="Set True button" + ) + set_false_button = require_entity( + entities, "set_false", description="Set False button" + ) + invalidate_button = require_entity( + entities, "invalidate", description="Invalidate button" + ) + + # Set up initial state helper to handle the initial state broadcast + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for initial states + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Check initial state - should be missing (unknown) + initial_state = initial_state_helper.initial_states.get(binary_sensor.key) + assert initial_state is not None, "No initial state received for binary sensor" + assert isinstance(initial_state, BinarySensorState) + assert initial_state.missing_state is True, ( + f"Initial state should have missing_state=True, got {initial_state}" + ) + + # Test 1: Set state to true + states_received.clear() + state_future = loop.create_future() + client.button_command(set_true_button.key) + + try: + state = await asyncio.wait_for(state_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for state=true") + + assert state.missing_state is False, ( + f"After setting true, missing_state should be False, got {state}" + ) + assert state.state is True, f"Expected state=True, got {state}" + + # Test 2: Set state to false + states_received.clear() + state_future = loop.create_future() + client.button_command(set_false_button.key) + + try: + state = await asyncio.wait_for(state_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for state=false") + + assert state.missing_state is False, ( + f"After setting false, missing_state should be False, got {state}" + ) + assert state.state is False, f"Expected state=False, got {state}" + + # Test 3: Invalidate state (set to unknown) + # This is the critical test for the bug fix + states_received.clear() + state_future = loop.create_future() + client.button_command(invalidate_button.key) + + try: + state = await asyncio.wait_for(state_future, timeout=5.0) + except TimeoutError: + pytest.fail( + "Timeout waiting for invalidated state - " + "binary_sensor.invalidate_state() may not be reporting to the API. " + "See issue #12252." + ) + + assert state.missing_state is True, ( + f"After invalidate_state(), missing_state should be True (unknown), " + f"got {state}. This is the regression from issue #12252." + ) diff --git a/tests/integration/test_build_info.py b/tests/integration/test_build_info.py new file mode 100644 index 0000000000..7079594471 --- /dev/null +++ b/tests/integration/test_build_info.py @@ -0,0 +1,117 @@ +"""Integration test for build_info values.""" + +from __future__ import annotations + +import asyncio +from datetime import datetime +import re +import time + +from aioesphomeapi import EntityState, TextSensorState +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_build_info( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that build_info values are sane.""" + async with run_compiled(yaml_config), api_client_connected() as client: + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "build-info-test" + + # Verify compilation_time from device_info is present and parseable + # The format is ISO 8601 with timezone: "YYYY-MM-DD HH:MM:SS +ZZZZ" + compilation_time = device_info.compilation_time + assert compilation_time is not None + + # Validate the ISO format: "YYYY-MM-DD HH:MM:SS +ZZZZ" + parsed = datetime.strptime(compilation_time, "%Y-%m-%d %H:%M:%S %z") + assert parsed.year >= time.localtime().tm_year + + # Get entities + entities, _ = await client.list_entities_services() + + # Find our text sensors by object_id + config_hash_entity = next( + (e for e in entities if e.object_id == "config_hash"), None + ) + build_time_entity = next( + (e for e in entities if e.object_id == "build_time"), None + ) + build_time_str_entity = next( + (e for e in entities if e.object_id == "build_time_string"), None + ) + + assert config_hash_entity is not None, "Config Hash sensor not found" + assert build_time_entity is not None, "Build Time sensor not found" + assert build_time_str_entity is not None, "Build Time String sensor not found" + + # Wait for all three text sensors to have valid states + loop = asyncio.get_running_loop() + states: dict[int, TextSensorState] = {} + all_received = loop.create_future() + expected_keys = { + config_hash_entity.key, + build_time_entity.key, + build_time_str_entity.key, + } + + def on_state(state: EntityState) -> None: + if isinstance(state, TextSensorState) and not state.missing_state: + states[state.key] = state + if expected_keys <= states.keys() and not all_received.done(): + all_received.set_result(True) + + client.subscribe_states(on_state) + + try: + await asyncio.wait_for(all_received, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for text sensor states. Got: {list(states.keys())}" + ) + + config_hash_state = states[config_hash_entity.key] + build_time_state = states[build_time_entity.key] + build_time_str_state = states[build_time_str_entity.key] + + # Validate config_hash format (0x followed by 8 hex digits) + config_hash = config_hash_state.state + assert re.match(r"^0x[0-9a-f]{8}$", config_hash), ( + f"config_hash should be 0x followed by 8 hex digits, got: {config_hash}" + ) + + # Validate build_time is a reasonable Unix timestamp + build_time = int(build_time_state.state) + current_time = int(time.time()) + # Build time should be within last hour and not in the future + assert build_time <= current_time, ( + f"build_time {build_time} should not be in the future (current: {current_time})" + ) + assert build_time > current_time - 3600, ( + f"build_time {build_time} should be within the last hour" + ) + + # Validate build_time_str matches the new ISO format + build_time_str = build_time_str_state.state + # Format: "YYYY-MM-DD HH:MM:SS +ZZZZ" + parsed_build_time = datetime.strptime(build_time_str, "%Y-%m-%d %H:%M:%S %z") + assert parsed_build_time.year >= time.localtime().tm_year + + # Verify build_time_str matches what we get from build_time timestamp + expected_str = time.strftime("%Y-%m-%d %H:%M:%S %z", time.localtime(build_time)) + assert build_time_str == expected_str, ( + f"build_time_str '{build_time_str}' should match timestamp '{expected_str}'" + ) + + # Verify compilation_time matches build_time_str (they should be the same) + assert compilation_time == build_time_str, ( + f"compilation_time '{compilation_time}' should match " + f"build_time_str '{build_time_str}'" + ) diff --git a/tests/integration/test_continuation_actions.py b/tests/integration/test_continuation_actions.py index 1069ee7581..e6020c711a 100644 --- a/tests/integration/test_continuation_actions.py +++ b/tests/integration/test_continuation_actions.py @@ -142,7 +142,7 @@ async def test_continuation_actions( # Test 1: IfAction with then branch test_service = next((s for s in services if s.name == "test_if_action"), None) assert test_service is not None, "test_if_action service not found" - client.execute_service(test_service, {"condition": True, "value": 42}) + await client.execute_service(test_service, {"condition": True, "value": 42}) await asyncio.wait_for(test1_complete, timeout=2.0) assert test_results["if_then"], "IfAction then branch not executed" assert test_results["if_complete"], "IfAction did not complete" @@ -150,7 +150,7 @@ async def test_continuation_actions( # Test 1b: IfAction with else branch test1_complete = loop.create_future() test_results["if_complete"] = False - client.execute_service(test_service, {"condition": False, "value": 99}) + await client.execute_service(test_service, {"condition": False, "value": 99}) await asyncio.wait_for(test1_complete, timeout=2.0) assert test_results["if_else"], "IfAction else branch not executed" assert test_results["if_complete"], "IfAction did not complete" @@ -160,14 +160,14 @@ async def test_continuation_actions( assert test_service is not None, "test_nested_if service not found" # Both true - client.execute_service(test_service, {"outer": True, "inner": True}) + await client.execute_service(test_service, {"outer": True, "inner": True}) await asyncio.wait_for(test2_complete, timeout=2.0) assert test_results["nested_both_true"], "Nested both true not executed" # Outer true, inner false test2_complete = loop.create_future() test_results["nested_complete"] = False - client.execute_service(test_service, {"outer": True, "inner": False}) + await client.execute_service(test_service, {"outer": True, "inner": False}) await asyncio.wait_for(test2_complete, timeout=2.0) assert test_results["nested_outer_true_inner_false"], ( "Nested outer true inner false not executed" @@ -176,7 +176,7 @@ async def test_continuation_actions( # Outer false test2_complete = loop.create_future() test_results["nested_complete"] = False - client.execute_service(test_service, {"outer": False, "inner": True}) + await client.execute_service(test_service, {"outer": False, "inner": True}) await asyncio.wait_for(test2_complete, timeout=2.0) assert test_results["nested_outer_false"], "Nested outer false not executed" @@ -185,7 +185,7 @@ async def test_continuation_actions( (s for s in services if s.name == "test_while_action"), None ) assert test_service is not None, "test_while_action service not found" - client.execute_service(test_service, {"max_count": 3}) + await client.execute_service(test_service, {"max_count": 3}) await asyncio.wait_for(test3_complete, timeout=2.0) assert test_results["while_iterations"] == 3, ( f"WhileAction expected 3 iterations, got {test_results['while_iterations']}" @@ -197,7 +197,7 @@ async def test_continuation_actions( (s for s in services if s.name == "test_repeat_action"), None ) assert test_service is not None, "test_repeat_action service not found" - client.execute_service(test_service, {"count": 5}) + await client.execute_service(test_service, {"count": 5}) await asyncio.wait_for(test4_complete, timeout=2.0) assert test_results["repeat_iterations"] == 5, ( f"RepeatAction expected 5 iterations, got {test_results['repeat_iterations']}" @@ -207,7 +207,7 @@ async def test_continuation_actions( # Test 5: Combined (if + repeat + while) test_service = next((s for s in services if s.name == "test_combined"), None) assert test_service is not None, "test_combined service not found" - client.execute_service(test_service, {"do_loop": True, "loop_count": 2}) + await client.execute_service(test_service, {"do_loop": True, "loop_count": 2}) await asyncio.wait_for(test5_complete, timeout=2.0) # Should execute: repeat 2 times, each iteration does while from iteration down to 0 # iteration 0: while 0 times = 0 @@ -221,7 +221,7 @@ async def test_continuation_actions( # Test 6: Rapid triggers (tests memory efficiency of ContinuationAction) test_service = next((s for s in services if s.name == "test_rapid_if"), None) assert test_service is not None, "test_rapid_if service not found" - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) await asyncio.wait_for(test6_complete, timeout=2.0) # Values 1, 2 should hit else (<=2), values 3, 4, 5 should hit then (>2) assert test_results["rapid_else"] == 2, ( diff --git a/tests/integration/test_fnv1_hash_object_id.py b/tests/integration/test_fnv1_hash_object_id.py new file mode 100644 index 0000000000..23e8ca04c2 --- /dev/null +++ b/tests/integration/test_fnv1_hash_object_id.py @@ -0,0 +1,75 @@ +"""Integration test for fnv1_hash_object_id function. + +This test verifies that the C++ fnv1_hash_object_id() function in +esphome/core/helpers.h produces the same hash values as the Python +fnv1_hash_object_id() function in esphome/helpers.py. + +If this test fails, one of the implementations has diverged and needs +to be updated to match the other. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_fnv1_hash_object_id( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that C++ fnv1_hash_object_id matches Python implementation.""" + + test_results: dict[str, str] = {} + all_tests_complete = asyncio.Event() + expected_tests = { + "foo", + "upper", + "space", + "underscore", + "hyphen", + "special", + "complex", + "empty", + } + + def on_log_line(line: str) -> None: + """Capture log lines with test results.""" + # Strip ANSI escape codes + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + # Look for our test result messages + # Format: "[timestamp][level][FNV1_OID:line]: test_name PASSED" + match = re.search(r"\[FNV1_OID:\d+\]:\s+(\w+)\s+(PASSED|FAILED)", clean_line) + if match: + test_name = match.group(1) + result = match.group(2) + test_results[test_name] = result + if set(test_results.keys()) >= expected_tests: + all_tests_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "fnv1-hash-object-id-test" + + # Wait for all tests to complete or timeout + try: + await asyncio.wait_for(all_tests_complete.wait(), timeout=2.0) + except TimeoutError: + pytest.fail(f"Tests timed out. Got results for: {set(test_results.keys())}") + + # Verify all tests passed + for test_name in expected_tests: + assert test_name in test_results, f"{test_name} test not found" + assert test_results[test_name] == "PASSED", ( + f"{test_name} test failed - C++ and Python hash mismatch" + ) diff --git a/tests/integration/test_fnv1a_hash.py b/tests/integration/test_fnv1a_hash.py new file mode 100644 index 0000000000..366ea42cda --- /dev/null +++ b/tests/integration/test_fnv1a_hash.py @@ -0,0 +1,69 @@ +"""Integration test for FNV-1a hash functions.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_fnv1a_hash( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that FNV-1a hash functions work correctly.""" + + test_results = {} + all_tests_complete = asyncio.Event() + expected_tests = {"empty", "known_hello", "known_helloworld", "extend", "string"} + + def on_log_line(line: str) -> None: + """Capture log lines with test results.""" + # Strip ANSI escape codes + clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) + # Look for our test result messages + # Format: "[timestamp][level][FNV1A:line]: test_name PASSED" + match = re.search(r"\[FNV1A:\d+\]:\s+(\w+)\s+(PASSED|FAILED)", clean_line) + if match: + test_name = match.group(1) + result = match.group(2) + test_results[test_name] = result + if set(test_results.keys()) >= expected_tests: + all_tests_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "fnv1a-hash-test" + + # Wait for all tests to complete or timeout + try: + await asyncio.wait_for(all_tests_complete.wait(), timeout=2.0) + except TimeoutError: + pytest.fail(f"Tests timed out. Got results for: {set(test_results.keys())}") + + # Verify all tests passed + assert "empty" in test_results, "empty string test not found" + assert test_results["empty"] == "PASSED", "empty string test failed" + + assert "known_hello" in test_results, "known_hello test not found" + assert test_results["known_hello"] == "PASSED", "known_hello test failed" + + assert "known_helloworld" in test_results, "known_helloworld test not found" + assert test_results["known_helloworld"] == "PASSED", ( + "known_helloworld test failed" + ) + + assert "extend" in test_results, "fnv1a_hash_extend test not found" + assert test_results["extend"] == "PASSED", "fnv1a_hash_extend test failed" + + assert "string" in test_results, "std::string test not found" + assert test_results["string"] == "PASSED", "std::string test failed" diff --git a/tests/integration/test_host_logger_thread_safety.py b/tests/integration/test_host_logger_thread_safety.py new file mode 100644 index 0000000000..922ce00155 --- /dev/null +++ b/tests/integration/test_host_logger_thread_safety.py @@ -0,0 +1,182 @@ +"""Integration test for host logger thread safety. + +This test verifies that the logger's MPSC ring buffer correctly handles +multiple threads racing to log messages without corruption or data loss. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + +# Expected pattern for log messages from threads +# Format: THREADn_MSGnnn_LEVEL_MESSAGE_WITH_DATA_xxxxxxxx +THREAD_MSG_PATTERN = re.compile( + r"THREAD(\d+)_MSG(\d{3})_(INFO|DEBUG|WARN|ERROR)_MESSAGE_WITH_DATA_([0-9A-F]{8})" +) + +# Pattern for test start/complete markers +TEST_START_PATTERN = re.compile(r"RACE_TEST_START.*Starting (\d+) threads") +TEST_COMPLETE_PATTERN = re.compile(r"RACE_TEST_COMPLETE.*total messages: (\d+)") + +# Expected values +NUM_THREADS = 3 +MESSAGES_PER_THREAD = 100 +EXPECTED_TOTAL_MESSAGES = NUM_THREADS * MESSAGES_PER_THREAD + + +@pytest.mark.asyncio +async def test_host_logger_thread_safety( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that multiple threads can log concurrently without corruption. + + This test: + 1. Spawns 3 threads that each log 100 messages + 2. Collects all log output + 3. Verifies no lines are corrupted (partially written or interleaved) + 4. Verifies all expected messages were received + """ + collected_lines: list[str] = [] + test_complete_event = asyncio.Event() + + def line_callback(line: str) -> None: + """Collect log lines and detect test completion.""" + collected_lines.append(line) + if "RACE_TEST_COMPLETE" in line: + test_complete_event.set() + + # Run the test binary and collect output + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + # Verify connection works + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "host-logger-thread-test" + + # Get the button entity - find by name + entities, _ = await client.list_entities_services() + button_entities = [e for e in entities if e.name == "Start Thread Race Test"] + assert button_entities, "Could not find Start Thread Race Test button" + button_key = button_entities[0].key + + # Press the button to start the thread race test + client.button_command(button_key) + + # Wait for test to complete (with timeout) + try: + await asyncio.wait_for(test_complete_event.wait(), timeout=30.0) + except TimeoutError: + pytest.fail( + "Test did not complete within timeout. " + f"Collected {len(collected_lines)} lines." + ) + + # Give a bit more time for any remaining buffered messages + await asyncio.sleep(0.5) + + # Analyze collected log lines + thread_messages: dict[int, set[int]] = {i: set() for i in range(NUM_THREADS)} + corrupted_lines: list[str] = [] + test_started = False + test_completed = False + reported_total = 0 + + for line in collected_lines: + # Check for test start + start_match = TEST_START_PATTERN.search(line) + if start_match: + test_started = True + assert int(start_match.group(1)) == NUM_THREADS, ( + f"Unexpected thread count: {start_match.group(1)}" + ) + continue + + # Check for test completion + complete_match = TEST_COMPLETE_PATTERN.search(line) + if complete_match: + test_completed = True + reported_total = int(complete_match.group(1)) + continue + + # Check for thread messages + msg_match = THREAD_MSG_PATTERN.search(line) + if msg_match: + thread_id = int(msg_match.group(1)) + msg_num = int(msg_match.group(2)) + # level = msg_match.group(3) # INFO, DEBUG, WARN, ERROR + data_hex = msg_match.group(4) + + # Verify data value matches expected calculation + expected_data = f"{msg_num * 12345:08X}" + if data_hex != expected_data: + corrupted_lines.append( + f"Data mismatch in line: {line} " + f"(expected {expected_data}, got {data_hex})" + ) + continue + + # Track which messages we received from each thread + if 0 <= thread_id < NUM_THREADS: + thread_messages[thread_id].add(msg_num) + else: + corrupted_lines.append(f"Invalid thread ID in line: {line}") + continue + + # Check for partial/corrupted thread messages + # If a line contains part of a thread message pattern but doesn't match fully + # This could indicate line corruption from interleaving + if ( + "THREAD" in line + and "MSG" in line + and not msg_match + and "_MESSAGE_WITH_DATA_" in line + ): + corrupted_lines.append(f"Possibly corrupted line: {line}") + + # Assertions + assert test_started, "Test start marker not found in output" + assert test_completed, "Test completion marker not found in output" + assert reported_total == EXPECTED_TOTAL_MESSAGES, ( + f"Reported total {reported_total} != expected {EXPECTED_TOTAL_MESSAGES}" + ) + + # Check for corrupted lines + assert not corrupted_lines, ( + f"Found {len(corrupted_lines)} corrupted lines:\n" + + "\n".join(corrupted_lines[:10]) # Show first 10 + ) + + # Count total messages received + total_received = sum(len(msgs) for msgs in thread_messages.values()) + + # We may not receive all messages due to ring buffer overflow when buffer is full + # The test primarily verifies no corruption, not that we receive every message + # However, we should receive a reasonable number of messages + min_expected = EXPECTED_TOTAL_MESSAGES // 2 # At least 50% + assert total_received >= min_expected, ( + f"Received only {total_received} messages, expected at least {min_expected}. " + f"Per-thread breakdown: " + + ", ".join(f"Thread{i}: {len(msgs)}" for i, msgs in thread_messages.items()) + ) + + # Verify we got messages from all threads (proves concurrent logging worked) + for thread_id in range(NUM_THREADS): + assert thread_messages[thread_id], ( + f"No messages received from thread {thread_id}" + ) + + # Log summary for debugging + print("\nThread safety test summary:") + print(f" Total messages received: {total_received}/{EXPECTED_TOTAL_MESSAGES}") + for thread_id in range(NUM_THREADS): + received = len(thread_messages[thread_id]) + print(f" Thread {thread_id}: {received}/{MESSAGES_PER_THREAD} messages") diff --git a/tests/integration/test_host_mode_api_password.py b/tests/integration/test_host_mode_api_password.py deleted file mode 100644 index 5c5e689e45..0000000000 --- a/tests/integration/test_host_mode_api_password.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Integration test for API password authentication.""" - -from __future__ import annotations - -import asyncio - -from aioesphomeapi import APIConnectionError, InvalidAuthAPIError -import pytest - -from .types import APIClientConnectedFactory, RunCompiledFunction - - -@pytest.mark.asyncio -async def test_host_mode_api_password( - yaml_config: str, - run_compiled: RunCompiledFunction, - api_client_connected: APIClientConnectedFactory, -) -> None: - """Test API authentication with password.""" - async with run_compiled(yaml_config): - # Connect with correct password - async with api_client_connected(password="test_password_123") as client: - # Verify we can get device info - device_info = await client.device_info() - assert device_info is not None - assert device_info.uses_password is True - assert device_info.name == "host-mode-api-password" - - # Subscribe to states to ensure authenticated connection works - loop = asyncio.get_running_loop() - state_future: asyncio.Future[bool] = loop.create_future() - states = {} - - def on_state(state): - states[state.key] = state - if not state_future.done(): - state_future.set_result(True) - - client.subscribe_states(on_state) - - # Wait for at least one state with timeout - try: - await asyncio.wait_for(state_future, timeout=5.0) - except TimeoutError: - pytest.fail("No states received within timeout") - - # Should have received at least one state (the test sensor) - assert len(states) > 0 - - # Test with wrong password - should fail - # Try connecting with wrong password - try: - async with api_client_connected( - password="wrong_password", timeout=5 - ) as client: - # If we get here without exception, try to use the connection - # which should fail if auth failed - await client.device_info_and_list_entities() - # If we successfully got device info and entities, auth didn't fail properly - pytest.fail("Connection succeeded with wrong password") - except (InvalidAuthAPIError, APIConnectionError) as e: - # Expected - auth should fail - # Accept either InvalidAuthAPIError or generic APIConnectionError - # since the client might not always distinguish - assert ( - "password" in str(e).lower() - or "auth" in str(e).lower() - or "invalid" in str(e).lower() - ) diff --git a/tests/integration/test_light_automations.py b/tests/integration/test_light_automations.py new file mode 100644 index 0000000000..9ff334548a --- /dev/null +++ b/tests/integration/test_light_automations.py @@ -0,0 +1,101 @@ +"""Integration test for light automation triggers. + +Tests that on_turn_on, on_turn_off, and on_state triggers work correctly +with the listener interface pattern. +""" + +import asyncio + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_light_automations( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test light on_turn_on, on_turn_off, and on_state triggers.""" + loop = asyncio.get_running_loop() + + # Futures for log line detection + on_turn_on_future: asyncio.Future[bool] = loop.create_future() + on_turn_off_future: asyncio.Future[bool] = loop.create_future() + on_state_count = 0 + counting_enabled = False + on_state_futures: list[asyncio.Future[bool]] = [] + + def create_on_state_future() -> asyncio.Future[bool]: + """Create a new future for on_state trigger.""" + future: asyncio.Future[bool] = loop.create_future() + on_state_futures.append(future) + return future + + def check_output(line: str) -> None: + """Check log output for trigger messages.""" + nonlocal on_state_count + if "TRIGGER: on_turn_on fired" in line: + if not on_turn_on_future.done(): + on_turn_on_future.set_result(True) + elif "TRIGGER: on_turn_off fired" in line: + if not on_turn_off_future.done(): + on_turn_off_future.set_result(True) + elif "TRIGGER: on_state fired" in line: + # Only count on_state after we start testing + if counting_enabled: + on_state_count += 1 + # Complete any pending on_state futures + for future in on_state_futures: + if not future.done(): + future.set_result(True) + break + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get entities + entities = await client.list_entities_services() + light = next(e for e in entities[0] if e.object_id == "test_light") + + # Start counting on_state events now + counting_enabled = True + + # Test 1: Turn light on - should trigger on_turn_on and on_state + on_state_future_1 = create_on_state_future() + client.light_command(key=light.key, state=True) + + # Wait for on_turn_on trigger + try: + await asyncio.wait_for(on_turn_on_future, timeout=5.0) + except TimeoutError: + pytest.fail("on_turn_on trigger did not fire") + + # Wait for on_state trigger + try: + await asyncio.wait_for(on_state_future_1, timeout=5.0) + except TimeoutError: + pytest.fail("on_state trigger did not fire after turn on") + + # Test 2: Turn light off - should trigger on_turn_off and on_state + on_state_future_2 = create_on_state_future() + client.light_command(key=light.key, state=False) + + # Wait for on_turn_off trigger + try: + await asyncio.wait_for(on_turn_off_future, timeout=5.0) + except TimeoutError: + pytest.fail("on_turn_off trigger did not fire") + + # Wait for on_state trigger + try: + await asyncio.wait_for(on_state_future_2, timeout=5.0) + except TimeoutError: + pytest.fail("on_state trigger did not fire after turn off") + + # Verify on_state fired exactly twice (once for on, once for off) + assert on_state_count == 2, ( + f"on_state should have triggered exactly twice, got {on_state_count}" + ) diff --git a/tests/integration/test_lock_automations.py b/tests/integration/test_lock_automations.py new file mode 100644 index 0000000000..e200a2eacd --- /dev/null +++ b/tests/integration/test_lock_automations.py @@ -0,0 +1,58 @@ +"""Integration test for lock automation triggers. + +Tests that on_lock and on_unlock triggers work correctly. +""" + +import asyncio + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_lock_automations( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test lock on_lock and on_unlock triggers.""" + loop = asyncio.get_running_loop() + + # Futures for log line detection + on_lock_future: asyncio.Future[bool] = loop.create_future() + on_unlock_future: asyncio.Future[bool] = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for trigger messages.""" + if "TRIGGER: on_lock fired" in line and not on_lock_future.done(): + on_lock_future.set_result(True) + elif "TRIGGER: on_unlock fired" in line and not on_unlock_future.done(): + on_unlock_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Import here to avoid import errors when aioesphomeapi is not installed + from aioesphomeapi import LockCommand + + # Get entities + entities = await client.list_entities_services() + lock = next(e for e in entities[0] if e.object_id == "test_lock") + + # Test 1: Lock - should trigger on_lock + client.lock_command(key=lock.key, command=LockCommand.LOCK) + + try: + await asyncio.wait_for(on_lock_future, timeout=5.0) + except TimeoutError: + pytest.fail("on_lock trigger did not fire") + + # Test 2: Unlock - should trigger on_unlock + client.lock_command(key=lock.key, command=LockCommand.UNLOCK) + + try: + await asyncio.wait_for(on_unlock_future, timeout=5.0) + except TimeoutError: + pytest.fail("on_unlock trigger did not fire") diff --git a/tests/integration/test_object_id_api_verification.py b/tests/integration/test_object_id_api_verification.py new file mode 100644 index 0000000000..c8603e0682 --- /dev/null +++ b/tests/integration/test_object_id_api_verification.py @@ -0,0 +1,176 @@ +"""Integration test to verify object_id from API matches Python computation. + +This test verifies a three-way match between: +1. C++ object_id generation (get_object_id_to using to_sanitized_char/to_snake_case_char) +2. C++ hash generation (fnv1_hash_object_id in helpers.h) +3. Python computation (sanitize/snake_case in helpers.py, fnv1_hash_object_id) + +The API response contains C++ computed values, so verifying API == Python +implicitly verifies C++ == Python == API for both object_id and hash. + +This is important for the planned migration to remove object_id from the API +protocol and have clients (like aioesphomeapi) compute it from the name. +See: https://github.com/esphome/backlog/issues/76 + +Test cases covered: +- Named entities with various characters (uppercase, special chars, hyphens, etc.) +- Empty-name entities on main device (uses device's friendly_name with MAC suffix) +- Empty-name entities on sub-devices (uses sub-device's name) +- Named entities on sub-devices (uses entity name, not device name) +- MAC suffix handling (name_add_mac_suffix modifies friendly_name at runtime) +- Both object_id string and hash (key) verification +""" + +from __future__ import annotations + +import pytest + +from esphome.helpers import fnv1_hash_object_id + +from .entity_utils import compute_object_id, verify_all_entities +from .types import APIClientConnectedFactory, RunCompiledFunction + +# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679" +MAC_SUFFIX = "abf679" + + +# Expected entities with their own names and expected object_ids +# Format: (entity_name, expected_object_id) +NAMED_ENTITIES = [ + # sensor platform + ("Temperature Sensor", "temperature_sensor"), + ("UPPERCASE NAME", "uppercase_name"), + ("Special!@Chars#", "special__chars_"), + ("Temp-Sensor", "temp-sensor"), + ("Temp_Sensor", "temp_sensor"), + ("Living Room Temperature", "living_room_temperature"), + # binary_sensor platform + ("Door Open", "door_open"), + ("Sensor 123", "sensor_123"), + # switch platform + ("My Very Long Switch Name Here", "my_very_long_switch_name_here"), + # text_sensor platform + ("123 Start", "123_start"), + # button platform - named entity on sub-device (uses entity name, not device name) + ("Device Button", "device_button"), +] + +# Sub-device names and their expected object_ids for empty-name entities +# Format: (device_name, expected_object_id) +SUB_DEVICE_EMPTY_NAME_ENTITIES = [ + ("Sub Device One", "sub_device_one"), + ("Sub Device Two", "sub_device_two"), +] + + +@pytest.mark.asyncio +async def test_object_id_api_verification( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that object_id from API matches Python computation. + + Tests: + 1. Named entities - object_id computed from entity name + 2. Empty-name entities - object_id computed from friendly_name (with MAC suffix) + 3. Hash verification - key can be computed from name + 4. Generic verification - all entities can have object_id computed from API data + """ + async with run_compiled(yaml_config), api_client_connected() as client: + # Get device info + device_info = await client.device_info() + assert device_info is not None + + # Device name should include MAC suffix (hyphen separator) + assert device_info.name == f"object-id-test-{MAC_SUFFIX}", ( + f"Device name mismatch: got '{device_info.name}'" + ) + # Friendly name should include MAC suffix (space separator) + expected_friendly_name = f"Test Device {MAC_SUFFIX}" + assert device_info.friendly_name == expected_friendly_name, ( + f"Friendly name mismatch: got '{device_info.friendly_name}'" + ) + + # Get all entities + entities, _ = await client.list_entities_services() + + # Create a map of entity names to entity info + entity_map = {} + for entity in entities: + entity_map[entity.name] = entity + + # === Test 1: Verify each named entity === + for entity_name, expected_object_id in NAMED_ENTITIES: + assert entity_name in entity_map, ( + f"Entity '{entity_name}' not found in API response. " + f"Available: {list(entity_map.keys())}" + ) + + entity = entity_map[entity_name] + + # Verify object_id matches expected + assert entity.object_id == expected_object_id, ( + f"Entity '{entity_name}': object_id mismatch. " + f"API returned '{entity.object_id}', expected '{expected_object_id}'" + ) + + # Verify Python computation matches + computed = compute_object_id(entity_name) + assert computed == expected_object_id, ( + f"Entity '{entity_name}': Python computation mismatch. " + f"Computed '{computed}', expected '{expected_object_id}'" + ) + + # Verify hash can be computed from the name + hash_from_name = fnv1_hash_object_id(entity_name) + assert hash_from_name == entity.key, ( + f"Entity '{entity_name}': hash mismatch. " + f"Python hash {hash_from_name:#x}, API key {entity.key:#x}" + ) + + # === Test 2: Verify empty-name entities === + # Empty-name entities have name="" in API, object_id comes from: + # - Main device: friendly_name (with MAC suffix) + # - Sub-device: device name + + # Get all empty-name entities + empty_name_entities = [e for e in entities if e.name == ""] + # We expect 3: 1 on main device, 2 on sub-devices + assert len(empty_name_entities) == 3, ( + f"Expected 3 empty-name entities, got {len(empty_name_entities)}" + ) + + # Build device_id -> device_name map from device_info + device_id_to_name = {d.device_id: d.name for d in device_info.devices} + + # Verify each empty-name entity + for entity in empty_name_entities: + if entity.device_id == 0: + # Main device - uses friendly_name with MAC suffix + expected_name = expected_friendly_name + else: + # Sub-device - uses device name + assert entity.device_id in device_id_to_name, ( + f"Entity device_id {entity.device_id} not found in devices" + ) + expected_name = device_id_to_name[entity.device_id] + + expected_object_id = compute_object_id(expected_name) + assert entity.object_id == expected_object_id, ( + f"Empty-name entity (device_id={entity.device_id}): object_id mismatch. " + f"API: '{entity.object_id}', expected: '{expected_object_id}' " + f"(from name '{expected_name}')" + ) + + # Verify hash matches + expected_hash = fnv1_hash_object_id(expected_name) + assert entity.key == expected_hash, ( + f"Empty-name entity (device_id={entity.device_id}): hash mismatch. " + f"API key: {entity.key:#x}, expected: {expected_hash:#x}" + ) + + # === Test 3: Verify ALL entities using the algorithm from entity_utils === + # This uses the algorithm that aioesphomeapi will use to compute object_id + # client-side from API data. + verify_all_entities(entities, device_info) diff --git a/tests/integration/test_object_id_friendly_name_no_mac_suffix.py b/tests/integration/test_object_id_friendly_name_no_mac_suffix.py new file mode 100644 index 0000000000..7199a2b371 --- /dev/null +++ b/tests/integration/test_object_id_friendly_name_no_mac_suffix.py @@ -0,0 +1,81 @@ +"""Integration test for object_id with friendly_name but no MAC suffix. + +This test covers Branch 4 of the algorithm: +- Empty name on main device +- NO MAC suffix enabled +- friendly_name IS set +- Result: use friendly_name for object_id +""" + +from __future__ import annotations + +import pytest + +from esphome.helpers import fnv1_hash_object_id + +from .entity_utils import ( + compute_object_id, + infer_name_add_mac_suffix, + verify_all_entities, +) +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_object_id_friendly_name_no_mac_suffix( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test object_id when friendly_name is set but no MAC suffix. + + This covers Branch 4 of the algorithm: + - Empty name entity + - name_add_mac_suffix = false (or not set) + - friendly_name = "My Friendly Device" + - Expected: object_id = "my_friendly_device" + """ + async with run_compiled(yaml_config), api_client_connected() as client: + device_info = await client.device_info() + assert device_info is not None + + # Device name should NOT include MAC suffix + assert device_info.name == "test-device" + + # Friendly name should be set + assert device_info.friendly_name == "My Friendly Device" + + entities, _ = await client.list_entities_services() + + # Find the empty-name entity + empty_name_entities = [e for e in entities if e.name == ""] + assert len(empty_name_entities) == 1 + + entity = empty_name_entities[0] + + # Should use friendly_name for object_id (Branch 4) + expected_object_id = compute_object_id("My Friendly Device") + assert expected_object_id == "my_friendly_device" # Verify our expectation + assert entity.object_id == expected_object_id, ( + f"Expected object_id '{expected_object_id}' from friendly_name, " + f"got '{entity.object_id}'" + ) + + # Hash should match friendly_name + expected_hash = fnv1_hash_object_id("My Friendly Device") + assert entity.key == expected_hash, ( + f"Expected hash {expected_hash:#x}, got {entity.key:#x}" + ) + + # Named entity should work normally + named_entities = [e for e in entities if e.name == "Temperature"] + assert len(named_entities) == 1 + assert named_entities[0].object_id == "temperature" + + # Verify our inference: no MAC suffix in this test + assert not infer_name_add_mac_suffix(device_info), ( + "Device name should NOT have MAC suffix" + ) + + # Verify the full algorithm from entity_utils works for ALL entities + verify_all_entities(entities, device_info) diff --git a/tests/integration/test_object_id_no_friendly_name.py b/tests/integration/test_object_id_no_friendly_name.py new file mode 100644 index 0000000000..b548f02fde --- /dev/null +++ b/tests/integration/test_object_id_no_friendly_name.py @@ -0,0 +1,140 @@ +"""Integration tests for object_id when friendly_name is not set. + +These tests verify bug-for-bug compatibility with the old behavior: + +1. With MAC suffix enabled + no friendly_name: + - OLD: is_object_id_dynamic_() was true, used App.get_friendly_name() directly + - OLD: object_id = "" (empty) because friendly_name was empty + - NEW: Must maintain same behavior for compatibility + +2. Without MAC suffix + no friendly_name: + - OLD: is_object_id_dynamic_() was false, used pre-computed object_id_c_str_ + - OLD: Python computed object_id with fallback to device name + - NEW: Must maintain same behavior (object_id = device name) +""" + +from __future__ import annotations + +import pytest + +from esphome.helpers import fnv1_hash_object_id + +from .entity_utils import compute_object_id, verify_all_entities +from .types import APIClientConnectedFactory, RunCompiledFunction + +# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679" +MAC_SUFFIX = "abf679" + +# FNV1 offset basis - hash of empty string +FNV1_OFFSET_BASIS = 2166136261 + + +@pytest.mark.asyncio +async def test_object_id_no_friendly_name_with_mac_suffix( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test object_id when friendly_name not set but MAC suffix enabled. + + OLD behavior (bug-for-bug compatibility): + - is_object_id_dynamic_() returned true (no own name AND mac suffix enabled) + - format_dynamic_object_id() used App.get_friendly_name() directly + - Since friendly_name was empty, object_id was empty + + This was arguably a bug, but we maintain it for compatibility. + """ + async with run_compiled(yaml_config), api_client_connected() as client: + device_info = await client.device_info() + assert device_info is not None + + # Device name should include MAC suffix + expected_device_name = f"test-device-{MAC_SUFFIX}" + assert device_info.name == expected_device_name + + # Friendly name should be empty (not set in config) + assert device_info.friendly_name == "" + + entities, _ = await client.list_entities_services() + + # Find the empty-name entity + empty_name_entities = [e for e in entities if e.name == ""] + assert len(empty_name_entities) == 1 + + entity = empty_name_entities[0] + + # OLD behavior: object_id was empty because App.get_friendly_name() was empty + # This is bug-for-bug compatibility + assert entity.object_id == "", ( + f"Expected empty object_id for bug-for-bug compatibility, " + f"got '{entity.object_id}'" + ) + + # Hash should be FNV1_OFFSET_BASIS (hash of empty string) + assert entity.key == FNV1_OFFSET_BASIS, ( + f"Expected hash of empty string ({FNV1_OFFSET_BASIS:#x}), " + f"got {entity.key:#x}" + ) + + # Named entity should work normally + named_entities = [e for e in entities if e.name == "Temperature"] + assert len(named_entities) == 1 + assert named_entities[0].object_id == "temperature" + + # Verify the full algorithm from entity_utils works for ALL entities + verify_all_entities(entities, device_info) + + +@pytest.mark.asyncio +async def test_object_id_no_friendly_name_no_mac_suffix( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test object_id when friendly_name not set and no MAC suffix. + + OLD behavior: + - is_object_id_dynamic_() returned false (mac suffix not enabled) + - Used object_id_c_str_ which was pre-computed in Python + - Python used get_base_entity_object_id() with fallback to CORE.name + + Result: object_id = sanitize(snake_case(device_name)) + """ + async with run_compiled(yaml_config), api_client_connected() as client: + device_info = await client.device_info() + assert device_info is not None + + # Device name should NOT include MAC suffix + assert device_info.name == "test-device" + + # Friendly name should be empty (not set in config) + assert device_info.friendly_name == "" + + entities, _ = await client.list_entities_services() + + # Find the empty-name entity + empty_name_entities = [e for e in entities if e.name == ""] + assert len(empty_name_entities) == 1 + + entity = empty_name_entities[0] + + # OLD behavior: object_id was computed from device name + expected_object_id = compute_object_id("test-device") + assert entity.object_id == expected_object_id, ( + f"Expected object_id '{expected_object_id}' from device name, " + f"got '{entity.object_id}'" + ) + + # Hash should match device name + expected_hash = fnv1_hash_object_id("test-device") + assert entity.key == expected_hash, ( + f"Expected hash {expected_hash:#x}, got {entity.key:#x}" + ) + + # Named entity should work normally + named_entities = [e for e in entities if e.name == "Temperature"] + assert len(named_entities) == 1 + assert named_entities[0].object_id == "temperature" + + # Verify the full algorithm from entity_utils works for ALL entities + verify_all_entities(entities, device_info) diff --git a/tests/integration/test_scheduler_bulk_cleanup.py b/tests/integration/test_scheduler_bulk_cleanup.py index b52a4a3496..973f59b838 100644 --- a/tests/integration/test_scheduler_bulk_cleanup.py +++ b/tests/integration/test_scheduler_bulk_cleanup.py @@ -98,7 +98,7 @@ async def test_scheduler_bulk_cleanup( ) # Execute the test - client.execute_service(trigger_bulk_cleanup_service, {}) + await client.execute_service(trigger_bulk_cleanup_service, {}) # Wait for test completion try: diff --git a/tests/integration/test_scheduler_defer_cancel.py b/tests/integration/test_scheduler_defer_cancel.py index 34c46bab82..bf34de9677 100644 --- a/tests/integration/test_scheduler_defer_cancel.py +++ b/tests/integration/test_scheduler_defer_cancel.py @@ -81,7 +81,7 @@ async def test_scheduler_defer_cancel( client.subscribe_states(on_state) # Execute the test - client.execute_service(test_defer_cancel_service, {}) + await client.execute_service(test_defer_cancel_service, {}) # Wait for test completion try: diff --git a/tests/integration/test_scheduler_defer_cancel_regular.py b/tests/integration/test_scheduler_defer_cancel_regular.py index c93d814fbe..4c37062844 100644 --- a/tests/integration/test_scheduler_defer_cancel_regular.py +++ b/tests/integration/test_scheduler_defer_cancel_regular.py @@ -59,7 +59,7 @@ async def test_scheduler_defer_cancels_regular( assert test_service is not None, "test_defer_cancels_regular service not found" # Execute the test - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) # Wait for test completion try: diff --git a/tests/integration/test_scheduler_defer_fifo_simple.py b/tests/integration/test_scheduler_defer_fifo_simple.py index 3502302368..4c5c2b56de 100644 --- a/tests/integration/test_scheduler_defer_fifo_simple.py +++ b/tests/integration/test_scheduler_defer_fifo_simple.py @@ -84,7 +84,7 @@ async def test_scheduler_defer_fifo_simple( client.subscribe_states(on_state) # Test 1: Test set_timeout(0) - client.execute_service(test_set_timeout_service, {}) + await client.execute_service(test_set_timeout_service, {}) # Wait for first test completion try: @@ -102,7 +102,7 @@ async def test_scheduler_defer_fifo_simple( test_result_future = loop.create_future() # Test 2: Test defer() - client.execute_service(test_defer_service, {}) + await client.execute_service(test_defer_service, {}) # Wait for second test completion try: diff --git a/tests/integration/test_scheduler_defer_stress.py b/tests/integration/test_scheduler_defer_stress.py index 6f4d997307..345ba9434c 100644 --- a/tests/integration/test_scheduler_defer_stress.py +++ b/tests/integration/test_scheduler_defer_stress.py @@ -92,7 +92,7 @@ async def test_scheduler_defer_stress( assert run_stress_test_service is not None, "run_stress_test service not found" # Call the run_stress_test service to start the test - client.execute_service(run_stress_test_service, {}) + await client.execute_service(run_stress_test_service, {}) # Wait for all defers to execute (should be quick) try: diff --git a/tests/integration/test_scheduler_heap_stress.py b/tests/integration/test_scheduler_heap_stress.py index 2d55b8ae89..cceadd0661 100644 --- a/tests/integration/test_scheduler_heap_stress.py +++ b/tests/integration/test_scheduler_heap_stress.py @@ -99,7 +99,7 @@ async def test_scheduler_heap_stress( ) # Call the run_heap_stress_test service to start the test - client.execute_service(run_stress_test_service, {}) + await client.execute_service(run_stress_test_service, {}) # Wait for all callbacks to execute (should be quick, but give more time for scheduling) try: diff --git a/tests/integration/test_scheduler_null_name.py b/tests/integration/test_scheduler_null_name.py index 75864ea2d2..9eeb648d59 100644 --- a/tests/integration/test_scheduler_null_name.py +++ b/tests/integration/test_scheduler_null_name.py @@ -48,7 +48,7 @@ async def test_scheduler_null_name( assert test_null_name_service is not None, "test_null_name service not found" # Execute the test - client.execute_service(test_null_name_service, {}) + await client.execute_service(test_null_name_service, {}) # Wait for test completion try: diff --git a/tests/integration/test_scheduler_pool.py b/tests/integration/test_scheduler_pool.py index b5f9f12631..021917cc25 100644 --- a/tests/integration/test_scheduler_pool.py +++ b/tests/integration/test_scheduler_pool.py @@ -120,42 +120,42 @@ async def test_scheduler_pool( try: # Phase 1: Component lifecycle - client.execute_service(phase_services[1], {}) + await client.execute_service(phase_services[1], {}) await asyncio.wait_for(phase_futures[1], timeout=1.0) await asyncio.sleep(0.05) # Let timeouts complete # Phase 2: Sensor polling - client.execute_service(phase_services[2], {}) + await client.execute_service(phase_services[2], {}) await asyncio.wait_for(phase_futures[2], timeout=1.0) await asyncio.sleep(0.1) # Let intervals run a bit # Phase 3: Communication patterns - client.execute_service(phase_services[3], {}) + await client.execute_service(phase_services[3], {}) await asyncio.wait_for(phase_futures[3], timeout=1.0) await asyncio.sleep(0.1) # Let heartbeat run # Phase 4: Defer patterns - client.execute_service(phase_services[4], {}) + await client.execute_service(phase_services[4], {}) await asyncio.wait_for(phase_futures[4], timeout=1.0) await asyncio.sleep(0.2) # Let everything settle and recycle # Phase 5: Pool reuse verification - client.execute_service(phase_services[5], {}) + await client.execute_service(phase_services[5], {}) await asyncio.wait_for(phase_futures[5], timeout=1.0) await asyncio.sleep(0.1) # Let Phase 5 timeouts complete and recycle # Phase 6: Full pool reuse verification - client.execute_service(phase_services[6], {}) + await client.execute_service(phase_services[6], {}) await asyncio.wait_for(phase_futures[6], timeout=1.0) await asyncio.sleep(0.1) # Let Phase 6 timeouts complete # Phase 7: Same-named defer optimization - client.execute_service(phase_services[7], {}) + await client.execute_service(phase_services[7], {}) await asyncio.wait_for(phase_futures[7], timeout=1.0) await asyncio.sleep(0.05) # Let the single defer execute # Complete test - client.execute_service(complete_service, {}) + await client.execute_service(complete_service, {}) await asyncio.wait_for(test_complete_future, timeout=0.5) except TimeoutError as e: diff --git a/tests/integration/test_scheduler_rapid_cancellation.py b/tests/integration/test_scheduler_rapid_cancellation.py index 1b7da32aaa..1b67e7fc33 100644 --- a/tests/integration/test_scheduler_rapid_cancellation.py +++ b/tests/integration/test_scheduler_rapid_cancellation.py @@ -108,7 +108,7 @@ async def test_scheduler_rapid_cancellation( ) # Call the service to start the test - client.execute_service(run_test_service, {}) + await client.execute_service(run_test_service, {}) # Wait for test to complete with timeout try: diff --git a/tests/integration/test_scheduler_recursive_timeout.py b/tests/integration/test_scheduler_recursive_timeout.py index d98d2ac5ee..7d7131f8f6 100644 --- a/tests/integration/test_scheduler_recursive_timeout.py +++ b/tests/integration/test_scheduler_recursive_timeout.py @@ -79,7 +79,7 @@ async def test_scheduler_recursive_timeout( ) # Call the service to start the test - client.execute_service(run_test_service, {}) + await client.execute_service(run_test_service, {}) # Wait for test to complete try: diff --git a/tests/integration/test_scheduler_removed_item_race.py b/tests/integration/test_scheduler_removed_item_race.py index 3e72bacc0d..5c78f829a4 100644 --- a/tests/integration/test_scheduler_removed_item_race.py +++ b/tests/integration/test_scheduler_removed_item_race.py @@ -81,7 +81,7 @@ async def test_scheduler_removed_item_race( assert run_test_service is not None, "run_test service not found" # Execute the test - client.execute_service(run_test_service, {}) + await client.execute_service(run_test_service, {}) # Wait for test completion try: diff --git a/tests/integration/test_scheduler_simultaneous_callbacks.py b/tests/integration/test_scheduler_simultaneous_callbacks.py index 82fd0fc01e..66b2862eef 100644 --- a/tests/integration/test_scheduler_simultaneous_callbacks.py +++ b/tests/integration/test_scheduler_simultaneous_callbacks.py @@ -98,7 +98,7 @@ async def test_scheduler_simultaneous_callbacks( ) # Call the service to start the test - client.execute_service(run_test_service, {}) + await client.execute_service(run_test_service, {}) # Wait for test to complete try: diff --git a/tests/integration/test_scheduler_string_lifetime.py b/tests/integration/test_scheduler_string_lifetime.py index 7ec5a54373..bfa581129b 100644 --- a/tests/integration/test_scheduler_string_lifetime.py +++ b/tests/integration/test_scheduler_string_lifetime.py @@ -134,27 +134,27 @@ async def test_scheduler_string_lifetime( # Run tests sequentially, waiting for each to complete try: # Test 1 - client.execute_service(test_services["test1"], {}) + await client.execute_service(test_services["test1"], {}) await asyncio.wait_for(test1_complete.wait(), timeout=5.0) # Test 2 - client.execute_service(test_services["test2"], {}) + await client.execute_service(test_services["test2"], {}) await asyncio.wait_for(test2_complete.wait(), timeout=5.0) # Test 3 - client.execute_service(test_services["test3"], {}) + await client.execute_service(test_services["test3"], {}) await asyncio.wait_for(test3_complete.wait(), timeout=5.0) # Test 4 - client.execute_service(test_services["test4"], {}) + await client.execute_service(test_services["test4"], {}) await asyncio.wait_for(test4_complete.wait(), timeout=5.0) # Test 5 - client.execute_service(test_services["test5"], {}) + await client.execute_service(test_services["test5"], {}) await asyncio.wait_for(test5_complete.wait(), timeout=5.0) # Final check - client.execute_service(test_services["final"], {}) + await client.execute_service(test_services["final"], {}) await asyncio.wait_for(all_tests_complete.wait(), timeout=5.0) except TimeoutError: diff --git a/tests/integration/test_scheduler_string_name_stress.py b/tests/integration/test_scheduler_string_name_stress.py index 4c52913e63..56b8998c56 100644 --- a/tests/integration/test_scheduler_string_name_stress.py +++ b/tests/integration/test_scheduler_string_name_stress.py @@ -92,7 +92,7 @@ async def test_scheduler_string_name_stress( ) # Call the service to start the test - client.execute_service(run_stress_test_service, {}) + await client.execute_service(run_stress_test_service, {}) # Wait for test to complete or crash try: diff --git a/tests/integration/test_script_delay_params.py b/tests/integration/test_script_delay_params.py new file mode 100644 index 0000000000..37c72f0f7d --- /dev/null +++ b/tests/integration/test_script_delay_params.py @@ -0,0 +1,121 @@ +"""Integration test for script.wait FIFO ordering (issues #12043, #12044). + +This test verifies that ScriptWaitAction processes queued items in FIFO order. + +PR #7972 introduced bugs in ScriptWaitAction: +- Used emplace_front() causing LIFO ordering instead of FIFO +- Called loop() synchronously causing reentrancy issues +- Used while loop processing entire queue causing infinite loops + +These bugs manifested as: +- Scripts becoming "zombies" (stuck in running state) +- script.wait hanging forever +- Incorrect execution order +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_script_delay_with_params( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that script.wait processes queued items in FIFO order. + + This reproduces issues #12043 and #12044 where scripts would hang or become + zombies due to LIFO ordering bugs in ScriptWaitAction from PR #7972. + """ + test_complete = asyncio.Event() + + # Patterns to match in logs + father_calling_pattern = re.compile(r"Father iteration (\d+): calling son") + son_started_pattern = re.compile(r"Son script started with iteration (\d+)") + son_delaying_pattern = re.compile(r"Son script delaying for iteration (\d+)") + son_finished_pattern = re.compile(r"Son script finished with iteration (\d+)") + father_wait_returned_pattern = re.compile( + r"Father iteration (\d+): son finished, wait returned" + ) + + # Track which iterations completed + father_calling = set() + son_started = set() + son_delaying = set() + son_finished = set() + wait_returned = set() + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if test_complete.is_set(): + return + + if mo := father_calling_pattern.search(line): + father_calling.add(int(mo.group(1))) + elif mo := son_started_pattern.search(line): + son_started.add(int(mo.group(1))) + elif mo := son_delaying_pattern.search(line): + son_delaying.add(int(mo.group(1))) + elif mo := son_finished_pattern.search(line): + son_finished.add(int(mo.group(1))) + elif mo := father_wait_returned_pattern.search(line): + iteration = int(mo.group(1)) + wait_returned.add(iteration) + # Test completes when iteration 9 finishes + if iteration == 9: + test_complete.set() + + # Run with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "test-script-delay-params" + + # Get services + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_repeat_with_delay"), None + ) + assert test_service is not None, "test_repeat_with_delay service not found" + + # Execute the test + await client.execute_service(test_service, {}) + + # Wait for test to complete (10 iterations * ~100ms each + margin) + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail( + f"Test timed out. Completed iterations: {sorted(wait_returned)}. " + f"This likely indicates the script became a zombie (issue #12044)." + ) + + # Verify all 10 iterations completed successfully + expected_iterations = set(range(10)) + assert father_calling == expected_iterations, "Not all iterations started" + assert son_started == expected_iterations, ( + "Son script not started for all iterations" + ) + assert son_finished == expected_iterations, ( + "Son script not finished for all iterations" + ) + assert wait_returned == expected_iterations, ( + "script.wait did not return for all iterations" + ) + + # Verify delays were triggered for iterations >= 5 + expected_delays = set(range(5, 10)) + assert son_delaying == expected_delays, ( + "Delays not triggered for iterations >= 5" + ) diff --git a/tests/integration/test_script_queued.py b/tests/integration/test_script_queued.py index ce1c25b649..c86c289719 100644 --- a/tests/integration/test_script_queued.py +++ b/tests/integration/test_script_queued.py @@ -136,7 +136,7 @@ async def test_script_queued( # Test 1: Queue depth limit test_service = next((s for s in services if s.name == "test_queue_depth"), None) assert test_service is not None, "test_queue_depth service not found" - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) await asyncio.wait_for(test1_complete, timeout=2.0) await asyncio.sleep(0.1) # Give time for rejections @@ -151,7 +151,7 @@ async def test_script_queued( # Test 2: Ring buffer order test_service = next((s for s in services if s.name == "test_ring_buffer"), None) assert test_service is not None, "test_ring_buffer service not found" - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) await asyncio.wait_for(test2_complete, timeout=2.0) # Verify Test 2 @@ -165,7 +165,7 @@ async def test_script_queued( # Test 3: Stop clears queue test_service = next((s for s in services if s.name == "test_stop_clears"), None) assert test_service is not None, "test_stop_clears service not found" - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) await asyncio.wait_for(test3_complete, timeout=2.0) # Verify Test 3 @@ -179,7 +179,7 @@ async def test_script_queued( # Test 4: Rejection enforcement (max_runs=3) test_service = next((s for s in services if s.name == "test_rejection"), None) assert test_service is not None, "test_rejection service not found" - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) await asyncio.wait_for(test4_complete, timeout=2.0) await asyncio.sleep(0.1) # Give time for rejections @@ -194,7 +194,7 @@ async def test_script_queued( # Test 5: No parameters test_service = next((s for s in services if s.name == "test_no_params"), None) assert test_service is not None, "test_no_params service not found" - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) await asyncio.wait_for(test5_complete, timeout=2.0) # Verify Test 5 diff --git a/tests/integration/test_script_wait_on_boot.py b/tests/integration/test_script_wait_on_boot.py new file mode 100644 index 0000000000..478090f782 --- /dev/null +++ b/tests/integration/test_script_wait_on_boot.py @@ -0,0 +1,130 @@ +"""Integration test for script.wait during on_boot (issue #12043). + +This test verifies that script.wait works correctly when triggered from on_boot. +The issue was that ScriptWaitAction::setup() unconditionally disabled the loop, +even if play_complex() had already been called (from an on_boot trigger at the +same priority level) and enabled it. + +The race condition occurs because: +1. on_boot's default priority is 600.0 (setup_priority::DATA) +2. ScriptWaitAction's default setup priority is also DATA (600.0) +3. When they have the same priority, if on_boot runs first and triggers a script, + ScriptWaitAction::play_complex() enables the loop +4. Then ScriptWaitAction::setup() runs and unconditionally disables the loop +5. The wait never completes because the loop is disabled + +The fix adds a conditional check (like WaitUntilAction has) to only disable the +loop in setup() if num_running_ is 0. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_script_wait_on_boot( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that script.wait works correctly when triggered from on_boot. + + This reproduces issue #12043 where script.wait would hang forever when + triggered from on_boot due to a race condition in ScriptWaitAction::setup(). + """ + test_complete = asyncio.Event() + + # Track progress through the boot sequence + boot_started = False + first_script_started = False + first_script_completed = False + first_wait_returned = False + second_script_started = False + second_script_completed = False + all_completed = False + + # Patterns for boot sequence logs + boot_start_pattern = re.compile(r"on_boot: Starting boot sequence") + show_start_pattern = re.compile(r"show_start_page: Starting") + show_complete_pattern = re.compile(r"show_start_page: Completed") + first_wait_pattern = re.compile(r"on_boot: First script completed") + flip_start_pattern = re.compile(r"flip_thru_pages: Starting") + flip_complete_pattern = re.compile(r"flip_thru_pages: Completed") + all_complete_pattern = re.compile(r"on_boot: All boot scripts completed") + + def check_output(line: str) -> None: + """Check log output for boot sequence progress.""" + nonlocal boot_started, first_script_started, first_script_completed + nonlocal first_wait_returned, second_script_started, second_script_completed + nonlocal all_completed + + if boot_start_pattern.search(line): + boot_started = True + elif show_start_pattern.search(line): + first_script_started = True + elif show_complete_pattern.search(line): + first_script_completed = True + elif first_wait_pattern.search(line): + first_wait_returned = True + elif flip_start_pattern.search(line): + second_script_started = True + elif flip_complete_pattern.search(line): + second_script_completed = True + elif all_complete_pattern.search(line): + all_completed = True + test_complete.set() + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "test-script-wait-on-boot" + + # Wait for on_boot sequence to complete + # The boot sequence should complete automatically + # Timeout is generous to allow for delays in the scripts + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + # Build a detailed error message showing where the boot sequence got stuck + progress = [] + if boot_started: + progress.append("boot started") + if first_script_started: + progress.append("show_start_page started") + if first_script_completed: + progress.append("show_start_page completed") + if first_wait_returned: + progress.append("first script.wait returned") + if second_script_started: + progress.append("flip_thru_pages started") + if second_script_completed: + progress.append("flip_thru_pages completed") + + if not first_wait_returned and first_script_completed: + pytest.fail( + f"Test timed out - script.wait hung after show_start_page completed! " + f"This is the issue #12043 bug. Progress: {', '.join(progress)}" + ) + else: + pytest.fail( + f"Test timed out. Progress: {', '.join(progress) if progress else 'none'}" + ) + + # Verify the complete boot sequence executed in order + assert boot_started, "on_boot did not start" + assert first_script_started, "show_start_page did not start" + assert first_script_completed, "show_start_page did not complete" + assert first_wait_returned, "First script.wait did not return" + assert second_script_started, "flip_thru_pages did not start" + assert second_script_completed, "flip_thru_pages did not complete" + assert all_completed, "Boot sequence did not complete" diff --git a/tests/integration/test_strftime_to.py b/tests/integration/test_strftime_to.py new file mode 100644 index 0000000000..9220da148b --- /dev/null +++ b/tests/integration/test_strftime_to.py @@ -0,0 +1,111 @@ +"""Integration test for ESPTime::strftime_to() method.""" + +from __future__ import annotations + +import asyncio + +from aioesphomeapi import EntityState, TextSensorState +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_strftime_to( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test ESPTime::strftime_to() formats time correctly.""" + async with run_compiled(yaml_config), api_client_connected() as client: + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "strftime-to-test" + + # Get entities + entities, _ = await client.list_entities_services() + + # Find our text sensors + format_test = require_entity( + entities, "time_format_test", description="Time Format Test sensor" + ) + short_format = require_entity( + entities, "time_short_format", description="Time Short Format sensor" + ) + string_format = require_entity( + entities, "time_string_format", description="Time String Format sensor" + ) + error_format = require_entity( + entities, "time_error_format", description="Time Error Format sensor" + ) + + # Set up state tracking with InitialStateHelper + loop = asyncio.get_running_loop() + states: dict[int, TextSensorState] = {} + all_received = loop.create_future() + expected_keys = { + format_test.key, + short_format.key, + string_format.key, + error_format.key, + } + initial_state_helper = InitialStateHelper(entities) + + def on_state(state: EntityState) -> None: + if isinstance(state, TextSensorState) and not state.missing_state: + states[state.key] = state + if expected_keys <= states.keys() and not all_received.done(): + all_received.set_result(True) + + # Subscribe with the wrapper that filters initial states + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for initial states to be broadcast + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Wait for all expected states + try: + await asyncio.wait_for(all_received, timeout=5.0) + except TimeoutError: + pytest.fail( + f"Timeout waiting for text sensor states. Got: {list(states.keys())}" + ) + + # Validate strftime_to with full format + # Note: The exact output depends on timezone, but should contain date components + format_test_state = states[format_test.key].state + assert "2024" in format_test_state or "2023" in format_test_state, ( + f"Expected year in format test output, got: {format_test_state}" + ) + # Should have format like "YYYY-MM-DD HH:MM:SS" + assert len(format_test_state) == 19, ( + f"Expected 19 chars for datetime format, got {len(format_test_state)}: {format_test_state}" + ) + + # Validate short format (HH:MM) + short_format_state = states[short_format.key].state + assert len(short_format_state) == 5, ( + f"Expected 5 chars for HH:MM format, got {len(short_format_state)}: {short_format_state}" + ) + assert ":" in short_format_state, ( + f"Expected colon in HH:MM format, got: {short_format_state}" + ) + + # Validate string format (the std::string returning version) + string_format_state = states[string_format.key].state + assert len(string_format_state) == 10, ( + f"Expected 10 chars for YYYY-MM-DD format, got {len(string_format_state)}: {string_format_state}" + ) + assert string_format_state.count("-") == 2, ( + f"Expected two dashes in YYYY-MM-DD format, got: {string_format_state}" + ) + + # Validate error format returns "ERROR" + error_format_state = states[error_format.key].state + assert error_format_state == "ERROR", ( + f"Expected 'ERROR' for empty format string, got: {error_format_state}" + ) diff --git a/tests/integration/test_syslog.py b/tests/integration/test_syslog.py new file mode 100644 index 0000000000..b31a19392c --- /dev/null +++ b/tests/integration/test_syslog.py @@ -0,0 +1,284 @@ +"""Integration test for syslog component.""" + +from __future__ import annotations + +import asyncio +from collections.abc import AsyncGenerator +import contextlib +from contextlib import asynccontextmanager +from dataclasses import dataclass, field +import re +import socket +from typing import TypedDict + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +class ParsedSyslogMessage(TypedDict): + """Parsed syslog message components.""" + + pri: int + facility: int + severity: int + timestamp: str + hostname: str + tag: str + message: str + + +# RFC 3164 syslog message pattern: +# TIMESTAMP HOSTNAME TAG: MESSAGE +# Example: <134>Dec 20 14:30:45 syslog-test app: [D][app:029]: Running... +SYSLOG_PATTERN = re.compile( + r"<(\d+)>" # PRI (priority = facility * 8 + severity) + r"(\S+ +\d+ \d+:\d+:\d+|-)" # TIMESTAMP (BSD-style "%b %e %H:%M:%S", e.g. "Dec 20 14:30:45", or NILVALUE "-") + r" (\S+)" # HOSTNAME + r" (\S+):" # TAG + r" (.*)" # MESSAGE +) + + +@dataclass +class SyslogReceiver: + """Collects syslog messages received over UDP.""" + + messages: list[str] = field(default_factory=list) + message_received: asyncio.Event = field(default_factory=asyncio.Event) + _waiters: list[tuple[re.Pattern, asyncio.Event]] = field(default_factory=list) + + def on_message(self, msg: str) -> None: + """Called when a message is received.""" + self.messages.append(msg) + self.message_received.set() + # Check pattern waiters + for pattern, event in self._waiters: + if pattern.search(msg): + event.set() + + async def wait_for_messages(self, timeout: float = 10.0) -> None: + """Wait for at least one message to be received.""" + await asyncio.wait_for(self.message_received.wait(), timeout=timeout) + + async def wait_for_pattern(self, pattern: str, timeout: float = 5.0) -> str: + """Wait for a message matching the pattern.""" + compiled = re.compile(pattern) + event = asyncio.Event() + self._waiters.append((compiled, event)) + try: + # Check existing messages first + for msg in self.messages: + if compiled.search(msg): + return msg + # Wait for new message + await asyncio.wait_for(event.wait(), timeout=timeout) + # Find and return the matching message + for msg in reversed(self.messages): + if compiled.search(msg): + return msg + raise RuntimeError("Event set but no matching message found") + finally: + self._waiters.remove((compiled, event)) + + +@asynccontextmanager +async def syslog_udp_listener() -> AsyncGenerator[tuple[int, SyslogReceiver]]: + """Async context manager that listens for syslog UDP messages. + + Yields: + Tuple of (port, SyslogReceiver) where port is the UDP port to send to + and SyslogReceiver contains the received messages. + """ + # Create and bind UDP socket + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(("127.0.0.1", 0)) + sock.setblocking(False) + port = sock.getsockname()[1] + + receiver = SyslogReceiver() + + async def receive_messages() -> None: + """Background task to receive syslog messages.""" + loop = asyncio.get_running_loop() + while True: + try: + data = await loop.sock_recv(sock, 4096) + if data: + msg = data.decode("utf-8", errors="replace") + receiver.on_message(msg) + except BlockingIOError: + await asyncio.sleep(0.01) + except Exception: + break + + task = asyncio.create_task(receive_messages()) + try: + yield port, receiver + finally: + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task + sock.close() + + +def parse_syslog_message(msg: str) -> ParsedSyslogMessage | None: + """Parse a syslog message and return its components.""" + match = SYSLOG_PATTERN.match(msg) + if not match: + return None + pri, timestamp, hostname, tag, message = match.groups() + pri_val = int(pri) + # PRI = facility * 8 + severity + facility = pri_val // 8 + severity = pri_val % 8 + return ParsedSyslogMessage( + pri=pri_val, + facility=facility, + severity=severity, + timestamp=timestamp, + hostname=hostname, + tag=tag, + message=message, + ) + + +@pytest.mark.asyncio +async def test_syslog( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test syslog component sends properly formatted messages.""" + async with syslog_udp_listener() as (udp_port, receiver): + # Replace the placeholder port in the config + config = yaml_config.replace("SYSLOG_PORT_PLACEHOLDER", str(udp_port)) + + async with run_compiled(config), api_client_connected() as client: + # Verify device is running + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "syslog-test" + + # Wait for syslog messages (ESPHome logs during startup) + try: + await receiver.wait_for_messages(timeout=10.0) + except TimeoutError: + pytest.fail("No syslog messages received within timeout") + + # Give it a moment to collect more messages + await asyncio.sleep(0.5) + + # Verify we received messages + assert len(receiver.messages) > 0, "No syslog messages received" + + # Parse and validate all messages + parsed_messages: list[ParsedSyslogMessage] = [] + for msg in receiver.messages: + parsed = parse_syslog_message(msg) + if parsed: + parsed_messages.append(parsed) + + assert len(parsed_messages) > 0, ( + f"No valid syslog messages found. Received: {receiver.messages[:5]}" + ) + + # Validate message format for all parsed messages + for parsed in parsed_messages: + # Validate PRI is in valid range (0-191) + assert 0 <= parsed["pri"] <= 191, f"Invalid PRI: {parsed['pri']}" + + # Validate facility matches config (16 = local0) + assert parsed["facility"] == 16, ( + f"Expected facility 16, got {parsed['facility']}" + ) + + # Validate severity is in valid range (0-7) + assert 0 <= parsed["severity"] <= 7, ( + f"Invalid severity: {parsed['severity']}" + ) + + # Validate hostname matches device name + assert parsed["hostname"] == "syslog-test", ( + f"Unexpected hostname: {parsed['hostname']}" + ) + + # Validate timestamp format (BSD or NILVALUE) + if parsed["timestamp"] != "-": + assert re.match( + r"[A-Z][a-z]{2} +\d+ \d{2}:\d{2}:\d{2}", + parsed["timestamp"], + ), f"Invalid timestamp format: {parsed['timestamp']}" + + # Verify we see different severity levels in the logs + severities_seen = {p["severity"] for p in parsed_messages} + # ESPHome startup logs should include at least INFO (5) or DEBUG (7) + assert len(severities_seen) >= 1, "Expected to see at least one severity" + + # Verify messages don't contain ANSI color codes (strip=true) + for parsed in parsed_messages: + assert "\x1b[" not in parsed["message"], ( + f"Color codes not stripped: {parsed['message'][:50]}" + ) + + # Verify message content is not empty for most messages + non_empty_messages = [p for p in parsed_messages if p["message"].strip()] + assert len(non_empty_messages) > 0, "All messages are empty" + + # Verify tag format (should be component name like "app", "wifi", etc.) + for parsed in parsed_messages: + assert len(parsed["tag"]) > 0, "Empty tag" + # Tag should not contain spaces or colons + assert " " not in parsed["tag"], f"Tag contains space: {parsed['tag']}" + + # Test message truncation - call service that logs a very long message + _, services = await client.list_entities_services() + log_service = next( + (s for s in services if s.name == "log_long_message"), None + ) + assert log_service is not None, "log_long_message service not found" + + # Call the service to trigger a long log message + await client.execute_service(log_service, {}) + + # Wait specifically for the truncation test message + try: + trunc_msg = await receiver.wait_for_pattern(r"trunctest.*START\|") + except TimeoutError: + pytest.fail( + f"Truncation test message not received. Got: {receiver.messages}" + ) + + # Verify message is truncated to max 508 bytes + assert len(trunc_msg) <= 508, f"Message exceeds 508 bytes: {len(trunc_msg)}" + + # Verify the message starts correctly but is truncated (no "|END") + assert "START|" in trunc_msg, "Message should contain START marker" + assert "|END" not in trunc_msg, ( + "Message should be truncated before END marker" + ) + + # Test short message - should arrive complete (not truncated) + short_service = next( + (s for s in services if s.name == "log_short_message"), None + ) + assert short_service is not None, "log_short_message service not found" + + await client.execute_service(short_service, {}) + + try: + short_msg = await receiver.wait_for_pattern(r"shorttest.*BEGIN\|") + except TimeoutError: + pytest.fail( + f"Short test message not received. Got: {receiver.messages[-10:]}" + ) + + # Verify short message arrived complete with both markers + assert "BEGIN|" in short_msg, "Short message missing BEGIN marker" + assert "|FINISH" in short_msg, ( + f"Short message truncated unexpectedly: {short_msg}" + ) + assert "SHORT_MESSAGE_CONTENT" in short_msg, ( + f"Short message content missing: {short_msg}" + ) diff --git a/tests/integration/test_template_alarm_control_panel_many_sensors.py b/tests/integration/test_template_alarm_control_panel_many_sensors.py new file mode 100644 index 0000000000..856815c731 --- /dev/null +++ b/tests/integration/test_template_alarm_control_panel_many_sensors.py @@ -0,0 +1,118 @@ +"""Integration test for template alarm control panel with many sensors.""" + +from __future__ import annotations + +import aioesphomeapi +from aioesphomeapi.model import APIIntEnum +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +class EspHomeACPFeatures(APIIntEnum): + """ESPHome AlarmControlPanel feature numbers.""" + + ARM_HOME = 1 + ARM_AWAY = 2 + ARM_NIGHT = 4 + TRIGGER = 8 + ARM_CUSTOM_BYPASS = 16 + ARM_VACATION = 32 + + +@pytest.mark.asyncio +async def test_template_alarm_control_panel_many_sensors( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test template alarm control panel with 10 binary sensors using FixedVector.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get entity info first + entities, _ = await client.list_entities_services() + + # Find the alarm control panel and binary sensors + alarm_info: aioesphomeapi.AlarmControlPanelInfo | None = None + binary_sensors: list[aioesphomeapi.BinarySensorInfo] = [] + + for entity in entities: + if isinstance(entity, aioesphomeapi.AlarmControlPanelInfo): + alarm_info = entity + elif isinstance(entity, aioesphomeapi.BinarySensorInfo): + binary_sensors.append(entity) + + assert alarm_info is not None, "Alarm control panel entity info not found" + assert alarm_info.name == "Test Alarm" + assert alarm_info.requires_code is True + assert alarm_info.requires_code_to_arm is True + + # Verify we have 10 binary sensors + assert len(binary_sensors) == 10, ( + f"Expected 10 binary sensors, got {len(binary_sensors)}" + ) + + # Verify sensor names + expected_sensor_names = { + "Door 1", + "Door 2", + "Window 1", + "Window 2", + "Motion 1", + "Motion 2", + "Glass Break 1", + "Glass Break 2", + "Smoke Detector", + "CO Detector", + } + actual_sensor_names = {sensor.name for sensor in binary_sensors} + assert actual_sensor_names == expected_sensor_names, ( + f"Sensor names mismatch. Expected: {expected_sensor_names}, " + f"Got: {actual_sensor_names}" + ) + + # Use InitialStateHelper to wait for all initial states + state_helper = InitialStateHelper(entities) + + def on_state(state: aioesphomeapi.EntityState) -> None: + # We'll receive subsequent states here after initial states + pass + + client.subscribe_states(state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states + await state_helper.wait_for_initial_states(timeout=5.0) + + # Verify the alarm state is disarmed initially + alarm_state = state_helper.initial_states.get(alarm_info.key) + assert alarm_state is not None, "Alarm control panel initial state not received" + assert isinstance(alarm_state, aioesphomeapi.AlarmControlPanelEntityState) + assert alarm_state.state == aioesphomeapi.AlarmControlPanelState.DISARMED, ( + f"Expected initial state DISARMED, got {alarm_state.state}" + ) + + # Verify all 10 binary sensors have initial states + binary_sensor_states = [ + state_helper.initial_states.get(sensor.key) for sensor in binary_sensors + ] + assert all(state is not None for state in binary_sensor_states), ( + "Not all binary sensors have initial states" + ) + + # Verify all binary sensor states are BinarySensorState type + for i, state in enumerate(binary_sensor_states): + assert isinstance(state, aioesphomeapi.BinarySensorState), ( + f"Binary sensor {i} state is not BinarySensorState: {type(state)}" + ) + + # Verify supported features + expected_features = ( + EspHomeACPFeatures.ARM_HOME + | EspHomeACPFeatures.ARM_AWAY + | EspHomeACPFeatures.ARM_NIGHT + | EspHomeACPFeatures.TRIGGER + ) + assert alarm_info.supported_features == expected_features, ( + f"Expected supported_features={expected_features} (ARM_HOME|ARM_AWAY|ARM_NIGHT|TRIGGER), " + f"got {alarm_info.supported_features}" + ) diff --git a/tests/integration/test_text_command.py b/tests/integration/test_text_command.py new file mode 100644 index 0000000000..82fe981578 --- /dev/null +++ b/tests/integration/test_text_command.py @@ -0,0 +1,126 @@ +"""Integration test for text command zero-copy optimization. + +Tests that TextCommandRequest correctly handles the pointer_to_buffer +optimization for the state field, ensuring text values are properly +transmitted via the API. +""" + +from __future__ import annotations + +import asyncio +from typing import Any + +from aioesphomeapi import TextInfo, TextState +import pytest + +from .state_utils import InitialStateHelper, require_entity +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_text_command( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test text command with various string values including edge cases.""" + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + # Verify we can get device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "host-text-command-test" + + # Get list of entities + entities, _ = await client.list_entities_services() + + # Find our text entities using require_entity + test_text = require_entity(entities, "test_text", TextInfo, "Test Text entity") + test_password = require_entity( + entities, "test_password", TextInfo, "Test Password entity" + ) + test_text_long = require_entity( + entities, "test_text_long", TextInfo, "Test Text Long entity" + ) + + # Track state changes + states: dict[int, Any] = {} + state_futures: dict[int, asyncio.Future[Any]] = {} + + def on_state(state: Any) -> None: + states[state.key] = state + if state.key in state_futures and not state_futures[state.key].done(): + state_futures[state.key].set_result(state) + + # Set up InitialStateHelper to swallow initial state broadcasts + initial_state_helper = InitialStateHelper(entities) + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states to be received + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Verify initial states were received + assert test_text.key in initial_state_helper.initial_states + initial_text_state = initial_state_helper.initial_states[test_text.key] + assert isinstance(initial_text_state, TextState) + assert initial_text_state.state == "initial" + + async def wait_for_state_change(key: int, timeout: float = 2.0) -> Any: + """Wait for a state change for the given entity key.""" + state_futures[key] = loop.create_future() + try: + return await asyncio.wait_for(state_futures[key], timeout) + finally: + state_futures.pop(key, None) + + # Test 1: Simple text value + client.text_command(key=test_text.key, state="hello world") + state = await wait_for_state_change(test_text.key) + assert state.state == "hello world" + + # Test 2: Empty string (edge case for zero-copy) + client.text_command(key=test_text.key, state="") + state = await wait_for_state_change(test_text.key) + assert state.state == "" + + # Test 3: Single character + client.text_command(key=test_text.key, state="x") + state = await wait_for_state_change(test_text.key) + assert state.state == "x" + + # Test 4: String with special characters + client.text_command(key=test_text.key, state="hello\tworld\n!") + state = await wait_for_state_change(test_text.key) + assert state.state == "hello\tworld\n!" + + # Test 5: Unicode characters + client.text_command(key=test_text.key, state="hello 世界 🌍") + state = await wait_for_state_change(test_text.key) + assert state.state == "hello 世界 🌍" + + # Test 6: Long string (tests buffer handling) + long_text = "a" * 200 + client.text_command(key=test_text_long.key, state=long_text) + state = await wait_for_state_change(test_text_long.key) + assert state.state == long_text + assert len(state.state) == 200 + + # Test 7: Password field (same mechanism, different mode) + client.text_command(key=test_password.key, state="newpassword123") + state = await wait_for_state_change(test_password.key) + assert state.state == "newpassword123" + + # Test 8: String with null bytes embedded (edge case) + # Note: protobuf strings should handle this but it's good to verify + client.text_command(key=test_text.key, state="before\x00after") + state = await wait_for_state_change(test_text.key) + assert state.state == "before\x00after" + + # Test 9: Rapid successive commands (tests buffer reuse) + for i in range(5): + client.text_command(key=test_text.key, state=f"rapid_{i}") + state = await wait_for_state_change(test_text.key) + assert state.state == f"rapid_{i}" diff --git a/tests/integration/test_text_sensor_raw_state.py b/tests/integration/test_text_sensor_raw_state.py new file mode 100644 index 0000000000..482ebbe9c2 --- /dev/null +++ b/tests/integration/test_text_sensor_raw_state.py @@ -0,0 +1,274 @@ +"""Integration test for TextSensor get_raw_state() and StringRef-based filters. + +This tests: +1. The optimization in PR #12205 where raw_state is only stored when filters + are configured. When no filters exist, get_raw_state() should return state. +2. StringRef-based filters (append, prepend, substitute, map) which store + static string data in flash instead of heap-allocating std::string. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_text_sensor_raw_state( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test text sensor filters and raw_state behavior. + + Tests: + 1. get_raw_state() without filters returns same as state + 2. get_raw_state() with filters returns original (unfiltered) value + 3. StringRef-based filters: append, prepend, substitute, map, chained + """ + loop = asyncio.get_running_loop() + + # Futures to track log messages + no_filter_future: asyncio.Future[tuple[str, str]] = loop.create_future() + with_filter_future: asyncio.Future[tuple[str, str]] = loop.create_future() + append_future: asyncio.Future[str] = loop.create_future() + prepend_future: asyncio.Future[str] = loop.create_future() + substitute_future: asyncio.Future[str] = loop.create_future() + map_on_future: asyncio.Future[str] = loop.create_future() + map_off_future: asyncio.Future[str] = loop.create_future() + map_unknown_future: asyncio.Future[str] = loop.create_future() + chained_future: asyncio.Future[str] = loop.create_future() + + # Patterns to match log output + # NO_FILTER: state='hello world' raw_state='hello world' + no_filter_pattern = re.compile(r"NO_FILTER: state='([^']*)' raw_state='([^']*)'") + # WITH_FILTER: state='HELLO WORLD' raw_state='hello world' + with_filter_pattern = re.compile( + r"WITH_FILTER: state='([^']*)' raw_state='([^']*)'" + ) + # StringRef-based filter patterns + append_pattern = re.compile(r"APPEND: state='([^']*)'") + prepend_pattern = re.compile(r"PREPEND: state='([^']*)'") + substitute_pattern = re.compile(r"SUBSTITUTE: state='([^']*)'") + map_on_pattern = re.compile(r"MAP_ON: state='([^']*)'") + map_off_pattern = re.compile(r"MAP_OFF: state='([^']*)'") + map_unknown_pattern = re.compile(r"MAP_UNKNOWN: state='([^']*)'") + chained_pattern = re.compile(r"CHAINED: state='([^']*)'") + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if not no_filter_future.done() and (match := no_filter_pattern.search(line)): + no_filter_future.set_result((match.group(1), match.group(2))) + + if not with_filter_future.done() and ( + match := with_filter_pattern.search(line) + ): + with_filter_future.set_result((match.group(1), match.group(2))) + + if not append_future.done() and (match := append_pattern.search(line)): + append_future.set_result(match.group(1)) + + if not prepend_future.done() and (match := prepend_pattern.search(line)): + prepend_future.set_result(match.group(1)) + + if not substitute_future.done() and (match := substitute_pattern.search(line)): + substitute_future.set_result(match.group(1)) + + if not map_on_future.done() and (match := map_on_pattern.search(line)): + map_on_future.set_result(match.group(1)) + + if not map_off_future.done() and (match := map_off_pattern.search(line)): + map_off_future.set_result(match.group(1)) + + if not map_unknown_future.done() and ( + match := map_unknown_pattern.search(line) + ): + map_unknown_future.set_result(match.group(1)) + + if not chained_future.done() and (match := chained_pattern.search(line)): + chained_future.set_result(match.group(1)) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "test-text-sensor-raw-state" + + # Get entities to find our buttons + entities, _ = await client.list_entities_services() + + # Find the test buttons + no_filter_button = next( + (e for e in entities if "test_no_filter_button" in e.object_id.lower()), + None, + ) + assert no_filter_button is not None, "Test No Filter Button not found" + + with_filter_button = next( + (e for e in entities if "test_with_filter_button" in e.object_id.lower()), + None, + ) + assert with_filter_button is not None, "Test With Filter Button not found" + + # Test 1: Text sensor without filters + # get_raw_state() should return the same as state + client.button_command(no_filter_button.key) + + try: + state, raw_state = await asyncio.wait_for(no_filter_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for NO_FILTER log message") + + assert state == "hello world", f"Expected state='hello world', got '{state}'" + assert raw_state == "hello world", ( + f"Expected raw_state='hello world', got '{raw_state}'" + ) + assert state == raw_state, ( + f"Without filters, state and raw_state should be equal. " + f"state='{state}', raw_state='{raw_state}'" + ) + + # Test 2: Text sensor with to_upper filter + # state should be filtered (uppercase), raw_state should be original + client.button_command(with_filter_button.key) + + try: + state, raw_state = await asyncio.wait_for(with_filter_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for WITH_FILTER log message") + + assert state == "HELLO WORLD", f"Expected state='HELLO WORLD', got '{state}'" + assert raw_state == "hello world", ( + f"Expected raw_state='hello world', got '{raw_state}'" + ) + assert state != raw_state, ( + f"With filters, state and raw_state should differ. " + f"state='{state}', raw_state='{raw_state}'" + ) + + # Test 3: Append filter (StringRef-based) + # "test" + " suffix" = "test suffix" + append_button = next( + (e for e in entities if "test_append_button" in e.object_id.lower()), + None, + ) + assert append_button is not None, "Test Append Button not found" + client.button_command(append_button.key) + + try: + state = await asyncio.wait_for(append_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for APPEND log message") + + assert state == "test suffix", ( + f"Append failed: expected 'test suffix', got '{state}'" + ) + + # Test 4: Prepend filter (StringRef-based) + # "prefix " + "test" = "prefix test" + prepend_button = next( + (e for e in entities if "test_prepend_button" in e.object_id.lower()), + None, + ) + assert prepend_button is not None, "Test Prepend Button not found" + client.button_command(prepend_button.key) + + try: + state = await asyncio.wait_for(prepend_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for PREPEND log message") + + assert state == "prefix test", ( + f"Prepend failed: expected 'prefix test', got '{state}'" + ) + + # Test 5: Substitute filter (StringRef-based) + # "foo says hello" with foo->bar, hello->world = "bar says world" + substitute_button = next( + (e for e in entities if "test_substitute_button" in e.object_id.lower()), + None, + ) + assert substitute_button is not None, "Test Substitute Button not found" + client.button_command(substitute_button.key) + + try: + state = await asyncio.wait_for(substitute_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for SUBSTITUTE log message") + + assert state == "bar says world", ( + f"Substitute failed: expected 'bar says world', got '{state}'" + ) + + # Test 6: Map filter - "ON" -> "Active" + map_on_button = next( + (e for e in entities if "test_map_on_button" in e.object_id.lower()), + None, + ) + assert map_on_button is not None, "Test Map ON Button not found" + client.button_command(map_on_button.key) + + try: + state = await asyncio.wait_for(map_on_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for MAP_ON log message") + + assert state == "Active", f"Map ON failed: expected 'Active', got '{state}'" + + # Test 7: Map filter - "OFF" -> "Inactive" + map_off_button = next( + (e for e in entities if "test_map_off_button" in e.object_id.lower()), + None, + ) + assert map_off_button is not None, "Test Map OFF Button not found" + client.button_command(map_off_button.key) + + try: + state = await asyncio.wait_for(map_off_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for MAP_OFF log message") + + assert state == "Inactive", ( + f"Map OFF failed: expected 'Inactive', got '{state}'" + ) + + # Test 8: Map filter - passthrough for unknown values + # "UNKNOWN" -> "UNKNOWN" (no match, passes through unchanged) + map_unknown_button = next( + (e for e in entities if "test_map_unknown_button" in e.object_id.lower()), + None, + ) + assert map_unknown_button is not None, "Test Map Unknown Button not found" + client.button_command(map_unknown_button.key) + + try: + state = await asyncio.wait_for(map_unknown_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for MAP_UNKNOWN log message") + + assert state == "UNKNOWN", ( + f"Map passthrough failed: expected 'UNKNOWN', got '{state}'" + ) + + # Test 9: Chained filters (prepend "[" + append "]") + # "[" + "value" + "]" = "[value]" + chained_button = next( + (e for e in entities if "test_chained_button" in e.object_id.lower()), + None, + ) + assert chained_button is not None, "Test Chained Button not found" + client.button_command(chained_button.key) + + try: + state = await asyncio.wait_for(chained_future, timeout=5.0) + except TimeoutError: + pytest.fail("Timeout waiting for CHAINED log message") + + assert state == "[value]", f"Chained failed: expected '[value]', got '{state}'" diff --git a/tests/integration/test_wait_until_mid_loop_timing.py b/tests/integration/test_wait_until_mid_loop_timing.py index 01cad747ae..b5dd1a0028 100644 --- a/tests/integration/test_wait_until_mid_loop_timing.py +++ b/tests/integration/test_wait_until_mid_loop_timing.py @@ -86,7 +86,7 @@ async def test_wait_until_mid_loop_timing( assert test_service is not None, "test_mid_loop_timeout service not found" # Execute the test - client.execute_service(test_service, {}) + await client.execute_service(test_service, {}) # Wait for test to complete (100ms delay + 200ms timeout + margins = ~500ms) await asyncio.wait_for(test_complete, timeout=5.0) diff --git a/tests/integration/test_wait_until_on_boot.py b/tests/integration/test_wait_until_on_boot.py index b42c530c54..da21a43200 100644 --- a/tests/integration/test_wait_until_on_boot.py +++ b/tests/integration/test_wait_until_on_boot.py @@ -74,7 +74,7 @@ async def test_wait_until_on_boot( ) assert set_flag_service is not None, "set_test_flag service not found" - client.execute_service(set_flag_service, {}) + await client.execute_service(set_flag_service, {}) # If the fix works, wait_until's loop() will check the condition and proceed # If the bug exists, wait_until is stuck with disabled loop and will timeout diff --git a/tests/integration/test_wait_until_ordering.py b/tests/integration/test_wait_until_ordering.py new file mode 100644 index 0000000000..96b3a8aed0 --- /dev/null +++ b/tests/integration/test_wait_until_ordering.py @@ -0,0 +1,90 @@ +"""Integration test for wait_until FIFO ordering. + +This test verifies that when multiple wait_until actions are queued, +they execute in FIFO (First In First Out) order, not LIFO. + +PR #7972 introduced a bug where emplace_front() was used, causing +LIFO ordering which is incorrect. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_fifo_ordering( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that wait_until executes queued items in FIFO order. + + With the bug (using emplace_front), the order would be 4,3,2,1,0 (LIFO). + With the fix (using emplace_back), the order should be 0,1,2,3,4 (FIFO). + """ + test_complete = asyncio.Event() + + # Track completion order + completed_order = [] + + # Patterns to match + queuing_pattern = re.compile(r"Queueing iteration (\d+)") + completed_pattern = re.compile(r"Completed iteration (\d+)") + + def check_output(line: str) -> None: + """Check log output for completion order.""" + if test_complete.is_set(): + return + + if mo := queuing_pattern.search(line): + iteration = int(mo.group(1)) + + elif mo := completed_pattern.search(line): + iteration = int(mo.group(1)) + completed_order.append(iteration) + + # Test completes when all 5 have completed + if len(completed_order) == 5: + test_complete.set() + + # Run with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "test-wait-until-ordering" + + # Get services + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_wait_until_fifo"), None + ) + assert test_service is not None, "test_wait_until_fifo service not found" + + # Execute the test + await client.execute_service(test_service, {}) + + # Wait for test to complete + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail( + f"Test timed out. Completed order: {completed_order}. " + f"Expected 5 completions but got {len(completed_order)}." + ) + + # Verify FIFO order + expected_order = [0, 1, 2, 3, 4] + assert completed_order == expected_order, ( + f"Unexpected order: {completed_order}. " + f"Expected FIFO order: {expected_order}" + ) diff --git a/tests/integration/test_water_heater_template.py b/tests/integration/test_water_heater_template.py new file mode 100644 index 0000000000..b5f1fb64c0 --- /dev/null +++ b/tests/integration/test_water_heater_template.py @@ -0,0 +1,109 @@ +"""Integration test for template water heater component.""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import WaterHeaterInfo, WaterHeaterMode, WaterHeaterState +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_water_heater_template( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test template water heater basic state and mode changes.""" + loop = asyncio.get_running_loop() + async with run_compiled(yaml_config), api_client_connected() as client: + states: dict[int, aioesphomeapi.EntityState] = {} + gas_mode_future: asyncio.Future[WaterHeaterState] = loop.create_future() + eco_mode_future: asyncio.Future[WaterHeaterState] = loop.create_future() + + def on_state(state: aioesphomeapi.EntityState) -> None: + states[state.key] = state + if isinstance(state, WaterHeaterState): + # Wait for GAS mode + if state.mode == WaterHeaterMode.GAS and not gas_mode_future.done(): + gas_mode_future.set_result(state) + # Wait for ECO mode (we start at OFF, so test transitioning to ECO) + elif state.mode == WaterHeaterMode.ECO and not eco_mode_future.done(): + eco_mode_future.set_result(state) + + # Get entities and set up state synchronization + entities, services = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + water_heater_infos = [e for e in entities if isinstance(e, WaterHeaterInfo)] + assert len(water_heater_infos) == 1, ( + f"Expected exactly 1 water heater entity, got {len(water_heater_infos)}. Entity types: {[type(e).__name__ for e in entities]}" + ) + + test_water_heater = water_heater_infos[0] + + # Verify water heater entity info + assert test_water_heater.object_id == "test_boiler" + assert test_water_heater.name == "Test Boiler" + assert test_water_heater.min_temperature == 30.0 + assert test_water_heater.max_temperature == 85.0 + assert test_water_heater.target_temperature_step == 0.5 + + # Verify supported modes + supported_modes = test_water_heater.supported_modes + assert WaterHeaterMode.OFF in supported_modes, "Expected OFF in supported modes" + assert WaterHeaterMode.ECO in supported_modes, "Expected ECO in supported modes" + assert WaterHeaterMode.GAS in supported_modes, "Expected GAS in supported modes" + assert WaterHeaterMode.PERFORMANCE in supported_modes, ( + "Expected PERFORMANCE in supported modes" + ) + assert len(supported_modes) == 4, ( + f"Expected 4 supported modes, got {len(supported_modes)}: {supported_modes}" + ) + + # Subscribe with the wrapper that filters initial states + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states to be broadcast + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Get initial state and verify + initial_state = initial_state_helper.initial_states.get(test_water_heater.key) + assert initial_state is not None, "Water heater initial state not found" + assert isinstance(initial_state, WaterHeaterState) + # Initial mode is OFF (default) since we don't have a mode lambda + # A mode lambda would override optimistic mode changes + assert initial_state.mode == WaterHeaterMode.OFF, ( + f"Expected initial mode OFF, got {initial_state.mode}" + ) + assert initial_state.current_temperature == 45.0, ( + f"Expected current temp 45.0, got {initial_state.current_temperature}" + ) + + # Test changing to GAS mode + client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS) + + try: + gas_state = await asyncio.wait_for(gas_mode_future, timeout=5.0) + except TimeoutError: + pytest.fail("GAS mode change not received within 5 seconds") + + assert isinstance(gas_state, WaterHeaterState) + assert gas_state.mode == WaterHeaterMode.GAS + + # Test changing to ECO mode (from GAS) + client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.ECO) + + try: + eco_state = await asyncio.wait_for(eco_mode_future, timeout=5.0) + except TimeoutError: + pytest.fail("ECO mode change not received within 5 seconds") + + assert isinstance(eco_state, WaterHeaterState) + assert eco_state.mode == WaterHeaterMode.ECO diff --git a/tests/script/test_clang_tidy_hash.py b/tests/script/test_clang_tidy_hash.py index b1690a6a2d..e19e7886a2 100644 --- a/tests/script/test_clang_tidy_hash.py +++ b/tests/script/test_clang_tidy_hash.py @@ -49,7 +49,7 @@ def test_calculate_clang_tidy_hash_with_sdkconfig(tmp_path: Path) -> None: clang_tidy_content = b"Checks: '-*,readability-*'\n" requirements_version = "clang-tidy==18.1.5" platformio_content = b"[env:esp32]\nplatform = espressif32\n" - sdkconfig_content = b"CONFIG_AUTOSTART_ARDUINO=y\n" + sdkconfig_content = b"" requirements_content = "clang-tidy==18.1.5\n" # Create temporary files diff --git a/tests/test_build_components/build_components_base.esp32-c5-idf.yaml b/tests/test_build_components/build_components_base.esp32-c5-idf.yaml new file mode 100644 index 0000000000..6468297e9a --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-c5-idf.yaml @@ -0,0 +1,17 @@ +esphome: + name: componenttestesp32c5idf + friendly_name: $component_name + +esp32: + board: esp32-c5-devkitc-1 + framework: + type: esp-idf + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.rtl87xx-ard.yaml b/tests/test_build_components/build_components_base.rtl87xx-ard.yaml new file mode 100644 index 0000000000..1720ef700d --- /dev/null +++ b/tests/test_build_components/build_components_base.rtl87xx-ard.yaml @@ -0,0 +1,15 @@ +esphome: + name: componenttestesprtl87xx + friendly_name: $component_name + +rtl87xx: + board: generic-rtl8710bn-2mb-788k + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/test_build_components/common/uart_1200_none_2stopbits/esp32-ard.yaml b/tests/test_build_components/common/uart_1200_none_2stopbits/esp32-ard.yaml new file mode 100644 index 0000000000..41ded5a763 --- /dev/null +++ b/tests/test_build_components/common/uart_1200_none_2stopbits/esp32-ard.yaml @@ -0,0 +1,13 @@ +# Common UART configuration for ESP32 Arduino tests - 1200 baud NONE parity 2 stop bits + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: NONE + stop_bits: 2 diff --git a/tests/test_build_components/common/uart_1200_none_2stopbits/esp32-c3-ard.yaml b/tests/test_build_components/common/uart_1200_none_2stopbits/esp32-c3-ard.yaml new file mode 100644 index 0000000000..1eb5d6d5f9 --- /dev/null +++ b/tests/test_build_components/common/uart_1200_none_2stopbits/esp32-c3-ard.yaml @@ -0,0 +1,13 @@ +# Common UART configuration for ESP32-C3 Arduino tests - 1200 baud NONE parity 2 stop bits + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: NONE + stop_bits: 2 diff --git a/tests/test_build_components/common/uart_1200_none_2stopbits/esp32-c3-idf.yaml b/tests/test_build_components/common/uart_1200_none_2stopbits/esp32-c3-idf.yaml new file mode 100644 index 0000000000..5181995a40 --- /dev/null +++ b/tests/test_build_components/common/uart_1200_none_2stopbits/esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +# Common UART configuration for ESP32-C3 IDF tests - 1200 baud NONE parity 2 stop bits + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: NONE + stop_bits: 2 diff --git a/tests/test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml b/tests/test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml new file mode 100644 index 0000000000..122f05aced --- /dev/null +++ b/tests/test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml @@ -0,0 +1,13 @@ +# Common UART configuration for ESP32 IDF tests - 1200 baud NONE parity 2 stop bits + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: NONE + stop_bits: 2 diff --git a/tests/test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml b/tests/test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml new file mode 100644 index 0000000000..3bffabf82d --- /dev/null +++ b/tests/test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml @@ -0,0 +1,13 @@ +# Common UART configuration for ESP8266 Arduino tests - 1200 baud NONE parity 2 stop bits + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: NONE + stop_bits: 2 diff --git a/tests/test_build_components/common/uart_1200_none_2stopbits/rp2040-ard.yaml b/tests/test_build_components/common/uart_1200_none_2stopbits/rp2040-ard.yaml new file mode 100644 index 0000000000..fb94939090 --- /dev/null +++ b/tests/test_build_components/common/uart_1200_none_2stopbits/rp2040-ard.yaml @@ -0,0 +1,13 @@ +# Common UART configuration for RP2040 Arduino tests - 1200 baud NONE parity 2 stop bits + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: NONE + stop_bits: 2 diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index fc61841500..1a1bfffd03 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -58,6 +58,7 @@ def mock_write_file_if_changed() -> Generator[Mock, None, None]: def mock_copy_file_if_changed() -> Generator[Mock, None, None]: """Mock copy_file_if_changed for core.config.""" with patch("esphome.core.config.copy_file_if_changed") as mock: + mock.return_value = True yield mock diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 90b2f5edba..ab7bdbb98c 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -892,3 +892,74 @@ async def test_add_includes_overwrites_existing_files( mock_copy_file_if_changed.assert_called_once_with( include_file, CORE.build_path / "src" / "header.h" ) + + +def test_config_hash_returns_int() -> None: + """Test that config_hash returns an integer.""" + CORE.reset() + CORE.config = {"esphome": {"name": "test"}} + assert isinstance(CORE.config_hash, int) + + +def test_config_hash_is_cached() -> None: + """Test that config_hash is computed once and cached.""" + CORE.reset() + CORE.config = {"esphome": {"name": "test"}} + + # First access computes the hash + hash1 = CORE.config_hash + + # Modify config (without resetting cache) + CORE.config = {"esphome": {"name": "different"}} + + # Second access returns cached value + hash2 = CORE.config_hash + + assert hash1 == hash2 + + +def test_config_hash_reset_clears_cache() -> None: + """Test that reset() clears the cached config_hash.""" + CORE.reset() + CORE.config = {"esphome": {"name": "test"}} + hash1 = CORE.config_hash + + # Reset clears the cache + CORE.reset() + CORE.config = {"esphome": {"name": "different"}} + + hash2 = CORE.config_hash + + # After reset, hash should be recomputed + assert hash1 != hash2 + + +def test_config_hash_deterministic_key_order() -> None: + """Test that config_hash is deterministic regardless of key insertion order.""" + CORE.reset() + # Create two configs with same content but different key order + config1 = {"z_key": 1, "a_key": 2, "nested": {"z_nested": "z", "a_nested": "a"}} + config2 = {"a_key": 2, "z_key": 1, "nested": {"a_nested": "a", "z_nested": "z"}} + + CORE.config = config1 + hash1 = CORE.config_hash + + CORE.reset() + CORE.config = config2 + hash2 = CORE.config_hash + + # Hashes should be equal because keys are sorted during serialization + assert hash1 == hash2 + + +def test_config_hash_different_for_different_configs() -> None: + """Test that different configs produce different hashes.""" + CORE.reset() + CORE.config = {"esphome": {"name": "test1"}} + hash1 = CORE.config_hash + + CORE.reset() + CORE.config = {"esphome": {"name": "test2"}} + hash2 = CORE.config_hash + + assert hash1 != hash2 diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 9ba5367413..a58d4784ce 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -27,8 +27,9 @@ from esphome.helpers import sanitize, snake_case from .common import load_config_from_fixture -# Pre-compiled regex pattern for extracting object IDs from expressions -OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)') +# Pre-compiled regex pattern for extracting names from set_name calls +# Matches: .set_name("name", hash) or .set_name("name") +SET_NAME_PATTERN = re.compile(r'\.set_name\(["\']([^"\']*)["\']') FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers" @@ -271,12 +272,21 @@ def setup_test_environment() -> Generator[list[str], None, None]: def extract_object_id_from_expressions(expressions: list[str]) -> str | None: - """Extract the object ID that was set from the generated expressions.""" + """Extract the object ID that would be computed from set_name calls. + + Since object_id is now computed from the name (via snake_case + sanitize), + we extract the name from set_name() calls and compute the expected object_id. + For empty names, we fall back to CORE.friendly_name or CORE.name. + """ for expr in expressions: - # Look for set_object_id calls with regex to handle various formats - # Matches: var.set_object_id("temperature_2") or var.set_object_id('temperature_2') - if match := OBJECT_ID_PATTERN.search(expr): - return match.group(1) + if match := SET_NAME_PATTERN.search(expr): + name = match.group(1) + if name: + return sanitize(snake_case(name)) + # Empty name - fall back to friendly_name or device name + if CORE.friendly_name: + return sanitize(snake_case(CORE.friendly_name)) + return sanitize(snake_case(CORE.name)) if CORE.name else None return None @@ -750,3 +760,140 @@ def test_entity_duplicate_validator_same_name_no_enhanced_message() -> None: r"Each entity on a device must have a unique name within its platform\.$", ): validator(config2) + + +@pytest.mark.asyncio +async def test_setup_entity_empty_name_with_device( + setup_test_environment: list[str], +) -> None: + """Test setup_entity with empty entity name on a sub-device. + + For empty-name entities, Python passes 0 and C++ calculates the hash + at runtime from the device's actual name. + """ + added_expressions = setup_test_environment + + # Mock get_variable to return a mock device + original_get_variable = entity_helpers.get_variable + + async def mock_get_variable(id_: ID) -> MockObj: + return MockObj("sub_device_1") + + entity_helpers.get_variable = mock_get_variable + + var = MockObj("sensor1") + device_id = ID("sub_device_1", type="Device") + + config = { + CONF_NAME: "", + CONF_DISABLED_BY_DEFAULT: False, + CONF_DEVICE_ID: device_id, + } + + await setup_entity(var, config, "sensor") + + entity_helpers.get_variable = original_get_variable + + # Check that set_device was called + assert any("sensor1.set_device" in expr for expr in added_expressions) + + # For empty-name entities, Python passes 0 - C++ calculates hash at runtime + assert any('set_name("", 0)' in expr for expr in added_expressions), ( + f"Expected set_name with hash 0, got {added_expressions}" + ) + + +@pytest.mark.asyncio +async def test_setup_entity_empty_name_with_mac_suffix( + setup_test_environment: list[str], +) -> None: + """Test setup_entity with empty name and MAC suffix enabled. + + For empty-name entities, Python passes 0 and C++ calculates the hash + at runtime from friendly_name (bug-for-bug compatibility). + """ + added_expressions = setup_test_environment + + # Set up CORE.config with name_add_mac_suffix enabled + CORE.config = {"name_add_mac_suffix": True} + # Set friendly_name to a specific value + CORE.friendly_name = "My Device" + + var = MockObj("sensor1") + + config = { + CONF_NAME: "", + CONF_DISABLED_BY_DEFAULT: False, + } + + await setup_entity(var, config, "sensor") + + # For empty-name entities, Python passes 0 - C++ calculates hash at runtime + assert any('set_name("", 0)' in expr for expr in added_expressions), ( + f"Expected set_name with hash 0, got {added_expressions}" + ) + + +@pytest.mark.asyncio +async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name( + setup_test_environment: list[str], +) -> None: + """Test setup_entity with empty name, MAC suffix enabled, but no friendly_name. + + For empty-name entities, Python passes 0 and C++ calculates the hash + at runtime. In this case C++ will hash the empty friendly_name + (bug-for-bug compatibility). + """ + added_expressions = setup_test_environment + + # Set up CORE.config with name_add_mac_suffix enabled + CORE.config = {"name_add_mac_suffix": True} + # Set friendly_name to empty + CORE.friendly_name = "" + + var = MockObj("sensor1") + + config = { + CONF_NAME: "", + CONF_DISABLED_BY_DEFAULT: False, + } + + await setup_entity(var, config, "sensor") + + # For empty-name entities, Python passes 0 - C++ calculates hash at runtime + assert any('set_name("", 0)' in expr for expr in added_expressions), ( + f"Expected set_name with hash 0, got {added_expressions}" + ) + + +@pytest.mark.asyncio +async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name( + setup_test_environment: list[str], +) -> None: + """Test setup_entity with empty name, no MAC suffix, and no friendly_name. + + For empty-name entities, Python passes 0 and C++ calculates the hash + at runtime from the device name. + """ + added_expressions = setup_test_environment + + # No MAC suffix (either not set or False) + CORE.config = {} + # No friendly_name + CORE.friendly_name = "" + # Device name is set + CORE.name = "my-test-device" + + var = MockObj("sensor1") + + config = { + CONF_NAME: "", + CONF_DISABLED_BY_DEFAULT: False, + } + + await setup_entity(var, config, "sensor") + + # For empty-name entities, Python passes 0 - C++ calculates hash at runtime + assert any('set_name("", 0)' in expr for expr in added_expressions), ( + f"Expected set_name with hash 0, got {added_expressions}" + ) diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml index 6f3bae1ac4..9ed9b99c49 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml @@ -8,6 +8,8 @@ substitutions: position: x: 79 y: 82 + a: 15 + b: 20 esphome: name: test @@ -34,3 +36,5 @@ test_list: - '{{{"AA"}}}' - '"HELLO"' - '{ 79, 82 }' + - a: 15 should be 15, overridden from command line + b: 20 should stay as 20, not overridden diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml index 306119b753..64701c03dd 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml @@ -11,6 +11,13 @@ substitutions: position: x: 79 y: 82 + a: 10 + b: 20 + +# The following key is only used by the test framework +# to simulate command line substitutions +command_line_substitutions: + a: 15 test_list: - "$var1" @@ -35,3 +42,5 @@ test_list: - ${ '{{{"AA"}}}' } - ${ '"HELLO"' } - '{ ${position.x}, ${position.y} }' + - a: ${a} should be 15, overridden from command line + b: ${b} should stay as 20, not overridden diff --git a/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml b/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml index a479370f4b..773a124f25 100644 --- a/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/05-extend-remove.approved.yaml @@ -7,3 +7,33 @@ some_component: value: 2 - id: component2 value: 5 +lvgl: + pages: + - id: page1 + widgets: + - obj: + id: object1 + x: 3 + y: 2 + width: 4 + - obj: + id: object3 + x: 6 + y: 12 + widgets: + - obj: + id: object4 + x: 14 + y: 9 + width: 15 + height: 13 + - obj: + id: object5 + x: 10 + y: 11 + - obj: + id: + - Invalid ID + - obj: + id: + invalid: id diff --git a/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml b/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml index 2e0e60798d..e6d46d6dc4 100644 --- a/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/05-extend-remove.input.yaml @@ -13,6 +13,34 @@ packages: value: 5 - id: component3 value: 6 + - lvgl: + pages: + - id: page1 + widgets: + - obj: + id: object1 + x: 1 + y: 2 + - obj: + id: object2 + x: 5 + - obj: + id: object3 + x: 6 + y: 7 + widgets: + - obj: + id: object4 + x: 8 + y: 9 + - obj: + id: object5 + x: 10 + y: 11 + - obj: + id: ["Invalid ID"] + - obj: + id: {"invalid": "id"} some_component: - id: !extend ${A} @@ -20,3 +48,23 @@ some_component: - id: component2 value: 3 - id: !remove ${C} + +lvgl: + pages: + - id: !extend page1 + widgets: + - obj: + id: !extend object1 + x: 3 + width: 4 + - obj: + id: !remove object2 + - obj: + id: !extend object3 + y: 12 + height: 13 + widgets: + - obj: + id: !extend object4 + x: 14 + width: 15 diff --git a/tests/unit_tests/fixtures/substitutions/06-package_merging.approved.yaml b/tests/unit_tests/fixtures/substitutions/06-package_merging.approved.yaml new file mode 100644 index 0000000000..3fbf5660d5 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/06-package_merging.approved.yaml @@ -0,0 +1,43 @@ +fancy_component: &id001 + - id: component9 + value: 9 +some_component: + - id: component1 + value: 1 + - id: component2 + value: 2 + - id: component3 + value: 3 + - id: component4 + value: 4 + - id: component5 + value: 79 + power: 200 + - id: component6 + value: 6 + - id: component7 + value: 7 +switch: &id002 + - platform: gpio + id: switch1 + pin: 12 + - platform: gpio + id: switch2 + pin: 13 +display: + - platform: ili9xxx + dimensions: + width: 100 + height: 480 +substitutions: + extended_component: component5 + package_options: + alternative_package: + alternative_component: + - id: component8 + value: 8 + fancy_package: + fancy_component: *id001 + pin: 12 + some_switches: *id002 + package_selection: fancy_package diff --git a/tests/unit_tests/fixtures/substitutions/06-package_merging.input.yaml b/tests/unit_tests/fixtures/substitutions/06-package_merging.input.yaml new file mode 100644 index 0000000000..d937a89306 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/06-package_merging.input.yaml @@ -0,0 +1,61 @@ +substitutions: + package_options: + alternative_package: + alternative_component: + - id: component8 + value: 8 + fancy_package: + fancy_component: + - id: component9 + value: 9 + + pin: 12 + some_switches: + - platform: gpio + id: switch1 + pin: ${pin} + - platform: gpio + id: switch2 + pin: ${pin+1} + + package_selection: fancy_package + +packages: + - ${ package_options[package_selection] } + - some_component: + - id: component1 + value: 1 + - some_component: + - id: component2 + value: 2 + - switch: ${ some_switches } + - packages: + package_with_defaults: !include + file: display.yaml + vars: + native_width: 100 + high_dpi: false + my_package: + packages: + - packages: + special_package: + substitutions: + extended_component: component5 + some_component: + - id: component3 + value: 3 + some_component: + - id: component4 + value: 4 + - id: !extend ${ extended_component } + power: 200 + value: 79 + some_component: + - id: component5 + value: 5 + +some_component: + - id: component6 + value: 6 + - id: component7 + value: 7 diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml new file mode 100644 index 0000000000..0fffbfb7cb --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.approved.yaml @@ -0,0 +1,30 @@ +substitutions: + x: 10 + y: 20 + z: 30 +values_from_repo1_main: + - package_name: package1 + x: 3 + y: 4 + z: 5 + volume: 60 + - package_name: package2 + x: 6 + y: 7 + z: 8 + volume: 336 + - package_name: default + x: 10 + y: 20 + z: 5 + volume: 1000 + - package_name: package4_from_repo2 + x: 9 + y: 10 + z: 11 + volume: 990 + - package_name: default + x: 10 + y: 20 + z: 5 + volume: 1000 diff --git a/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml new file mode 100644 index 0000000000..772860bf19 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/06-remote_packages.input.yaml @@ -0,0 +1,43 @@ +substitutions: + x: 10 + y: 20 + z: 30 +packages: + package1: + url: https://github.com/esphome/repo1 + files: + - path: file1.yaml + vars: + package_name: package1 + x: 3 + y: 4 + ref: main + package2: !include # a package that just includes the given remote package + file: remote_package_proxy.yaml + vars: + url: https://github.com/esphome/repo1 + ref: main + files: + - path: file1.yaml + vars: + package_name: package2 + x: 6 + y: 7 + z: 8 + package3: github://esphome/repo1/file1.yaml@main # a package that uses the shorthand syntax + package4: # include repo2, which itself includes repo1 + url: https://github.com/esphome/repo2 + files: + - path: file2.yaml + vars: + package_name: package4 + a: 9 + b: 10 + c: 11 + ref: main + package5: !include + file: remote_package_shorthand.yaml + vars: + repo: repo1 + file: file1.yaml + ref: main diff --git a/tests/unit_tests/fixtures/substitutions/remote_package_proxy.yaml b/tests/unit_tests/fixtures/substitutions/remote_package_proxy.yaml new file mode 100644 index 0000000000..05da30acb4 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remote_package_proxy.yaml @@ -0,0 +1,6 @@ +# acts as a proxy to be able to include a remote package +# in which the url/ref/files come from a substitution +packages: + - url: ${url} + ref: ${ref} + files: ${files} diff --git a/tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml b/tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml new file mode 100644 index 0000000000..f49e85e038 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remote_package_shorthand.yaml @@ -0,0 +1,4 @@ +# acts as a proxy to be able to include a remote package +# in which the shorthand comes from a substitution +packages: + - github://esphome/${repo}/${file}@${ref} diff --git a/tests/unit_tests/fixtures/substitutions/remotes/README.md b/tests/unit_tests/fixtures/substitutions/remotes/README.md new file mode 100644 index 0000000000..09d9f38699 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remotes/README.md @@ -0,0 +1,3 @@ +This folder contains fake repos for remote packages testing +These are used by `test_substitutions.py`. +To add repos, create a folder and add its path to the `REMOTES` constant in `test_substitutions.py`. diff --git a/tests/unit_tests/fixtures/substitutions/remotes/repo1/main/file1.yaml b/tests/unit_tests/fixtures/substitutions/remotes/repo1/main/file1.yaml new file mode 100644 index 0000000000..3830b1650f --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remotes/repo1/main/file1.yaml @@ -0,0 +1,9 @@ +defaults: + z: 5 + package_name: default +values_from_repo1_main: + - package_name: ${package_name} + x: ${x} + y: ${y} + z: ${z} + volume: ${x*y*z} diff --git a/tests/unit_tests/fixtures/substitutions/remotes/repo2/main/file2.yaml b/tests/unit_tests/fixtures/substitutions/remotes/repo2/main/file2.yaml new file mode 100644 index 0000000000..7f62ab8926 --- /dev/null +++ b/tests/unit_tests/fixtures/substitutions/remotes/repo2/main/file2.yaml @@ -0,0 +1,10 @@ +packages: + - url: https://github.com/esphome/repo1 + ref: main + files: + - path: file1.yaml + vars: + package_name: ${package_name}_from_repo2 + x: ${a} + y: ${b} + z: ${c} diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 104cdc2b7a..9602010ad3 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -6,7 +6,7 @@ import pytest import voluptuous as vol from esphome import config_validation -from esphome.components.esp32.const import ( +from esphome.components.esp32 import ( VARIANT_ESP32, VARIANT_ESP32C2, VARIANT_ESP32C3, @@ -221,7 +221,7 @@ def hex_int__valid(value): ], ) def test_split_default(framework, platform, variant, full, idf, arduino, simple): - from esphome.components.esp32.const import KEY_ESP32 + from esphome.components.esp32 import KEY_ESP32 from esphome.const import ( KEY_CORE, KEY_TARGET_FRAMEWORK, @@ -251,15 +251,6 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple) "host": "24", } - idf_mappings = { - "esp32_idf": "4", - "esp32_s2_idf": "7", - "esp32_s3_idf": "10", - "esp32_c3_idf": "13", - "esp32_c6_idf": "16", - "esp32_h2_idf": "19", - } - arduino_mappings = { "esp32_arduino": "3", "esp32_s2_arduino": "6", @@ -269,6 +260,15 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple) "esp32_h2_arduino": "18", } + idf_mappings = { + "esp32_idf": "4", + "esp32_s2_idf": "7", + "esp32_s3_idf": "10", + "esp32_c3_idf": "13", + "esp32_c6_idf": "16", + "esp32_h2_idf": "19", + } + schema = config_validation.Schema( { config_validation.SplitDefault( @@ -293,8 +293,8 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple) @pytest.mark.parametrize( "framework, platform, message", [ - ("esp-idf", PLATFORM_ESP32, "ESP32 using esp-idf framework"), ("arduino", PLATFORM_ESP32, "ESP32 using arduino framework"), + ("esp-idf", PLATFORM_ESP32, "ESP32 using esp-idf framework"), ("arduino", PLATFORM_ESP8266, "ESP8266 using arduino framework"), ("arduino", PLATFORM_RP2040, "RP2040 using arduino framework"), ("arduino", PLATFORM_BK72XX, "BK72XX using arduino framework"), @@ -502,3 +502,77 @@ def test_only_with_user_value_overrides_default() -> None: result = schema({"mqtt_id": "custom_id"}) assert result.get("mqtt_id") == "custom_id" + + +@pytest.mark.parametrize("value", ("hello", "Hello World", "test_name", "温度")) +def test_string_no_slash__valid(value: str) -> None: + actual = config_validation.string_no_slash(value) + assert actual == value + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + ("has/slash", "has⁄slash"), + ("a/b/c", "a⁄b⁄c"), + ("/leading", "⁄leading"), + ("trailing/", "trailing⁄"), + ), +) +def test_string_no_slash__slash_replaced_with_warning( + value: str, expected: str, caplog: pytest.LogCaptureFixture +) -> None: + """Test that '/' is auto-replaced with fraction slash and warning is logged.""" + actual = config_validation.string_no_slash(value) + assert actual == expected + assert "reserved as a URL path separator" in caplog.text + assert "will become an error in ESPHome 2026.7.0" in caplog.text + + +def test_string_no_slash__long_string_allowed() -> None: + # string_no_slash doesn't enforce length - use cv.Length() separately + long_value = "x" * 200 + assert config_validation.string_no_slash(long_value) == long_value + + +def test_string_no_slash__empty() -> None: + assert config_validation.string_no_slash("") == "" + + +@pytest.mark.parametrize("value", ("Temperature", "Living Room Light", "温度传感器")) +def test_validate_entity_name__valid(value: str) -> None: + actual = config_validation._validate_entity_name(value) + assert actual == value + + +def test_validate_entity_name__slash_replaced_with_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that '/' in entity names is auto-replaced with fraction slash.""" + actual = config_validation._validate_entity_name("has/slash") + assert actual == "has⁄slash" + assert "reserved as a URL path separator" in caplog.text + + +def test_validate_entity_name__max_length() -> None: + # 120 chars should pass + assert config_validation._validate_entity_name("x" * 120) == "x" * 120 + + # 121 chars should fail + with pytest.raises(Invalid, match="too long.*121 chars.*Maximum.*120"): + config_validation._validate_entity_name("x" * 121) + + +def test_validate_entity_name__none_without_friendly_name() -> None: + # When name is "None" and friendly_name is not set, it should fail + CORE.friendly_name = None + with pytest.raises(Invalid, match="friendly_name is not set"): + config_validation._validate_entity_name("None") + + +def test_validate_entity_name__none_with_friendly_name() -> None: + # When name is "None" but friendly_name is set, it should return None + CORE.friendly_name = "My Device" + result = config_validation._validate_entity_name("None") + assert result is None + CORE.friendly_name = None # Reset diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index e52cb24831..1fc8dab358 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -718,3 +718,65 @@ class TestEsphomeCore: # Even though "web_server" is in loaded_integrations due to the platform, # web_port must return None because the full web_server component is not configured assert target.web_port is None + + def test_has_at_least_one_component__none_configured(self, target): + """Test has_at_least_one_component returns False when none of the components are configured.""" + target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}} + + assert target.has_at_least_one_component("wifi", "ethernet") is False + + def test_has_at_least_one_component__one_configured(self, target): + """Test has_at_least_one_component returns True when one component is configured.""" + target.config = {const.CONF_WIFI: {}, "logger": {}} + + assert target.has_at_least_one_component("wifi", "ethernet") is True + + def test_has_at_least_one_component__multiple_configured(self, target): + """Test has_at_least_one_component returns True when multiple components are configured.""" + target.config = { + const.CONF_WIFI: {}, + const.CONF_ETHERNET: {}, + "logger": {}, + } + + assert ( + target.has_at_least_one_component("wifi", "ethernet", "bluetooth") is True + ) + + def test_has_at_least_one_component__single_component(self, target): + """Test has_at_least_one_component works with a single component.""" + target.config = {const.CONF_MQTT: {}} + + assert target.has_at_least_one_component("mqtt") is True + assert target.has_at_least_one_component("wifi") is False + + def test_has_at_least_one_component__config_not_loaded(self, target): + """Test has_at_least_one_component raises ValueError when config is not loaded.""" + target.config = None + + with pytest.raises(ValueError, match="Config has not been loaded yet"): + target.has_at_least_one_component("wifi") + + def test_has_networking__with_wifi(self, target): + """Test has_networking returns True when wifi is configured.""" + target.config = {const.CONF_WIFI: {}} + + assert target.has_networking is True + + def test_has_networking__with_ethernet(self, target): + """Test has_networking returns True when ethernet is configured.""" + target.config = {const.CONF_ETHERNET: {}} + + assert target.has_networking is True + + def test_has_networking__with_openthread(self, target): + """Test has_networking returns True when openthread is configured.""" + target.config = {const.CONF_OPENTHREAD: {}} + + assert target.has_networking is True + + def test_has_networking__without_networking(self, target): + """Test has_networking returns False when no networking component is configured.""" + target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}} + + assert target.has_networking is False diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 47b945e0eb..159d3230ab 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -279,6 +279,77 @@ def test_sanitize(text, expected): assert actual == expected +@pytest.mark.parametrize( + ("name", "expected_hash"), + ( + # Basic strings - hash of sanitize(snake_case(name)) + ("foo", 0x408F5E13), + ("Foo", 0x408F5E13), # Same as "foo" (lowercase) + ("FOO", 0x408F5E13), # Same as "foo" (lowercase) + # Spaces become underscores + ("foo bar", 0x3AE35AA1), # Transforms to "foo_bar" + ("Foo Bar", 0x3AE35AA1), # Same (lowercase + underscore) + # Already snake_case + ("foo_bar", 0x3AE35AA1), + # Special chars become underscores + ("foo!bar", 0x3AE35AA1), # Transforms to "foo_bar" + ("foo@bar", 0x3AE35AA1), # Transforms to "foo_bar" + # Hyphens are preserved + ("foo-bar", 0x438B12E3), + # Numbers are preserved + ("foo123", 0xF3B0067D), + # Empty string + ("", 0x811C9DC5), # FNV1_OFFSET_BASIS (no chars processed) + # Single char + ("a", 0x050C5D7E), + # Mixed case and spaces + ("My Sensor Name", 0x2760962A), # Transforms to "my_sensor_name" + ), +) +def test_fnv1_hash_object_id(name, expected_hash): + """Test fnv1_hash_object_id produces expected hashes. + + These expected values were computed to match the C++ implementation + in esphome/core/helpers.h. If this test fails after modifying either + implementation, ensure both Python and C++ versions stay in sync. + """ + actual = helpers.fnv1_hash_object_id(name) + + assert actual == expected_hash + + +def _fnv1_hash_py(s: str) -> int: + """Python implementation of FNV-1 hash for verification.""" + hash_val = 2166136261 # FNV1_OFFSET_BASIS + for c in s: + hash_val = (hash_val * 16777619) & 0xFFFFFFFF # FNV1_PRIME + hash_val ^= ord(c) + return hash_val + + +@pytest.mark.parametrize( + "name", + ( + "Simple", + "With Space", + "MixedCase", + "special!@#chars", + "already_snake_case", + "123numbers", + ), +) +def test_fnv1_hash_object_id_matches_manual_calculation(name): + """Verify fnv1_hash_object_id matches snake_case + sanitize + standard FNV-1.""" + # Manual calculation: snake_case -> sanitize -> fnv1_hash + transformed = helpers.sanitize(helpers.snake_case(name)) + expected = _fnv1_hash_py(transformed) + + # Direct calculation via fnv1_hash_object_id + actual = helpers.fnv1_hash_object_id(name) + + assert actual == expected + + @pytest.mark.parametrize( "text, expected", ((["127.0.0.1", "fe80::1", "2001::2"], ["2001::2", "127.0.0.1", "fe80::1"]),), diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index ccbc5a1306..fd8f04ded5 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -4,9 +4,11 @@ from __future__ import annotations from collections.abc import Generator from dataclasses import dataclass +import json import logging from pathlib import Path import re +import time from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -22,6 +24,7 @@ from esphome.__main__ import ( command_rename, command_update_all, command_wizard, + compile_program, detect_external_components, get_port_type, has_ip_address, @@ -31,18 +34,21 @@ from esphome.__main__ import ( has_non_ip_address, has_resolvable_address, mqtt_get_ip, + run_miniterm, show_logs, upload_program, upload_using_esptool, ) -from esphome.components.esp32.const import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32 +from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANT_ESP32 from esphome.const import ( CONF_API, + CONF_BAUD_RATE, CONF_BROKER, CONF_DISABLED, CONF_ESPHOME, CONF_LEVEL, CONF_LOG_TOPIC, + CONF_LOGGER, CONF_MDNS, CONF_MQTT, CONF_NAME, @@ -269,6 +275,16 @@ def mock_memory_analyzer_cli() -> Generator[Mock]: yield mock_class +@pytest.fixture +def mock_ram_strings_analyzer() -> Generator[Mock]: + """Mock RamStringsAnalyzer for testing.""" + with patch("esphome.analyze_memory.ram_strings.RamStringsAnalyzer") as mock_class: + mock_analyzer = MagicMock() + mock_analyzer.generate_report.return_value = "Mock RAM Strings Report" + mock_class.return_value = mock_analyzer + yield mock_class + + def test_choose_upload_log_host_with_string_default() -> None: """Test with a single string default device.""" setup_core() @@ -825,6 +841,7 @@ class MockArgs: configuration: str | None = None name: str | None = None dashboard: bool = False + reset: bool = False def test_upload_program_serial_esp32( @@ -2424,6 +2441,7 @@ def test_command_analyze_memory_success( mock_get_idedata: Mock, mock_get_esphome_components: Mock, mock_memory_analyzer_cli: Mock, + mock_ram_strings_analyzer: Mock, ) -> None: """Test command_analyze_memory with successful compilation and analysis.""" setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device") @@ -2464,6 +2482,7 @@ def test_command_analyze_memory_success( "/path/to/objdump", "/path/to/readelf", set(), # No external components + idedata=mock_get_idedata.return_value, ) # Verify analysis was run @@ -2471,9 +2490,20 @@ def test_command_analyze_memory_success( mock_analyzer.analyze.assert_called_once() mock_analyzer.generate_report.assert_called_once() - # Verify report was printed + # Verify RAM strings analyzer was created and run + mock_ram_strings_analyzer.assert_called_once_with( + str(firmware_elf), + objdump_path="/path/to/objdump", + platform="esp32", + ) + mock_ram_analyzer = mock_ram_strings_analyzer.return_value + mock_ram_analyzer.analyze.assert_called_once() + mock_ram_analyzer.generate_report.assert_called_once() + + # Verify reports were printed captured = capfd.readouterr() assert "Mock Memory Report" in captured.out + assert "Mock RAM Strings Report" in captured.out def test_command_analyze_memory_with_external_components( @@ -2483,6 +2513,7 @@ def test_command_analyze_memory_with_external_components( mock_get_idedata: Mock, mock_get_esphome_components: Mock, mock_memory_analyzer_cli: Mock, + mock_ram_strings_analyzer: Mock, ) -> None: """Test command_analyze_memory detects external components.""" setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device") @@ -2521,6 +2552,7 @@ def test_command_analyze_memory_with_external_components( "/path/to/objdump", "/path/to/readelf", {"my_custom_component"}, # External component detected + idedata=mock_get_idedata.return_value, ) @@ -2582,3 +2614,561 @@ def test_command_analyze_memory_no_idedata( assert result == 1 assert "Failed to get IDE data for memory analysis" in caplog.text + + +@pytest.fixture +def mock_compile_build_info_run_compile() -> Generator[Mock]: + """Mock platformio_api.run_compile for build_info tests.""" + with patch("esphome.platformio_api.run_compile", return_value=0) as mock: + yield mock + + +@pytest.fixture +def mock_compile_build_info_get_idedata() -> Generator[Mock]: + """Mock platformio_api.get_idedata for build_info tests.""" + mock_idedata = MagicMock() + with patch("esphome.platformio_api.get_idedata", return_value=mock_idedata) as mock: + yield mock + + +def _setup_build_info_test( + tmp_path: Path, + *, + create_firmware: bool = True, + create_build_info: bool = True, + build_info_content: str | None = None, + firmware_first: bool = False, +) -> tuple[Path, Path]: + """Set up build directory structure for build_info tests. + + Args: + tmp_path: Temporary directory path. + create_firmware: Whether to create firmware.bin file. + create_build_info: Whether to create build_info.json file. + build_info_content: Custom content for build_info.json, or None for default. + firmware_first: If True, create firmware before build_info (makes firmware older). + + Returns: + Tuple of (build_info_path, firmware_path). + """ + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device") + + build_path = tmp_path / ".esphome" / "build" / "test_device" + pioenvs_path = build_path / ".pioenvs" / "test_device" + pioenvs_path.mkdir(parents=True, exist_ok=True) + + build_info_path = build_path / "build_info.json" + firmware_path = pioenvs_path / "firmware.bin" + + default_build_info = json.dumps( + { + "config_hash": 0x12345678, + "build_time": int(time.time()), + "build_time_str": "Dec 13 2025, 12:00:00", + "esphome_version": "2025.1.0", + } + ) + + def create_build_info_file() -> None: + if create_build_info: + content = ( + build_info_content + if build_info_content is not None + else default_build_info + ) + build_info_path.write_text(content) + + def create_firmware_file() -> None: + if create_firmware: + firmware_path.write_bytes(b"fake firmware") + + if firmware_first: + create_firmware_file() + time.sleep(0.01) # Ensure different timestamps + create_build_info_file() + else: + create_build_info_file() + time.sleep(0.01) # Ensure different timestamps + create_firmware_file() + + return build_info_path, firmware_path + + +def test_compile_program_emits_build_info_when_firmware_rebuilt( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + mock_compile_build_info_run_compile: Mock, + mock_compile_build_info_get_idedata: Mock, +) -> None: + """Test that compile_program logs build_info when firmware is rebuilt.""" + _setup_build_info_test(tmp_path, firmware_first=False) + + config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}} + args = MockArgs() + + with caplog.at_level(logging.INFO): + result = compile_program(args, config) + + assert result == 0 + assert "Build Info: config_hash=0x12345678" in caplog.text + + +def test_compile_program_no_build_info_when_firmware_not_rebuilt( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + mock_compile_build_info_run_compile: Mock, + mock_compile_build_info_get_idedata: Mock, +) -> None: + """Test that compile_program doesn't log build_info when firmware wasn't rebuilt.""" + _setup_build_info_test(tmp_path, firmware_first=True) + + config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}} + args = MockArgs() + + with caplog.at_level(logging.INFO): + result = compile_program(args, config) + + assert result == 0 + assert "Build Info:" not in caplog.text + + +def test_compile_program_no_build_info_when_firmware_missing( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + mock_compile_build_info_run_compile: Mock, + mock_compile_build_info_get_idedata: Mock, +) -> None: + """Test that compile_program doesn't log build_info when firmware.bin doesn't exist.""" + _setup_build_info_test(tmp_path, create_firmware=False) + + config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}} + args = MockArgs() + + with caplog.at_level(logging.INFO): + result = compile_program(args, config) + + assert result == 0 + assert "Build Info:" not in caplog.text + + +def test_compile_program_no_build_info_when_json_missing( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + mock_compile_build_info_run_compile: Mock, + mock_compile_build_info_get_idedata: Mock, +) -> None: + """Test that compile_program doesn't log build_info when build_info.json doesn't exist.""" + _setup_build_info_test(tmp_path, create_build_info=False) + + config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}} + args = MockArgs() + + with caplog.at_level(logging.INFO): + result = compile_program(args, config) + + assert result == 0 + assert "Build Info:" not in caplog.text + + +def test_compile_program_no_build_info_when_json_invalid( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + mock_compile_build_info_run_compile: Mock, + mock_compile_build_info_get_idedata: Mock, +) -> None: + """Test that compile_program doesn't log build_info when build_info.json is invalid.""" + _setup_build_info_test(tmp_path, build_info_content="not valid json {{{") + + config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}} + args = MockArgs() + + with caplog.at_level(logging.DEBUG): + result = compile_program(args, config) + + assert result == 0 + assert "Build Info:" not in caplog.text + + +def test_compile_program_no_build_info_when_json_missing_keys( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + mock_compile_build_info_run_compile: Mock, + mock_compile_build_info_get_idedata: Mock, +) -> None: + """Test that compile_program doesn't log build_info when build_info.json is missing required keys.""" + _setup_build_info_test( + tmp_path, build_info_content=json.dumps({"build_time": 1234567890}) + ) + + config: dict[str, Any] = {CONF_ESPHOME: {CONF_NAME: "test_device"}} + args = MockArgs() + + with caplog.at_level(logging.INFO): + result = compile_program(args, config) + + assert result == 0 + assert "Build Info:" not in caplog.text + + +# Tests for run_miniterm serial log batching + + +# Sentinel to signal end of mock serial data (raises SerialException) +MOCK_SERIAL_END = object() + + +class MockSerial: + """Mock serial port for testing run_miniterm.""" + + def __init__(self, chunks: list[bytes | object]) -> None: + """Initialize with a list of chunks to return from read(). + + Args: + chunks: List of byte chunks to return sequentially. + Use MOCK_SERIAL_END sentinel to signal end of data. + Empty bytes b"" simulate timeout (no data available). + """ + self.chunks = list(chunks) + self.chunk_index = 0 + self.baudrate = 0 + self.port = "" + self.dtr = True + self.rts = True + self.timeout = 0.1 + self._is_open = False + + def __enter__(self) -> MockSerial: + self._is_open = True + return self + + def __exit__(self, *args: Any) -> None: + self._is_open = False + + @property + def in_waiting(self) -> int: + """Return number of bytes available.""" + if self.chunk_index < len(self.chunks): + chunk = self.chunks[self.chunk_index] + if chunk is MOCK_SERIAL_END: + return 0 + return len(chunk) # type: ignore[arg-type] + return 0 + + def read(self, size: int = 1) -> bytes: + """Read up to size bytes from the current chunk. + + This method respects the size argument and keeps any unconsumed + bytes in the current chunk so that subsequent calls to in_waiting + and read see the remaining data. + """ + if self.chunk_index < len(self.chunks): + chunk = self.chunks[self.chunk_index] + if chunk is MOCK_SERIAL_END: + # Sentinel means we're done - simulate port closed + import serial + + raise serial.SerialException("Port closed") + # Respect the requested size and keep any remaining bytes + if size <= 0: + return b"" + data = chunk[:size] # type: ignore[index] + remaining = chunk[size:] # type: ignore[index] + if remaining: + # Keep remaining bytes for the next read + self.chunks[self.chunk_index] = remaining # type: ignore[assignment] + else: + # Entire chunk consumed; advance to the next one + self.chunk_index += 1 + return data # type: ignore[return-value] + import serial + + raise serial.SerialException("Port closed") + + +def test_run_miniterm_batches_lines_with_same_timestamp( + capfd: CaptureFixture[str], +) -> None: + """Test that lines from the same chunk get the same timestamp.""" + # Simulate receiving multiple log lines in a single chunk + # This is how data arrives over USB - many lines at once + chunk = b"[I][app:100]: Line 1\r\n[I][app:100]: Line 2\r\n[I][app:100]: Line 3\r\n" + + mock_serial = MockSerial([chunk, MOCK_SERIAL_END]) + + config = { + CONF_LOGGER: { + CONF_BAUD_RATE: 115200, + "deassert_rts_dtr": False, + } + } + args = MockArgs() + + with ( + patch("serial.Serial", return_value=mock_serial), + patch.object(platformio_api, "process_stacktrace") as mock_bt, + ): + mock_bt.return_value = False + result = run_miniterm(config, "/dev/ttyUSB0", args) + + assert result == 0 + + captured = capfd.readouterr() + lines = [line for line in captured.out.strip().split("\n") if line] + + # All 3 lines should have the same timestamp (first 13 chars like "[HH:MM:SS.mmm]") + assert len(lines) == 3 + timestamps = [line[:13] for line in lines] + assert timestamps[0] == timestamps[1] == timestamps[2], ( + f"Lines from same chunk should have same timestamp: {timestamps}" + ) + + +def test_run_miniterm_different_chunks_different_timestamps( + capfd: CaptureFixture[str], +) -> None: + """Test that lines from different chunks can have different timestamps.""" + # Two separate chunks - could have different timestamps + chunk1 = b"[I][app:100]: Chunk 1 Line\r\n" + chunk2 = b"[I][app:100]: Chunk 2 Line\r\n" + + mock_serial = MockSerial([chunk1, chunk2, MOCK_SERIAL_END]) + + config = { + CONF_LOGGER: { + CONF_BAUD_RATE: 115200, + "deassert_rts_dtr": False, + } + } + args = MockArgs() + + with ( + patch("serial.Serial", return_value=mock_serial), + patch.object(platformio_api, "process_stacktrace") as mock_bt, + ): + mock_bt.return_value = False + result = run_miniterm(config, "/dev/ttyUSB0", args) + + assert result == 0 + + captured = capfd.readouterr() + lines = [line for line in captured.out.strip().split("\n") if line] + assert len(lines) == 2 + + +def test_run_miniterm_handles_split_lines() -> None: + """Test that partial lines are buffered until complete.""" + # Line split across two chunks + chunk1 = b"[I][app:100]: Start of " + chunk2 = b"line\r\n" + + mock_serial = MockSerial([chunk1, chunk2, MOCK_SERIAL_END]) + + config = { + CONF_LOGGER: { + CONF_BAUD_RATE: 115200, + "deassert_rts_dtr": False, + } + } + args = MockArgs() + + with ( + patch("serial.Serial", return_value=mock_serial), + patch.object(platformio_api, "process_stacktrace") as mock_bt, + patch("esphome.__main__.safe_print") as mock_print, + ): + mock_bt.return_value = False + run_miniterm(config, "/dev/ttyUSB0", args) + + # Should have printed exactly one complete line + assert mock_print.call_count == 1 + printed_line = mock_print.call_args[0][0] + assert "Start of line" in printed_line + + +def test_run_miniterm_backtrace_state_maintained() -> None: + """Test that backtrace_state is properly maintained across lines. + + ESP8266 backtraces span multiple lines between >>>stack>>> and <<>>stack>>>\r\n" + b"3ffffe90: 40220ef8 b66aa8c0 3fff0a4c 40204c84\r\n" + b"3ffffea0: 00000005 0000a635 3fff191c 4020413c\r\n" + b"<< bool: + """Track the backtrace_state progression.""" + backtrace_states.append((line, backtrace_state)) + # Simulate actual behavior + if ">>>stack>>>" in line: + return True + if "<<>>stack>>> - state should be False (before processing) + assert ">>>stack>>>" in backtrace_states[0][0] + assert backtrace_states[0][1] is False + + # Line 2: stack data - state should be True (after >>>stack>>>) + assert "40220ef8" in backtrace_states[1][0] + assert backtrace_states[1][1] is True + + # Line 3: more stack data - state should be True + assert "4020413c" in backtrace_states[2][0] + assert backtrace_states[2][1] is True + + # Line 4: << None: + """Test that empty reads (timeouts) are handled correctly. + + When read() returns empty bytes, the code should continue waiting + for more data without processing anything. + """ + # Simulate: empty read (timeout), then data, then empty read, then end + chunk = b"[I][app:100]: Test line\r\n" + + mock_serial = MockSerial([b"", chunk, b"", MOCK_SERIAL_END]) + + config = { + CONF_LOGGER: { + CONF_BAUD_RATE: 115200, + "deassert_rts_dtr": False, + } + } + args = MockArgs() + + with ( + patch("serial.Serial", return_value=mock_serial), + patch.object(platformio_api, "process_stacktrace") as mock_bt, + ): + mock_bt.return_value = False + result = run_miniterm(config, "/dev/ttyUSB0", args) + + assert result == 0 + + captured = capfd.readouterr() + lines = [line for line in captured.out.strip().split("\n") if line] + # Should have exactly one line despite empty reads + assert len(lines) == 1 + assert "Test line" in lines[0] + + +def test_run_miniterm_no_logger_returns_early( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that run_miniterm returns early if logger is not configured.""" + config: dict[str, Any] = {} # No logger config + args = MockArgs() + + with caplog.at_level(logging.INFO): + result = run_miniterm(config, "/dev/ttyUSB0", args) + + assert result == 1 + assert "Logger is not enabled" in caplog.text + + +def test_run_miniterm_baud_rate_zero_returns_early( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that run_miniterm returns early if baud_rate is 0.""" + config = { + CONF_LOGGER: { + CONF_BAUD_RATE: 0, + "deassert_rts_dtr": False, + } + } + args = MockArgs() + + with caplog.at_level(logging.INFO): + result = run_miniterm(config, "/dev/ttyUSB0", args) + + assert result == 1 + assert "UART logging is disabled" in caplog.text + + +def test_run_miniterm_buffer_limit_prevents_unbounded_growth() -> None: + """Test that buffer is limited to prevent unbounded memory growth. + + If a device sends data without newlines, the buffer should be truncated + to SERIAL_BUFFER_MAX_SIZE to prevent memory exhaustion. + """ + # Use a small buffer limit for testing + test_buffer_limit = 100 + + # Create data larger than the limit without newlines + large_data_no_newline = b"X" * 150 # 150 bytes, no newline + final_line = b"END\r\n" + + mock_serial = MockSerial([large_data_no_newline, final_line, MOCK_SERIAL_END]) + + config = { + CONF_LOGGER: { + CONF_BAUD_RATE: 115200, + "deassert_rts_dtr": False, + } + } + args = MockArgs() + + with ( + patch("serial.Serial", return_value=mock_serial), + patch.object(platformio_api, "process_stacktrace") as mock_bt, + patch("esphome.__main__.safe_print") as mock_print, + patch("esphome.__main__.SERIAL_BUFFER_MAX_SIZE", test_buffer_limit), + ): + mock_bt.return_value = False + run_miniterm(config, "/dev/ttyUSB0", args) + + # Should have printed exactly one line + assert mock_print.call_count == 1 + printed_line = mock_print.call_args[0][0] + + # The line should contain "END" and some X's, but not all 150 X's + # because the buffer was truncated + assert "END" in printed_line + assert "X" in printed_line + # Verify truncation happened - we shouldn't have all 150 X's + # The buffer logic is: + # 1. Add 150 X's -> buffer = 150 bytes -> truncate to last 100 = 100 X's + # 2. Add "END\r\n" (5 bytes) -> buffer = 105 bytes -> truncate to last 100 + # = 95 X's + "END\r\n" + # 3. Find newline, extract line = "95 X's + END" + x_count = printed_line.count("X") + assert x_count < 150, f"Expected truncation but got {x_count} X's" + assert x_count == 95, f"Expected 95 X's after truncation but got {x_count}" diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index 13ef3516e4..4d7b635e59 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -1,6 +1,7 @@ """Tests for platformio_api.py path functions.""" import json +import logging import os from pathlib import Path import shutil @@ -670,3 +671,100 @@ def test_process_stacktrace_bad_alloc( assert "Memory allocation of 512 bytes failed at 40201234" in caplog.text mock_decode_pc.assert_called_once_with(config, "40201234") assert state is False + + +def test_platformio_log_filter_allows_non_platformio_messages() -> None: + """Test that non-platformio logger messages are allowed through.""" + log_filter = platformio_api.PlatformioLogFilter() + record = logging.LogRecord( + name="esphome.core", + level=logging.INFO, + pathname="", + lineno=0, + msg="Some esphome message", + args=(), + exc_info=None, + ) + assert log_filter.filter(record) is True + + +@pytest.mark.parametrize( + "msg", + [ + "Verbose mode can be enabled via `-v, --verbose` option", + "Found 5 compatible libraries", + "Found 123 compatible libraries", + "Building in release mode", + "Building in debug mode", + "Merged 2 ELF section", + "esptool.py v4.7.0", + "esptool v4.8.1", + "PLATFORM: espressif32 @ 6.4.0", + "Using cache: /path/to/cache", + "Package configuration completed successfully", + "Scanning dependencies...", + "Installing dependencies", + "Library Manager: Already installed, built-in library", + "Memory Usage -> https://bit.ly/pio-memory-usage", + ], +) +def test_platformio_log_filter_blocks_noisy_messages(msg: str) -> None: + """Test that noisy platformio messages are filtered out.""" + log_filter = platformio_api.PlatformioLogFilter() + record = logging.LogRecord( + name="platformio.builder", + level=logging.INFO, + pathname="", + lineno=0, + msg=msg, + args=(), + exc_info=None, + ) + assert log_filter.filter(record) is False + + +@pytest.mark.parametrize( + "msg", + [ + "Compiling .pio/build/test/src/main.cpp.o", + "Linking .pio/build/test/firmware.elf", + "Error: something went wrong", + "warning: unused variable", + ], +) +def test_platformio_log_filter_allows_other_platformio_messages(msg: str) -> None: + """Test that non-noisy platformio messages are allowed through.""" + log_filter = platformio_api.PlatformioLogFilter() + record = logging.LogRecord( + name="platformio.builder", + level=logging.INFO, + pathname="", + lineno=0, + msg=msg, + args=(), + exc_info=None, + ) + assert log_filter.filter(record) is True + + +@pytest.mark.parametrize( + "logger_name", + [ + "PLATFORMIO.builder", + "PlatformIO.core", + "platformio.run", + ], +) +def test_platformio_log_filter_case_insensitive_logger_name(logger_name: str) -> None: + """Test that platformio logger name matching is case insensitive.""" + log_filter = platformio_api.PlatformioLogFilter() + record = logging.LogRecord( + name=logger_name, + level=logging.INFO, + pathname="", + lineno=0, + msg="Found 5 compatible libraries", + args=(), + exc_info=None, + ) + assert log_filter.filter(record) is False diff --git a/tests/unit_tests/test_substitutions.py b/tests/unit_tests/test_substitutions.py index 7d50b44506..1d8cb7631d 100644 --- a/tests/unit_tests/test_substitutions.py +++ b/tests/unit_tests/test_substitutions.py @@ -2,12 +2,16 @@ import glob import logging from pathlib import Path from typing import Any +from unittest.mock import MagicMock, patch + +import pytest from esphome import config as config_module, yaml_util from esphome.components import substitutions +from esphome.components.packages import do_packages_pass, merge_packages from esphome.config import resolve_extend_remove from esphome.config_helpers import merge_config -from esphome.const import CONF_PACKAGES, CONF_SUBSTITUTIONS +from esphome.const import CONF_SUBSTITUTIONS from esphome.core import CORE from esphome.util import OrderedDict @@ -70,6 +74,8 @@ def verify_database(value: Any, path: str = "") -> str | None: return None if isinstance(value, dict): for k, v in value.items(): + if path == "" and k == CONF_SUBSTITUTIONS: + return None # ignore substitutions key at top level since it is merged. key_result = verify_database(k, f"{path}/{k}") if key_result is not None: return key_result @@ -84,73 +90,103 @@ def verify_database(value: Any, path: str = "") -> str | None: return None -def test_substitutions_fixtures(fixture_path): - base_dir = fixture_path / "substitutions" - sources = sorted(glob.glob(str(base_dir / "*.input.yaml"))) - assert sources, f"No input YAML files found in {base_dir}" +# Mapping of (url, ref) to local test repository path under fixtures/substitutions +REMOTES = { + ("https://github.com/esphome/repo1", "main"): "remotes/repo1/main", + ("https://github.com/esphome/repo2", "main"): "remotes/repo2/main", +} - failures = [] - for source_path in sources: - source_path = Path(source_path) - try: - expected_path = source_path.with_suffix("").with_suffix(".approved.yaml") - test_case = source_path.with_suffix("").stem +# Collect all input YAML files for test_substitutions_fixtures parametrized tests: +HERE = Path(__file__).parent +BASE_DIR = HERE / "fixtures" / "substitutions" +SOURCES = sorted(glob.glob(str(BASE_DIR / "*.input.yaml"))) +assert SOURCES, f"test_substitutions_fixtures: No input YAML files found in {BASE_DIR}" - # Load using ESPHome's YAML loader - config = yaml_util.load_yaml(source_path) - if CONF_PACKAGES in config: - from esphome.components.packages import do_packages_pass - - config = do_packages_pass(config) - - substitutions.do_substitution_pass(config, None) - - resolve_extend_remove(config) - verify_database_result = verify_database(config) - if verify_database_result is not None: - raise AssertionError(verify_database_result) - - # Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE - if expected_path.is_file(): - expected = yaml_util.load_yaml(expected_path) - elif DEV_MODE: - expected = {} - else: - assert expected_path.is_file(), ( - f"Expected file missing: {expected_path}" +@pytest.mark.parametrize( + "source_path", + [Path(p) for p in SOURCES], + ids=lambda p: p.name, +) +@patch("esphome.git.clone_or_update") +def test_substitutions_fixtures( + mock_clone_or_update: MagicMock, source_path: Path +) -> None: + def fake_clone_or_update( + *, + url: str, + ref: str | None = None, + refresh=None, + domain: str, + username: str | None = None, + password: str | None = None, + submodules: list[str] | None = None, + _recover_broken: bool = True, + ) -> tuple[Path, None]: + path = REMOTES.get((url, ref)) + if path is None: + path = REMOTES.get((url.rstrip(".git"), ref)) + if path is None: + raise RuntimeError( + f"Cannot find test repository for {url} @ {ref}. Check the REMOTES mapping in test_substitutions.py" ) + return BASE_DIR / path, None - # Sort dicts only (not lists) for comparison - got_sorted = sort_dicts(config) - expected_sorted = sort_dicts(expected) + mock_clone_or_update.side_effect = fake_clone_or_update - if got_sorted != expected_sorted: - diff = "\n".join(dict_diff(got_sorted, expected_sorted)) - msg = ( - f"Substitution result mismatch for {source_path.name}\n" - f"Diff:\n{diff}\n\n" - f"Got: {got_sorted}\n" - f"Expected: {expected_sorted}" - ) - # Write out the received file when test fails - if DEV_MODE: - received_path = source_path.with_name(f"{test_case}.received.yaml") - write_yaml(received_path, config) - print(msg) - failures.append(msg) - else: - raise AssertionError(msg) - except Exception as err: - _LOGGER.error("Error in test file %s", source_path) - raise err + expected_path = source_path.with_suffix("").with_suffix(".approved.yaml") + test_case = source_path.with_suffix("").stem - if DEV_MODE and failures: - print(f"\n{len(failures)} substitution test case(s) failed.") + # Load using ESPHome's YAML loader + config = yaml_util.load_yaml(source_path) + + command_line_substitutions = config.pop("command_line_substitutions", None) + + config = do_packages_pass(config) + + substitutions.do_substitution_pass(config, command_line_substitutions) + + config = merge_packages(config) + + resolve_extend_remove(config) + verify_database_result = verify_database(config) + if verify_database_result is not None: + raise AssertionError(verify_database_result) + + # Also load expected using ESPHome's loader, or use {} if missing and DEV_MODE + if expected_path.is_file(): + expected = yaml_util.load_yaml(expected_path) + elif DEV_MODE: + expected = {} + else: + assert expected_path.is_file(), f"Expected file missing: {expected_path}" + + # Sort dicts only (not lists) for comparison + got_sorted = sort_dicts(config) + expected_sorted = sort_dicts(expected) + + if got_sorted != expected_sorted: + diff = "\n".join(dict_diff(got_sorted, expected_sorted)) + msg = ( + f"Substitution result mismatch for {source_path.name}\n" + f"Diff:\n{diff}\n\n" + f"Got: {got_sorted}\n" + f"Expected: {expected_sorted}" + ) + # Write out the received file when test fails + if DEV_MODE: + received_path = source_path.with_name(f"{test_case}.received.yaml") + write_yaml(received_path, config) + msg += f"\nWrote received file to {received_path}." + raise AssertionError(msg) if DEV_MODE: _LOGGER.error("Tests passed, but Dev mode is enabled.") - assert not DEV_MODE # make sure DEV_MODE is disabled after you are finished. + assert ( + not DEV_MODE # make sure DEV_MODE is disabled after you are finished. + ), ( + "Test passed but DEV_MODE must be disabled when running tests. Please set DEV_MODE=False." + ) def test_substitutions_with_command_line_maintains_ordered_dict() -> None: diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index a4490fbbc0..ac05e0d31b 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -1,12 +1,25 @@ """Test writer module functionality.""" from collections.abc import Callable +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import datetime +import json +import os from pathlib import Path +import stat from typing import Any from unittest.mock import MagicMock, patch import pytest +from esphome.const import ( + PLATFORM_BK72XX, + PLATFORM_ESP32, + PLATFORM_ESP8266, + PLATFORM_RP2040, + PLATFORM_RTL87XX, +) from esphome.core import EsphomeError from esphome.storage_json import StorageJSON from esphome.writer import ( @@ -15,9 +28,14 @@ from esphome.writer import ( CPP_INCLUDE_BEGIN, CPP_INCLUDE_END, GITIGNORE_CONTENT, + clean_all, clean_build, clean_cmake_cache, + copy_src_tree, + generate_build_info_data_h, + get_build_info, storage_should_clean, + storage_should_update_cmake_cache, update_storage_json, write_cpp, write_gitignore, @@ -161,6 +179,86 @@ def test_storage_edge_case_from_empty_integrations( assert storage_should_clean(old, new) is False +# Tests for storage_should_update_cmake_cache + + +@pytest.mark.parametrize("framework", ["arduino", "esp-idf"]) +def test_storage_should_update_cmake_cache_when_integration_added_esp32( + create_storage: Callable[..., StorageJSON], + framework: str, +) -> None: + """Test cmake cache update triggered when integration added on ESP32.""" + old = create_storage( + loaded_integrations=["api", "wifi"], + core_platform=PLATFORM_ESP32, + framework=framework, + ) + new = create_storage( + loaded_integrations=["api", "wifi", "restart"], + core_platform=PLATFORM_ESP32, + framework=framework, + ) + assert storage_should_update_cmake_cache(old, new) is True + + +def test_storage_should_update_cmake_cache_when_platform_changed_esp32( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test cmake cache update triggered when platforms change on ESP32.""" + old = create_storage( + loaded_integrations=["api", "wifi"], + loaded_platforms={"sensor"}, + core_platform=PLATFORM_ESP32, + framework="arduino", + ) + new = create_storage( + loaded_integrations=["api", "wifi"], + loaded_platforms={"sensor", "binary_sensor"}, + core_platform=PLATFORM_ESP32, + framework="arduino", + ) + assert storage_should_update_cmake_cache(old, new) is True + + +def test_storage_should_not_update_cmake_cache_when_nothing_changes( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test cmake cache not updated when nothing changes.""" + old = create_storage( + loaded_integrations=["api", "wifi"], + core_platform=PLATFORM_ESP32, + framework="arduino", + ) + new = create_storage( + loaded_integrations=["api", "wifi"], + core_platform=PLATFORM_ESP32, + framework="arduino", + ) + assert storage_should_update_cmake_cache(old, new) is False + + +@pytest.mark.parametrize( + "core_platform", + [PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_BK72XX, PLATFORM_RTL87XX], +) +def test_storage_should_not_update_cmake_cache_for_non_esp32( + create_storage: Callable[..., StorageJSON], + core_platform: str, +) -> None: + """Test cmake cache not updated for non-ESP32 platforms.""" + old = create_storage( + loaded_integrations=["api", "wifi"], + core_platform=core_platform, + framework="arduino", + ) + new = create_storage( + loaded_integrations=["api", "wifi", "restart"], + core_platform=core_platform, + framework="arduino", + ) + assert storage_should_update_cmake_cache(old, new) is False + + @patch("esphome.writer.clean_build") @patch("esphome.writer.StorageJSON") @patch("esphome.writer.storage_path") @@ -737,6 +835,37 @@ def test_write_cpp_with_duplicate_markers( write_cpp("// New code") +@patch("esphome.writer.CORE") +def test_clean_all_with_yaml_file( + mock_core: MagicMock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test clean_all with a .yaml file uses parent directory.""" + # Create config directory with yaml file + config_dir = tmp_path / "config" + config_dir.mkdir() + yaml_file = config_dir / "test.yaml" + yaml_file.write_text("esphome:\n name: test\n") + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + (build_dir / "dummy.txt").write_text("x") + + from esphome.writer import clean_all + + with caplog.at_level("INFO"): + clean_all([str(yaml_file)]) + + # Verify .esphome directory still exists but contents cleaned + assert build_dir.exists() + assert not (build_dir / "dummy.txt").exists() + + # Verify logging mentions the build dir + assert "Cleaning" in caplog.text + assert str(build_dir) in caplog.text + + @patch("esphome.writer.CORE") def test_clean_all( mock_core: MagicMock, @@ -1031,3 +1160,875 @@ def test_clean_all_preserves_json_files( # Verify logging mentions cleaning assert "Cleaning" in caplog.text assert str(build_dir) in caplog.text + + +@patch("esphome.writer.CORE") +def test_clean_build_handles_readonly_files( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_build handles read-only files (e.g., git pack files on Windows).""" + # Create directory structure with read-only files + pioenvs_dir = tmp_path / ".pioenvs" + pioenvs_dir.mkdir() + git_dir = pioenvs_dir / ".git" / "objects" / "pack" + git_dir.mkdir(parents=True) + + # Create a read-only file (simulating git pack files on Windows) + readonly_file = git_dir / "pack-abc123.pack" + readonly_file.write_text("pack data") + os.chmod(readonly_file, stat.S_IRUSR) # Read-only + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = pioenvs_dir + mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps" + mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock" + + # Verify file is read-only + assert not os.access(readonly_file, os.W_OK) + + # Call the function - should not crash + clean_build() + + # Verify directory was removed despite read-only files + assert not pioenvs_dir.exists() + + +@patch("esphome.writer.CORE") +def test_clean_all_handles_readonly_files( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_all handles read-only files.""" + # Create config directory + config_dir = tmp_path / "config" + config_dir.mkdir() + + build_dir = config_dir / ".esphome" + build_dir.mkdir() + + # Create a subdirectory with read-only files + subdir = build_dir / "subdir" + subdir.mkdir() + readonly_file = subdir / "readonly.txt" + readonly_file.write_text("content") + os.chmod(readonly_file, stat.S_IRUSR) # Read-only + + # Verify file is read-only + assert not os.access(readonly_file, os.W_OK) + + # Call the function - should not crash + clean_all([str(config_dir)]) + + # Verify directory was removed despite read-only files + assert not subdir.exists() + assert build_dir.exists() # .esphome dir itself is preserved + + +@patch("esphome.writer.CORE") +def test_clean_build_reraises_for_other_errors( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test clean_build re-raises errors that are not read-only permission issues.""" + # Create directory structure with a read-only subdirectory + # This prevents file deletion and triggers the error handler + pioenvs_dir = tmp_path / ".pioenvs" + pioenvs_dir.mkdir() + subdir = pioenvs_dir / "subdir" + subdir.mkdir() + test_file = subdir / "test.txt" + test_file.write_text("content") + + # Make subdir read-only so files inside can't be deleted + os.chmod(subdir, stat.S_IRUSR | stat.S_IXUSR) + + # Setup mocks + mock_core.relative_pioenvs_path.return_value = pioenvs_dir + mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps" + mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock" + + try: + # Mock os.access in writer module to return True (writable) + # This simulates a case where the error is NOT due to read-only permissions + # so the error handler should re-raise instead of trying to fix permissions + with ( + patch("esphome.writer.os.access", return_value=True), + pytest.raises(PermissionError), + ): + clean_build() + finally: + # Cleanup - restore write permission so tmp_path cleanup works + os.chmod(subdir, stat.S_IRWXU) + + +# Tests for get_build_info() + + +@patch("esphome.writer.CORE") +def test_get_build_info_new_build( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test get_build_info returns new build_time when no existing build_info.json.""" + build_info_path = tmp_path / "build_info.json" + mock_core.relative_build_path.return_value = build_info_path + mock_core.config_hash = 0x12345678 + mock_core.comment = "Test comment" + + config_hash, build_time, build_time_str, comment = get_build_info() + + assert config_hash == 0x12345678 + assert isinstance(build_time, int) + assert build_time > 0 + assert isinstance(build_time_str, str) + # Verify build_time_str format matches expected pattern + assert len(build_time_str) >= 19 # e.g., "2025-12-15 16:27:44 +0000" + assert comment == "Test comment" + + +@patch("esphome.writer.CORE") +def test_get_build_info_always_returns_current_time( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test get_build_info always returns current build_time.""" + build_info_path = tmp_path / "build_info.json" + mock_core.relative_build_path.return_value = build_info_path + mock_core.config_hash = 0x12345678 + mock_core.comment = "" + + # Create existing build_info.json with matching config_hash and version + existing_build_time = 1700000000 + existing_build_time_str = "2023-11-14 22:13:20 +0000" + build_info_path.write_text( + json.dumps( + { + "config_hash": 0x12345678, + "build_time": existing_build_time, + "build_time_str": existing_build_time_str, + "esphome_version": "2025.1.0-dev", + } + ) + ) + + with patch("esphome.writer.__version__", "2025.1.0-dev"): + config_hash, build_time, build_time_str, comment = get_build_info() + + assert config_hash == 0x12345678 + # get_build_info now always returns current time + assert build_time != existing_build_time + assert build_time > existing_build_time + assert build_time_str != existing_build_time_str + + +@patch("esphome.writer.CORE") +def test_get_build_info_config_changed( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test get_build_info returns new build_time when config hash changed.""" + build_info_path = tmp_path / "build_info.json" + mock_core.relative_build_path.return_value = build_info_path + mock_core.config_hash = 0xABCDEF00 # Different from existing + mock_core.comment = "" + + # Create existing build_info.json with different config_hash + existing_build_time = 1700000000 + build_info_path.write_text( + json.dumps( + { + "config_hash": 0x12345678, # Different + "build_time": existing_build_time, + "build_time_str": "2023-11-14 22:13:20 +0000", + "esphome_version": "2025.1.0-dev", + } + ) + ) + + with patch("esphome.writer.__version__", "2025.1.0-dev"): + config_hash, build_time, build_time_str, comment = get_build_info() + + assert config_hash == 0xABCDEF00 + assert build_time != existing_build_time # New time generated + assert build_time > existing_build_time + + +@patch("esphome.writer.CORE") +def test_get_build_info_version_changed( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test get_build_info returns new build_time when ESPHome version changed.""" + build_info_path = tmp_path / "build_info.json" + mock_core.relative_build_path.return_value = build_info_path + mock_core.config_hash = 0x12345678 + mock_core.comment = "" + + # Create existing build_info.json with different version + existing_build_time = 1700000000 + build_info_path.write_text( + json.dumps( + { + "config_hash": 0x12345678, + "build_time": existing_build_time, + "build_time_str": "2023-11-14 22:13:20 +0000", + "esphome_version": "2024.12.0", # Old version + } + ) + ) + + with patch("esphome.writer.__version__", "2025.1.0-dev"): # New version + config_hash, build_time, build_time_str, comment = get_build_info() + + assert config_hash == 0x12345678 + assert build_time != existing_build_time # New time generated + assert build_time > existing_build_time + + +@patch("esphome.writer.CORE") +def test_get_build_info_invalid_json( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test get_build_info handles invalid JSON gracefully.""" + build_info_path = tmp_path / "build_info.json" + mock_core.relative_build_path.return_value = build_info_path + mock_core.config_hash = 0x12345678 + mock_core.comment = "" + + # Create invalid JSON file + build_info_path.write_text("not valid json {{{") + + config_hash, build_time, build_time_str, comment = get_build_info() + + assert config_hash == 0x12345678 + assert isinstance(build_time, int) + assert build_time > 0 + + +@patch("esphome.writer.CORE") +def test_get_build_info_missing_keys( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test get_build_info handles missing keys gracefully.""" + build_info_path = tmp_path / "build_info.json" + mock_core.relative_build_path.return_value = build_info_path + mock_core.config_hash = 0x12345678 + mock_core.comment = "" + + # Create JSON with missing keys + build_info_path.write_text(json.dumps({"config_hash": 0x12345678})) + + with patch("esphome.writer.__version__", "2025.1.0-dev"): + config_hash, build_time, build_time_str, comment = get_build_info() + + assert config_hash == 0x12345678 + assert isinstance(build_time, int) + assert build_time > 0 + + +@patch("esphome.writer.CORE") +def test_get_build_info_build_time_str_format( + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test get_build_info returns correctly formatted build_time_str.""" + build_info_path = tmp_path / "build_info.json" + mock_core.relative_build_path.return_value = build_info_path + mock_core.config_hash = 0x12345678 + mock_core.comment = "" + + config_hash, build_time, build_time_str, comment = get_build_info() + + # Verify the format matches "%Y-%m-%d %H:%M:%S %z" + # e.g., "2025-12-15 16:27:44 +0000" + parsed = datetime.strptime(build_time_str, "%Y-%m-%d %H:%M:%S %z") + assert parsed.year >= 2024 + + +def test_generate_build_info_data_h_format() -> None: + """Test generate_build_info_data_h produces correct header content.""" + config_hash = 0x12345678 + build_time = 1700000000 + build_time_str = "2023-11-14 22:13:20 +0000" + comment = "Test comment" + + result = generate_build_info_data_h( + config_hash, build_time, build_time_str, comment + ) + + assert "#pragma once" in result + assert "#define ESPHOME_CONFIG_HASH 0x12345678U" in result + assert "#define ESPHOME_BUILD_TIME 1700000000" in result + assert "#define ESPHOME_COMMENT_SIZE 13" in result # len("Test comment") + 1 + assert 'ESPHOME_BUILD_TIME_STR[] = "2023-11-14 22:13:20 +0000"' in result + assert 'ESPHOME_COMMENT_STR[] = "Test comment"' in result + + +def test_generate_build_info_data_h_esp8266_progmem() -> None: + """Test generate_build_info_data_h includes PROGMEM for ESP8266.""" + result = generate_build_info_data_h(0xABCDEF01, 1700000000, "test", "comment") + + # Should have ESP8266 PROGMEM conditional + assert "#ifdef USE_ESP8266" in result + assert "#include " in result + assert "PROGMEM" in result + # Both build time and comment should have PROGMEM versions + assert 'ESPHOME_BUILD_TIME_STR[] PROGMEM = "test"' in result + assert 'ESPHOME_COMMENT_STR[] PROGMEM = "comment"' in result + + +def test_generate_build_info_data_h_hash_formatting() -> None: + """Test generate_build_info_data_h formats hash with leading zeros.""" + # Test with small hash value that needs leading zeros + result = generate_build_info_data_h(0x00000001, 0, "test", "") + assert "#define ESPHOME_CONFIG_HASH 0x00000001U" in result + + # Test with larger hash value + result = generate_build_info_data_h(0xFFFFFFFF, 0, "test", "") + assert "#define ESPHOME_CONFIG_HASH 0xffffffffU" in result + + +def test_generate_build_info_data_h_comment_escaping() -> None: + r"""Test generate_build_info_data_h properly escapes special characters in comment. + + Uses cpp_string_escape which outputs octal escapes for special characters: + - backslash (ASCII 92) -> \134 + - double quote (ASCII 34) -> \042 + - newline (ASCII 10) -> \012 + """ + # Test backslash escaping (ASCII 92 = octal 134) + result = generate_build_info_data_h(0, 0, "test", "backslash\\here") + assert 'ESPHOME_COMMENT_STR[] = "backslash\\134here"' in result + + # Test quote escaping (ASCII 34 = octal 042) + result = generate_build_info_data_h(0, 0, "test", 'has "quotes"') + assert 'ESPHOME_COMMENT_STR[] = "has \\042quotes\\042"' in result + + # Test newline escaping (ASCII 10 = octal 012) + result = generate_build_info_data_h(0, 0, "test", "line1\nline2") + assert 'ESPHOME_COMMENT_STR[] = "line1\\012line2"' in result + + +def test_generate_build_info_data_h_empty_comment() -> None: + """Test generate_build_info_data_h handles empty comment.""" + result = generate_build_info_data_h(0, 0, "test", "") + + assert "#define ESPHOME_COMMENT_SIZE 1" in result # Just null terminator + assert 'ESPHOME_COMMENT_STR[] = ""' in result + + +@patch("esphome.writer.CORE") +@patch("esphome.writer.iter_components") +@patch("esphome.writer.walk_files") +def test_copy_src_tree_writes_build_info_files( + mock_walk_files: MagicMock, + mock_iter_components: MagicMock, + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test copy_src_tree writes build_info_data.h and build_info.json.""" + # Setup directory structure + src_path = tmp_path / "src" + src_path.mkdir() + esphome_core_path = src_path / "esphome" / "core" + esphome_core_path.mkdir(parents=True) + build_path = tmp_path / "build" + build_path.mkdir() + + # Create mock source files for defines.h and version.h + mock_defines_h = esphome_core_path / "defines.h" + mock_defines_h.write_text("// mock defines.h") + mock_version_h = esphome_core_path / "version.h" + mock_version_h.write_text("// mock version.h") + + # Create mock FileResource that returns our temp files + @dataclass(frozen=True) + class MockFileResource: + package: str + resource: str + _path: Path + + @contextmanager + def path(self): + yield self._path + + # Create mock resources for defines.h and version.h (required by copy_src_tree) + mock_resources = [ + MockFileResource( + package="esphome.core", + resource="defines.h", + _path=mock_defines_h, + ), + MockFileResource( + package="esphome.core", + resource="version.h", + _path=mock_version_h, + ), + ] + + # Create mock component with resources + mock_component = MagicMock() + mock_component.resources = mock_resources + + # Setup mocks + mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) + mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.defines = [] + mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "Test comment" + mock_core.target_platform = "test_platform" + mock_core.config = {} + mock_iter_components.return_value = [("core", mock_component)] + mock_walk_files.return_value = [] + + # Create mock module without copy_files attribute (causes AttributeError which is caught) + mock_module = MagicMock(spec=[]) # Empty spec = no copy_files attribute + + with ( + patch("esphome.writer.__version__", "2025.1.0-dev"), + patch("esphome.writer.importlib.import_module", return_value=mock_module), + ): + copy_src_tree() + + # Verify build_info_data.h was written + build_info_h_path = esphome_core_path / "build_info_data.h" + assert build_info_h_path.exists() + build_info_h_content = build_info_h_path.read_text() + assert "#define ESPHOME_CONFIG_HASH 0xdeadbeefU" in build_info_h_content + assert "#define ESPHOME_BUILD_TIME" in build_info_h_content + assert "ESPHOME_BUILD_TIME_STR" in build_info_h_content + assert "#define ESPHOME_COMMENT_SIZE" in build_info_h_content + assert "ESPHOME_COMMENT_STR" in build_info_h_content + + # Verify build_info.json was written + build_info_json_path = build_path / "build_info.json" + assert build_info_json_path.exists() + build_info_json = json.loads(build_info_json_path.read_text()) + assert build_info_json["config_hash"] == 0xDEADBEEF + assert "build_time" in build_info_json + assert "build_time_str" in build_info_json + assert build_info_json["esphome_version"] == "2025.1.0-dev" + + +@patch("esphome.writer.CORE") +@patch("esphome.writer.iter_components") +@patch("esphome.writer.walk_files") +def test_copy_src_tree_detects_config_hash_change( + mock_walk_files: MagicMock, + mock_iter_components: MagicMock, + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test copy_src_tree detects when config_hash changes.""" + # Setup directory structure + src_path = tmp_path / "src" + src_path.mkdir() + esphome_core_path = src_path / "esphome" / "core" + esphome_core_path.mkdir(parents=True) + build_path = tmp_path / "build" + build_path.mkdir() + + # Create existing build_info.json with different config_hash + build_info_json_path = build_path / "build_info.json" + build_info_json_path.write_text( + json.dumps( + { + "config_hash": 0x12345678, # Different from current + "build_time": 1700000000, + "build_time_str": "2023-11-14 22:13:20 +0000", + "esphome_version": "2025.1.0-dev", + } + ) + ) + + # Create existing build_info_data.h + build_info_h_path = esphome_core_path / "build_info_data.h" + build_info_h_path.write_text("// old build_info_data.h") + + # Setup mocks + mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) + mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.defines = [] + mock_core.config_hash = 0xDEADBEEF # Different from existing + mock_core.comment = "" + mock_core.target_platform = "test_platform" + mock_core.config = {} + mock_iter_components.return_value = [] + mock_walk_files.return_value = [] + + with ( + patch("esphome.writer.__version__", "2025.1.0-dev"), + patch("esphome.writer.importlib.import_module") as mock_import, + ): + mock_import.side_effect = AttributeError + copy_src_tree() + + # Verify build_info files were updated due to config_hash change + assert build_info_h_path.exists() + new_content = build_info_h_path.read_text() + assert "0xdeadbeef" in new_content.lower() + + new_json = json.loads(build_info_json_path.read_text()) + assert new_json["config_hash"] == 0xDEADBEEF + + +@patch("esphome.writer.CORE") +@patch("esphome.writer.iter_components") +@patch("esphome.writer.walk_files") +def test_copy_src_tree_detects_version_change( + mock_walk_files: MagicMock, + mock_iter_components: MagicMock, + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test copy_src_tree detects when esphome_version changes.""" + # Setup directory structure + src_path = tmp_path / "src" + src_path.mkdir() + esphome_core_path = src_path / "esphome" / "core" + esphome_core_path.mkdir(parents=True) + build_path = tmp_path / "build" + build_path.mkdir() + + # Create existing build_info.json with different version + build_info_json_path = build_path / "build_info.json" + build_info_json_path.write_text( + json.dumps( + { + "config_hash": 0xDEADBEEF, + "build_time": 1700000000, + "build_time_str": "2023-11-14 22:13:20 +0000", + "esphome_version": "2024.12.0", # Old version + } + ) + ) + + # Create existing build_info_data.h + build_info_h_path = esphome_core_path / "build_info_data.h" + build_info_h_path.write_text("// old build_info_data.h") + + # Setup mocks + mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) + mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.defines = [] + mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" + mock_core.target_platform = "test_platform" + mock_core.config = {} + mock_iter_components.return_value = [] + mock_walk_files.return_value = [] + + with ( + patch("esphome.writer.__version__", "2025.1.0-dev"), # New version + patch("esphome.writer.importlib.import_module") as mock_import, + ): + mock_import.side_effect = AttributeError + copy_src_tree() + + # Verify build_info files were updated due to version change + assert build_info_h_path.exists() + new_json = json.loads(build_info_json_path.read_text()) + assert new_json["esphome_version"] == "2025.1.0-dev" + + +@patch("esphome.writer.CORE") +@patch("esphome.writer.iter_components") +@patch("esphome.writer.walk_files") +def test_copy_src_tree_handles_invalid_build_info_json( + mock_walk_files: MagicMock, + mock_iter_components: MagicMock, + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test copy_src_tree handles invalid build_info.json gracefully.""" + # Setup directory structure + src_path = tmp_path / "src" + src_path.mkdir() + esphome_core_path = src_path / "esphome" / "core" + esphome_core_path.mkdir(parents=True) + build_path = tmp_path / "build" + build_path.mkdir() + + # Create invalid build_info.json + build_info_json_path = build_path / "build_info.json" + build_info_json_path.write_text("invalid json {{{") + + # Create existing build_info_data.h + build_info_h_path = esphome_core_path / "build_info_data.h" + build_info_h_path.write_text("// old build_info_data.h") + + # Setup mocks + mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) + mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.defines = [] + mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" + mock_core.target_platform = "test_platform" + mock_core.config = {} + mock_iter_components.return_value = [] + mock_walk_files.return_value = [] + + with ( + patch("esphome.writer.__version__", "2025.1.0-dev"), + patch("esphome.writer.importlib.import_module") as mock_import, + ): + mock_import.side_effect = AttributeError + copy_src_tree() + + # Verify build_info files were created despite invalid JSON + assert build_info_h_path.exists() + new_json = json.loads(build_info_json_path.read_text()) + assert new_json["config_hash"] == 0xDEADBEEF + + +@patch("esphome.writer.CORE") +@patch("esphome.writer.iter_components") +@patch("esphome.writer.walk_files") +def test_copy_src_tree_build_info_timestamp_behavior( + mock_walk_files: MagicMock, + mock_iter_components: MagicMock, + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test build_info behaviour: regenerated on change, preserved when unchanged.""" + # Setup directory structure + src_path = tmp_path / "src" + src_path.mkdir() + esphome_core_path = src_path / "esphome" / "core" + esphome_core_path.mkdir(parents=True) + esphome_components_path = src_path / "esphome" / "components" + esphome_components_path.mkdir(parents=True) + build_path = tmp_path / "build" + build_path.mkdir() + + # Create a source file + source_file = tmp_path / "source" / "test.cpp" + source_file.parent.mkdir() + source_file.write_text("// version 1") + + # Create destination file in build tree + dest_file = esphome_components_path / "test.cpp" + + # Create mock FileResource + @dataclass(frozen=True) + class MockFileResource: + package: str + resource: str + _path: Path + + @contextmanager + def path(self): + yield self._path + + mock_resources = [ + MockFileResource( + package="esphome.components", + resource="test.cpp", + _path=source_file, + ), + ] + + mock_component = MagicMock() + mock_component.resources = mock_resources + + # Setup mocks + mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) + mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.defines = [] + mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" + mock_core.target_platform = "test_platform" + mock_core.config = {} + mock_iter_components.return_value = [("test", mock_component)] + + build_info_json_path = build_path / "build_info.json" + + # First run: initial setup, should create build_info + mock_walk_files.return_value = [] + with ( + patch("esphome.writer.__version__", "2025.1.0-dev"), + patch("esphome.writer.importlib.import_module") as mock_import, + ): + mock_import.side_effect = AttributeError + copy_src_tree() + + # Manually set an old timestamp for testing + old_timestamp = 1700000000 + old_timestamp_str = "2023-11-14 22:13:20 +0000" + build_info_json_path.write_text( + json.dumps( + { + "config_hash": 0xDEADBEEF, + "build_time": old_timestamp, + "build_time_str": old_timestamp_str, + "esphome_version": "2025.1.0-dev", + } + ) + ) + + # Second run: no changes, should NOT regenerate build_info + mock_walk_files.return_value = [str(dest_file)] + with ( + patch("esphome.writer.__version__", "2025.1.0-dev"), + patch("esphome.writer.importlib.import_module") as mock_import, + ): + mock_import.side_effect = AttributeError + copy_src_tree() + + second_json = json.loads(build_info_json_path.read_text()) + second_timestamp = second_json["build_time"] + + # Verify timestamp was NOT changed + assert second_timestamp == old_timestamp, ( + f"build_info should not be regenerated when no files change: " + f"{old_timestamp} != {second_timestamp}" + ) + + # Third run: change source file, should regenerate build_info with new timestamp + source_file.write_text("// version 2") + with ( + patch("esphome.writer.__version__", "2025.1.0-dev"), + patch("esphome.writer.importlib.import_module") as mock_import, + ): + mock_import.side_effect = AttributeError + copy_src_tree() + + third_json = json.loads(build_info_json_path.read_text()) + third_timestamp = third_json["build_time"] + + # Verify timestamp WAS changed + assert third_timestamp != old_timestamp, ( + f"build_info should be regenerated when source file changes: " + f"{old_timestamp} == {third_timestamp}" + ) + assert third_timestamp > old_timestamp + + +@patch("esphome.writer.CORE") +@patch("esphome.writer.iter_components") +@patch("esphome.writer.walk_files") +def test_copy_src_tree_detects_removed_source_file( + mock_walk_files: MagicMock, + mock_iter_components: MagicMock, + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test copy_src_tree detects when a non-generated source file is removed.""" + # Setup directory structure + src_path = tmp_path / "src" + src_path.mkdir() + esphome_components_path = src_path / "esphome" / "components" + esphome_components_path.mkdir(parents=True) + build_path = tmp_path / "build" + build_path.mkdir() + + # Create an existing source file in the build tree + existing_file = esphome_components_path / "test.cpp" + existing_file.write_text("// test file") + + # Setup mocks - no components, so the file should be removed + mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) + mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.defines = [] + mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" + mock_core.target_platform = "test_platform" + mock_core.config = {} + mock_iter_components.return_value = [] # No components = file should be removed + mock_walk_files.return_value = [str(existing_file)] + + # Create existing build_info.json + build_info_json_path = build_path / "build_info.json" + old_timestamp = 1700000000 + build_info_json_path.write_text( + json.dumps( + { + "config_hash": 0xDEADBEEF, + "build_time": old_timestamp, + "build_time_str": "2023-11-14 22:13:20 +0000", + "esphome_version": "2025.1.0-dev", + } + ) + ) + + with ( + patch("esphome.writer.__version__", "2025.1.0-dev"), + patch("esphome.writer.importlib.import_module") as mock_import, + ): + mock_import.side_effect = AttributeError + copy_src_tree() + + # Verify file was removed + assert not existing_file.exists() + + # Verify build_info was regenerated due to source file removal + new_json = json.loads(build_info_json_path.read_text()) + assert new_json["build_time"] != old_timestamp + + +@patch("esphome.writer.CORE") +@patch("esphome.writer.iter_components") +@patch("esphome.writer.walk_files") +def test_copy_src_tree_ignores_removed_generated_file( + mock_walk_files: MagicMock, + mock_iter_components: MagicMock, + mock_core: MagicMock, + tmp_path: Path, +) -> None: + """Test copy_src_tree doesn't mark sources_changed when only generated file removed.""" + # Setup directory structure + src_path = tmp_path / "src" + src_path.mkdir() + esphome_core_path = src_path / "esphome" / "core" + esphome_core_path.mkdir(parents=True) + build_path = tmp_path / "build" + build_path.mkdir() + + # Create existing build_info_data.h (a generated file) + build_info_h = esphome_core_path / "build_info_data.h" + build_info_h.write_text("// old generated file") + + # Setup mocks + mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) + mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.defines = [] + mock_core.config_hash = 0xDEADBEEF + mock_core.comment = "" + mock_core.target_platform = "test_platform" + mock_core.config = {} + mock_iter_components.return_value = [] + # walk_files returns the generated file, but it's not in source_files_copy + mock_walk_files.return_value = [str(build_info_h)] + + # Create existing build_info.json with old timestamp + build_info_json_path = build_path / "build_info.json" + old_timestamp = 1700000000 + build_info_json_path.write_text( + json.dumps( + { + "config_hash": 0xDEADBEEF, + "build_time": old_timestamp, + "build_time_str": "2023-11-14 22:13:20 +0000", + "esphome_version": "2025.1.0-dev", + } + ) + ) + + with ( + patch("esphome.writer.__version__", "2025.1.0-dev"), + patch("esphome.writer.importlib.import_module") as mock_import, + ): + mock_import.side_effect = AttributeError + copy_src_tree() + + # Verify build_info_data.h was regenerated (not removed) + assert build_info_h.exists() + + # Note: build_info.json will have a new timestamp because get_build_info() + # always returns current time. The key test is that the old build_info_data.h + # file was removed and regenerated, not that it triggered sources_changed. + new_json = json.loads(build_info_json_path.read_text()) + assert new_json["config_hash"] == 0xDEADBEEF diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index eac0ceabb8..c8cb3e144f 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -278,3 +278,31 @@ def test_secret_values_tracking(fixture_path: Path) -> None: assert yaml_util._SECRET_VALUES["super_secret_wifi"] == "wifi_password" assert "0123456789abcdef" in yaml_util._SECRET_VALUES assert yaml_util._SECRET_VALUES["0123456789abcdef"] == "api_key" + + +def test_dump_sort_keys() -> None: + """Test that dump with sort_keys=True produces sorted output.""" + # Create a dict with unsorted keys + data = { + "zebra": 1, + "alpha": 2, + "nested": { + "z_key": "z_value", + "a_key": "a_value", + }, + } + + # Without sort_keys, keys are in insertion order + unsorted = yaml_util.dump(data, sort_keys=False) + lines_unsorted = unsorted.strip().split("\n") + # First key should be "zebra" (insertion order) + assert lines_unsorted[0].startswith("zebra:") + + # With sort_keys, keys are alphabetically sorted + sorted_dump = yaml_util.dump(data, sort_keys=True) + lines_sorted = sorted_dump.strip().split("\n") + # First key should be "alpha" (alphabetical order) + assert lines_sorted[0].startswith("alpha:") + # nested keys should also be sorted + assert "a_key:" in sorted_dump + assert sorted_dump.index("a_key:") < sorted_dump.index("z_key:")